-Protocol is trivial. Both peers have shared 256-bit key.
-SHA3 is used to derive four more keys from it:
+After TLS 1.3 handshake is finished, 512-bit keying material is
+exported. Various 256-bit keys are derived from it:
- SHAKE128("go.cypherpunks.ru/udpobfs" || key) ->
- 256-bit InitiatorEncryptionKey ||
- 256-bit InitiatorObfuscationKey ||
- 256-bit ResponderEncryptionKey ||
- 256-bit ResponderObfuscationKey
+ InitEncKey = BLAKE3-DeriveKey(seed, "go.cypherpunks.ru/udpobfs/v2 init enc")
+ InitMACKey = BLAKE3-DeriveKey(seed, "go.cypherpunks.ru/udpobfs/v2 init mac")
+ InitObfsKey = BLAKE3-DeriveKey(seed, "go.cypherpunks.ru/udpobfs/v2 init obfs")
+ RespEncKey = BLAKE3-DeriveKey(seed, "go.cypherpunks.ru/udpobfs/v2 resp enc")
+ RespMACKey = BLAKE3-DeriveKey(seed, "go.cypherpunks.ru/udpobfs/v2 resp mac")
+ RespObfsKey = BLAKE3-DeriveKey(seed, "go.cypherpunks.ru/udpobfs/v2 resp obfs")
-Each side has big-endian 64-bit packet number counter, that is used as a
-nonce. That counter is kept in memory and only its lower 24 bits are
-sent. When remote side receives 24-bit counter with lower value, then it
-increments in-memory counter's remaining part. Completely the same as
-Extended Sequence Numbers are done in IPsec's ESP.
+Each peer has 64-bit packet sequence counter. It is fed to
+BLAKE3(len=256, key=*EncKey) and then its XOF output is XORed with
+plaintext packet. Then BLAKE3(len=48, key=*MACKey) is taken over the
+sequence counter concatenated with the ciphertext. That MAC, lower
+16-bits of the sequence counter are encrypted with Blowfish(key=*ObfsKey)
+and prepended to the ciphertext.
-ChaCha20 is initialised with corresponding EncryptionKey and nonce equal
-to the full sequence number value. Its first 256-bit of output will be
-Poly1305's one-time key. Next 256-bits are ignored. Further ones are
-XORed with the plaintext (UDP's payload). Poly1305 is calculated over
-the full 64-bit sequence number value and the whole ciphertext. Higher
-40-bits of the resulting tag with lower 24-bits of the sequence number
-are prepended to the ciphertext, encrypted with Blowfish:
+ ciphertext = BLAKE3(len=256, key=*EncKey)(SeqNum).XOF(len=len(plaintext))
+ ciphertext = ciphertext XOR plaintext
+ mac = BLAKE3(len=48, key=*MACKey)(SeqNum || ciphertext)
+ send(Blowfish(key=*ObfsKey)(mac || SeqNum[6:]) || ciphertext)
- Blowfish(Seq || MAC) || Ciphertext
-
-Blowfish is initialized with ObfuscationKey. 40-bit MAC is rather weak,
-but enough for obfuscation purposes. 24-bit part of sequence number is
-enough for medium data-rate transmission where reordering or packets
-drops may occur.
+All of that gives only 8-byte overhead, providing at least some 48-bit
+authentication and invisibility of cleartext nonces/sequences. BLAKE3 is
+pretty fast algorithm, however ChaCha20-Poly1305 can be faster for small
+messages -- so performance depends on message sizes. Blowfish is just
+the fastest (serious) cipher with 64-bit blocksize in the Go's library.