Purge impurity

pull/110/head
Andy Wang 4 years ago
parent bbc0752883
commit 444182f5bb

@ -6,6 +6,7 @@ import (
"encoding/base64"
"flag"
"fmt"
"github.com/cbeuw/Cloak/internal/common"
"net"
"os"
@ -126,7 +127,7 @@ func main() {
}
}
localConfig, remoteConfig, authInfo, err := rawConfig.SplitConfigs()
localConfig, remoteConfig, authInfo, err := rawConfig.SplitConfigs(common.RealWorldState)
if err != nil {
log.Fatal(err)
}

@ -3,16 +3,15 @@ package main
import (
"flag"
"fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/server"
log "github.com/sirupsen/logrus"
"net"
"net/http"
_ "net/http/pprof"
"os"
"runtime"
"strings"
"time"
"github.com/cbeuw/Cloak/internal/server"
log "github.com/sirupsen/logrus"
)
var version string
@ -145,7 +144,7 @@ func main() {
}
}
sta, err := server.InitState(raw, time.Now)
sta, err := server.InitState(raw, common.RealWorldState)
if err != nil {
log.Fatalf("unable to initialise server state: %v", err)
}

@ -1,14 +1,11 @@
package client
import (
"crypto/rand"
"encoding/binary"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/util"
"net"
"time"
log "github.com/sirupsen/logrus"
"net"
)
const appDataMaxLength = 16401
@ -67,7 +64,7 @@ type DirectTLS struct {
// NewClientTransport handles the TLS handshake for a given conn and returns the sessionKey
// if the server proceed with Cloak authentication
func (tls *DirectTLS) Handshake(rawConn net.Conn, authInfo authInfo) (sessionKey [32]byte, err error) {
payload, sharedSecret := makeAuthenticationPayload(authInfo, rand.Reader, time.Now())
payload, sharedSecret := makeAuthenticationPayload(authInfo)
chOnly := tls.browser.composeClientHello(genStegClientHello(payload, authInfo.MockDomain))
chWithRecordLayer := common.AddRecordLayer(chOnly, common.Handshake, common.VersionTLS11)
_, err = rawConn.Write(chWithRecordLayer)

@ -4,8 +4,6 @@ import (
"encoding/binary"
"github.com/cbeuw/Cloak/internal/ecdh"
"github.com/cbeuw/Cloak/internal/util"
"io"
"time"
)
const (
@ -19,7 +17,7 @@ type authenticationPayload struct {
// makeAuthenticationPayload generates the ephemeral key pair, calculates the shared secret, and then compose and
// encrypt the authenticationPayload
func makeAuthenticationPayload(authInfo authInfo, randReader io.Reader, time time.Time) (ret authenticationPayload, sharedSecret [32]byte) {
func makeAuthenticationPayload(authInfo authInfo) (ret authenticationPayload, sharedSecret [32]byte) {
/*
Authentication data:
+----------+----------------+---------------------+-------------+--------------+--------+------------+
@ -28,14 +26,14 @@ func makeAuthenticationPayload(authInfo authInfo, randReader io.Reader, time tim
| 16 bytes | 12 bytes | 1 byte | 8 bytes | 4 bytes | 1 byte | 6 bytes |
+----------+----------------+---------------------+-------------+--------------+--------+------------+
*/
ephPv, ephPub, _ := ecdh.GenerateKey(randReader)
ephPv, ephPub, _ := ecdh.GenerateKey(authInfo.WorldState.Rand)
copy(ret.randPubKey[:], ecdh.Marshal(ephPub))
plaintext := make([]byte, 48)
copy(plaintext, authInfo.UID)
copy(plaintext[16:28], authInfo.ProxyMethod)
plaintext[28] = authInfo.EncryptionMethod
binary.BigEndian.PutUint64(plaintext[29:37], uint64(time.Unix()))
binary.BigEndian.PutUint64(plaintext[29:37], uint64(authInfo.WorldState.Now().Unix()))
binary.BigEndian.PutUint32(plaintext[37:41], authInfo.SessionId)
if authInfo.Unordered {

@ -19,7 +19,7 @@ func MakeSession(connConfig remoteConnConfig, authInfo authInfo, dialer common.D
// sessionID is usergenerated. There shouldn't be a security concern because the scope of
// sessionID is limited to its UID.
quad := make([]byte, 4)
util.CryptoRandRead(quad)
util.RandRead(authInfo.WorldState.Rand, quad)
authInfo.SessionId = binary.BigEndian.Uint32(quad)
} else {
authInfo.SessionId = 0

@ -4,6 +4,7 @@ import (
"crypto"
"encoding/json"
"fmt"
"github.com/cbeuw/Cloak/internal/common"
"io/ioutil"
"net"
"strings"
@ -13,11 +14,11 @@ import (
mux "github.com/cbeuw/Cloak/internal/multiplex"
)
// rawConfig represents the fields in the config json file
// RawConfig represents the fields in the config json file
// nullable means if it's empty, a default value will be chosen in SplitConfigs
// jsonOptional means if the json's empty, its value will be set from environment variables or commandline args
// but it mustn't be empty when SplitConfigs is called
type rawConfig struct {
type RawConfig struct {
ServerName string
ProxyMethod string
EncryptionMethod string
@ -57,6 +58,7 @@ type authInfo struct {
Unordered bool
ServerPubKey crypto.PublicKey
MockDomain string
WorldState common.WorldState
}
// semi-colon separated value. This is for Android plugin options
@ -98,7 +100,7 @@ func ssvToJson(ssv string) (ret []byte) {
return ret
}
func ParseConfig(conf string) (raw *rawConfig, err error) {
func ParseConfig(conf string) (raw *RawConfig, err error) {
var content []byte
// Checking if it's a path to json or a ssv string
if strings.Contains(conf, ";") && strings.Contains(conf, "=") {
@ -110,7 +112,7 @@ func ParseConfig(conf string) (raw *rawConfig, err error) {
}
}
raw = new(rawConfig)
raw = new(RawConfig)
err = json.Unmarshal(content, &raw)
if err != nil {
return
@ -118,7 +120,7 @@ func ParseConfig(conf string) (raw *rawConfig, err error) {
return
}
func (raw *rawConfig) SplitConfigs() (local localConnConfig, remote remoteConnConfig, auth authInfo, err error) {
func (raw *RawConfig) SplitConfigs(worldState common.WorldState) (local localConnConfig, remote remoteConnConfig, auth authInfo, err error) {
nullErr := func(field string) (local localConnConfig, remote remoteConnConfig, auth authInfo, err error) {
err = fmt.Errorf("%v cannot be empty", field)
return
@ -148,6 +150,7 @@ func (raw *rawConfig) SplitConfigs() (local localConnConfig, remote remoteConnCo
return
}
auth.ServerPubKey = pub
auth.WorldState = worldState
// Encryption method
switch strings.ToLower(raw.EncryptionMethod) {

@ -1,6 +1,8 @@
package client
import "net"
import (
"net"
)
type Transport interface {
Handshake(rawConn net.Conn, authInfo authInfo) (sessionKey [32]byte, err error)

@ -1,19 +1,16 @@
package client
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/util"
"github.com/gorilla/websocket"
utls "github.com/refraction-networking/utls"
"net"
"net/http"
"net/url"
"time"
utls "github.com/refraction-networking/utls"
)
type WSOverTLS struct {
@ -37,7 +34,7 @@ func (ws *WSOverTLS) Handshake(rawConn net.Conn, authInfo authInfo) (sessionKey
return sessionKey, fmt.Errorf("failed to parse ws url: %v", err)
}
payload, sharedSecret := makeAuthenticationPayload(authInfo, rand.Reader, time.Now())
payload, sharedSecret := makeAuthenticationPayload(authInfo)
header := http.Header{}
header.Add("hidden", base64.StdEncoding.EncodeToString(append(payload.randPubKey[:], payload.ciphertextWithTag[:]...)))
c, _, err := websocket.NewClient(uconn, u, header, 16480, 16480)

@ -0,0 +1,17 @@
package common
import (
"crypto/rand"
"io"
"time"
)
var RealWorldState = WorldState{
Rand: rand.Reader,
Now: time.Now,
}
type WorldState struct {
Rand io.Reader
Now func() time.Time
}

@ -6,6 +6,9 @@ import (
"fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/ecdh"
"github.com/cbeuw/Cloak/internal/util"
"io"
"math/rand"
"net"
log "github.com/sirupsen/logrus"
@ -39,8 +42,24 @@ func (TLS) processFirstPacket(clientHello []byte, privateKey crypto.PrivateKey)
}
func (TLS) makeResponder(clientHelloSessionId []byte, sharedSecret [32]byte) Responder {
respond := func(originalConn net.Conn, sessionKey [32]byte) (preparedConn net.Conn, err error) {
reply, err := composeReply(clientHelloSessionId, sharedSecret, sessionKey)
respond := func(originalConn net.Conn, sessionKey [32]byte, randSource io.Reader) (preparedConn net.Conn, err error) {
// the cert length needs to be the same for all handshakes belonging to the same session
// we can use sessionKey as a seed here to ensure consistency
possibleCertLengths := []int{42, 27, 68, 59, 36, 44, 46}
rand.Seed(int64(sessionKey[0]))
cert := make([]byte, possibleCertLengths[rand.Intn(len(possibleCertLengths))])
util.RandRead(randSource, cert)
var nonce [12]byte
util.RandRead(randSource, nonce[:])
encryptedSessionKey, err := util.AESGCMEncrypt(nonce[:], sharedSecret[:], sessionKey[:])
if err != nil {
return
}
var encryptedSessionKeyArr [48]byte
copy(encryptedSessionKeyArr[:], encryptedSessionKey)
reply, err := composeReply(clientHelloSessionId, nonce, encryptedSessionKeyArr, cert)
if err != nil {
err = fmt.Errorf("failed to compose TLS reply: %v", err)
return

@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"github.com/cbeuw/Cloak/internal/util"
"math/rand"
)
// ClientHello contains every field in a ClientHello message
@ -162,29 +161,21 @@ func parseClientHello(data []byte) (ret *ClientHello, err error) {
return
}
func composeServerHello(sessionId []byte, sharedSecret [32]byte, sessionKey [32]byte) ([]byte, error) {
nonce := make([]byte, 12)
util.CryptoRandRead(nonce)
encryptedKey, err := util.AESGCMEncrypt(nonce, sharedSecret[:], sessionKey[:]) // 32 + 16 = 48 bytes
if err != nil {
return nil, err
}
func composeServerHello(sessionId []byte, nonce [12]byte, encryptedSessionKeyWithTag [48]byte) ([]byte, error) {
var serverHello [11][]byte
serverHello[0] = []byte{0x02} // handshake type
serverHello[1] = []byte{0x00, 0x00, 0x76} // length 77
serverHello[2] = []byte{0x03, 0x03} // server version
serverHello[3] = append(nonce[0:12], encryptedKey[0:20]...) // random 32 bytes
serverHello[4] = []byte{0x20} // session id length 32
serverHello[5] = sessionId // session id
serverHello[6] = []byte{0xc0, 0x30} // cipher suite TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
serverHello[7] = []byte{0x00} // compression method null
serverHello[8] = []byte{0x00, 0x2e} // extensions length 46
serverHello[0] = []byte{0x02} // handshake type
serverHello[1] = []byte{0x00, 0x00, 0x76} // length 77
serverHello[2] = []byte{0x03, 0x03} // server version
serverHello[3] = append(nonce[0:12], encryptedSessionKeyWithTag[0:20]...) // random 32 bytes
serverHello[4] = []byte{0x20} // session id length 32
serverHello[5] = sessionId // session id
serverHello[6] = []byte{0xc0, 0x30} // cipher suite TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
serverHello[7] = []byte{0x00} // compression method null
serverHello[8] = []byte{0x00, 0x2e} // extensions length 46
keyShare, _ := hex.DecodeString("00330024001d0020")
keyExchange := make([]byte, 32)
copy(keyExchange, encryptedKey[20:48])
copy(keyExchange, encryptedSessionKeyWithTag[20:48])
util.CryptoRandRead(keyExchange[28:32])
serverHello[9] = append(keyShare, keyExchange...)
@ -198,21 +189,15 @@ func composeServerHello(sessionId []byte, sharedSecret [32]byte, sessionKey [32]
// composeReply composes the ServerHello, ChangeCipherSpec and an ApplicationData messages
// together with their respective record layers into one byte slice.
func composeReply(clientHelloSessionId []byte, sharedSecret [32]byte, sessionKey [32]byte) ([]byte, error) {
func composeReply(clientHelloSessionId []byte, nonce [12]byte, encryptedSessionKeyWithTag [48]byte, cert []byte) ([]byte, error) {
TLS12 := []byte{0x03, 0x03}
sh, err := composeServerHello(clientHelloSessionId, sharedSecret, sessionKey)
sh, err := composeServerHello(clientHelloSessionId, nonce, encryptedSessionKeyWithTag)
if err != nil {
return nil, err
}
shBytes := addRecordLayer(sh, []byte{0x16}, TLS12)
ccsBytes := addRecordLayer([]byte{0x01}, []byte{0x14}, TLS12)
// the cert length needs to be the same for all handshakes belonging to the same session
// we can use sessionKey as a seed here to ensure consistency
possibleCertLengths := []int{42, 27, 68, 59, 36, 44, 46}
rand.Seed(int64(sessionKey[0]))
cert := make([]byte, possibleCertLengths[rand.Intn(len(possibleCertLengths))])
util.CryptoRandRead(cert)
encryptedCertBytes := addRecordLayer(cert, []byte{0x17}, TLS12)
ret := append(shBytes, ccsBytes...)
ret = append(ret, encryptedCertBytes...)

@ -34,7 +34,7 @@ var ErrTimestampOutOfWindow = errors.New("timestamp is outside of the accepting
var ErrUnreconisedProtocol = errors.New("unreconised protocol")
// decryptClientInfo checks if a the authFragments are valid. It doesn't check if the UID is authorised
func decryptClientInfo(fragments authFragments, now func() time.Time) (info ClientInfo, err error) {
func decryptClientInfo(fragments authFragments, serverTime time.Time) (info ClientInfo, err error) {
var plaintext []byte
plaintext, err = util.AESGCMDecrypt(fragments.randPubKey[0:12], fragments.sharedSecret[:], fragments.ciphertextWithTag[:])
if err != nil {
@ -51,7 +51,6 @@ func decryptClientInfo(fragments authFragments, now func() time.Time) (info Clie
timestamp := int64(binary.BigEndian.Uint64(plaintext[29:37]))
clientTime := time.Unix(timestamp, 0)
serverTime := now()
if !(clientTime.After(serverTime.Truncate(TIMESTAMP_TOLERANCE)) && clientTime.Before(serverTime.Add(TIMESTAMP_TOLERANCE))) {
err = fmt.Errorf("%v: received timestamp %v", ErrTimestampOutOfWindow, timestamp)
return
@ -89,7 +88,7 @@ func AuthFirstPacket(firstPacket []byte, sta *State) (info ClientInfo, finisher
return
}
info, err = decryptClientInfo(fragments, sta.Now)
info, err = decryptClientInfo(fragments, sta.WorldState.Now())
if err != nil {
log.Debug(err)
err = fmt.Errorf("transport %v in correct format but not Cloak: %v", transport, err)

@ -65,7 +65,7 @@ func DispatchConnection(conn net.Conn, sta *State) {
}
var sessionKey [32]byte
util.CryptoRandRead(sessionKey[:])
util.RandRead(sta.WorldState.Rand, sessionKey[:])
obfuscator, err := mux.MakeObfuscator(ci.EncryptionMethod, sessionKey)
if err != nil {
log.Error(err)
@ -84,7 +84,7 @@ func DispatchConnection(conn net.Conn, sta *State) {
// added to the userinfo database. The distinction between going into the admin mode
// and normal proxy mode is that sessionID needs == 0 for admin mode
if bytes.Equal(ci.UID, sta.AdminUID) && ci.SessionId == 0 {
preparedConn, err := finishHandshake(conn, sessionKey)
preparedConn, err := finishHandshake(conn, sessionKey, sta.WorldState.Rand)
if err != nil {
log.Error(err)
return
@ -125,7 +125,7 @@ func DispatchConnection(conn net.Conn, sta *State) {
}
if existing {
preparedConn, err := finishHandshake(conn, sesh.SessionKey)
preparedConn, err := finishHandshake(conn, sesh.SessionKey, sta.WorldState.Rand)
if err != nil {
log.Error(err)
return
@ -135,7 +135,7 @@ func DispatchConnection(conn net.Conn, sta *State) {
return
}
preparedConn, err := finishHandshake(conn, sessionKey)
preparedConn, err := finishHandshake(conn, sessionKey, sta.WorldState.Rand)
if err != nil {
log.Error(err)
return

@ -34,9 +34,9 @@ type State struct {
ProxyBook map[string]net.Addr
ProxyDialer common.Dialer
Now func() time.Time
AdminUID []byte
Timeout time.Duration
WorldState common.WorldState
AdminUID []byte
Timeout time.Duration
//KeepAlive time.Duration
BypassUID map[[16]byte]struct{}
@ -144,13 +144,13 @@ func ParseConfig(conf string) (raw RawConfig, err error) {
}
// ParseConfig parses the config (either a path to json or the json itself as argument) into a State variable
func InitState(preParse RawConfig, nowFunc func() time.Time) (sta *State, err error) {
func InitState(preParse RawConfig, worldState common.WorldState) (sta *State, err error) {
sta = &State{
Now: nowFunc,
BypassUID: make(map[[16]byte]struct{}),
ProxyBook: map[string]net.Addr{},
usedRandom: map[[32]byte]int64{},
RedirDialer: &net.Dialer{},
WorldState: worldState,
}
if preParse.CncMode {
err = errors.New("command & control mode not implemented")
@ -220,10 +220,10 @@ const CACHE_CLEAN_INTERVAL = 12 * time.Hour
func (sta *State) UsedRandomCleaner() {
for {
time.Sleep(CACHE_CLEAN_INTERVAL)
now := sta.Now()
sta.usedRandomM.Lock()
for key, t := range sta.usedRandom {
if time.Unix(t, 0).Before(now.Add(TIMESTAMP_TOLERANCE)) {
// todo: inpure time
if time.Unix(t, 0).Before(sta.WorldState.Now().Add(TIMESTAMP_TOLERANCE)) {
delete(sta.usedRandom, key)
}
}
@ -234,7 +234,7 @@ func (sta *State) UsedRandomCleaner() {
func (sta *State) registerRandom(r [32]byte) bool {
sta.usedRandomM.Lock()
_, used := sta.usedRandom[r]
sta.usedRandom[r] = sta.Now().Unix()
sta.usedRandom[r] = sta.WorldState.Now().Unix()
sta.usedRandomM.Unlock()
return used
}

@ -3,10 +3,11 @@ package server
import (
"crypto"
"errors"
"io"
"net"
)
type Responder = func(originalConn net.Conn, sessionKey [32]byte) (preparedConn net.Conn, err error)
type Responder = func(originalConn net.Conn, sessionKey [32]byte, randSource io.Reader) (preparedConn net.Conn, err error)
type Transport interface {
processFirstPacket(reqPacket []byte, privateKey crypto.PrivateKey) (authFragments, Responder, error)
}

@ -9,6 +9,7 @@ import (
"fmt"
"github.com/cbeuw/Cloak/internal/ecdh"
"github.com/cbeuw/Cloak/internal/util"
"io"
"net"
"net/http"
)
@ -39,7 +40,7 @@ func (WebSocket) processFirstPacket(reqPacket []byte, privateKey crypto.PrivateK
}
func (WebSocket) makeResponder(reqPacket []byte, sharedSecret [32]byte) Responder {
respond := func(originalConn net.Conn, sessionKey [32]byte) (preparedConn net.Conn, err error) {
respond := func(originalConn net.Conn, sessionKey [32]byte, randSource io.Reader) (preparedConn net.Conn, err error) {
handler := newWsHandshakeHandler()
// For an explanation of the following 3 lines, see the comments in websocketAux.go
@ -48,7 +49,7 @@ func (WebSocket) makeResponder(reqPacket []byte, sharedSecret [32]byte) Responde
<-handler.finished
preparedConn = handler.conn
nonce := make([]byte, 12)
util.CryptoRandRead(nonce)
util.RandRead(randSource, nonce)
// reply: [12 bytes nonce][32 bytes encrypted session key][16 bytes authentication tag]
encryptedKey, err := util.AESGCMEncrypt(nonce, sharedSecret[:], sessionKey[:]) // 32 + 16 = 48 bytes

@ -4,6 +4,7 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"io"
"time"
log "github.com/sirupsen/logrus"
@ -38,7 +39,11 @@ func AESGCMDecrypt(nonce []byte, key []byte, ciphertext []byte) ([]byte, error)
}
func CryptoRandRead(buf []byte) {
_, err := rand.Read(buf)
RandRead(rand.Reader, buf)
}
func RandRead(randSource io.Reader, buf []byte) {
_, err := randSource.Read(buf)
if err == nil {
return
}

Loading…
Cancel
Save