]> Cypherpunks.ru repositories - govpn.git/commitdiff
Obfuscate/randomize message nonces 1.5
authorSergey Matveev <stargrave@stargrave.org>
Fri, 27 Feb 2015 20:52:09 +0000 (23:52 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Mon, 2 Mar 2015 20:16:11 +0000 (23:16 +0300)
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 <stargrave@stargrave.org>
README
govpn.go
handshake.go

diff --git a/README b/README
index 26a6cb46c3ab6a8565c74ae54b06727d33c51eaa..dfef9cbda7c98c12f5d9931a350b2ab87eca9cf5 100644 (file)
--- 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
index 70d48d295a2a363df7a3756b344cb75243823bb4..6f662c60ca208d5d4a8bce9d69ce1dbc0382ae65 100644 (file)
--- 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])
index 59afb5dd75c24c6be51f83df5defb5e63b87a07c..9faf9ec9d3abfc91f6c5cfb5a4bf4c282c87c483 100644 (file)
@@ -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: