README | 23 ++++++++++++++++++----- govpn.go | 29 +++++++++++++++++++---------- handshake.go | 25 +++++++++++++++++++------ diff --git a/README b/README index 26a6cb46c3ab6a8565c74ae54b06727d33c51eaa..dfef9cbda7c98c12f5d9931a350b2ab87eca9cf5 100644 --- a/README +++ b/README @@ -122,6 +122,7 @@ cases you have to rehandshake again. TECHNICAL INTERNALS +Nonce encryption: XTEA Encryption: Salsa20 Message authentication: Poly1305 Password authenticated key agreement: Curve25519 based DH-EKE @@ -131,12 +132,24 @@ 232 bytes total payload Transport protocol - SERIAL + ENC(KEY, SERIAL, DATA) + AUTH(SERIAL + ENC_DATA) + ENCn(SERIAL) + ENC(KEY, ENCn(SERIAL), DATA) + AUTH(ENCn(SERIAL) + ENC_DATA) + +Each transport message is indistinguishable from pseudo random noise. + +SERIAL is an encrypted message serial number. Odds are reserved for +client(→server) messages, evens for server(→client) messages. + +ENCn is XTEA block cipher algorithm used here as PRP (pseudo random +permutation) to randomize, obfuscate SERIAL. Plaintext SERIAL state is +kept in peers internal state, but encrypted before transmission. XTEA is +compact and fast enough. Salsa20 is PRF function and requires much more +code to create PRP from it. XTEA's encryption key is the first 128-bit +of Salsa20's output with established common key and zero nonce (message +nonces start from 1). -where SERIAL is message serial number. Odds are reserved for -client->server, evens are for server->client. SERIAL is used as a nonce -for DATA encryption: encryption key is different during each handshake, -so (key, nonce) pair is always used once. +Encrypted SERIAL is used as a nonce for DATA encryption: encryption key +is different during each handshake, so (key, nonce) pair is always used +only once. We generate Salsa20's output using this key and nonce for each message: * first 256 bits are used as a one-time key for Poly1305 authentication diff --git a/govpn.go b/govpn.go index 70d48d295a2a363df7a3756b344cb75243823bb4..6f662c60ca208d5d4a8bce9d69ce1dbc0382ae65 100644 --- a/govpn.go +++ b/govpn.go @@ -36,6 +36,7 @@ "time" "golang.org/x/crypto/poly1305" "golang.org/x/crypto/salsa20" + "golang.org/x/crypto/xtea" ) var ( @@ -68,10 +69,11 @@ io.Writer } type Peer struct { - addr *net.UDPAddr - key *[KeySize]byte // encryption key - nonceOur uint64 // nonce for our messages - nonceRecv uint64 // latest received nonce from remote peer + addr *net.UDPAddr + key *[KeySize]byte // encryption key + nonceOur uint64 // nonce for our messages + nonceRecv uint64 // latest received nonce from remote peer + nonceCipher *xtea.Cipher // nonce cipher } type UDPPkt struct { @@ -278,12 +280,6 @@ if peer == nil { udpSinkReady <- true continue } - nonceRecv, _ = binary.Uvarint(udpPktData[:8]) - if nonceRecv < peer.nonceRecv-noncediff { - fmt.Print("R") - udpSinkReady <- true - continue - } copy(buf[:KeySize], emptyKey) copy(tag[:], udpPktData[udpPkt.size-poly1305.TagSize:]) copy(buf[S20BS:], udpPktData[NonceSize:udpPkt.size-poly1305.TagSize]) @@ -299,6 +295,13 @@ udpSinkReady <- true fmt.Print("T") continue } + peer.nonceCipher.Decrypt(buf, udpPktData[:NonceSize]) + nonceRecv, _ = binary.Uvarint(buf[:NonceSize]) + if nonceRecv < peer.nonceRecv-noncediff { + fmt.Print("R") + udpSinkReady <- true + continue + } udpSinkReady <- true peer.nonceRecv = nonceRecv timeouts = 0 @@ -321,8 +324,14 @@ if peer == nil { ethSinkReady <- true continue } + peer.nonceOur = peer.nonceOur + 2 + for i := 0; i < NonceSize; i++ { + nonce[i] = '\x00' + } binary.PutUvarint(nonce, peer.nonceOur) + peer.nonceCipher.Encrypt(nonce, nonce) + copy(buf[:KeySize], emptyKey) if ethPktSize > -1 { copy(buf[S20BS:], ethBuf[:ethPktSize]) diff --git a/handshake.go b/handshake.go index 59afb5dd75c24c6be51f83df5defb5e63b87a07c..9faf9ec9d3abfc91f6c5cfb5a4bf4c282c87c483 100644 --- a/handshake.go +++ b/handshake.go @@ -30,6 +30,7 @@ "golang.org/x/crypto/curve25519" "golang.org/x/crypto/poly1305" "golang.org/x/crypto/salsa20" "golang.org/x/crypto/salsa20/salsa" + "golang.org/x/crypto/xtea" ) type Handshake struct { @@ -50,6 +51,16 @@ for i := 0; i < 32; i++ { k[i] = server[i] ^ client[i] } return k +} + +func NewNonceCipher(key *[32]byte) *xtea.Cipher { + nonceKey := make([]byte, 16) + salsa20.XORKeyStream(nonceKey, make([]byte, 32), make([]byte, 8), key) + ciph, err := xtea.NewCipher(nonceKey) + if err != nil { + panic(err) + } + return ciph } // Check if it is valid handshake-related message @@ -181,11 +192,12 @@ } // Switch peer peer := Peer{ - addr: h.addr, - nonceOur: noncediff + 0, + addr: h.addr, + nonceOur: noncediff + 0, nonceRecv: noncediff + 0, + key: KeyFromSecrets(h.sServer[:], decRs[8+8:]), } - peer.key = KeyFromSecrets(h.sServer[:], decRs[8+8:]) + peer.nonceCipher = NewNonceCipher(peer.key) fmt.Print("[OK]") return &peer default: @@ -252,11 +264,12 @@ } // Switch peer peer := Peer{ - addr: h.addr, - nonceOur: noncediff + 1, + addr: h.addr, + nonceOur: noncediff + 1, nonceRecv: noncediff + 0, + key: KeyFromSecrets(h.sServer[:], h.sClient[:]), } - peer.key = KeyFromSecrets(h.sServer[:], h.sClient[:]) + peer.nonceCipher = NewNonceCipher(peer.key) fmt.Print("[OK]") return &peer default: