From 0dd52d857032fc14a2b81fa0087911f94d01c04f Mon Sep 17 00:00:00 2001 From: Qian Wang Date: Sun, 9 Jun 2019 21:05:41 +1000 Subject: [PATCH] Add optional encryption --- cmd/ck-client/ck-client.go | 18 ++++++++++-- cmd/ck-server/ck-server.go | 21 ++++++++++++-- config/ckclient.json | 1 + internal/client/auth.go | 11 +++++--- internal/client/state.go | 37 +++++++++++++++++-------- internal/multiplex/crypto.go | 53 ++++++++++++++++++++++++++++++++++++ internal/multiplex/obfs.go | 23 +++++++++------- internal/server/auth.go | 16 +++++------ internal/server/auth_test.go | 2 +- 9 files changed, 143 insertions(+), 39 deletions(-) create mode 100644 internal/multiplex/crypto.go diff --git a/cmd/ck-client/ck-client.go b/cmd/ck-client/ck-client.go index 4b79a31..9482083 100644 --- a/cmd/ck-client/ck-client.go +++ b/cmd/ck-client/ck-client.go @@ -21,6 +21,7 @@ import ( var version string +//TODO: take iv into account func pipe(dst io.ReadWriteCloser, src io.ReadWriteCloser) { // The maximum size of TLS message will be 16396+12. 12 because of the stream header // 16408 is the max TLS message size on Firefox @@ -180,8 +181,21 @@ start: var UNLIMITED_DOWN int64 = 1e15 var UNLIMITED_UP int64 = 1e15 valve := mux.MakeValve(1e12, 1e12, &UNLIMITED_DOWN, &UNLIMITED_UP) - obfs := mux.MakeObfs(sta.UID) - deobfs := mux.MakeDeobfs(sta.UID) + + var crypto mux.Crypto + switch sta.EncryptionMethod { + case 0x00: + crypto = &mux.Plain{} + case 0x01: + crypto, err = mux.MakeAESCipher(sta.UID) + if err != nil { + log.Println(err) + return + } + } + + obfs := mux.MakeObfs(sta.UID, crypto) + deobfs := mux.MakeDeobfs(sta.UID, crypto) sesh := mux.MakeSession(sessionID, valve, obfs, deobfs, util.ReadTLS) var wg sync.WaitGroup diff --git a/cmd/ck-server/ck-server.go b/cmd/ck-server/ck-server.go index 76f0942..99df4f5 100644 --- a/cmd/ck-server/ck-server.go +++ b/cmd/ck-server/ck-server.go @@ -20,6 +20,7 @@ import ( var version string +//TODO: take iv into account func pipe(dst io.ReadWriteCloser, src io.ReadWriteCloser) { // The maximum size of TLS message will be 16396+12. 12 because of the stream header // 16408 is the max TLS message size on Firefox @@ -69,7 +70,7 @@ func dispatchConnection(conn net.Conn, sta *server.State) { return } - isCloak, UID, sessionID, proxyMethod := server.TouchStone(ch, sta) + isCloak, UID, sessionID, proxyMethod, encryptionMethod := server.TouchStone(ch, sta) if !isCloak { log.Printf("+1 non Cloak TLS traffic from %v\n", conn.RemoteAddr()) goWeb(data) @@ -148,7 +149,23 @@ func dispatchConnection(conn net.Conn, sta *server.State) { return } - sesh, existing, err := user.GetSession(sessionID, mux.MakeObfs(UID), mux.MakeDeobfs(UID), util.ReadTLS) + var crypto mux.Crypto + switch encryptionMethod { + case 0x00: + crypto = &mux.Plain{} + case 0x01: + crypto, err = mux.MakeAESCipher(UID) + if err != nil { + log.Println(err) + goWeb(data) + return + } + default: + goWeb(data) + return + } + + sesh, existing, err := user.GetSession(sessionID, mux.MakeObfs(UID, crypto), mux.MakeDeobfs(UID, crypto), util.ReadTLS) if err != nil { user.DelSession(sessionID) log.Println(err) diff --git a/config/ckclient.json b/config/ckclient.json index b564245..3927f69 100644 --- a/config/ckclient.json +++ b/config/ckclient.json @@ -1,5 +1,6 @@ { "ProxyMethod":"shadowsocks", + "EncryptionMethod":"plain", "UID":"iGAO85zysIyR4c09CyZSLdNhtP/ckcYu7nIPI082AHA=", "PublicKey":"IYoUzkle/T/kriE+Ufdm7AHQtIeGnBWbhhlTbmDpUUI=", "ServerName":"www.bing.com", diff --git a/internal/client/auth.go b/internal/client/auth.go index c5fb401..f111741 100644 --- a/internal/client/auth.go +++ b/internal/client/auth.go @@ -34,7 +34,7 @@ func MakeRandomField(sta *State) []byte { } func MakeSessionTicket(sta *State) []byte { - // sessionTicket: [marshalled ephemeral pub key 32 bytes][encrypted UID+sessionID 36 bytes and proxy method 16 bytes][padding 108 bytes] + // sessionTicket: [marshalled ephemeral pub key 32 bytes][encrypted UID+sessionID 36 bytes, proxy method 16 bytes, encryption method 1 byte][padding 107 bytes] // The first 16 bytes of the marshalled ephemeral public key is used as the IV // for encrypting the UID tthInterval := sta.Now().Unix() / int64(sta.TicketTimeHint) @@ -52,12 +52,15 @@ func MakeSessionTicket(sta *State) []byte { ticket := make([]byte, 192) copy(ticket[0:32], ecdh.Marshal(ephKP.PublicKey)) key := ecdh.GenerateSharedSecret(ephKP.PrivateKey, sta.staticPub) - plain := make([]byte, 52) + + plain := make([]byte, 53) copy(plain, sta.UID) binary.BigEndian.PutUint32(plain[32:36], sta.sessionID) copy(plain[36:52], []byte(sta.ProxyMethod)) + plain[52] = sta.EncryptionMethod + cipher := util.AESEncrypt(ticket[0:16], key, plain) - copy(ticket[32:84], cipher) + copy(ticket[32:85], cipher) // The purpose of adding sessionID is that, the generated padding of sessionTicket needs to be unpredictable. // As shown in auth.go, the padding is generated by a psudo random generator. The seed // needs to be the same for each TicketTimeHint interval. However the value of epoch/TicketTimeHint @@ -68,6 +71,6 @@ func MakeSessionTicket(sta *State) []byte { // With the sessionID value generated at startup of ckclient and used as a part of the seed, the // sessionTicket is still identical for each TicketTimeHint interval, but others won't be able to know // how it was generated. It will also be different for each client. - copy(ticket[84:192], util.PsudoRandBytes(108, tthInterval+int64(sta.sessionID))) + copy(ticket[85:192], util.PsudoRandBytes(107, tthInterval+int64(sta.sessionID))) return ticket } diff --git a/internal/client/state.go b/internal/client/state.go index d2ac28d..f03a807 100644 --- a/internal/client/state.go +++ b/internal/client/state.go @@ -14,13 +14,14 @@ import ( ) type rawConfig struct { - ServerName string - ProxyMethod string - UID string - PublicKey string - TicketTimeHint int - BrowserSig string - NumConn int + ServerName string + ProxyMethod string + EncryptionMethod string + UID string + PublicKey string + TicketTimeHint int + BrowserSig string + NumConn int } // State stores global variables @@ -37,11 +38,12 @@ type State struct { keyPairsM sync.RWMutex keyPairs map[int64]*keyPair - ProxyMethod string - TicketTimeHint int - ServerName string - BrowserSig string - NumConn int + ProxyMethod string + EncryptionMethod byte + TicketTimeHint int + ServerName string + BrowserSig string + NumConn int } func InitState(localHost, localPort, remoteHost, remotePort string, nowFunc func() time.Time) *State { @@ -104,11 +106,22 @@ func (sta *State) ParseConfig(conf string) (err error) { if err != nil { return err } + + switch preParse.EncryptionMethod { + case "plain": + sta.EncryptionMethod = 0x00 + case "aes": + sta.EncryptionMethod = 0x01 + default: + return errors.New("Unknown encryption method") + } + sta.ProxyMethod = preParse.ProxyMethod sta.ServerName = preParse.ServerName sta.TicketTimeHint = preParse.TicketTimeHint sta.BrowserSig = preParse.BrowserSig sta.NumConn = preParse.NumConn + uid, err := base64.StdEncoding.DecodeString(preParse.UID) if err != nil { return errors.New("Failed to parse UID: " + err.Error()) diff --git a/internal/multiplex/crypto.go b/internal/multiplex/crypto.go new file mode 100644 index 0000000..91260e3 --- /dev/null +++ b/internal/multiplex/crypto.go @@ -0,0 +1,53 @@ +package multiplex + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" +) + +type Crypto interface { + encrypt([]byte) []byte + decrypt([]byte) []byte +} + +type Plain struct{} + +func (p *Plain) encrypt(plaintext []byte) []byte { + return plaintext +} + +func (p *Plain) decrypt(buf []byte) []byte { + return buf +} + +type AES struct { + cipher cipher.Block +} + +func MakeAESCipher(key []byte) (*AES, error) { + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + ret := AES{ + c, + } + return &ret, nil +} + +func (a *AES) encrypt(plaintext []byte) []byte { + iv := make([]byte, 16) + rand.Read(iv) + ciphertext := make([]byte, 16+len(plaintext)) + stream := cipher.NewCTR(a.cipher, iv) + stream.XORKeyStream(ciphertext[16:], plaintext) + copy(ciphertext[:16], iv) + return ciphertext +} + +func (a *AES) decrypt(buf []byte) []byte { + stream := cipher.NewCTR(a.cipher, buf[0:16]) + stream.XORKeyStream(buf[16:], buf[16:]) + return buf[16:] +} diff --git a/internal/multiplex/obfs.go b/internal/multiplex/obfs.go index 2d576df..c3acb49 100644 --- a/internal/multiplex/obfs.go +++ b/internal/multiplex/obfs.go @@ -15,15 +15,13 @@ var u32 = binary.BigEndian.Uint32 const headerLen = 12 -// For each frame, the three parts of the header is xored with three keys. -// The keys are generated from the SID and the payload of the frame. func genXorKeys(key, nonce []byte) (i uint32, ii uint32, iii uint8) { h := sha1.New() hashed := h.Sum(append(key, nonce...)) return u32(hashed[0:4]), u32(hashed[4:8]), hashed[8] } -func MakeObfs(key []byte) Obfser { +func MakeObfs(key []byte, algo Crypto) Obfser { obfs := func(f *Frame) ([]byte, error) { obfsedHeader := make([]byte, headerLen) // header: [StreamID 4 bytes][Seq 4 bytes][Closing 1 byte][Nonce 3 bytes] @@ -33,22 +31,24 @@ func MakeObfs(key []byte) Obfser { binary.BigEndian.PutUint32(obfsedHeader[4:8], f.Seq^ii) obfsedHeader[8] = f.Closing ^ iii + encryptedPayload := algo.encrypt(f.Payload) + // Composing final obfsed message // We don't use util.AddRecordLayer here to avoid unnecessary malloc - obfsed := make([]byte, 5+headerLen+len(f.Payload)) + obfsed := make([]byte, 5+headerLen+len(encryptedPayload)) obfsed[0] = 0x17 obfsed[1] = 0x03 obfsed[2] = 0x03 - binary.BigEndian.PutUint16(obfsed[3:5], uint16(headerLen+len(f.Payload))) + binary.BigEndian.PutUint16(obfsed[3:5], uint16(headerLen+len(encryptedPayload))) copy(obfsed[5:5+headerLen], obfsedHeader) - copy(obfsed[5+headerLen:], f.Payload) + copy(obfsed[5+headerLen:], encryptedPayload) // obfsed: [record layer 5 bytes][cipherheader 12 bytes][payload] return obfsed, nil } return obfs } -func MakeDeobfs(key []byte) Deobfser { +func MakeDeobfs(key []byte, algo Crypto) Deobfser { deobfs := func(in []byte) (*Frame, error) { if len(in) < 5+headerLen { return nil, errors.New("Input cannot be shorter than 17 bytes") @@ -58,13 +58,16 @@ func MakeDeobfs(key []byte) Deobfser { streamID := u32(peeled[0:4]) ^ i seq := u32(peeled[4:8]) ^ ii closing := peeled[8] ^ iii - payload := make([]byte, len(peeled)-headerLen) - copy(payload, peeled[headerLen:]) + + rawPayload := make([]byte, len(peeled)-headerLen) + copy(rawPayload, peeled[headerLen:]) + decryptedPayload := algo.decrypt(rawPayload) + ret := &Frame{ StreamID: streamID, Seq: seq, Closing: closing, - Payload: payload, + Payload: decryptedPayload, } return ret, nil } diff --git a/internal/server/auth.go b/internal/server/auth.go index ac354a7..fc87c6a 100644 --- a/internal/server/auth.go +++ b/internal/server/auth.go @@ -12,12 +12,12 @@ import ( ) // input ticket, return UID -func decryptSessionTicket(staticPv crypto.PrivateKey, ticket []byte) ([]byte, uint32, string) { +func decryptSessionTicket(staticPv crypto.PrivateKey, ticket []byte) ([]byte, uint32, string, byte) { ephPub, _ := ecdh.Unmarshal(ticket[0:32]) key := ecdh.GenerateSharedSecret(staticPv, ephPub) - plain := util.AESDecrypt(ticket[0:16], key, ticket[32:84]) + plain := util.AESDecrypt(ticket[0:16], key, ticket[32:85]) sessionID := binary.BigEndian.Uint32(plain[32:36]) - return plain[0:32], sessionID, string(bytes.Trim(plain[36:52], "\x00")) + return plain[0:32], sessionID, string(bytes.Trim(plain[36:52], "\x00")), plain[52] } func validateRandom(random []byte, UID []byte, time int64) bool { @@ -32,7 +32,7 @@ func validateRandom(random []byte, UID []byte, time int64) bool { h.Write(preHash) return bytes.Equal(h.Sum(nil)[0:16], random[16:32]) } -func TouchStone(ch *ClientHello, sta *State) (isCK bool, UID []byte, sessionID uint32, proxyMethod string) { +func TouchStone(ch *ClientHello, sta *State) (isCK bool, UID []byte, sessionID uint32, proxyMethod string, encryptionMethod byte) { var random [32]byte copy(random[:], ch.random) @@ -43,17 +43,17 @@ func TouchStone(ch *ClientHello, sta *State) (isCK bool, UID []byte, sessionID u if used != 0 { log.Println("Replay! Duplicate random") - return false, nil, 0, "" + return } ticket := ch.extensions[[2]byte{0x00, 0x23}] if len(ticket) < 68 { - return false, nil, 0, "" + return } - UID, sessionID, proxyMethod = decryptSessionTicket(sta.staticPv, ticket) + UID, sessionID, proxyMethod, encryptionMethod = decryptSessionTicket(sta.staticPv, ticket) isCK = validateRandom(ch.random, UID, sta.Now().Unix()) if !isCK { - return false, nil, 0, "" + return } return diff --git a/internal/server/auth_test.go b/internal/server/auth_test.go index 8d0233d..475c5ce 100644 --- a/internal/server/auth_test.go +++ b/internal/server/auth_test.go @@ -16,7 +16,7 @@ func TestDecryptSessionTicket(t *testing.T) { staticPv, _ := ecdh.Unmarshal(pvb) sessionTicket, _ := hex.DecodeString("f586223b50cada583d61dc9bf3d01cc3a45aab4b062ed6a31ead0badb87f7761aab4f9f737a1d8ff2a2aa4d50ceb808844588ee3c8fdf36c33a35ef5003e287337659c8164a7949e9e63623090763fc24d0386c8904e47bdd740e09dd9b395c72de669629c2a865ed581452d23306adf26de0c8a46ee05e3dac876f2bcd9a2de946d319498f579383d06b3e66b3aca05f533fdc5f017eeba45b42080aabd4f71151fa0dfc1b0e23be4ed3abdb47adc0d5740ca7b7689ad34426309fb6984a086") - decryUID, decrySessionID, _ := decryptSessionTicket(staticPv, sessionTicket) + decryUID, decrySessionID, _, _ := decryptSessionTicket(staticPv, sessionTicket) if !bytes.Equal(decryUID, UID) { t.Error( "For", "UID",