diff --git a/cmd/ck-client/ck-client.go b/cmd/ck-client/ck-client.go index b81fd1d..db24b9a 100644 --- a/cmd/ck-client/ck-client.go +++ b/cmd/ck-client/ck-client.go @@ -112,13 +112,7 @@ func main() { log.Printf("Starting standalone mode. Listening for ss on %v:%v\n", localHost, localPort) } - sta := &client.State{ - SS_LOCAL_HOST: localHost, - SS_LOCAL_PORT: localPort, - SS_REMOTE_HOST: remoteHost, - SS_REMOTE_PORT: remotePort, - Now: time.Now, - } + sta := client.InitState(localHost, localPort, remoteHost, remotePort, time.Now) err := sta.ParseConfig(pluginOpts) if err != nil { log.Fatal(err) diff --git a/cmd/ck-server/ck-server.go b/cmd/ck-server/ck-server.go index 59846bf..e9137d7 100644 --- a/cmd/ck-server/ck-server.go +++ b/cmd/ck-server/ck-server.go @@ -19,8 +19,9 @@ var version string func pipe(dst io.ReadWriteCloser, src io.ReadWriteCloser) { for { - i, err := io.Copy(dst, src) - if err != nil || i == 0 { + _, err := io.Copy(dst, src) + if err != nil { + log.Println(err) go dst.Close() go src.Close() return @@ -159,15 +160,7 @@ func main() { localPort = strings.Split(*localAddr, ":")[1] log.Printf("Starting standalone mode, listening on %v:%v to ss at %v:%v\n", remoteHost, remotePort, localHost, localPort) } - sta := &server.State{ - SS_LOCAL_HOST: localHost, - SS_LOCAL_PORT: localPort, - SS_REMOTE_HOST: remoteHost, - SS_REMOTE_PORT: remotePort, - Now: time.Now, - UsedRandom: map[[32]byte]int{}, - Sessions: map[[32]byte]*mux.Session{}, - } + sta := server.InitState(localHost, localPort, remoteHost, remotePort, time.Now) err := sta.ParseConfig(pluginOpts) if err != nil { log.Fatalf("Configuration file error: %v", err) diff --git a/config/ckclient.json b/config/ckclient.json index ee8efe6..4c1a2bc 100644 --- a/config/ckclient.json +++ b/config/ckclient.json @@ -1,6 +1,6 @@ { "ServerName":"www.bing.com", - "Key":"UNhY4JhezH9gQYqvDMWrWH9CwlcKiECVqejMrND2VFwEOF8c8XRX8iYVdjKW2BAfym2zppExMPteovDB/Q8phdD53FnH39tQ1daaVLn9+FIGOAdk+UZZ2aOt5jSK638YPg==", + "Key":"UNhY4JhezH9gQYqvDMWrWH9CwlcKiECVqejMrND2VFy2wjljjjqJWGiNoAYpWscJ0VEVkewo6o8S/jcNdNxFLQ==", "TicketTimeHint":3600, "NumConn":4, "MaskBrowser":"chrome" diff --git a/config/ckserver.json b/config/ckserver.json index bb4f0df..001a8a7 100644 --- a/config/ckserver.json +++ b/config/ckserver.json @@ -1,4 +1,4 @@ { "WebServerAddr":"204.79.197.200:443", - "Key":"H2pMM834RzkouOoRGNhbiQRnm4Ggy8sg+S6ve5yYfqUEOF8c8XRX8iYVdjKW2BAfym2zppExMPteovDB/Q8phdD53FnH39tQ1daaVLn9+FIGOAdk+UZZ2aOt5jSK638YPg==" + "Key":"CN+VRP9OqZR0+Im2X/1y6FvaK7+GBnX6qCiovbo+eVo=" } diff --git a/internal/client/auth.go b/internal/client/auth.go index 67fa4d7..d5c74f7 100644 --- a/internal/client/auth.go +++ b/internal/client/auth.go @@ -1,59 +1,57 @@ package client import ( + "crypto" "crypto/rand" "crypto/sha256" "encoding/binary" "github.com/cbeuw/Cloak/internal/util" - "github.com/cbeuw/ecies" + ecdh "github.com/cbeuw/go-ecdh" + "io" ) -func MakeRandomField(sta *State) []byte { +type keyPair struct { + crypto.PrivateKey + crypto.PublicKey +} +func MakeRandomField(sta *State) []byte { t := make([]byte, 8) - binary.BigEndian.PutUint64(t, uint64(sta.Now().Unix()/12*60*60)) - rand := util.PsudoRandBytes(16, sta.Now().UnixNano()) + binary.BigEndian.PutUint64(t, uint64(sta.Now().Unix()/(12*60*60))) + rdm := make([]byte, 16) + io.ReadFull(rand.Reader, rdm) preHash := make([]byte, 56) copy(preHash[0:32], sta.SID) copy(preHash[32:40], t) - copy(preHash[40:56], rand) + copy(preHash[40:56], rdm) h := sha256.New() h.Write(preHash) ret := make([]byte, 32) - copy(ret[0:16], rand) + copy(ret[0:16], rdm) copy(ret[16:32], h.Sum(nil)[0:16]) return ret } func MakeSessionTicket(sta *State) []byte { - t := make([]byte, 8) - binary.BigEndian.PutUint64(t, uint64(sta.Now().Unix()/int64(sta.TicketTimeHint))) - plain := make([]byte, 40) - copy(plain, sta.SID) - copy(plain[32:], t) - // With the default settings (P256, AES128, SHA256) of the ecies package, len(ct)==153. - // - // ciphertext is composed of 3 parts: marshalled X and Y coordinates on the curve, - // iv+ciphertext of the block cipher (aes128 in this case), - // and the hmac which is 32 bytes because it's sha256 - // - // The marshalling is done by crypto/elliptic.Marshal. According to the code, - // the size after marshall is 65 - // - // IV is 16 bytes. The size of ciphertext is equal to the plaintext, which is 40, - // that is 32 bytes of SID + 8 bytes of timestamp/tickettimehint. - // 16+40 = 56 - // - // Then the hmac is 32 bytes - // - // 65+56+32=153 - ct, _ := ecies.Encrypt(rand.Reader, sta.pub, plain, nil, nil) - sessionTicket := make([]byte, 192) - // The reason for ct[1:] is that, the first byte of ct is always 0x04 - // This is specified in the section 4.3.6 of ANSI X9.62 (the uncompressed form). - // This is a flag that is useless to us and it will expose our pattern - // (because the sessionTicket isn't fully random anymore). Therefore we drop it. - copy(sessionTicket, ct[1:]) - copy(sessionTicket[152:], util.PsudoRandBytes(40, sta.Now().UnixNano())) - return sessionTicket + // sessionTicket: [marshalled ephemeral pub key 32 bytes][encrypted SID 32 bytes][padding 128 bytes] + // The first 16 bytes of the marshalled ephemeral public key is used as the IV + // for encrypting the SID + tthInterval := sta.Now().Unix() / int64(sta.TicketTimeHint) + ec := ecdh.NewCurve25519ECDH() + ephKP := sta.getKeyPair(tthInterval) + if ephKP == nil { + ephPv, ephPub, _ := ec.GenerateKey(rand.Reader) + ephKP = &keyPair{ + ephPv, + ephPub, + } + sta.putKeyPair(tthInterval, ephKP) + } + ticket := make([]byte, 192) + copy(ticket[0:32], ec.Marshal(ephKP.PublicKey)) + key, _ := ec.GenerateSharedSecret(ephKP.PrivateKey, sta.staticPub) + cipherSID := util.AESEncrypt(ticket[0:16], key, sta.SID) + copy(ticket[32:64], cipherSID) + io.ReadFull(rand.Reader, ticket[64:192]) + return ticket } diff --git a/internal/client/state.go b/internal/client/state.go index 1c6a963..242bbc4 100644 --- a/internal/client/state.go +++ b/internal/client/state.go @@ -1,14 +1,16 @@ package client import ( - "crypto/elliptic" + "crypto" "encoding/base64" "encoding/json" "errors" - "github.com/cbeuw/ecies" "io/ioutil" "strings" + "sync" "time" + + ecdh "github.com/cbeuw/go-ecdh" ) type rawConfig struct { @@ -25,15 +27,31 @@ type State struct { SS_LOCAL_PORT string SS_REMOTE_HOST string SS_REMOTE_PORT string - Now func() time.Time - SID []byte - pub *ecies.PublicKey + + Now func() time.Time + SID []byte + staticPub crypto.PublicKey + keyPairsM sync.RWMutex + keyPairs map[int64]*keyPair + TicketTimeHint int ServerName string MaskBrowser string NumConn int } +func InitState(localHost, localPort, remoteHost, remotePort string, nowFunc func() time.Time) *State { + ret := &State{ + SS_LOCAL_HOST: localHost, + SS_LOCAL_PORT: localPort, + SS_REMOTE_HOST: remoteHost, + SS_REMOTE_PORT: remotePort, + Now: nowFunc, + } + ret.keyPairs = make(map[int64]*keyPair) + return ret +} + // semi-colon separated value. This is for Android plugin options func ssvToJson(ssv string) (ret []byte) { unescape := func(s string) string { @@ -89,24 +107,29 @@ func (sta *State) ParseConfig(conf string) (err error) { return errors.New("Failed to parse Key: " + err.Error()) } sta.SID = sid - sta.pub = pub + sta.staticPub = pub return nil } -// Structure: [SID 32 bytes][marshalled public key] -func parseKey(b64 string) ([]byte, *ecies.PublicKey, error) { +// Structure: [SID 32 bytes][marshalled public key 32 bytes] +func parseKey(b64 string) ([]byte, crypto.PublicKey, error) { b, err := base64.StdEncoding.DecodeString(b64) if err != nil { return nil, nil, err } - sid := b[0:32] - marshalled := b[32:] - x, y := elliptic.Unmarshal(ecies.DefaultCurve, marshalled) - pub := &ecies.PublicKey{ - X: x, - Y: y, - Curve: ecies.DefaultCurve, - Params: ecies.ParamsFromCurve(ecies.DefaultCurve), - } - return sid, pub, nil + ec := ecdh.NewCurve25519ECDH() + pub, _ := ec.Unmarshal(b[32:64]) + return b[0:32], pub, nil +} + +func (sta *State) getKeyPair(tthInterval int64) *keyPair { + sta.keyPairsM.Lock() + defer sta.keyPairsM.Unlock() + return sta.keyPairs[tthInterval] +} + +func (sta *State) putKeyPair(tthInterval int64, kp *keyPair) { + sta.keyPairsM.Lock() + sta.keyPairs[tthInterval] = kp + sta.keyPairsM.Unlock() } diff --git a/internal/multiplex/stream.go b/internal/multiplex/stream.go index 419dd02..272975c 100644 --- a/internal/multiplex/stream.go +++ b/internal/multiplex/stream.go @@ -3,6 +3,7 @@ package multiplex import ( "errors" "io" + "log" "sync" ) @@ -70,6 +71,7 @@ func (stream *Stream) Read(buf []byte) (n int, err error) { func (stream *Stream) Write(in []byte) (n int, err error) { select { case <-stream.die: + log.Printf("Stream %v dying\n", stream.id) return 0, errors.New(errBrokenPipe) default: } @@ -100,6 +102,8 @@ func (stream *Stream) Write(in []byte) (n int, err error) { } func (stream *Stream) Close() error { + log.Printf("ID: %v closing\n", stream.id) + // Because closing a closed channel causes panic stream.closingM.Lock() defer stream.closingM.Unlock() diff --git a/internal/multiplex/switchboard.go b/internal/multiplex/switchboard.go index dfe1799..cf9449a 100644 --- a/internal/multiplex/switchboard.go +++ b/internal/multiplex/switchboard.go @@ -79,7 +79,10 @@ type sentNotifier struct { func (ce *connEnclave) send(data []byte) { // TODO: error handling - n, _ := ce.remoteConn.Write(data) + n, err := ce.remoteConn.Write(data) + if err != nil { + log.Println(err) + } sn := &sentNotifier{ ce, n, @@ -121,7 +124,7 @@ func (sb *switchboard) dispatch() { } func (sb *switchboard) deplex(ce *connEnclave) { - buf := make([]byte, 20480) + buf := make([]byte, 204800) for { i, err := sb.session.obfsedReader(ce.remoteConn, buf) if err != nil { @@ -136,6 +139,7 @@ func (sb *switchboard) deplex(ce *connEnclave) { stream = sb.session.addStream(frame.StreamID) } if closing := sb.session.getStream(frame.ClosingStreamID); closing != nil { + log.Printf("HeaderClosing: %v\n", frame.ClosingStreamID) closing.Close() } stream.newFrameCh <- frame diff --git a/internal/server/auth.go b/internal/server/auth.go index 7a3c2d6..a06d4f1 100644 --- a/internal/server/auth.go +++ b/internal/server/auth.go @@ -2,32 +2,35 @@ package server import ( "bytes" + "crypto" "crypto/sha256" "encoding/binary" - "github.com/cbeuw/ecies" "log" + + "github.com/cbeuw/Cloak/internal/util" + ecdh "github.com/cbeuw/go-ecdh" ) // input ticket, return SID -func decryptSessionTicket(pv *ecies.PrivateKey, ticket []byte) ([]byte, error) { - ciphertext := make([]byte, 153) - ciphertext[0] = 0x04 - copy(ciphertext[1:], ticket) - plaintext, err := pv.Decrypt(ciphertext, nil, nil) +func decryptSessionTicket(staticPv crypto.PrivateKey, ticket []byte) ([]byte, error) { + ec := ecdh.NewCurve25519ECDH() + ephPub, _ := ec.Unmarshal(ticket[0:32]) + key, err := ec.GenerateSharedSecret(staticPv, ephPub) if err != nil { return nil, err } - return plaintext[0:32], nil + SID := util.AESDecrypt(ticket[0:16], key, ticket[32:64]) + return SID, nil } func validateRandom(random []byte, SID []byte, time int64) bool { t := make([]byte, 8) - binary.BigEndian.PutUint64(t, uint64(time/12*60*60)) - rand := random[0:16] + binary.BigEndian.PutUint64(t, uint64(time/(12*60*60))) + rdm := random[0:16] preHash := make([]byte, 56) copy(preHash[0:32], SID) copy(preHash[32:40], t) - copy(preHash[40:56], rand) + copy(preHash[40:56], rdm) h := sha256.New() h.Write(preHash) return bytes.Equal(h.Sum(nil)[0:16], random[16:32]) @@ -42,10 +45,12 @@ func TouchStone(ch *ClientHello, sta *State) (bool, []byte) { } sta.putUsedRandom(random) - SID, err := decryptSessionTicket(sta.pv, ch.extensions[[2]byte{0x00, 0x23}]) + SID, err := decryptSessionTicket(sta.staticPv, ch.extensions[[2]byte{0x00, 0x23}]) if err != nil { + log.Printf("ts: %v\n", err) return false, nil } + log.Printf("SID: %x\n", SID) isSS := validateRandom(ch.random, SID, sta.Now().Unix()) if !isSS { return false, nil diff --git a/internal/server/state.go b/internal/server/state.go index 2f3aabe..8f564bb 100644 --- a/internal/server/state.go +++ b/internal/server/state.go @@ -1,17 +1,15 @@ package server import ( - "crypto/elliptic" + "crypto" "encoding/base64" "encoding/json" "io/ioutil" - "math/big" "strings" "sync" "time" mux "github.com/cbeuw/Cloak/internal/multiplex" - "github.com/cbeuw/ecies" ) type rawConfig struct { @@ -26,18 +24,32 @@ type stateManager interface { // State type stores the global state of the program type State struct { - WebServerAddr string - Now func() time.Time SS_LOCAL_HOST string SS_LOCAL_PORT string SS_REMOTE_HOST string SS_REMOTE_PORT string - UsedRandomM sync.RWMutex - UsedRandom map[[32]byte]int - pv *ecies.PrivateKey - SessionsM sync.RWMutex - Sessions map[[32]byte]*mux.Session + Now func() time.Time + staticPv crypto.PrivateKey + usedRandomM sync.RWMutex + usedRandom map[[32]byte]int + sessionsM sync.RWMutex + sessions map[[32]byte]*mux.Session + + WebServerAddr string +} + +func InitState(localHost, localPort, remoteHost, remotePort string, nowFunc func() time.Time) *State { + ret := &State{ + SS_LOCAL_HOST: localHost, + SS_LOCAL_PORT: localPort, + SS_REMOTE_HOST: remoteHost, + SS_REMOTE_PORT: remotePort, + Now: nowFunc, + } + ret.usedRandom = make(map[[32]byte]int) + ret.sessions = make(map[[32]byte]*mux.Session) + return ret } // semi-colon separated value. @@ -65,28 +77,15 @@ func ssvToJson(ssv string) (ret []byte) { return ret } -// Structue: [D 32 bytes][marshalled public key] -func parseKey(b64 string) (*ecies.PrivateKey, error) { +// base64 encoded 32 byte private key +func parseKey(b64 string) (crypto.PrivateKey, error) { b, err := base64.StdEncoding.DecodeString(b64) if err != nil { return nil, err } - - d := b[0:32] - marshalled := b[32:] - x, y := elliptic.Unmarshal(ecies.DefaultCurve, marshalled) - pub := ecies.PublicKey{ - X: x, - Y: y, - Curve: ecies.DefaultCurve, - Params: ecies.ParamsFromCurve(ecies.DefaultCurve), - } - - pv := &ecies.PrivateKey{ - PublicKey: pub, - D: new(big.Int).SetBytes(d), - } - return pv, nil + var pv [32]byte + copy(pv[:], b) + return &pv, nil } // ParseConfig parses the config (either a path to json or in-line ssv config) into a State variable @@ -109,14 +108,17 @@ func (sta *State) ParseConfig(conf string) (err error) { sta.WebServerAddr = preParse.WebServerAddr pv, err := parseKey(preParse.Key) - sta.pv = pv + if err != nil { + return err + } + sta.staticPv = pv return nil } func (sta *State) GetSession(SID [32]byte) *mux.Session { - sta.SessionsM.Lock() - defer sta.SessionsM.Unlock() - if sesh, ok := sta.Sessions[SID]; ok { + sta.sessionsM.Lock() + defer sta.sessionsM.Unlock() + if sesh, ok := sta.sessions[SID]; ok { return sesh } else { return nil @@ -124,23 +126,23 @@ func (sta *State) GetSession(SID [32]byte) *mux.Session { } func (sta *State) PutSession(SID [32]byte, sesh *mux.Session) { - sta.SessionsM.Lock() - sta.Sessions[SID] = sesh - sta.SessionsM.Unlock() + sta.sessionsM.Lock() + sta.sessions[SID] = sesh + sta.sessionsM.Unlock() } func (sta *State) getUsedRandom(random [32]byte) int { - sta.UsedRandomM.Lock() - defer sta.UsedRandomM.Unlock() - return sta.UsedRandom[random] + sta.usedRandomM.Lock() + defer sta.usedRandomM.Unlock() + return sta.usedRandom[random] } -// PutUsedRandom adds a random field into map UsedRandom +// PutUsedRandom adds a random field into map usedRandom func (sta *State) putUsedRandom(random [32]byte) { - sta.UsedRandomM.Lock() - sta.UsedRandom[random] = int(sta.Now().Unix()) - sta.UsedRandomM.Unlock() + sta.usedRandomM.Lock() + sta.usedRandom[random] = int(sta.Now().Unix()) + sta.usedRandomM.Unlock() } // UsedRandomCleaner clears the cache of used random fields every 12 hours @@ -148,12 +150,12 @@ func (sta *State) UsedRandomCleaner() { for { time.Sleep(12 * time.Hour) now := int(sta.Now().Unix()) - sta.UsedRandomM.Lock() - for key, t := range sta.UsedRandom { + sta.usedRandomM.Lock() + for key, t := range sta.usedRandom { if now-t > 12*3600 { - delete(sta.UsedRandom, key) + delete(sta.usedRandom, key) } } - sta.UsedRandomM.Unlock() + sta.usedRandomM.Unlock() } } diff --git a/internal/util/obfs.go b/internal/util/obfs.go index 85abbcf..2232945 100644 --- a/internal/util/obfs.go +++ b/internal/util/obfs.go @@ -3,12 +3,14 @@ package util import ( "crypto/aes" "crypto/cipher" + "crypto/rand" "encoding/binary" + "io" mux "github.com/cbeuw/Cloak/internal/multiplex" ) -func encrypt(iv []byte, key []byte, plaintext []byte) []byte { +func AESEncrypt(iv []byte, key []byte, plaintext []byte) []byte { block, _ := aes.NewCipher(key) ciphertext := make([]byte, len(plaintext)) stream := cipher.NewCTR(block, iv) @@ -16,7 +18,7 @@ func encrypt(iv []byte, key []byte, plaintext []byte) []byte { return ciphertext } -func decrypt(iv []byte, key []byte, ciphertext []byte) []byte { +func AESDecrypt(iv []byte, key []byte, ciphertext []byte) []byte { ret := make([]byte, len(ciphertext)) copy(ret, ciphertext) // Because XORKeyStream is inplace, but we don't want the input to be changed block, _ := aes.NewCipher(key) @@ -32,8 +34,9 @@ func MakeObfs(key []byte) func(*mux.Frame) []byte { binary.BigEndian.PutUint32(header[4:8], f.Seq) binary.BigEndian.PutUint32(header[8:12], f.ClosingStreamID) // header: [StreamID 4 bytes][Seq 4 bytes][ClosingStreamID 4 bytes] - iv := CryptoRandBytes(16) - cipherheader := encrypt(iv, key, header) + iv := make([]byte, 16) + io.ReadFull(rand.Reader, iv) + cipherheader := AESEncrypt(iv, key, header) obfsed := make([]byte, len(f.Payload)+12+16) copy(obfsed[0:16], iv) copy(obfsed[16:28], cipherheader) @@ -48,7 +51,7 @@ func MakeObfs(key []byte) func(*mux.Frame) []byte { func MakeDeobfs(key []byte) func([]byte) *mux.Frame { deobfs := func(in []byte) *mux.Frame { peeled := PeelRecordLayer(in) - header := decrypt(peeled[0:16], key, peeled[16:28]) + header := AESDecrypt(peeled[0:16], key, peeled[16:28]) streamID := binary.BigEndian.Uint32(header[0:4]) seq := binary.BigEndian.Uint32(header[4:8]) closingStreamID := binary.BigEndian.Uint32(header[8:12]) diff --git a/internal/util/util.go b/internal/util/util.go index bac43ed..4960e79 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,11 +1,9 @@ package util import ( - "crypto/rand" "encoding/binary" "errors" "io" - "math/big" prand "math/rand" "net" "time" @@ -24,18 +22,6 @@ func BtoInt(b []byte) int { return int(sum) } -// CryptoRandBytes generates a byte slice filled with cryptographically secure random bytes -func CryptoRandBytes(length int) []byte { - byteMax := big.NewInt(int64(256)) - ret := make([]byte, length) - for i := 0; i < length; i++ { - randInt, _ := rand.Int(rand.Reader, byteMax) - randByte := byte(randInt.Int64()) - ret[i] = randByte - } - return ret -} - // PsudoRandBytes returns a byte slice filled with psudorandom bytes generated by the seed func PsudoRandBytes(length int, seed int64) []byte { prand.Seed(seed) @@ -65,7 +51,7 @@ func ReadTillDrain(conn net.Conn, buffer []byte) (n int, err error) { conn.SetReadDeadline(time.Now().Add(3 * time.Second)) for left != 0 { if readPtr > len(buffer) || readPtr+left > len(buffer) { - err = errors.New("Reading TLS message: actual size greater than header's specification") + err = errors.New("Reading TLS message: message size greater than buffer") return } // If left > buffer size (i.e. our message got segmented), the entire MTU is read @@ -82,7 +68,6 @@ func ReadTillDrain(conn net.Conn, buffer []byte) (n int, err error) { conn.SetReadDeadline(time.Time{}) n = 5 + dataLength - buffer = buffer[:n] return }