From ca2a2fad9ef8b019efb4d85de74ebd904d27454f23ad7487d93ecf3782bcd96c Mon Sep 17 00:00:00 2001 From: Sergey Matveev <stargrave@stargrave.org> Date: Tue, 16 Apr 2024 00:50:34 +0300 Subject: [PATCH] SipHash24 for short messages is much faster and secure enough --- cmd/client/main.go | 63 ++++++++++++++++++++++----------------------- cmd/server/main.go | 40 ++++++++++++---------------- cmd/server/peer.go | 2 ++ doc/features.texi | 8 +++--- doc/index.texi | 2 +- doc/integrity.texi | 4 +-- doc/proto.texi | 8 +++--- go.mod | 1 + go.sum | 2 ++ internal/noise.go | 2 +- internal/var.go | 2 -- internal/version.go | 2 +- 12 files changed, 66 insertions(+), 70 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index fadaf51..6ac7509 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -32,13 +32,13 @@ import ( "strings" "time" + "github.com/dchest/siphash" "github.com/flynn/noise" "github.com/jroimartin/gocui" "go.stargrave.org/opus/v2" vors "go.stargrave.org/vors/internal" "golang.org/x/crypto/blake2s" "golang.org/x/crypto/chacha20" - "golang.org/x/crypto/poly1305" ) type Stream struct { @@ -268,14 +268,20 @@ func main() { } } - var keyOur []byte + var keyCiphOur []byte + var keyMACOur []byte { - h, err := blake2s.New256(hs.ChannelBinding()) + xof, err := blake2s.NewXOF(32+16, nil) if err != nil { log.Fatalln(err) } - h.Write([]byte(vors.NoisePrologue)) - keyOur = h.Sum(nil) + xof.Write([]byte(vors.NoisePrologue)) + xof.Write(hs.ChannelBinding()) + buf := make([]byte, 32+16) + if _, err = io.ReadFull(xof, buf); err != nil { + log.Fatalln(err) + } + keyCiphOur, keyMACOur = buf[:32], buf[32:] } seen := time.Now() @@ -352,6 +358,7 @@ func main() { if err != nil { log.Fatal(err) } + keyCiph, keyMAC := key[:32], key[32:] stream := &Stream{ name: name, in: make(chan []byte, 1<<10), @@ -402,9 +409,8 @@ func main() { } var ciph *chacha20.Cipher - var macKey [32]byte - var mac *poly1305.MAC - tag := make([]byte, poly1305.TagSize) + mac := siphash.New(keyMAC) + tag := make([]byte, siphash.Size) var ctr uint32 pcm := make([]int16, vors.FrameLen) nonce := make([]byte, 12) @@ -413,26 +419,23 @@ func main() { var lastDur int for buf := range stream.in { copy(nonce[len(nonce)-4:], buf) - ciph, err = chacha20.NewUnauthenticatedCipher(key, nonce) - if err != nil { - log.Fatal(err) - } - clear(macKey[:]) - ciph.XORKeyStream(macKey[:], macKey[:]) - ciph.SetCounter(1) - mac = poly1305.New(&macKey) - if _, err = mac.Write(buf[4 : len(buf)-vors.TagLen]); err != nil { + mac.Reset() + if _, err = mac.Write(buf[: len(buf)-siphash.Size]); err != nil { log.Fatal(err) } mac.Sum(tag[:0]) if subtle.ConstantTimeCompare( - tag[:vors.TagLen], - buf[len(buf)-vors.TagLen:], + tag[:siphash.Size], + buf[len(buf)-siphash.Size:], ) != 1 { stream.stats.bads++ continue } - pkt = buf[4 : len(buf)-vors.TagLen] + ciph, err = chacha20.NewUnauthenticatedCipher(keyCiph, nonce) + if err != nil { + log.Fatal(err) + } + pkt = buf[4 : len(buf)-siphash.Size] ciph.XORKeyStream(pkt, pkt) ctr = binary.BigEndian.Uint32(nonce[len(nonce)-4:]) @@ -532,7 +535,7 @@ func main() { log.Println("wrong addr:", from) continue } - if n <= 4+vors.TagLen { + if n <= 4+siphash.Size { log.Println("too small:", n) continue } @@ -573,9 +576,8 @@ func main() { } <-LoggerReady var ciph *chacha20.Cipher - var macKey [32]byte - var mac *poly1305.MAC - tag := make([]byte, poly1305.TagSize) + mac := siphash.New(keyMACOur) + tag := make([]byte, siphash.Size) buf := make([]byte, 2*vors.FrameLen) pcm := make([]int16, vors.FrameLen) nonce := make([]byte, 12) @@ -608,21 +610,18 @@ func main() { incr(nonce[len(nonce)-3:]) copy(buf, nonce[len(nonce)-4:]) - ciph, err = chacha20.NewUnauthenticatedCipher(keyOur, nonce) + ciph, err = chacha20.NewUnauthenticatedCipher(keyCiphOur, nonce) if err != nil { log.Fatal(err) } - clear(macKey[:]) - ciph.XORKeyStream(macKey[:], macKey[:]) - ciph.SetCounter(1) ciph.XORKeyStream(buf[4:4+n], buf[4:4+n]) - mac = poly1305.New(&macKey) - if _, err = mac.Write(buf[4 : 4+n]); err != nil { + mac.Reset() + if _, err = mac.Write(buf[: 4+n]); err != nil { log.Fatal(err) } mac.Sum(tag[:0]) - copy(buf[4+n:], tag[:vors.TagLen]) - pkt = buf[:4+n+vors.TagLen] + copy(buf[4+n:], tag) + pkt = buf[:4+n+siphash.Size] OurStats.pkts++ OurStats.bytes += vors.IPHdrLen(srvAddrUDP.IP) + 8 + uint64(len(pkt)) diff --git a/cmd/server/main.go b/cmd/server/main.go index 514e8fe..b6b7255 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -33,12 +33,11 @@ import ( "strings" "time" + "github.com/dchest/siphash" "github.com/flynn/noise" "github.com/jroimartin/gocui" vors "go.stargrave.org/vors/internal" "golang.org/x/crypto/blake2s" - "golang.org/x/crypto/chacha20" - "golang.org/x/crypto/poly1305" ) var ( @@ -257,12 +256,17 @@ func newPeer(conn *net.TCPConn) { } { - h, err := blake2s.New256(hs.ChannelBinding()) + xof, err := blake2s.NewXOF(32+16, nil) if err != nil { log.Fatalln(err) } - h.Write([]byte(vors.NoisePrologue)) - peer.key = h.Sum(nil) + xof.Write([]byte(vors.NoisePrologue)) + xof.Write(hs.ChannelBinding()) + peer.key = make([]byte, 32+16) + if _, err = io.ReadFull(xof, peer.key); err != nil { + log.Fatalln(err) + } + peer.mac = siphash.New(peer.key[32:]) } { @@ -379,11 +383,7 @@ func main() { var err error var sid byte var peer *Peer - var ciph *chacha20.Cipher - var macKey [32]byte - var mac *poly1305.MAC - tag := make([]byte, poly1305.TagSize) - nonce := make([]byte, 12) + tag := make([]byte, siphash.Size) for { n, from, err = lnUDP.ReadFromUDP(buf) if err != nil { @@ -422,27 +422,19 @@ func main() { if n == 1 { continue } - if n <= 4+vors.TagLen { + if n <= 4+siphash.Size { peer.stats.bads++ continue } - copy(nonce[len(nonce)-4:], buf) - ciph, err = chacha20.NewUnauthenticatedCipher(peer.key, nonce) - if err != nil { - log.Fatal(err) - } - clear(macKey[:]) - ciph.XORKeyStream(macKey[:], macKey[:]) - ciph.SetCounter(1) - mac = poly1305.New(&macKey) - if _, err = mac.Write(buf[4 : n-vors.TagLen]); err != nil { + peer.mac.Reset() + if _, err = peer.mac.Write(buf[: n-siphash.Size]); err != nil { log.Fatal(err) } - mac.Sum(tag[:0]) + peer.mac.Sum(tag[:0]) if subtle.ConstantTimeCompare( - tag[:vors.TagLen], - buf[n-vors.TagLen:n], + tag[:siphash.Size], + buf[n-siphash.Size:n], ) != 1 { peer.stats.bads++ continue diff --git a/cmd/server/peer.go b/cmd/server/peer.go index d1e078a..82c569d 100644 --- a/cmd/server/peer.go +++ b/cmd/server/peer.go @@ -1,6 +1,7 @@ package main import ( + "hash" "log/slog" "net" "sync" @@ -29,6 +30,7 @@ type Peer struct { sid byte addr *net.UDPAddr key []byte + mac hash.Hash stats *Stats room *Room diff --git a/doc/features.texi b/doc/features.texi index 8987ded..27f5881 100644 --- a/doc/features.texi +++ b/doc/features.texi @@ -11,9 +11,11 @@ is single client speaking at one time. Concealment) and DTX (discontinuous transmission) features enabled. Optional VAD (voice activity detection). -@item Noise protocol-based handshake over TCP between client and server -for creating authenticated encrypted channel and authentication based on -server's public key knowledge. +@item Noise protocol-based 0-RTT handshake over TCP between client and +server for creating authenticated encrypted channel and authentication +based on server's public key knowledge. + +@item Fast ChaCha20 encryption with SipHash24 message authentication. @item Rooms, optionally password protected. diff --git a/doc/index.texi b/doc/index.texi index b1b19ee..b1c2fb9 100644 --- a/doc/index.texi +++ b/doc/index.texi @@ -43,7 +43,7 @@ stop trying to use and revive legacy obsolete IPv4. Either use some overlay network on top of it, or VPN, whatever. @item Mono-cypher, mono-codec protocol. The @url{https://opus-codec.org/, Opus} -audio codec is perfect for VoIP tasks. ChaCha20-Poly1305 is more than +audio codec is perfect for VoIP tasks. ChaCha20 is more than appropriate and satisfies as fast and secure encryption solution. @float diff --git a/doc/integrity.texi b/doc/integrity.texi index e26a34d..59815f8 100644 --- a/doc/integrity.texi +++ b/doc/integrity.texi @@ -6,6 +6,6 @@ that you retrieved trusted and untampered software. Its fingerprint: @code{SHA256:qmlbyzvDRNXGJNxteapAWOmJRrBrZ7afLsEqr36M6kA}. @example -$ ssh-keygen -Y verify -f PUBKEY-SSH.pub -I vors@@cypherpunks.ru -n file \ - -s vors-@value{VERSION}.tar.zst.sig < vors-@value{VERSION}.tar.zst +$ ssh-keygen -Y verify -f PUBKEY-SSH.pub -I vors@@stargrave.org -n file \ + -s vors-@value{VERSION}.tar.zst.sig <vors-@value{VERSION}.tar.zst @end example diff --git a/doc/proto.texi b/doc/proto.texi index b420984..a1af4d4 100644 --- a/doc/proto.texi +++ b/doc/proto.texi @@ -10,10 +10,10 @@ Each frame has a single byte stream identifier (unique identifier of the participant) and 24-bit big-endian packet counter. Reordered packets are dropped. 24-bit counter is long enough for very long talk sessions. -Each packet is encrypted with ChaCha20-Poly1305. The key is generated -during the handshake procedure with the server and is shared among the -other participants. The stream identifier together with the packet -counter is used as a nonce. Only 64-bits of Poly1305 are used. +Each packet is encrypted with ChaCha20 and authenticated with SipHash24. +The keys are generated during the handshake procedure with the server +and is shared among the other participants. The stream identifier +together with the packet counter is used as a nonce. It is tuned for 24Kbps bandwidth. But remember that it has additional 8B of MAC tag, 4B VoRS, 8B UDP and 40B IPv6 headers. diff --git a/go.mod b/go.mod index 0276bb7..563ffc7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module go.stargrave.org/vors go 1.21 require ( + github.com/dchest/siphash v1.2.3 github.com/dustin/go-humanize v1.0.1 github.com/flynn/noise v1.1.0 github.com/jroimartin/gocui v0.5.0 diff --git a/go.sum b/go.sum index c4ccfe2..4646fd1 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= diff --git a/internal/noise.go b/internal/noise.go index 5e4b883..365a39c 100644 --- a/internal/noise.go +++ b/internal/noise.go @@ -8,7 +8,7 @@ import ( "github.com/flynn/noise" ) -const NoisePrologue = "VoRS v1" +const NoisePrologue = "VoRS v2" var NoiseCipherSuite = noise.NewCipherSuite( noise.DH25519, diff --git a/internal/var.go b/internal/var.go index 8236a4d..0ef1d5a 100644 --- a/internal/var.go +++ b/internal/var.go @@ -6,8 +6,6 @@ import ( ) const ( - TagLen = 8 - CmdPing = "PING" CmdPong = "PONG" CmdAdd = "ADD" diff --git a/internal/version.go b/internal/version.go index 28dd1a0..f454ea5 100644 --- a/internal/version.go +++ b/internal/version.go @@ -3,7 +3,7 @@ package internal import "runtime" const ( - Version = "1.0.0" + Version = "2.0.0" Warranty = `Copyright (C) 2024 Sergey Matveev This program is free software: you can redistribute it and/or modify -- 2.51.0