What does the associated data in AES-GCM bind, what exactly leaks when a nonce is reused, and which errors do you see when you open a box wrongly?

14.6.1 · L5 · Interview

Predict first

What do nonce reuse, associated data and the failure modes look like?

import Foundation
import CryptoKit

let key = SymmetricKey(size: .bits256)
let nonce = try AES.GCM.Nonce(data: Data(repeating: 7, count: 12))
let p1 = Data("attack at dawn!!".utf8), p2 = Data("retreat at noon!".utf8)
let b1 = try AES.GCM.seal(p1, using: key, nonce: nonce)
let b2 = try AES.GCM.seal(p2, using: key, nonce: nonce)
let leak = zip(b1.ciphertext, b2.ciphertext).map { $0 ^ $1 }
print("nonce reuse leaks p1 xor p2:", leak == zip(p1, p2).map { $0 ^ $1 })
let aad = Data("user=42".utf8)
let box = try AES.GCM.seal(p1, using: key, authenticating: aad)
print("sizes combined/nonce/tag:", box.combined!.count, box.nonce.withUnsafeBytes { $0.count }, box.tag.count)
print("right aad opens:", (try? AES.GCM.open(box, using: key, authenticating: aad)) != nil)
func fail(_ label: String, _ body: () throws -> Void) { do { try body(); print(label, "opened") } catch { print(label, error) } }
fail("wrong aad:") { _ = try AES.GCM.open(box, using: key, authenticating: Data("user=43".utf8)) }
fail("missing aad:") { _ = try AES.GCM.open(box, using: key) }
var bytes = box.combined!; bytes[14] ^= 1
fail("flipped bit:") { _ = try AES.GCM.open(try AES.GCM.SealedBox(combined: bytes), using: key, authenticating: aad) }
fail("wrong key:") { _ = try AES.GCM.open(box, using: SymmetricKey(size: .bits256), authenticating: aad) }
fail("short box:") { _ = try AES.GCM.SealedBox(combined: Data(count: 5)) }
print("chacha same size:", try ChaChaPoly.seal(p1, using: key).combined.count == box.combined!.count)
  • nonce reuse leaks p1 xor p2: true / sizes combined/nonce/tag: 44 12 16 / right aad opens: true / wrong aad: authenticationFailure / missing aad: authenticationFailure / flipped bit: authenticationFailure / wrong key: authenticationFailure / short box: incorrectParameterSize / chacha same size: true
  • nonce reuse leaks p1 xor p2: false / sizes combined/nonce/tag: 32 12 16 / wrong aad: opened
  • wrong aad: incorrectParameterSize / wrong key: incorrectKeySize

Mental model. Associated data is the address written on the outside of a sealed envelope: not secret, but the seal only holds if the address was not changed. A repeated nonce is using the same one-time pad twice.

Authenticated encryption with associated data (AEAD) protects two things: the secret plaintext, and extra context you send in the clear but want bound to the ciphertext (a user id, a message type, a protocol version). In CryptoKit that context is the authenticating: argument. It is not encrypted and not stored in the box, but it is mixed into the tag, so opening with different associated data fails exactly like a tampered ciphertext. The check shows every failure mode giving the same authenticationFailure: a wrong key, a wrong or missing associated data, and one flipped bit in the combined bytes. A box that is too short to hold a 12-byte nonce and a 16-byte tag is a different error, incorrectParameterSize, because it is rejected before any cryptography. A sealed 16-byte message is 44 bytes combined (12 + 16 + 16), and ChaCha20-Poly1305 has the same layout. Use associated data to stop a valid ciphertext being replayed in the wrong place, such as a token encrypted for user 42 being presented as user 43.

The nonce rule is not a style point. In the check two different messages are sealed with the same key and the same nonce, and the XOR of the two ciphertexts equals the XOR of the two plaintexts: an attacker who knows or guesses one message learns the other, and with GCM a repeated nonce also leaks the authentication key, which lets them forge tags. CryptoKit avoids the trap by choosing a random 96-bit nonce when you call seal without one; I confirmed that two seals of the same plaintext differ. Supplying a nonce yourself is for test vectors and for protocols that define it. With random 96-bit nonces keep the number of messages under one key well below about 232 and rotate keys before that. Not run: nonce-misuse-resistant modes, because CryptoKit does not offer them.

Same key and nonce expose the XOR of the plaintexts; changed context or bytes fail with authenticationFailure; a truncated box fails earlier.

Common traps

  • Treating associated data as encrypted. It is authenticated only; anyone can read it.
  • Supplying a counter nonce that restarts after a crash or restore. A repeat under the same key breaks confidentiality and integrity; let the library pick a random one.
  • Expecting different errors for wrong key and tampering. Both are authenticationFailure by design, so do not build logic on telling them apart.

They might follow up with

  • Why verify before parsing? Opening checks the tag first, so you never parse attacker-controlled bytes.
  • When would you pick ChaCha20-Poly1305? Same layout and safety; it is fast without AES hardware and is what HPKE suites often use.

Related questions

  • 14.6.2 How do two parties agree on a key with CryptoKit, why must the shared secret go through HKDF, and what does…
  • 14.1.1 What is the difference between hashing, a MAC, encryption and a digital signature, and when do you use…
  • 14.1.2 How do you use cryptography safely: key management, nonces, key derivation, password storage and comparison?

How do two parties agree on a key with CryptoKit, why must the shared… →