Use ECDH instead of ECIES

pull/2/head
Qian Wang 6 years ago
parent b9f2aa4ed0
commit a8786a5576

@ -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)

@ -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)

@ -1,6 +1,6 @@
{
"ServerName":"www.bing.com",
"Key":"UNhY4JhezH9gQYqvDMWrWH9CwlcKiECVqejMrND2VFwEOF8c8XRX8iYVdjKW2BAfym2zppExMPteovDB/Q8phdD53FnH39tQ1daaVLn9+FIGOAdk+UZZ2aOt5jSK638YPg==",
"Key":"UNhY4JhezH9gQYqvDMWrWH9CwlcKiECVqejMrND2VFy2wjljjjqJWGiNoAYpWscJ0VEVkewo6o8S/jcNdNxFLQ==",
"TicketTimeHint":3600,
"NumConn":4,
"MaskBrowser":"chrome"

@ -1,4 +1,4 @@
{
"WebServerAddr":"204.79.197.200:443",
"Key":"H2pMM834RzkouOoRGNhbiQRnm4Ggy8sg+S6ve5yYfqUEOF8c8XRX8iYVdjKW2BAfym2zppExMPteovDB/Q8phdD53FnH39tQ1daaVLn9+FIGOAdk+UZZ2aOt5jSK638YPg=="
"Key":"CN+VRP9OqZR0+Im2X/1y6FvaK7+GBnX6qCiovbo+eVo="
}

@ -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
}

@ -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()
}

@ -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()

@ -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

@ -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

@ -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()
}
}

@ -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])

@ -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
}

Loading…
Cancel
Save