Deterministic AEAD (AES-SIV)
The daed package — AES-SIV-CMAC per RFC 5297 with a mandatory 64-byte key. Nonce-free and misuse-resistant, at the price of leaking plaintext equality.
github.com/sonr-io/crypto/daed implements AES-SIV-CMAC as specified in RFC 5297. “DAED” is deterministic authenticated encryption with associated data: same key, same plaintext, same associated data, byte-identical ciphertext, every time. There is no nonce parameter and nothing to keep unique.
The implementation is a port of Tink’s subtle AES-SIV — the test file even aliases the import as subtle and loads Wycheproof vectors — and it is restricted to a single associated-data component, unlike the general SIV construction which takes a vector of headers.
The trade
Randomized AEAD like aead hides everything, but only because a fresh nonce makes every ciphertext unique. That safety is contingent: repeat the nonce once and AES-GCM collapses. AES-SIV removes the nonce entirely by deriving the IV from the message itself (the S2V PRF over the associated data and the plaintext), then running AES-CTR with that IV.
What you get:
- Nothing to keep unique. No nonce store, no counter, no rand call on the encrypt path.
- Misuse resistance. There is no parameter you can repeat to break it.
- Stable ciphertext. You can index it, dedupe it, or use it as a lookup key.
What you pay:
- Plaintext equality leaks. Two records that encrypt to the same bytes had the same plaintext and the same associated data. An observer learns that without touching the key.
When to use it
Good fits:
- Wrapping key material. Encrypting a data key under a key-encryption key, where there is no room in the format for a nonce and no natural place to store one.
- Deterministic encryption of identifiers. An opaque token or blind index that you must still be able to look up by equality.
- Dedupe-able ciphertext. Content-addressed storage where identical inputs should collapse to one object.
- Protocols that give you no nonce channel. Fixed-width fields, legacy record formats, anything where the only bytes you control are the ciphertext.
Bad fits: message payloads, session data, anything user-visible and repeated, and anything where two equal plaintexts appearing twice would be a disclosure.
Constants and key size
| Constant | Value | Meaning |
|---|---|---|
daed.AESSIVKeySize |
64 |
The only accepted key length: 512 bits |
The 64-byte key is a double-length key and it is not padding. NewAESSIV splits it as K1 = key[:32] (the CMAC/S2V key, used to build the AES cipher for the PRF) and K2 = key[32:] (the CTR encryption key). RFC 5297 requires the MAC and encryption keys to be the same size, so a 256-bit security level means 2 × 256 bits of key material.
The package’s doc comment explains why 64 and not 32, and this is the one place the source names a paper, so it is worth repeating verbatim in substance: Chatterjee, Menezes and Sarkar’s tightness analysis (Section 5.1) shows AES-SIV is attackable in the multi-user setting — given the encryption of one message under k different keys, a MAC key can be recovered in time 2^b / k for MAC-key size b. That makes 128-bit MAC keys insufficient, and since 192-bit AES keys are not supported, the key must be 2 × 256 bits.
NewAESSIV rejects every other length with aes_siv: invalid key size N — the package’s TestAESSIV_KeySizes walks every prefix length from 0 to 300+ and asserts that exactly 64 is accepted.
Ciphertext layout
EncryptDeterministically output:
┌────────────────────┬───────────────────────────┐
│ SIV / tag 16 bytes │ ciphertext len(plaintext) │
└────────────────────┴───────────────────────────┘
= S2V(plaintext, ad) = AES-CTR(K2, masked SIV)
The synthetic IV goes first and doubles as the authentication tag. Output length is always len(plaintext) + 16. An empty plaintext produces a 16-byte ciphertext, and DecryptDeterministically rejects anything shorter than 16 bytes with aes_siv: ciphertext is too short.
Decryption decrypts first, then recomputes S2V over the recovered plaintext and compares it to the stored SIV byte-by-byte with an accumulating XOR. A mismatch returns aes_siv: invalid ciphertext.
Usage
Grounded in daed/aes_siv_test.go (TestAESSIV_EncryptDecrypt):
package main
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/sonr-io/crypto/daed"
)
func main() {
// 64 bytes = AESSIVKeySize. Two 32-byte halves: CMAC key, then CTR key.
keyStr := "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" +
"00112233445566778899aabbccddeefff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
key, err := hex.DecodeString(keyStr)
if err != nil {
panic(err)
}
a, err := daed.NewAESSIV(key)
if err != nil {
panic(err)
}
msg := []byte("Some data to encrypt.")
ad := []byte("Additional data")
ct, err := a.EncryptDeterministically(msg, ad)
if err != nil {
panic(err)
}
fmt.Println(len(ct) == len(msg)+16) // true: SIV || ciphertext
// Determinism: identical inputs, identical output.
again, _ := a.EncryptDeterministically(msg, ad)
fmt.Println(bytes.Equal(ct, again)) // true
// Changing only the associated data changes the whole ciphertext.
other, _ := a.EncryptDeterministically(msg, []byte("Different data"))
fmt.Println(bytes.Equal(ct, other)) // false
pt, err := a.DecryptDeterministically(ct, ad)
if err != nil {
panic(err) // "aes_siv: invalid ciphertext"
}
fmt.Printf("%s\n", pt)
}
Note the associated data is a full input to the PRF, not a side channel: it changes the SIV and therefore the CTR keystream, so the entire ciphertext changes. Using a per-row identifier as associated data is the standard way to scope the equality leak.
API reference
NewAESSIV(key []byte)(*AESSIV, error)
Constructor. Errors unless len(key) == 64. Splits the key, builds the AES cipher over K1, and precomputes the two CMAC subkeys.
(*AESSIV, error)EncryptDeterministically(plaintext, associatedData []byte)([]byte, error)
Returns SIV(16) || ciphertext. Accepts nil and empty plaintext and nil associated data. Errors only if the plaintext is within one AES block of max int.
([]byte, error)DecryptDeterministically(ciphertext, associatedData []byte)([]byte, error)
Errors on ciphertext shorter than 16 bytes, or when the recomputed SIV does not match the stored one.
([]byte, error)AESSIV exposes its internals as exported struct fields:
Cipher?cipher.Block
AES cipher instance built over K1; used by the CMAC/S2V path.
cipher.BlockK1?[]byte
First 32 bytes of the key — the CMAC/S2V key.
[]byteK2?[]byte
Last 32 bytes of the key — the AES-CTR encryption key.
[]byteCmacK1?[]byte
Precomputed CMAC subkey K1 (one GF(2^128) doubling of E(0)).
[]byteCmacK2?[]byte
Precomputed CMAC subkey K2 (a second doubling).
[]byteCaveats
Related
Randomized AEAD
AES-256-GCM: the default when equality of plaintexts must stay hidden.
Key derivation
Producing the 64 bytes AES-SIV needs — ask HKDF for a 64-byte tag.
To build a 64-byte AES-SIV key from one master secret, ask HKDF for 64 bytes rather than concatenating two independent 32-byte derivations:
kek, err := subtle.ComputeHKDF("SHA256", master, salt, []byte("aes-siv key v1"), daed.AESSIVKeySize)