From b97d37d9494cf064a50d30b4936393eeab32b0e1 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Mon, 9 May 2016 17:02:19 +0300 Subject: [PATCH] Replace XTEA with BLAKE2b-based MAC --- doc/developer.texi | 6 +- doc/encless.texi | 7 - doc/handshake.texi | 8 +- doc/news.ru.texi | 3 + doc/news.texi | 3 + doc/transport.texi | 17 +- .../govpn/cmd/govpn-client/main.go | 4 +- .../govpn/cmd/govpn-client/tcp.go | 5 +- .../govpn/cmd/govpn-server/conf.go | 4 +- .../govpn/cmd/govpn-server/tcp.go | 5 +- src/cypherpunks.ru/govpn/handshake.go | 54 ++---- src/cypherpunks.ru/govpn/identity.go | 70 +++---- src/cypherpunks.ru/govpn/peer.go | 174 ++++++++++-------- src/cypherpunks.ru/govpn/peer_test.go | 22 ++- 14 files changed, 193 insertions(+), 189 deletions(-) diff --git a/doc/developer.texi b/doc/developer.texi index 4293f80..b32f1cb 100644 --- a/doc/developer.texi +++ b/doc/developer.texi @@ -4,8 +4,8 @@ Pay attention how to get @ref{Sources, development source code}. @table @asis -@item Nonce and identity encryption - @url{http://www.cix.co.uk/~klockstone/xtea.pdf, XTEA}. +@item Nonce and identity obfuscation + @url{https://blake2.net/, BLAKE2b-MAC}. @item Data encryption @url{http://cr.yp.to/snuffle.html, Salsa20}. @item Message authentication @@ -24,7 +24,7 @@ Pay attention how to get @ref{Sources, development source code}. @url{http://theory.lcs.mit.edu/~cis/pubs/rivest/fusion.ps, All-Or-Nothing-Transformed} (based on @url{http://cseweb.ucsd.edu/~mihir/papers/oaep.html, OAEP} using - Salsa20 with @url{https://blake2.net/, BLAKE2b-256} based + Salsa20 with BLAKE2b-256 based @url{http://crypto.stanford.edu/~dabo/abstracts/saep.html, SAEP+} checksums) data with 128-bits of feeded random. @item Packet overhead diff --git a/doc/encless.texi b/doc/encless.texi index 6d44191..6d694b2 100644 --- a/doc/encless.texi +++ b/doc/encless.texi @@ -25,12 +25,5 @@ signature algorithms. No encryption and steganography involved. In this mode each outgoing packet became larger on 4128 bytes and @ref{Noise, noise} is forcefully enabled. So this is resource hungry mode! -@strong{Beware}: by default packet serial numbers are still processed -through the XTEA encryption. It is not required for confidentiality and -security, but for randomizing some parts of the traffic to make it -indistinguishable from the noise, for making it more DPI-proof. It -safely can be disabled, turned off or maybe its keys even can be -revealed without security and forward secrecy loss. - See @code{govpn/cnw} and @code{govpn/aont} packages for details of AONT and chaffing operations. diff --git a/doc/handshake.texi b/doc/handshake.texi index f19fde0..75a6508 100644 --- a/doc/handshake.texi +++ b/doc/handshake.texi @@ -3,10 +3,10 @@ @verbatiminclude handshake.utxt -Each handshake message ends with so called @code{IDtag}: it is an XTEA -encrypted first 64 bits of each message with client's @ref{Identity} as -a key. It is used to transmit identity and to mark packet as handshake -message. +Each handshake message ends with so called @code{IDtag}: it is +BLAKE2b-MAC of the first 64 bits of the handshake message, with client's +@ref{Identity} used as a key. It is used to transmit identity and to +mark packet as handshake message. If @ref{Noise, noise} is enabled, then data is padded to fill up packet to MTU's size. diff --git a/doc/news.ru.texi b/doc/news.ru.texi index da02168..a654dbf 100644 --- a/doc/news.ru.texi +++ b/doc/news.ru.texi @@ -7,6 +7,9 @@ @item Опциональная возможность использовать syslog для журналирования, с @url{https://tools.ietf.org/html/rfc5424, RFC 5424}-похожими структурированными записями. +@item XTEA алгоритм больше не используется для обфускации nonce. Вместо +него BLAKE2b-MAC. Теперь нешифрованный режим действительно не зависит +от алгоритмов шифрования. @end itemize @node Релиз 5.7 diff --git a/doc/news.texi b/doc/news.texi index cdb7d98..70e5051 100644 --- a/doc/news.texi +++ b/doc/news.texi @@ -9,6 +9,9 @@ See also this page @ref{Новости, on russian}. @item Optional ability to use syslog for logging, with @url{https://tools.ietf.org/html/rfc5424, RFC 5424}-like structured records. +@item XTEA algorithm is not used anymore for nonce obfuscation. +BLAKE2-MAC is used instead. Encryptionless mode now really does not +depend on encryption functions. @end itemize @node Release 5.7 diff --git a/doc/transport.texi b/doc/transport.texi index 4b8413b..cdb5950 100644 --- a/doc/transport.texi +++ b/doc/transport.texi @@ -21,23 +21,18 @@ TAG || ENCRYPTED || NONCE --> PACKET | | | +--< DATA || PAD [|| ZEROS] | - +--< PRP(PRP_KEY, SERIAL) + +--< MAC(MAC_KEY, SERIAL) @end verbatim @code{SERIAL} is message's serial number. Odds are reserved for client (to server) messages, evens for server (to client) messages. -@code{PRP} is XTEA block cipher algorithm used here as PRP (pseudo -random permutation function) to obfuscate @code{SERIAL}. Plaintext -@code{SERIAL} state is kept in peers internal state, but encrypted -before transmission. - -XTEA's encryption key @code{PRP_KEY} is the first 128-bit of Salsa20's -output with established common key and zero nonce (message nonces start -from 1). +@code{MAC} is BLAKE2b-MAC used to obfuscate @code{SERIAL}. MAC's key +@code{MAC_KEY} is the first 256-bit of Salsa20's output with established +common key and zero nonce (message nonces start from 1). @verbatim -PRP_KEY = 128bit(ENCRYPT(KEY, 0)) +MAC_KEY = 256bit(ENCRYPT(KEY, 0)) @end verbatim @code{ENCRYPT} is Salsa20 stream cipher, with established session @@ -63,7 +58,7 @@ In @ref{Encless, encryptionless mode} this scheme is slightly different: @verbatim PACKET = ENCODED || NONCE ENCODED = ENCLESS(DATA || PAD || ZEROS) - NONCE = PRP(PRP_KEY, SERIAL) + NONCE = MAC(MAC_KEY, SERIAL) @end verbatim @code{ENCLESS} is AONT and chaffing function. There is no need in diff --git a/src/cypherpunks.ru/govpn/cmd/govpn-client/main.go b/src/cypherpunks.ru/govpn/cmd/govpn-client/main.go index 38bdae3..3d8cbe6 100644 --- a/src/cypherpunks.ru/govpn/cmd/govpn-client/main.go +++ b/src/cypherpunks.ru/govpn/cmd/govpn-client/main.go @@ -57,7 +57,7 @@ var ( timeout int firstUpCall bool = true knownPeers govpn.KnownPeers - idsCache *govpn.CipherCache + idsCache *govpn.MACCache ) func main() { @@ -108,7 +108,7 @@ func main() { Verifier: verifier, DSAPriv: priv, } - idsCache = govpn.NewCipherCache() + idsCache = govpn.NewMACCache() confs := map[govpn.PeerId]*govpn.PeerConf{*verifier.Id: conf} idsCache.Update(&confs) log.Println(govpn.VersionGet()) diff --git a/src/cypherpunks.ru/govpn/cmd/govpn-client/tcp.go b/src/cypherpunks.ru/govpn/cmd/govpn-client/tcp.go index dc0eb60..57a412d 100644 --- a/src/cypherpunks.ru/govpn/cmd/govpn-client/tcp.go +++ b/src/cypherpunks.ru/govpn/cmd/govpn-client/tcp.go @@ -98,8 +98,6 @@ HandshakeCycle: return } - nonceExpectation := make([]byte, govpn.NonceSize) - peer.NonceExpectation(nonceExpectation) prev = 0 var i int TransportCycle: @@ -126,7 +124,7 @@ TransportCycle: if prev < govpn.MinPktLength { continue } - i = bytes.Index(buf[:prev], nonceExpectation) + i = bytes.Index(buf[:prev], peer.NonceExpect) if i == -1 { continue } @@ -140,7 +138,6 @@ TransportCycle: rehandshaking <- struct{}{} break TransportCycle } - peer.NonceExpectation(nonceExpectation) copy(buf, buf[i+govpn.NonceSize:prev]) prev = prev - i - govpn.NonceSize goto CheckMore diff --git a/src/cypherpunks.ru/govpn/cmd/govpn-server/conf.go b/src/cypherpunks.ru/govpn/cmd/govpn-server/conf.go index 28ee501..ea61c81 100644 --- a/src/cypherpunks.ru/govpn/cmd/govpn-server/conf.go +++ b/src/cypherpunks.ru/govpn/cmd/govpn-server/conf.go @@ -35,7 +35,7 @@ const ( var ( confs map[govpn.PeerId]*govpn.PeerConf - idsCache *govpn.CipherCache + idsCache *govpn.MACCache ) func confRead() (*map[govpn.PeerId]*govpn.PeerConf, error) { @@ -99,7 +99,7 @@ func confRefresh() error { } func confInit() { - idsCache = govpn.NewCipherCache() + idsCache = govpn.NewMACCache() if err := confRefresh(); err != nil { log.Fatalln(err) } diff --git a/src/cypherpunks.ru/govpn/cmd/govpn-server/tcp.go b/src/cypherpunks.ru/govpn/cmd/govpn-server/tcp.go index 7a8f738..bfb4709 100644 --- a/src/cypherpunks.ru/govpn/cmd/govpn-server/tcp.go +++ b/src/cypherpunks.ru/govpn/cmd/govpn-server/tcp.go @@ -164,8 +164,6 @@ func handleTCP(conn net.Conn) { return } - nonceExpectation := make([]byte, govpn.NonceSize) - peer.NonceExpectation(nonceExpectation) prev = 0 var i int for { @@ -183,7 +181,7 @@ func handleTCP(conn net.Conn) { if prev < govpn.MinPktLength { continue } - i = bytes.Index(buf[:prev], nonceExpectation) + i = bytes.Index(buf[:prev], peer.NonceExpect) if i == -1 { continue } @@ -194,7 +192,6 @@ func handleTCP(conn net.Conn) { ) break } - peer.NonceExpectation(nonceExpectation) copy(buf, buf[i+govpn.NonceSize:prev]) prev = prev - i - govpn.NonceSize goto CheckMore diff --git a/src/cypherpunks.ru/govpn/handshake.go b/src/cypherpunks.ru/govpn/handshake.go index 9d5f688..9c536b3 100644 --- a/src/cypherpunks.ru/govpn/handshake.go +++ b/src/cypherpunks.ru/govpn/handshake.go @@ -30,7 +30,6 @@ import ( "github.com/dchest/blake2b" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/salsa20" - "golang.org/x/crypto/xtea" ) const ( @@ -134,14 +133,12 @@ func NewHandshake(addr string, conn io.Writer, conf *PeerConf) *Handshake { // Generate ID tag from client identification and data. func idTag(id *PeerId, timeSync int, data []byte) []byte { - ciph, err := xtea.NewCipher(id[:]) - if err != nil { - panic(err) - } - enc := make([]byte, xtea.BlockSize) + enc := make([]byte, 8) copy(enc, data) AddTimeSync(timeSync, enc) - ciph.Encrypt(enc, enc) + mac := blake2b.NewMAC(8, id[:]) + mac.Write(enc) + mac.Sum(enc[:0]) return enc } @@ -159,7 +156,7 @@ func HandshakeStart(addr string, conn io.Writer, conf *PeerConf) *Handshake { } var enc []byte if conf.Noise { - enc = make([]byte, conf.MTU-xtea.BlockSize-RSize) + enc = make([]byte, conf.MTU-8-RSize) } else { enc = make([]byte, 32) } @@ -197,7 +194,7 @@ func (h *Handshake) Server(data []byte) *Peer { out, err := EnclessDecode( h.dsaPubH, h.rNonce[:], - data[RSize:len(data)-xtea.BlockSize], + data[RSize:len(data)-8], ) if err != nil { log.Println("Unable to decode packet from", h.addr, err) @@ -205,12 +202,7 @@ func (h *Handshake) Server(data []byte) *Peer { } copy(cDHRepr[:], out) } else { - salsa20.XORKeyStream( - cDHRepr[:], - data[RSize:RSize+32], - h.rNonce[:], - h.dsaPubH, - ) + salsa20.XORKeyStream(cDHRepr[:], data[RSize:RSize+32], h.rNonce[:], h.dsaPubH) } // Generate DH keypair @@ -247,9 +239,9 @@ func (h *Handshake) Server(data []byte) *Peer { } var encRs []byte if h.Conf.Noise && !h.Conf.Encless { - encRs = make([]byte, h.Conf.MTU-len(encPub)-xtea.BlockSize) + encRs = make([]byte, h.Conf.MTU-len(encPub)-8) } else if h.Conf.Encless { - encRs = make([]byte, h.Conf.MTU-xtea.BlockSize) + encRs = make([]byte, h.Conf.MTU-8) } else { encRs = make([]byte, RSize+SSize) } @@ -278,7 +270,7 @@ func (h *Handshake) Server(data []byte) *Peer { dec, err = EnclessDecode( h.key, h.rNonceNext(1), - data[:len(data)-xtea.BlockSize], + data[:len(data)-8], ) if err != nil { log.Println("Unable to decode packet from", h.addr, err) @@ -308,7 +300,7 @@ func (h *Handshake) Server(data []byte) *Peer { // Send final answer to client var enc []byte if h.Conf.Noise { - enc = make([]byte, h.Conf.MTU-xtea.BlockSize) + enc = make([]byte, h.Conf.MTU-8) } else { enc = make([]byte, RSize) } @@ -364,12 +356,7 @@ func (h *Handshake) Client(data []byte) *Peer { } copy(sDHRepr[:], tmp[:32]) } else { - salsa20.XORKeyStream( - sDHRepr[:], - data[:32], - h.rNonceNext(1), - h.dsaPubH, - ) + salsa20.XORKeyStream(sDHRepr[:], data[:32], h.rNonceNext(1), h.dsaPubH) } // Compute shared key @@ -384,7 +371,7 @@ func (h *Handshake) Client(data []byte) *Peer { tmp, err = EnclessDecode( h.key, h.rNonce[:], - data[len(data)/2:len(data)-xtea.BlockSize], + data[len(data)/2:len(data)-8], ) if err != nil { log.Println("Unable to decode packet from", h.addr, err) @@ -394,12 +381,7 @@ func (h *Handshake) Client(data []byte) *Peer { copy(h.sServer[:], tmp[RSize:RSize+SSize]) } else { decRs := make([]byte, RSize+SSize) - salsa20.XORKeyStream( - decRs, - data[SSize:SSize+RSize+SSize], - h.rNonce[:], - h.key, - ) + salsa20.XORKeyStream(decRs, data[SSize:SSize+RSize+SSize], h.rNonce[:], h.key) copy(h.rServer[:], decRs[:RSize]) copy(h.sServer[:], decRs[RSize:]) } @@ -417,7 +399,7 @@ func (h *Handshake) Client(data []byte) *Peer { var enc []byte if h.Conf.Noise { - enc = make([]byte, h.Conf.MTU-xtea.BlockSize) + enc = make([]byte, h.Conf.MTU-8) } else { enc = make([]byte, RSize+RSize+SSize+ed25519.SignatureSize) } @@ -445,11 +427,7 @@ func (h *Handshake) Client(data []byte) *Peer { // Decrypt rClient var dec []byte if h.Conf.Encless { - dec, err = EnclessDecode( - h.key, - h.rNonceNext(2), - data[:len(data)-xtea.BlockSize], - ) + dec, err = EnclessDecode(h.key, h.rNonceNext(2), data[:len(data)-8]) if err != nil { log.Println("Unable to decode packet from", h.addr, err) return nil diff --git a/src/cypherpunks.ru/govpn/identity.go b/src/cypherpunks.ru/govpn/identity.go index f85631b..4dc74ee 100644 --- a/src/cypherpunks.ru/govpn/identity.go +++ b/src/cypherpunks.ru/govpn/identity.go @@ -22,11 +22,12 @@ import ( "crypto/subtle" "encoding/base64" "encoding/binary" + "hash" "log" "sync" "time" - "golang.org/x/crypto/xtea" + "github.com/dchest/blake2b" ) const ( @@ -43,42 +44,42 @@ func (id PeerId) MarshalJSON() ([]byte, error) { return []byte(`"` + id.String() + `"`), nil } -type CipherAndTimeSync struct { - c *xtea.Cipher - t int +type MACAndTimeSync struct { + mac hash.Hash + ts int + l sync.Mutex } -type CipherCache struct { - c map[PeerId]*CipherAndTimeSync - l sync.RWMutex +type MACCache struct { + cache map[PeerId]*MACAndTimeSync + l sync.RWMutex } -func NewCipherCache() *CipherCache { - return &CipherCache{c: make(map[PeerId]*CipherAndTimeSync)} +func NewMACCache() *MACCache { + return &MACCache{cache: make(map[PeerId]*MACAndTimeSync)} } -// Remove disappeared keys, add missing ones with initialized ciphers. -func (cc *CipherCache) Update(peers *map[PeerId]*PeerConf) { - cc.l.Lock() - for pid, _ := range cc.c { +// Remove disappeared keys, add missing ones with initialized MACs. +func (mc *MACCache) Update(peers *map[PeerId]*PeerConf) { + mc.l.Lock() + for pid, _ := range mc.cache { if _, exists := (*peers)[pid]; !exists { log.Println("Cleaning key:", pid) - delete(cc.c, pid) + delete(mc.cache, pid) } } for pid, pc := range *peers { - if _, exists := cc.c[pid]; exists { - cc.c[pid].t = pc.TimeSync + if _, exists := mc.cache[pid]; exists { + mc.cache[pid].ts = pc.TimeSync } else { log.Println("Adding key", pid) - cipher, err := xtea.NewCipher(pid[:]) - if err != nil { - panic(err) + mc.cache[pid] = &MACAndTimeSync{ + mac: blake2b.NewMAC(8, pid[:]), + ts: pc.TimeSync, } - cc.c[pid] = &CipherAndTimeSync{cipher, pc.TimeSync} } } - cc.l.Unlock() + mc.l.Unlock() } // If timeSync > 0, then XOR timestamp with the data. @@ -93,24 +94,29 @@ func AddTimeSync(ts int, data []byte) { } } -// Try to find peer's identity (that equals to an encryption key) +// Try to find peer's identity (that equals to MAC) // by taking first blocksize sized bytes from data at the beginning // as plaintext and last bytes as cyphertext. -func (cc *CipherCache) Find(data []byte) *PeerId { - if len(data) < xtea.BlockSize*2 { +func (mc *MACCache) Find(data []byte) *PeerId { + if len(data) < 8*2 { return nil } - buf := make([]byte, xtea.BlockSize) - cc.l.RLock() - for pid, ct := range cc.c { - ct.c.Decrypt(buf, data[len(data)-xtea.BlockSize:]) - AddTimeSync(ct.t, buf) - if subtle.ConstantTimeCompare(buf, data[:xtea.BlockSize]) == 1 { + buf := make([]byte, 8) + mc.l.RLock() + for pid, mt := range mc.cache { + copy(buf, data) + AddTimeSync(mt.ts, buf) + mt.l.Lock() + mt.mac.Reset() + mt.mac.Write(buf) + mt.mac.Sum(buf[:0]) + mt.l.Unlock() + if subtle.ConstantTimeCompare(buf, data[len(data)-8:]) == 1 { ppid := PeerId(pid) - cc.l.RUnlock() + mc.l.RUnlock() return &ppid } } - cc.l.RUnlock() + mc.l.RUnlock() return nil } diff --git a/src/cypherpunks.ru/govpn/peer.go b/src/cypherpunks.ru/govpn/peer.go index 9312b64..fc36179 100644 --- a/src/cypherpunks.ru/govpn/peer.go +++ b/src/cypherpunks.ru/govpn/peer.go @@ -20,6 +20,7 @@ package govpn import ( "bytes" + "crypto/subtle" "encoding/binary" "io" "log" @@ -27,14 +28,14 @@ import ( "sync/atomic" "time" + "github.com/dchest/blake2b" "golang.org/x/crypto/poly1305" "golang.org/x/crypto/salsa20" - "golang.org/x/crypto/xtea" ) const ( NonceSize = 8 - NonceBucketSize = 128 + NonceBucketSize = 256 TagSize = poly1305.TagSize // S20BS is Salsa20's internal blocksize in bytes S20BS = 64 @@ -48,19 +49,23 @@ const ( PadByte = byte(0x80) ) -func newNonceCipher(key *[32]byte) *xtea.Cipher { - nonceKey := make([]byte, 16) - salsa20.XORKeyStream( - nonceKey, - make([]byte, 32), - make([]byte, xtea.BlockSize), - key, - ) - ciph, err := xtea.NewCipher(nonceKey) - if err != nil { - panic(err) - } - return ciph +func newNonces(key *[32]byte, i uint64) chan *[NonceSize]byte { + macKey := make([]byte, 32) + salsa20.XORKeyStream(macKey, make([]byte, 32), make([]byte, 8), key) + mac := blake2b.NewMAC(NonceSize, macKey) + nonces := make(chan *[NonceSize]byte, NonceBucketSize*3) + go func() { + for { + buf := new([NonceSize]byte) + binary.BigEndian.PutUint64(buf[:], i) + mac.Write(buf[:]) + mac.Sum(buf[:0]) + nonces <- buf + mac.Reset() + i += 2 + } + }() + return nonces } type Peer struct { @@ -88,18 +93,7 @@ type Peer struct { Encless bool MTU int - // Cryptography related - Key *[SSize]byte `json:"-"` - NonceCipher *xtea.Cipher `json:"-"` - nonceRecv uint64 - nonceLatest uint64 - nonceOur uint64 - NonceExpect uint64 `json:"-"` - nonceBucket0 map[uint64]struct{} - nonceBucket1 map[uint64]struct{} - nonceFound0 bool - nonceFound1 bool - nonceBucketN int32 + key *[SSize]byte `json:"-"` // Timers Timeout time.Duration `json:"-"` @@ -113,12 +107,24 @@ type Peer struct { keyAuthR *[SSize]byte pktSizeR int + // UDP-related + noncesR chan *[NonceSize]byte + nonceRecv [NonceSize]byte + nonceBucketL map[[NonceSize]byte]struct{} + nonceBucketM map[[NonceSize]byte]struct{} + nonceBucketH map[[NonceSize]byte]struct{} + + // TCP-related + NonceExpect []byte `json:"-"` + noncesExpect chan *[NonceSize]byte + // Transmitter BusyT sync.Mutex `json:"-"` bufT []byte tagT *[TagSize]byte keyAuthT *[SSize]byte frameT []byte + noncesT chan *[NonceSize]byte } func (p *Peer) String() string { @@ -129,7 +135,7 @@ func (p *Peer) String() string { func (p *Peer) Zero() { p.BusyT.Lock() p.BusyR.Lock() - SliceZero(p.Key[:]) + SliceZero(p.key[:]) SliceZero(p.bufR) SliceZero(p.bufT) SliceZero(p.keyAuthR[:]) @@ -138,11 +144,6 @@ func (p *Peer) Zero() { p.BusyR.Unlock() } -func (p *Peer) NonceExpectation(buf []byte) { - binary.BigEndian.PutUint64(buf, p.NonceExpect) - p.NonceCipher.Encrypt(buf, buf) -} - func cprCycleCalculate(conf *PeerConf) time.Duration { if conf.CPR == 0 { return time.Duration(0) @@ -174,6 +175,7 @@ func newPeer(isClient bool, addr string, conn io.Writer, conf *PeerConf, key *[S bufSize += EnclessEnlargeSize noiseEnable = true } + peer := Peer{ Addr: addr, Id: conf.Id, @@ -185,10 +187,7 @@ func newPeer(isClient bool, addr string, conn io.Writer, conf *PeerConf, key *[S Encless: conf.Encless, MTU: conf.MTU, - Key: key, - NonceCipher: newNonceCipher(key), - nonceBucket0: make(map[uint64]struct{}, NonceBucketSize), - nonceBucket1: make(map[uint64]struct{}, NonceBucketSize), + key: key, Timeout: timeout, Established: now, @@ -201,15 +200,39 @@ func newPeer(isClient bool, addr string, conn io.Writer, conf *PeerConf, key *[S keyAuthR: new([SSize]byte), keyAuthT: new([SSize]byte), } + if isClient { - peer.nonceOur = 1 - peer.NonceExpect = 0 + 2 + peer.noncesT = newNonces(peer.key, 1 + 2) + peer.noncesR = newNonces(peer.key, 0 + 2) + peer.noncesExpect = newNonces(peer.key, 0 + 2) } else { - peer.nonceOur = 0 - peer.NonceExpect = 1 + 2 + peer.noncesT = newNonces(peer.key, 0 + 2) + peer.noncesR = newNonces(peer.key, 1 + 2) + peer.noncesExpect = newNonces(peer.key, 1 + 2) } - return &peer + peer.NonceExpect = make([]byte, NonceSize) + nonce := <-peer.noncesExpect + copy(peer.NonceExpect, nonce[:]) + + var i int + peer.nonceBucketL = make(map[[NonceSize]byte]struct{}, NonceBucketSize) + for i = 0; i < NonceBucketSize; i++ { + nonce = <-peer.noncesR + peer.nonceBucketL[*nonce] = struct{}{} + } + peer.nonceBucketM = make(map[[NonceSize]byte]struct{}, NonceBucketSize) + for i = 0; i < NonceBucketSize; i++ { + nonce = <-peer.noncesR + peer.nonceBucketM[*nonce] = struct{}{} + } + peer.nonceBucketH = make(map[[NonceSize]byte]struct{}, NonceBucketSize) + for i = 0; i < NonceBucketSize; i++ { + nonce = <-peer.noncesR + peer.nonceBucketH[*nonce] = struct{}{} + } + + return &peer } // Process incoming Ethernet packet. @@ -243,17 +266,12 @@ func (p *Peer) EthProcess(data []byte) { } else { p.frameT = p.bufT[S20BS : S20BS+len(data)+1+NonceSize] } - p.nonceOur += 2 - binary.BigEndian.PutUint64(p.frameT[len(p.frameT)-NonceSize:], p.nonceOur) - p.NonceCipher.Encrypt( - p.frameT[len(p.frameT)-NonceSize:], - p.frameT[len(p.frameT)-NonceSize:], - ) + copy(p.frameT[len(p.frameT)-NonceSize:], (<-p.noncesT)[:]) var out []byte if p.Encless { var err error out, err = EnclessEncode( - p.Key, + p.key, p.frameT[len(p.frameT)-NonceSize:], p.frameT[:len(p.frameT)-NonceSize], ) @@ -266,7 +284,7 @@ func (p *Peer) EthProcess(data []byte) { p.bufT[:S20BS+len(p.frameT)-NonceSize], p.bufT[:S20BS+len(p.frameT)-NonceSize], p.frameT[len(p.frameT)-NonceSize:], - p.Key, + p.key, ) copy(p.keyAuthT[:], p.bufT[:SSize]) poly1305.Sum(p.tagT, p.frameT, p.keyAuthT) @@ -290,7 +308,7 @@ func (p *Peer) PktProcess(data []byte, tap io.Writer, reorderable bool) bool { if p.Encless { var err error out, err = EnclessDecode( - p.Key, + p.key, data[len(data)-NonceSize:], data[:len(data)-NonceSize], ) @@ -308,7 +326,7 @@ func (p *Peer) PktProcess(data []byte, tap io.Writer, reorderable bool) bool { p.bufR[:S20BS+len(data)-TagSize-NonceSize], p.bufR[:S20BS+len(data)-TagSize-NonceSize], data[len(data)-NonceSize:], - p.Key, + p.key, ) copy(p.keyAuthR[:], p.bufR[:SSize]) copy(p.tagR[:], data[:TagSize]) @@ -320,41 +338,45 @@ func (p *Peer) PktProcess(data []byte, tap io.Writer, reorderable bool) bool { out = p.bufR[S20BS : S20BS+len(data)-TagSize-NonceSize] } - // Check if received nonce is known to us in either of two buckets. - // If yes, then this is ignored duplicate. - // Check from the oldest bucket, as in most cases this will result - // in constant time check. - // If Bucket0 is filled, then it becomes Bucket1. - p.NonceCipher.Decrypt( - data[len(data)-NonceSize:], - data[len(data)-NonceSize:], - ) - p.nonceRecv = binary.BigEndian.Uint64(data[len(data)-NonceSize:]) if reorderable { - _, p.nonceFound0 = p.nonceBucket0[p.nonceRecv] - _, p.nonceFound1 = p.nonceBucket1[p.nonceRecv] - if p.nonceFound0 || p.nonceFound1 || p.nonceRecv+2*NonceBucketSize < p.nonceLatest { + copy(p.nonceRecv[:], data[len(data)-NonceSize:]) + _, foundL := p.nonceBucketL[p.nonceRecv] + _, foundM := p.nonceBucketM[p.nonceRecv] + _, foundH := p.nonceBucketH[p.nonceRecv] + // If found is none of buckets: either it is too old, + // or too new (many packets were lost) + if !(foundL || foundM || foundH) { p.FramesDup++ p.BusyR.Unlock() return false } - p.nonceBucket0[p.nonceRecv] = struct{}{} - p.nonceBucketN++ - if p.nonceBucketN == NonceBucketSize { - p.nonceBucket1 = p.nonceBucket0 - p.nonceBucket0 = make(map[uint64]struct{}, NonceBucketSize) - p.nonceBucketN = 0 + // Delete seen nonce + if foundL { + delete(p.nonceBucketL, p.nonceRecv) + } + if foundM { + delete(p.nonceBucketM, p.nonceRecv) + } + if foundH { + delete(p.nonceBucketH, p.nonceRecv) + } + // If we are dealing with the latest bucket, create the new one + if foundH { + p.nonceBucketL, p.nonceBucketM = p.nonceBucketM, p.nonceBucketH + p.nonceBucketH = make(map[[NonceSize]byte]struct{}) + var nonce *[NonceSize]byte + for i := 0; i < NonceBucketSize; i++ { + nonce = <-p.noncesR + p.nonceBucketH[*nonce] = struct{}{} + } } } else { - if p.nonceRecv != p.NonceExpect { + if subtle.ConstantTimeCompare(data[len(data)-NonceSize:], p.NonceExpect) != 1 { p.FramesDup++ p.BusyR.Unlock() return false } - p.NonceExpect += 2 - } - if p.nonceRecv > p.nonceLatest { - p.nonceLatest = p.nonceRecv + copy(p.NonceExpect, (<-p.noncesExpect)[:]) } p.FramesIn++ diff --git a/src/cypherpunks.ru/govpn/peer_test.go b/src/cypherpunks.ru/govpn/peer_test.go index 7fa8aab..4a0c752 100644 --- a/src/cypherpunks.ru/govpn/peer_test.go +++ b/src/cypherpunks.ru/govpn/peer_test.go @@ -51,12 +51,16 @@ func init() { MTU: MTUDefault, Timeout: time.Second * time.Duration(TimeoutDefault), } - testPeer = newPeer(true, "foo", Dummy{&testCt}, testConf, new([SSize]byte)) testPt = make([]byte, 789) } +func testPeerNew() { + testPeer = newPeer(true, "foo", Dummy{&testCt}, testConf, new([SSize]byte)) +} + func TestTransportSymmetric(t *testing.T) { - peerd := newPeer(true, "foo", Dummy{nil}, testConf, new([SSize]byte)) + testPeerNew() + peerd := newPeer(false, "foo", Dummy{nil}, testConf, new([SSize]byte)) f := func(payload []byte) bool { if len(payload) == 0 { return true @@ -70,7 +74,8 @@ func TestTransportSymmetric(t *testing.T) { } func TestTransportSymmetricNoise(t *testing.T) { - peerd := newPeer(true, "foo", Dummy{nil}, testConf, new([SSize]byte)) + testPeerNew() + peerd := newPeer(false, "foo", Dummy{nil}, testConf, new([SSize]byte)) testPeer.NoiseEnable = true peerd.NoiseEnable = true f := func(payload []byte) bool { @@ -87,7 +92,8 @@ func TestTransportSymmetricNoise(t *testing.T) { } func TestTransportSymmetricEncless(t *testing.T) { - peerd := newPeer(true, "foo", Dummy{nil}, testConf, new([SSize]byte)) + testPeerNew() + peerd := newPeer(false, "foo", Dummy{nil}, testConf, new([SSize]byte)) testPeer.Encless = true testPeer.NoiseEnable = true peerd.Encless = true @@ -119,10 +125,14 @@ func BenchmarkDec(b *testing.B) { testPeer = newPeer(true, "foo", Dummy{nil}, testConf, new([SSize]byte)) orig := make([]byte, len(testCt)) copy(orig, testCt) + nonce := new([NonceSize]byte) + copy(nonce[:], testCt[len(testCt)-NonceSize:]) b.ResetTimer() for i := 0; i < b.N; i++ { - testPeer.nonceBucket0 = make(map[uint64]struct{}, 1) - testPeer.nonceBucket1 = make(map[uint64]struct{}, 1) + testPeer.nonceBucketL = make(map[[NonceSize]byte]struct{}, 1) + testPeer.nonceBucketM = make(map[[NonceSize]byte]struct{}, 1) + testPeer.nonceBucketH = make(map[[NonceSize]byte]struct{}, 1) + testPeer.nonceBucketL[*nonce] = struct{}{} copy(testCt, orig) if !testPeer.PktProcess(testCt, Dummy{nil}, true) { b.Fail() -- 2.44.0