Next: Handshake, Previous: Verifier structure, Up: Developer
NONCE = 64bit(ZEROS) || 64bit(MAC(MAC_KEY, SERIAL))
PAYLOAD = DATA || PAD [|| ZEROS]
CIPHERTEXT = ENCRYPT(KEY, NONCE, PAYLOAD)
TAG = AUTH(AUTH_KEY, CIPHERTEXT || NONCE)
MESSAGE = TAG || CIPHERTEXT || NONCE
SERIAL is message’s serial number. Odds are reserved for
client (to server) messages, evens for server (to client) messages.
MAC is BLAKE2b-MAC used to obfuscate SERIAL. MAC’s key
MAC_KEY is the first 256-bit of ChaCha20’s output with established
common key and zero nonce (message nonces start from 1).
MAC_KEY = 256bit(ENCRYPT(KEY, 0))
ENCRYPT is ChaCha20 stream cipher, with established session
KEY and obfuscated SERIAL used as a nonce. 512 bit of
ChaCha20’s output is ignored and only remaining is XORed with ther data,
encrypting it.
DATA is padded using ISO/IEC 7816-4 format (PAD (0x80
byte) with optional ZEROS following), to fill up packet to
conceal payload packet length.
AUTH is Poly1305 authentication function. First 256 bits of
ChaCha20’s output are used as a one-time key for AUTH.
AUTH_KEY = 256bit(ENCRYPT(KEY, NONCE))
To prevent replay attacks we must remember received SERIALs and
drop when receiving duplicate ones.
In encryptionless mode this scheme is slightly different:
NONCE = MAC(MAC_KEY, SERIAL) ENCODED = ENCLESS(DATA || PAD || ZEROS) PACKET = ENCODED || NONCE
ENCLESS is AONT and chaffing function. There is no need in
explicit separate authentication.