From 8f627a4324b97e976b6ae1aff4cd62036a7cb8bc Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Fri, 27 Feb 2015 23:52:09 +0300 Subject: [PATCH] Obfuscate/randomize message nonces Nonce is directly written inside transport messages and it is the only part that is different from randomness (because it does not require it actually). One can use them as GoVPN's traffic fingerprint. Apply simple PRP function on the nonce before it's usage. Internal counters are keeped in the state, but encrypted and decrypted during actual use using XTEA algorithm. It is rather simple, fast enough, simplier than applying Luby-Rackoff to make PRP from Salsa20. Signed-off-by: Sergey Matveev --- README | 23 ++++++++++++++++++----- govpn.go | 29 +++++++++++++++++++---------- handshake.go | 25 +++++++++++++++++++------ 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/README b/README index 26a6cb4..dfef9cb 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 @@ Handshake overhead: 4 UDP (2 from client, 2 from server) packets, Transport protocol - SERIAL + ENC(KEY, SERIAL, DATA) + AUTH(SERIAL + ENC_DATA) + ENCn(SERIAL) + ENC(KEY, ENCn(SERIAL), DATA) + AUTH(ENCn(SERIAL) + ENC_DATA) -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. +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). + +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 70d48d2..6f662c6 100644 --- a/govpn.go +++ b/govpn.go @@ -36,6 +36,7 @@ import ( "golang.org/x/crypto/poly1305" "golang.org/x/crypto/salsa20" + "golang.org/x/crypto/xtea" ) var ( @@ -68,10 +69,11 @@ type TAP interface { } 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 @@ func main() { 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 @@ func main() { 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 @@ func main() { 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 59afb5d..9faf9ec 100644 --- a/handshake.go +++ b/handshake.go @@ -30,6 +30,7 @@ import ( "golang.org/x/crypto/poly1305" "golang.org/x/crypto/salsa20" "golang.org/x/crypto/salsa20/salsa" + "golang.org/x/crypto/xtea" ) type Handshake struct { @@ -52,6 +53,16 @@ func KeyFromSecrets(server, client []byte) *[32]byte { 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 // Minimal size and last 16 zero bytes func isValidHandshakePkt(pkt []byte) bool { @@ -181,11 +192,12 @@ func (h *Handshake) Server(noncediff uint64, conn *net.UDPConn, key *[32]byte, d // 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 @@ func (h *Handshake) Client(noncediff uint64, conn *net.UDPConn, key *[32]byte, d // 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: -- 2.44.0