diff --git a/internal/client/auth.go b/internal/client/auth.go index aa23eb2..b32d761 100644 --- a/internal/client/auth.go +++ b/internal/client/auth.go @@ -13,11 +13,11 @@ const ( ) type chHiddenData struct { - rawCiphertextWithTag []byte - chRandom []byte - chSessionId []byte - chX25519KeyShare []byte - chExtSNI []byte + fullRaw []byte // pubkey, ciphertext, tag + chRandom []byte + chSessionId []byte + chX25519KeyShare []byte + chExtSNI []byte } // makeHiddenData generates the ephemeral key pair, calculates the shared secret, and then compose and @@ -50,7 +50,7 @@ func makeHiddenData(sta *State) (ret chHiddenData, sharedSecret []byte) { sharedSecret = ecdh.GenerateSharedSecret(ephPv, sta.staticPub) nonce := ret.chRandom[0:12] ciphertextWithTag, _ := util.AESGCMEncrypt(nonce, sharedSecret, plaintext) - ret.rawCiphertextWithTag = ciphertextWithTag + ret.fullRaw = append(ret.chRandom, ciphertextWithTag...) ret.chSessionId = ciphertextWithTag[0:32] ret.chX25519KeyShare = ciphertextWithTag[32:64] ret.chExtSNI = makeServerName(sta.ServerName) diff --git a/internal/client/websocket.go b/internal/client/websocket.go index 9ad1875..d9a3c04 100644 --- a/internal/client/websocket.go +++ b/internal/client/websocket.go @@ -3,53 +3,14 @@ package client import ( "encoding/base64" "errors" + "fmt" "github.com/cbeuw/Cloak/internal/util" + "github.com/gorilla/websocket" "net" "net/http" "net/url" - "time" - - "github.com/gorilla/websocket" ) -type WebSocketConn struct { - c *websocket.Conn -} - -func (ws *WebSocketConn) Write(data []byte) (int, error) { - err := ws.c.WriteMessage(websocket.BinaryMessage, data) - if err != nil { - return 0, err - } else { - return len(data), nil - } -} - -func (ws *WebSocketConn) Read(buf []byte) (int, error) { - _, r, err := ws.c.NextReader() - if err != nil { - return 0, err - } - return r.Read(buf) -} - -func (ws *WebSocketConn) Close() error { return ws.c.Close() } -func (ws *WebSocketConn) LocalAddr() net.Addr { return ws.c.LocalAddr() } -func (ws *WebSocketConn) RemoteAddr() net.Addr { return ws.c.RemoteAddr() } -func (ws *WebSocketConn) SetDeadline(t time.Time) error { - err := ws.c.SetReadDeadline(t) - if err != nil { - return err - } - err = ws.c.SetWriteDeadline(t) - if err != nil { - return err - } - return nil -} -func (ws *WebSocketConn) SetReadDeadline(t time.Time) error { return ws.c.SetReadDeadline(t) } -func (ws *WebSocketConn) SetWriteDeadline(t time.Time) error { return ws.c.SetWriteDeadline(t) } - type WebSocket struct { Transport } @@ -57,27 +18,31 @@ type WebSocket struct { func (WebSocket) PrepareConnection(sta *State, conn net.Conn) (sessionKey []byte, err error) { u, err := url.Parse("ws://" + sta.RemoteHost + ":" + sta.RemotePort) //TODO IPv6 if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse ws url: %v") } hd, sharedSecret := makeHiddenData(sta) header := http.Header{} - header.Add("hidden", base64.StdEncoding.EncodeToString(hd.rawCiphertextWithTag)) - c, resp, err := websocket.NewClient(conn, u, header, 16480, 16480) + header.Add("hidden", base64.StdEncoding.EncodeToString(hd.fullRaw)) + c, _, err := websocket.NewClient(conn, u, header, 16480, 16480) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to handshake: %v", err) } - reply, err := base64.StdEncoding.DecodeString(resp.Header.Get("reply")) + conn = &util.WebSocketConn{c} + + buf := make([]byte, 128) + n, err := conn.Read(buf) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read reply: %v", err) } - if len(reply) != 60 { + if n != 60 { return nil, errors.New("reply must be 60 bytes") } + + reply := buf[:60] sessionKey, err = util.AESGCMDecrypt(reply[:12], sharedSecret, reply[12:]) - conn = &WebSocketConn{c: c} return } diff --git a/internal/server/TLS.go b/internal/server/TLS.go index cc8aa10..09ac687 100644 --- a/internal/server/TLS.go +++ b/internal/server/TLS.go @@ -10,9 +10,6 @@ import ( "fmt" "github.com/cbeuw/Cloak/internal/ecdh" "github.com/cbeuw/Cloak/internal/util" - "net" - - log "github.com/sirupsen/logrus" ) // ClientHello contains every field in a ClientHello message @@ -247,51 +244,3 @@ func unmarshalClientHello(ch *ClientHello, staticPv crypto.PrivateKey) (ai authe } return } - -// PrepareConnection checks if the first packet of data is ClientHello, and checks if it was from a Cloak client -// if it is from a Cloak client, it returns the ClientInfo with the decrypted fields. It doesn't check if the user -// is authorised. It also returns a finisher callback function to be called when the caller wishes to proceed with -// the handshake -func PrepareConnection(firstPacket []byte, sta *State, conn net.Conn) (info ClientInfo, finisher func([]byte) error, err error) { - ch, err := parseClientHello(firstPacket) - if err != nil { - log.Debug(err) - err = ErrBadClientHello - return - } - - if sta.registerRandom(ch.random) { - err = ErrReplay - return - } - - var ai authenticationInfo - ai, err = unmarshalClientHello(ch, sta.staticPv) - if err != nil { - return - } - info, err = touchStone(ai, sta.Now) - if err != nil { - log.Debug(err) - err = ErrNotCloak - return - } - if _, ok := sta.ProxyBook[info.ProxyMethod]; !ok { - err = ErrBadProxyMethod - return - } - - finisher = func(sessionKey []byte) error { - reply, err := composeReply(ch, ai.sharedSecret, sessionKey) - if err != nil { - return err - } - _, err = conn.Write(reply) - if err != nil { - go conn.Close() - return err - } - return nil - } - return -} diff --git a/internal/server/auth.go b/internal/server/auth.go index a7f15b4..c537a16 100644 --- a/internal/server/auth.go +++ b/internal/server/auth.go @@ -1,12 +1,19 @@ package server import ( + "bufio" "bytes" + "crypto/rand" + "encoding/base64" "encoding/binary" "errors" "fmt" "github.com/cbeuw/Cloak/internal/util" + "net" + "net/http" "time" + + log "github.com/sirupsen/logrus" ) type ClientInfo struct { @@ -30,11 +37,11 @@ const ( var ErrInvalidPubKey = errors.New("public key has invalid format") var ErrCiphertextLength = errors.New("ciphertext has the wrong length") var ErrTimestampOutOfWindow = errors.New("timestamp is outside of the accepting window") +var ErrUnreconisedProtocol = errors.New("unreconised protocol") // touchStone checks if a ClientHello came from a Cloak client by checking and decrypting the fields Cloak hides data in // It returns the ClientInfo, but it doesn't check if the UID is authorised func touchStone(ai authenticationInfo, now func() time.Time) (info ClientInfo, err error) { - var plaintext []byte plaintext, err = util.AESGCMDecrypt(ai.nonce, ai.sharedSecret, ai.ciphertextWithTag) if err != nil { @@ -59,3 +66,99 @@ func touchStone(ai authenticationInfo, now func() time.Time) (info ClientInfo, e info.SessionId = binary.BigEndian.Uint32(plaintext[37:41]) return } + +// PrepareConnection checks if the first packet of data is ClientHello or HTTP GET, and checks if it was from a Cloak client +// if it is from a Cloak client, it returns the ClientInfo with the decrypted fields. It doesn't check if the user +// is authorised. It also returns a finisher callback function to be called when the caller wishes to proceed with +// the handshake +func PrepareConnection(firstPacket []byte, sta *State, conn net.Conn) (info ClientInfo, finisher func([]byte) error, err error) { + var ai authenticationInfo + switch firstPacket[0] { + case 0x47: + var req *http.Request + req, err = http.ReadRequest(bufio.NewReader(bytes.NewBuffer(firstPacket))) + if err != nil { + err = fmt.Errorf("failed to parse first HTTP GET: %v", err) + return + } + var hiddenData []byte + hiddenData, err = base64.StdEncoding.DecodeString(req.Header.Get("hidden")) + + ai, err = unmarshalHidden(hiddenData, sta.staticPv) + if err != nil { + err = fmt.Errorf("failed to unmarshal hidden data from WS into authenticationInfo: %v", err) + return + } + + finisher = func(sessionKey []byte) error { + handler := newWsHandshakeHandler() + + go http.Serve(newWsAcceptor(conn, firstPacket), handler) + + <-handler.finished + conn = handler.conn + nonce := make([]byte, 12) + rand.Read(nonce) + + // reply: [12 bytes nonce][32 bytes encrypted session key][16 bytes authentication tag] + encryptedKey, err := util.AESGCMEncrypt(nonce, ai.sharedSecret, sessionKey) // 32 + 16 = 48 bytes + if err != nil { + return fmt.Errorf("failed to encrypt reply: %v", err) + } + reply := append(nonce, encryptedKey...) + _, err = conn.Write(reply) + if err != nil { + go conn.Close() + return fmt.Errorf("failed to write reply: %v", err) + } + return nil + } + case 0x16: + var ch *ClientHello + ch, err = parseClientHello(firstPacket) + if err != nil { + log.Debug(err) + err = ErrBadClientHello + return + } + + if sta.registerRandom(ch.random) { + err = ErrReplay + return + } + + ai, err = unmarshalClientHello(ch, sta.staticPv) + if err != nil { + err = fmt.Errorf("failed to unmarshal ClientHello into authenticationInfo: %v", err) + return + } + finisher = func(sessionKey []byte) error { + reply, err := composeReply(ch, ai.sharedSecret, sessionKey) + if err != nil { + return fmt.Errorf("failed to compose TLS reply: %v", err) + } + _, err = conn.Write(reply) + if err != nil { + go conn.Close() + return fmt.Errorf("failed to write TLS reply: %v", err) + } + return nil + } + default: + err = ErrUnreconisedProtocol + return + } + + info, err = touchStone(ai, sta.Now) + if err != nil { + log.Debug(err) + err = ErrNotCloak + return + } + if _, ok := sta.ProxyBook[info.ProxyMethod]; !ok { + err = ErrBadProxyMethod + return + } + + return +} diff --git a/internal/server/websocket.go b/internal/server/websocket.go new file mode 100644 index 0000000..e9bee82 --- /dev/null +++ b/internal/server/websocket.go @@ -0,0 +1,108 @@ +package server + +import ( + "crypto" + "errors" + "fmt" + "github.com/cbeuw/Cloak/internal/ecdh" + "github.com/cbeuw/Cloak/internal/util" + "github.com/gorilla/websocket" + "net" + "net/http" + + log "github.com/sirupsen/logrus" +) + +type firstBuffedConn struct { + net.Conn + firstRead bool + firstPacket []byte +} + +func (c *firstBuffedConn) Read(buf []byte) (int, error) { + if !c.firstRead { + copy(buf, c.firstPacket) + n := len(c.firstPacket) + c.firstPacket = []byte{} + return n, nil + } + return c.Read(buf) +} + +type wsAcceptor struct { + done bool + c *firstBuffedConn +} + +func newWsAcceptor(conn net.Conn, first []byte) *wsAcceptor { + f := make([]byte, len(first)) + copy(f, first) + return &wsAcceptor{ + c: &firstBuffedConn{Conn: conn, firstPacket: first}, + } +} + +func (w *wsAcceptor) Accept() (net.Conn, error) { + if w.done { + return nil, errors.New("already accepted") + } + w.done = true + return w.c, nil +} + +func (w *wsAcceptor) Close() error { + w.done = true + return nil +} + +func (w *wsAcceptor) Addr() net.Addr { + return w.c.LocalAddr() +} + +type wsHandshakeHandler struct { + conn net.Conn + finished chan struct{} +} + +func newWsHandshakeHandler() *wsHandshakeHandler { + return &wsHandshakeHandler{finished: make(chan struct{})} +} + +func (ws *wsHandshakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + upgrader := websocket.Upgrader{ + ReadBufferSize: 16380, + WriteBufferSize: 16380, + } + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Errorf("failed to upgrade connection to ws: %v", err) + return + } + ws.conn = &util.WebSocketConn{c} + ws.finished <- struct{}{} +} + +var ErrBadGET = errors.New("non (or malformed) HTTP GET") + +func unmarshalHidden(hidden []byte, staticPv crypto.PrivateKey) (ai authenticationInfo, err error) { + if len(hidden) < 96 { + err = ErrBadGET + return + } + ephPub, ok := ecdh.Unmarshal(hidden[0:32]) + if !ok { + err = ErrInvalidPubKey + return + } + + ai.nonce = hidden[:12] + + ai.sharedSecret = ecdh.GenerateSharedSecret(staticPv, ephPub) + + ai.ciphertextWithTag = hidden[32:] + if len(ai.ciphertextWithTag) != 64 { + err = fmt.Errorf("%v: %v", ErrCiphertextLength, len(ai.ciphertextWithTag)) + return + } + return +} diff --git a/internal/util/websocket.go b/internal/util/websocket.go new file mode 100644 index 0000000..37ddfa0 --- /dev/null +++ b/internal/util/websocket.go @@ -0,0 +1,39 @@ +package util + +import ( + "github.com/gorilla/websocket" + "time" +) + +type WebSocketConn struct { + *websocket.Conn +} + +func (ws *WebSocketConn) Write(data []byte) (int, error) { + err := ws.WriteMessage(websocket.BinaryMessage, data) + if err != nil { + return 0, err + } else { + return len(data), nil + } +} + +func (ws *WebSocketConn) Read(buf []byte) (int, error) { + _, r, err := ws.NextReader() + if err != nil { + return 0, err + } + return r.Read(buf) +} + +func (ws *WebSocketConn) SetDeadline(t time.Time) error { + err := ws.SetReadDeadline(t) + if err != nil { + return err + } + err = ws.SetWriteDeadline(t) + if err != nil { + return err + } + return nil +}