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