From 9fa37e327f38871e997a86574a7c9099f43d3ea3 Mon Sep 17 00:00:00 2001 From: Qian Wang Date: Sat, 3 Aug 2019 22:05:06 +0100 Subject: [PATCH] Use exclusively salsa20 for header encryption --- cmd/ck-client/ck-client.go | 2 +- cmd/ck-server/ck-server.go | 2 +- internal/multiplex/obfs.go | 75 +++++++++++++++++--- internal/multiplex/obfs_test.go | 112 +++++++++++++++++++++++++----- internal/multiplex/stream_test.go | 83 +--------------------- internal/util/util.go | 39 ----------- 6 files changed, 162 insertions(+), 151 deletions(-) diff --git a/cmd/ck-client/ck-client.go b/cmd/ck-client/ck-client.go index 72cf478..b680d2d 100644 --- a/cmd/ck-client/ck-client.go +++ b/cmd/ck-client/ck-client.go @@ -95,7 +95,7 @@ func makeSession(sta *client.State) *mux.Session { wg.Wait() sessionKey := _sessionKey.Load().([]byte) - obfuscator, err := util.GenerateObfs(sta.EncryptionMethod, sessionKey) + obfuscator, err := mux.GenerateObfs(sta.EncryptionMethod, sessionKey) if err != nil { log.Fatal(err) } diff --git a/cmd/ck-server/ck-server.go b/cmd/ck-server/ck-server.go index 1a059f0..7bdf3cf 100644 --- a/cmd/ck-server/ck-server.go +++ b/cmd/ck-server/ck-server.go @@ -101,7 +101,7 @@ func dispatchConnection(conn net.Conn, sta *server.State) { sessionKey := make([]byte, 32) rand.Read(sessionKey) - obfuscator, err := util.GenerateObfs(encryptionMethod, sessionKey) + obfuscator, err := mux.GenerateObfs(encryptionMethod, sessionKey) if err != nil { log.Error(err) goWeb() diff --git a/internal/multiplex/obfs.go b/internal/multiplex/obfs.go index 49cbca5..dd988d6 100644 --- a/internal/multiplex/obfs.go +++ b/internal/multiplex/obfs.go @@ -1,10 +1,13 @@ package multiplex import ( + "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/binary" "errors" + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/salsa20" ) type Obfser func(*Frame) ([]byte, error) @@ -15,9 +18,15 @@ var putU32 = binary.BigEndian.PutUint32 const HEADER_LEN = 12 -func MakeObfs(headerCipher cipher.Block, payloadCipher cipher.AEAD) Obfser { +func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser { + var tagLen int + if payloadCipher == nil { + tagLen = 8 //nonce + } else { + tagLen = payloadCipher.Overhead() + } obfs := func(f *Frame) ([]byte, error) { - ret := make([]byte, 5+HEADER_LEN+len(f.Payload)+16) + ret := make([]byte, 5+HEADER_LEN+len(f.Payload)+tagLen) recordLayer := ret[0:5] header := ret[5 : 5+HEADER_LEN] encryptedPayload := ret[5+HEADER_LEN:] @@ -30,14 +39,14 @@ func MakeObfs(headerCipher cipher.Block, payloadCipher cipher.AEAD) Obfser { if payloadCipher == nil { copy(encryptedPayload, f.Payload) - rand.Read(encryptedPayload[len(encryptedPayload)-16:]) + rand.Read(encryptedPayload[len(encryptedPayload)-tagLen:]) } else { ciphertext := payloadCipher.Seal(nil, header, f.Payload, nil) copy(encryptedPayload, ciphertext) } - iv := encryptedPayload[len(encryptedPayload)-16:] - cipher.NewCTR(headerCipher, iv).XORKeyStream(header, header) + nonce := encryptedPayload[len(encryptedPayload)-8:] + salsa20.XORKeyStream(header, header, nonce, &salsaKey) // Composing final obfsed message // We don't use util.AddRecordLayer here to avoid unnecessary malloc @@ -50,24 +59,30 @@ func MakeObfs(headerCipher cipher.Block, payloadCipher cipher.AEAD) Obfser { return obfs } -func MakeDeobfs(headerCipher cipher.Block, payloadCipher cipher.AEAD) Deobfser { +func MakeDeobfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Deobfser { + var tagLen int + if payloadCipher == nil { + tagLen = 8 // nonce + } else { + tagLen = payloadCipher.Overhead() + } deobfs := func(in []byte) (*Frame, error) { - if len(in) < 5+HEADER_LEN+16 { + if len(in) < 5+HEADER_LEN+tagLen { return nil, errors.New("Input cannot be shorter than 33 bytes") } peeled := in[5:] header := peeled[0:12] payload := peeled[12:] - iv := peeled[len(peeled)-16:] - cipher.NewCTR(headerCipher, iv).XORKeyStream(header, header) + nonce := peeled[len(peeled)-8:] + salsa20.XORKeyStream(header, header, nonce, &salsaKey) streamID := u32(header[0:4]) seq := u32(header[4:8]) closing := header[8] - outputPayload := make([]byte, len(payload)-16) + outputPayload := make([]byte, len(payload)-tagLen) if payloadCipher == nil { copy(outputPayload, payload) @@ -89,3 +104,43 @@ func MakeDeobfs(headerCipher cipher.Block, payloadCipher cipher.AEAD) Deobfser { } return deobfs } + +func GenerateObfs(encryptionMethod byte, sessionKey []byte) (obfuscator *Obfuscator, err error) { + if len(sessionKey) != 32 { + err = errors.New("sessionKey size must be 32 bytes") + } + + blockKey := sessionKey[:16] + var salsaKey [32]byte + copy(salsaKey[:], sessionKey) + + var payloadCipher cipher.AEAD + switch encryptionMethod { + case 0x00: + payloadCipher = nil + case 0x01: + var c cipher.Block + c, err = aes.NewCipher(blockKey) + if err != nil { + return + } + payloadCipher, err = cipher.NewGCM(c) + if err != nil { + return + } + case 0x02: + payloadCipher, err = chacha20poly1305.New(blockKey) + if err != nil { + return + } + default: + return nil, errors.New("Unknown encryption method") + } + + obfuscator = &Obfuscator{ + MakeObfs(salsaKey, payloadCipher), + MakeDeobfs(salsaKey, payloadCipher), + sessionKey, + } + return +} diff --git a/internal/multiplex/obfs_test.go b/internal/multiplex/obfs_test.go index 45ff213..ca3344f 100644 --- a/internal/multiplex/obfs_test.go +++ b/internal/multiplex/obfs_test.go @@ -1,6 +1,10 @@ package multiplex import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "golang.org/x/crypto/chacha20poly1305" "math/rand" "reflect" "testing" @@ -10,26 +14,96 @@ import ( func TestOobfs(t *testing.T) { sessionKey := make([]byte, 32) rand.Read(sessionKey) - obfuscator, err := GenerateObfs(0x01, sessionKey) - if err != nil { - t.Errorf("failed to generate obfuscator %v", err) + + run := func(obfuscator *Obfuscator) { + f := &Frame{} + _testFrame, _ := quick.Value(reflect.TypeOf(f), rand.New(rand.NewSource(42))) + testFrame := _testFrame.Interface().(*Frame) + obfsed, err := obfuscator.Obfs(testFrame) + if err != nil { + t.Error("failed to obfs ", err) + } + + resultFrame, err := obfuscator.Deobfs(obfsed) + if err != nil { + t.Error("failed to deobfs ", err) + } + if !bytes.Equal(testFrame.Payload, resultFrame.Payload) || testFrame.StreamID != resultFrame.StreamID { + t.Error("expecting", testFrame, + "got", resultFrame) + } } - f := &Frame{} - _testFrame, _ := quick.Value(reflect.TypeOf(f), rand.New(rand.NewSource(42))) - testFrame := _testFrame.Interface().(*Frame) - obfsed, err := obfuscator.Obfs(testFrame) - if err != nil { - t.Error("failed to obfs ", err) - } - - resultFrame, err := obfuscator.Deobfs(obfsed) - if err != nil { - t.Error("failed to deobfs ", err) - } - if !reflect.DeepEqual(testFrame, resultFrame) { - t.Error("expecting", testFrame, - "got", resultFrame) - } + t.Run("plain", func(t *testing.T) { + obfuscator, err := GenerateObfs(0x01, sessionKey) + if err != nil { + t.Errorf("failed to generate obfuscator %v", err) + } + run(obfuscator) + }) + t.Run("aes-gcm", func(t *testing.T) { + obfuscator, err := GenerateObfs(0x01, sessionKey) + if err != nil { + t.Errorf("failed to generate obfuscator %v", err) + } + run(obfuscator) + }) + t.Run("chacha20-poly1305", func(t *testing.T) { + obfuscator, err := GenerateObfs(0x01, sessionKey) + if err != nil { + t.Errorf("failed to generate obfuscator %v", err) + } + run(obfuscator) + }) } + +func BenchmarkObfs(b *testing.B) { + testPayload := make([]byte, 1024) + rand.Read(testPayload) + testFrame := &Frame{ + 1, + 0, + 0, + testPayload, + } + + var key [32]byte + rand.Read(key[:]) + b.Run("AES256GCM", func(b *testing.B) { + c, _ := aes.NewCipher(key[:]) + payloadCipher, _ := cipher.NewGCM(c) + + obfs := MakeObfs(key, payloadCipher) + b.ResetTimer() + for i := 0; i < b.N; i++ { + obfs(testFrame) + } + }) + b.Run("AES128GCM", func(b *testing.B) { + c, _ := aes.NewCipher(key[:16]) + payloadCipher, _ := cipher.NewGCM(c) + + obfs := MakeObfs(key, payloadCipher) + b.ResetTimer() + for i := 0; i < b.N; i++ { + obfs(testFrame) + } + }) + b.Run("plain", func(b *testing.B) { + obfs := MakeObfs(key, nil) + b.ResetTimer() + for i := 0; i < b.N; i++ { + obfs(testFrame) + } + }) + b.Run("chacha20Poly1305", func(b *testing.B) { + payloadCipher, _ := chacha20poly1305.New(key[:16]) + + obfs := MakeObfs(key, payloadCipher) + b.ResetTimer() + for i := 0; i < b.N; i++ { + obfs(testFrame) + } + }) +} diff --git a/internal/multiplex/stream_test.go b/internal/multiplex/stream_test.go index 86d4f88..928a9cf 100644 --- a/internal/multiplex/stream_test.go +++ b/internal/multiplex/stream_test.go @@ -3,98 +3,19 @@ package multiplex import ( "bufio" "bytes" - "crypto/aes" - "crypto/cipher" - "encoding/binary" - "errors" - "golang.org/x/crypto/chacha20poly1305" - "io" + "github.com/cbeuw/Cloak/internal/util" "io/ioutil" "math/rand" "net" - "strconv" "testing" "time" ) -// ReadTLS reads TLS data according to its record layer -func ReadTLS(conn net.Conn, buffer []byte) (n int, err error) { - // TCP is a stream. Multiple TLS messages can arrive at the same time, - // a single message can also be segmented due to MTU of the IP layer. - // This function guareentees a single TLS message to be read and everything - // else is left in the buffer. - i, err := io.ReadFull(conn, buffer[:5]) - if err != nil { - return - } - - dataLength := int(binary.BigEndian.Uint16(buffer[3:5])) - if dataLength > len(buffer) { - err = errors.New("Reading TLS message: message size greater than buffer. message size: " + strconv.Itoa(dataLength)) - return - } - left := dataLength - readPtr := 5 - - for left != 0 { - // If left > buffer size (i.e. our message got segmented), the entire MTU is read - // if left = buffer size, the entire buffer is all there left to read - // if left < buffer size (i.e. multiple messages came together), - // only the message we want is read - i, err = io.ReadFull(conn, buffer[readPtr:readPtr+left]) - if err != nil { - return - } - left -= i - readPtr += i - } - - n = 5 + dataLength - return -} - -func GenerateObfs(encryptionMethod byte, sessionKey []byte) (obfuscator *Obfuscator, err error) { - var payloadCipher cipher.AEAD - switch encryptionMethod { - case 0x00: - payloadCipher = nil - case 0x01: - var c cipher.Block - c, err = aes.NewCipher(sessionKey) - if err != nil { - return - } - payloadCipher, err = cipher.NewGCM(c) - if err != nil { - return - } - case 0x02: - payloadCipher, err = chacha20poly1305.New(sessionKey) - if err != nil { - return - } - default: - return nil, errors.New("Unknown encryption method") - } - - headerCipher, err := aes.NewCipher(sessionKey) - if err != nil { - return - } - - obfuscator = &Obfuscator{ - MakeObfs(headerCipher, payloadCipher), - MakeDeobfs(headerCipher, payloadCipher), - sessionKey, - } - return -} - func setupSesh() *Session { sessionKey := make([]byte, 32) rand.Read(sessionKey) obfuscator, _ := GenerateObfs(0x00, sessionKey) - return MakeSession(0, UNLIMITED_VALVE, obfuscator, ReadTLS) + return MakeSession(0, UNLIMITED_VALVE, obfuscator, util.ReadTLS) } type blackhole struct { diff --git a/internal/util/util.go b/internal/util/util.go index 4e82d66..0393775 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -5,8 +5,6 @@ import ( "crypto/cipher" "encoding/binary" "errors" - mux "github.com/cbeuw/Cloak/internal/multiplex" - "golang.org/x/crypto/chacha20poly1305" "io" "net" "strconv" @@ -76,43 +74,6 @@ func ReadTLS(conn net.Conn, buffer []byte) (n int, err error) { return } -func GenerateObfs(encryptionMethod byte, sessionKey []byte) (obfuscator *mux.Obfuscator, err error) { - var payloadCipher cipher.AEAD - switch encryptionMethod { - case 0x00: - payloadCipher = nil - case 0x01: - var c cipher.Block - c, err = aes.NewCipher(sessionKey) - if err != nil { - return - } - payloadCipher, err = cipher.NewGCM(c) - if err != nil { - return - } - case 0x02: - payloadCipher, err = chacha20poly1305.New(sessionKey) - if err != nil { - return - } - default: - return nil, errors.New("Unknown encryption method") - } - - headerCipher, err := aes.NewCipher(sessionKey) - if err != nil { - return - } - - obfuscator = &mux.Obfuscator{ - mux.MakeObfs(headerCipher, payloadCipher), - mux.MakeDeobfs(headerCipher, payloadCipher), - sessionKey, - } - return -} - // AddRecordLayer adds record layer to data func AddRecordLayer(input []byte, typ []byte, ver []byte) []byte { length := make([]byte, 2)