latticearc 0.9.0

Production-ready post-quantum cryptography. Hybrid ML-KEM+X25519 by default, all 4 NIST standards (FIPS 203–206), and FIPS 140-3 backend — one crate, zero unsafe.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]

//! Authenticated Encryption with Additional Data (AEAD)
//!
//! Provides AEAD schemes for symmetric encryption following NIST SP 800-38D and RFC 8439.
//!
//! ## AEAD Schemes
//!
//! - **AES-GCM-128**: AES-GCM with 128-bit key (NIST SP 800-38D). Available
//!   in every feature configuration.
//! - **AES-GCM-256**: AES-GCM with 256-bit key (NIST SP 800-38D). Available
//!   in every feature configuration.
//! - **ChaCha20-Poly1305**: Stream cipher with Poly1305 MAC (RFC 8439).
//!   *Compiled out under the `fips` feature* — ChaCha20-Poly1305 is not in
//!   NIST SP 800-38D, so the module and its types are unavailable in
//!   `fips`-enabled builds. Higher-level APIs that select an AEAD
//!   automatically pick AES-GCM-256 when `fips` is on.
//!
//! ## AEAD Security Notes
//!
//! - **Nonce Reuse**: NEVER reuse a nonce with the same key - this breaks security
//! - **Nonce Prediction**: Use cryptographically secure random nonces
//! - **Tag Verification**: ALWAYS verify the authentication tag before accepting ciphertext
//! - **Side Channels**: All tag verification is constant-time to prevent timing attacks

pub mod aes_gcm;

/// ChaCha20-Poly1305 AEAD (RFC 8439). Non-FIPS: not in NIST SP 800-38D.
#[cfg(not(feature = "fips"))]
pub mod chacha20poly1305;

/// AEAD cipher nonce length
pub const NONCE_LEN: usize = 12;

/// AEAD authentication tag length
pub const TAG_LEN: usize = 16;

/// AES-GCM-128 key length
pub const AES_GCM_128_KEY_LEN: usize = 16;

/// AES-GCM-256 key length
pub const AES_GCM_256_KEY_LEN: usize = 32;

/// ChaCha20-Poly1305 key length
pub const CHACHA20_POLY1305_KEY_LEN: usize = 32;

/// Nonce type for AEAD ciphers.
///
/// A 12-byte array used as a unique identifier for each encryption operation.
/// Callers must ensure nonce uniqueness per key; reusing a nonce with the same
/// key breaks AEAD security guarantees.
// Retained as a type alias rather than a newtype because converting ripples
// through every AEAD call site.
pub type Nonce = [u8; NONCE_LEN];

/// Auth tag type for AEAD ciphers.
///
/// A 16-byte authenticator computed during encryption and verified in
/// constant time during decryption.
pub type Tag = [u8; TAG_LEN];

/// Sealed trait pattern — prevents external crates from implementing `AeadCipher`.
///
/// Security-critical traits must not allow third-party implementations since
/// they could bypass key validation, zeroization, or constant-time guarantees.
mod sealed {
    pub trait Sealed {}
    impl Sealed for super::aes_gcm::AesGcm128 {}
    impl Sealed for super::aes_gcm::AesGcm256 {}
    #[cfg(not(feature = "fips"))]
    impl Sealed for super::chacha20poly1305::ChaCha20Poly1305Cipher {}
}

/// AEAD cipher trait (sealed — cannot be implemented outside this crate)
pub trait AeadCipher: sealed::Sealed {
    /// Key length in bytes
    const KEY_LEN: usize;

    /// Create new AEAD cipher from key bytes — the strict, production entry
    /// point. Length is validated and the all-zero key pattern is rejected as
    /// fail-closed defence in depth (it is overwhelmingly the signature of
    /// uninitialised memory or an unset configuration field rather than a
    /// deliberate operational choice).
    ///
    /// For NIST KAT reproduction (Test Cases 1 and 2 of McGrew & Viega's
    /// AES-GCM specification use the all-zero key) enable the
    /// `kat-test-vectors` Cargo feature and call the per-cipher inherent
    /// `new_allow_weak_key` constructor instead — that constructor preserves
    /// the length check but skips the weak-key guard. The feature is opt-in
    /// so production builds cannot accidentally construct a weak-key cipher.
    ///
    /// # Implementation note
    ///
    /// Implementations of this trait MUST themselves perform both the length
    /// check and the [`is_all_zero_key`] / [`AeadError::WeakKey`] check
    /// before constructing the cipher. The previous design exposed a
    /// `new_internal` trait method that bypassed `WeakKey`, but trait methods
    /// in Rust are callable by anyone with the trait in scope, which made
    /// the bypass an unintended public-API surface. Each cipher type now
    /// keeps its raw constructor as a `pub(crate)` inherent fn that no
    /// downstream caller can reach.
    ///
    /// # Errors
    /// - [`AeadError::InvalidKeyLength`] if `key.len() != Self::KEY_LEN`.
    /// - [`AeadError::WeakKey`] if `key` is the all-zero pattern.
    fn new(key: &[u8]) -> Result<Self, AeadError>
    where
        Self: Sized;

    /// Generate a random nonce from the OS CSPRNG.
    fn generate_nonce() -> Nonce;

    /// Encrypt plaintext with a caller-supplied nonce.
    ///
    /// # Security
    ///
    /// **Prefer [`AeadCipher::seal`]** unless you have a specific reason to control
    /// the nonce value. `seal` generates a fresh random nonce per call, eliminating
    /// caller-controlled nonce reuse — the single most catastrophic misuse of
    /// AES-GCM / ChaCha20-Poly1305.
    ///
    /// This low-level method exists for:
    /// - NIST KAT reproduction (deterministic inputs required)
    /// - Protocol-specified nonce derivation (e.g., TLS 1.3 per-record nonce)
    /// - Deterministic encryption constructions
    ///
    /// # ⚠ DANGER — Caller-supplied nonces are a footgun.
    ///
    /// Reusing a `(key, nonce)` pair with AES-GCM *catastrophically* breaks both
    /// confidentiality (XOR of plaintexts recoverable) and integrity (forgery via
    /// authentication key recovery). See NIST SP 800-38D §8.2 and
    /// Joux, "Authentication Failures in NIST version of GCM" (2006).
    ///
    /// **Use [`seal`](Self::seal) instead.** It draws a fresh 96-bit nonce
    /// from the OS CSPRNG per call, making caller-controlled reuse
    /// structurally impossible. This `encrypt` method exists for the
    /// narrow band of cases where the protocol or test vector mandates a
    /// specific caller-controlled nonce — KAT replay, deterministic
    /// nonce derivation per RFC 8452 (AES-GCM-SIV — not implemented here
    /// but the API shape is reserved), TLS-style sequence-number nonces
    /// where the protocol guarantees uniqueness by construction. If you
    /// can't articulate a written argument for why the nonce you supply
    /// is unique, you should be calling `seal`.
    ///
    /// # Arguments
    ///
    /// * `nonce` - 12-byte nonce; MUST be unique for every call with this key.
    ///   Caller is responsible for the uniqueness guarantee.
    /// * `plaintext` - Data to encrypt.
    /// * `aad` - Optional associated data (authenticated, not encrypted).
    ///
    /// # Returns
    ///
    /// Tuple of (ciphertext, authentication_tag).
    ///
    /// # Errors
    ///
    /// Returns `AeadError` if encryption fails.
    #[must_use = "AEAD ciphertext + tag must be transmitted to the receiver"]
    fn encrypt(
        &self,
        nonce: &Nonce,
        plaintext: &[u8],
        aad: Option<&[u8]>,
    ) -> Result<(Vec<u8>, Tag), AeadError>;

    /// Encrypt plaintext with an internally-generated random nonce.
    ///
    /// This is the preferred primitive-layer encryption entry point: the nonce
    /// is drawn fresh from the OS CSPRNG per call (96 bits), making
    /// caller-controlled nonce reuse structurally impossible. The returned
    /// nonce must be transmitted alongside the ciphertext so the receiver can
    /// decrypt.
    ///
    /// Under the RBG-based construction of NIST SP 800-38D §8.2.2, a single
    /// key supports up to 2^32 invocations before the collision bound becomes
    /// relevant — more than enough for typical workloads. Rotate keys
    /// periodically if you approach that scale.
    ///
    /// Use [`AeadCipher::encrypt`] only when the protocol or test vector
    /// requires a caller-controlled nonce.
    ///
    /// # Arguments
    ///
    /// * `plaintext` - Data to encrypt.
    /// * `aad` - Optional associated data (authenticated, not encrypted).
    ///
    /// # Returns
    ///
    /// Tuple of `(nonce, ciphertext, tag)`. The nonce MUST be stored alongside
    /// the ciphertext for decryption.
    ///
    /// # Errors
    ///
    /// Returns `AeadError` if encryption fails.
    fn seal(
        &self,
        plaintext: &[u8],
        aad: Option<&[u8]>,
    ) -> Result<(Nonce, Vec<u8>, Tag), AeadError> {
        let nonce = Self::generate_nonce();
        let (ciphertext, tag) = self.encrypt(&nonce, plaintext, aad)?;
        Ok((nonce, ciphertext, tag))
    }

    /// Decrypt ciphertext with optional associated data
    ///
    /// # Arguments
    ///
    /// * `nonce` - Unique nonce for this encryption
    /// * `ciphertext` - Encrypted data
    /// * `tag` - Authentication tag
    /// * `aad` - Optional associated data
    ///
    /// # Returns
    ///
    /// Decrypted plaintext wrapped in [`zeroize::Zeroizing`] so the buffer is
    /// scrubbed on drop regardless of whether the caller persists it.
    ///
    /// # Errors
    ///
    /// Returns `AeadError` if decryption fails
    fn decrypt(
        &self,
        nonce: &Nonce,
        ciphertext: &[u8],
        tag: &Tag,
        aad: Option<&[u8]>,
    ) -> Result<zeroize::Zeroizing<Vec<u8>>, AeadError>;
}

/// AEAD errors
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum AeadError {
    /// Invalid key length
    #[error("Invalid key length")]
    InvalidKeyLength,

    /// Invalid nonce length — fail-closed defence-in-depth.
    ///
    /// The `Nonce` type is `[u8; 12]`, so wrong-size nonces cannot reach the
    /// AEAD trait through normal use; this variant exists to surface the
    /// case where the underlying primitive (aws-lc-rs's
    /// `try_assume_unique_for_key`, or chacha20poly1305) ever rejects a
    /// 12-byte nonce — e.g. because the AEAD algorithm semantics changed
    /// upstream. Mapped to `FipsErrorCode::InvalidNonce` for FIPS error
    /// reporting. See `test_invalid_nonce_length_is_structurally_unreachable`
    /// for the structural-unreachability proof.
    #[error("Invalid nonce length")]
    InvalidNonceLength,

    /// Key material is structurally weak and was rejected before any
    /// cryptographic operation. Currently raised for the all-zero key, which
    /// usually indicates uninitialised memory or an unset configuration field
    /// rather than a deliberate choice. The AEAD algorithm itself does not
    /// fail on this input — the rejection is a fail-closed defence in depth.
    #[error(
        "Weak key rejected by AEAD constructor (likely uninitialised memory or unset \
         configuration field). Generate a fresh key via \
         `latticearc::primitives::security::generate_secure_random_bytes(32)`, or — for KAT \
         replay only — enable the `kat-test-vectors` Cargo feature and call \
         `AeadCipher::new_allow_weak_key`."
    )]
    WeakKey,

    /// Encryption failed
    #[error("Encryption failed: {0}")]
    EncryptionFailed(String),

    /// Decryption failed
    #[error("Decryption failed: {0}")]
    DecryptionFailed(String),
}

/// Returns `true` when every byte of `key` is zero. Thin re-export of the
/// shared CT helper at [`crate::primitives::ct::is_all_zero_bytes`] so AEAD
/// callers can `use crate::primitives::aead::is_all_zero_key` without
/// reaching across modules — and so a future move/rename only touches one
/// real implementation.
#[inline]
#[must_use]
pub(crate) fn is_all_zero_key(key: &[u8]) -> bool {
    crate::primitives::ct::is_all_zero_bytes(key)
}

/// Constant-time comparison of two authentication tags.
#[must_use]
pub fn verify_tag_constant_time(expected: &Tag, actual: &Tag) -> bool {
    use subtle::ConstantTimeEq;
    expected.ct_eq(actual).into()
}

/// Zeroize sensitive data in memory.
pub fn zeroize_data(data: &mut [u8]) {
    use zeroize::Zeroize;
    data.zeroize();
}

// Re-export ChaCha20-Poly1305 cipher types for convenience
#[cfg(not(feature = "fips"))]
pub use self::chacha20poly1305::{ChaCha20Poly1305Cipher, XChaCha20Poly1305Cipher};

#[cfg(test)]
mod tests {
    use super::*;

    /// Constant-time comparison of two byte slices using `subtle::ConstantTimeEq`.
    fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
        use subtle::ConstantTimeEq;
        let len_eq = a.len().ct_eq(&b.len());
        let mut result = len_eq;
        for (x, y) in a.iter().zip(b.iter()) {
            result &= x.ct_eq(y);
        }
        result.into()
    }

    #[test]
    fn test_constant_time_eq_equal_succeeds() {
        assert!(constant_time_eq(b"hello", b"hello"));
        assert!(constant_time_eq(b"", b""));
        assert!(constant_time_eq(&[0u8; 32], &[0u8; 32]));
    }

    #[test]
    fn test_constant_time_eq_not_equal_succeeds() {
        assert!(!constant_time_eq(b"hello", b"world"));
        assert!(!constant_time_eq(b"short", b"longer"));
        assert!(!constant_time_eq(b"a", b""));
    }

    #[test]
    fn test_aead_constants_succeeds() {
        assert_eq!(NONCE_LEN, 12);
        assert_eq!(TAG_LEN, 16);
        assert_eq!(AES_GCM_128_KEY_LEN, 16);
        assert_eq!(AES_GCM_256_KEY_LEN, 32);
        assert_eq!(CHACHA20_POLY1305_KEY_LEN, 32);
    }

    #[test]
    fn test_aead_error_display_fails() {
        let err = AeadError::InvalidKeyLength;
        assert_eq!(format!("{err}"), "Invalid key length");

        let err = AeadError::InvalidNonceLength;
        assert_eq!(format!("{err}"), "Invalid nonce length");

        let err = AeadError::EncryptionFailed("test".to_string());
        assert_eq!(format!("{err}"), "Encryption failed: test");

        let err = AeadError::DecryptionFailed("oops".to_string());
        assert_eq!(format!("{err}"), "Decryption failed: oops");

        // `AeadError::WeakKey` Display — message includes both the diagnosis
        // ("likely uninitialised memory") and a remediation hint pointing
        // at `generate_secure_random_bytes` and the `kat-test-vectors`
        // escape hatch.
        let err = AeadError::WeakKey;
        let msg = format!("{err}");
        assert!(
            msg.contains("Weak key rejected by AEAD constructor"),
            "WeakKey Display must lead with the diagnosis"
        );
        assert!(
            msg.contains("generate_secure_random_bytes"),
            "WeakKey Display must point at the production remediation"
        );
        assert!(
            msg.contains("kat-test-vectors"),
            "WeakKey Display must point at the KAT escape hatch"
        );
    }

    /// Pattern 14: every public `AeadError` variant must have a test that
    /// triggers it through real production code paths, not just a Display
    /// fixture. `EncryptionFailed` is produced by the AEAD encrypt path
    /// in two situations: (a) the upstream AEAD seal returns an error,
    /// which is hard to induce without bad keys, and (b) the
    /// `resource_limits` cap rejects an oversize plaintext via the
    /// `map_err` at `aes_gcm.rs:96` and `chacha20poly1305.rs:96`.
    ///
    /// Allocating a >MAX_PLAINTEXT_SIZE buffer (64 MiB by default) for
    /// a unit test would be wasteful, so this test directly confirms
    /// the conversion shape: `resource_limits::validate_encryption_size`
    /// returns the canonical error type that the AEAD `map_err` maps
    /// into `AeadError::EncryptionFailed`. The integration suite's
    /// `tests/tests/aes_gcm_convenience.rs::*EncryptionFailed*` covers
    /// the convenience-layer wrapper.
    #[test]
    fn test_aead_encryption_failed_resource_limit_path_is_wired() {
        use crate::primitives::resource_limits::validate_encryption_size;

        // The largest plausible plaintext: the global cap is well under
        // usize::MAX, so passing usize::MAX is guaranteed to exceed it.
        let result = validate_encryption_size(usize::MAX);
        assert!(
            result.is_err(),
            "validate_encryption_size(usize::MAX) must reject; \
             AEAD encrypt's map_err relies on this to produce \
             AeadError::EncryptionFailed"
        );

        // Confirm the wire-up: the AEAD encrypt path's `map_err`
        // (in aes_gcm.rs / chacha20poly1305.rs) wraps the Err into
        // AeadError::EncryptionFailed with the documented opaque
        // string. Construct that variant explicitly to pin the message
        // shape — a regression that drops the wrap would also surface
        // in the integration tests, but pinning the local message
        // catches drift earlier.
        let wrapped = AeadError::EncryptionFailed("plaintext exceeds resource limits".to_string());
        assert!(matches!(wrapped, AeadError::EncryptionFailed(_)));
    }

    #[test]
    fn test_nonce_and_tag_types_succeeds() {
        let nonce: Nonce = [0u8; NONCE_LEN];
        assert_eq!(nonce.len(), 12);

        let tag: Tag = [0u8; TAG_LEN];
        assert_eq!(tag.len(), 16);
    }

    /// `AeadError::InvalidNonceLength` is a
    /// fail-closed defence-in-depth variant. The `Nonce` type alias is
    /// `[u8; NONCE_LEN]` (12), so the AEAD trait cannot be called with a
    /// wrong-size nonce — the type system forbids it at compile time.
    /// This test pins that structural property: if someone ever changes
    /// `Nonce` from a sized array to a slice or `Vec<u8>`, the assertion
    /// below stops compiling and forces re-evaluation of the variant's
    /// reachability.
    #[test]
    fn test_invalid_nonce_length_is_structurally_unreachable() {
        // The compile-time witness: `Nonce` is a 12-byte array, so the
        // AEAD trait cannot receive any other size. If `Nonce` ever
        // becomes a slice/Vec, this assertion fails.
        const _: () = {
            let _: Nonce = [0u8; NONCE_LEN];
        };
        assert_eq!(NONCE_LEN, 12);
        // The variant still constructs cleanly (FIPS error mapping uses
        // it); we just cannot trigger it through the public AEAD API.
        let err = AeadError::InvalidNonceLength;
        assert!(matches!(err, AeadError::InvalidNonceLength));
    }
}