feat(crypto,proto): explicit-nonce AeadKey + datagram record codec
Contract for the custom UDP transport (v2): - aura-crypto: AeadKey — ChaCha20-Poly1305 with an EXPLICIT per-message nonce (caller passes the counter), for datagram transports where packets may be lost or reordered. AeadSession::into_parts() hands off (AeadKey, counter). Same nonce scheme as AeadSession, so they interoperate on one key with disjoint counter ranges. +4 tests. - aura-proto: DatagramSender/DatagramReceiver (record = seq(8) || AEAD(frame, aad=seq), sliding replay window) and Session::into_datagram_parts(); reuse for a UDP data path. +1 test. Existing 16 crypto / 13 proto tests still green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,14 @@ impl AeadSession {
|
||||
result.map_err(|_| CryptoError::AeadDecrypt)
|
||||
}
|
||||
|
||||
/// Consume the session, returning a reusable explicit-nonce [`AeadKey`] plus the current
|
||||
/// counter value. Datagram transports use this to continue from the post-handshake counter
|
||||
/// while carrying the nonce on the wire (see [`AeadKey`]).
|
||||
#[must_use]
|
||||
pub fn into_parts(self) -> (AeadKey, u64) {
|
||||
(AeadKey::new(self.key), self.counter)
|
||||
}
|
||||
|
||||
/// Current counter value (next nonce to be used). Test-only accessor.
|
||||
#[cfg(test)]
|
||||
#[must_use]
|
||||
@@ -114,6 +122,79 @@ impl Drop for AeadSession {
|
||||
|
||||
impl zeroize::ZeroizeOnDrop for AeadSession {}
|
||||
|
||||
/// A 256-bit ChaCha20-Poly1305 key used with EXPLICIT per-message nonces.
|
||||
///
|
||||
/// Unlike [`AeadSession`] (which derives the nonce from an internal, lock-step counter), `AeadKey`
|
||||
/// takes the nonce counter as an argument on every call. Datagram transports need exactly this:
|
||||
/// packets may be lost or reordered, so the per-record counter is carried on the wire and supplied
|
||||
/// by the caller rather than tracked implicitly. The nonce scheme is identical to [`AeadSession`]
|
||||
/// (`LE(counter) || 0x0000_0000`), so the two interoperate on the same key as long as their
|
||||
/// counter ranges do not overlap.
|
||||
pub struct AeadKey {
|
||||
key: [u8; 32],
|
||||
}
|
||||
|
||||
impl AeadKey {
|
||||
/// Create a key holder from 256 bits of key material.
|
||||
#[must_use]
|
||||
pub fn new(key: [u8; 32]) -> Self {
|
||||
Self { key }
|
||||
}
|
||||
|
||||
/// Build the cipher instance for the current key.
|
||||
fn cipher(&self) -> ChaCha20Poly1305 {
|
||||
ChaCha20Poly1305::new(Key::from_slice(&self.key))
|
||||
}
|
||||
|
||||
/// Encrypt `plaintext` with associated data `aad` under the nonce derived from `counter`,
|
||||
/// returning `ciphertext || tag`. The caller owns nonce uniqueness: never reuse a `counter`
|
||||
/// value with the same key.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics only if the underlying AEAD reports an error, which for ChaCha20-Poly1305 encryption
|
||||
/// happens solely when the plaintext exceeds the cipher's maximum supported length.
|
||||
#[must_use]
|
||||
pub fn seal(&self, counter: u64, plaintext: &[u8], aad: &[u8]) -> Vec<u8> {
|
||||
let nonce = AeadSession::nonce_for(counter);
|
||||
self.cipher()
|
||||
.encrypt(
|
||||
Nonce::from_slice(&nonce),
|
||||
Payload {
|
||||
msg: plaintext,
|
||||
aad,
|
||||
},
|
||||
)
|
||||
.expect("ChaCha20-Poly1305 encryption never fails for in-range plaintext")
|
||||
}
|
||||
|
||||
/// Decrypt `ciphertext` (`ciphertext || tag`) with associated data `aad` under the nonce
|
||||
/// derived from `counter`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`CryptoError::AeadDecrypt`] if authentication fails (tampered ciphertext, wrong
|
||||
/// AAD, wrong key, or wrong counter).
|
||||
pub fn open(&self, counter: u64, ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
let nonce = AeadSession::nonce_for(counter);
|
||||
self.cipher()
|
||||
.decrypt(
|
||||
Nonce::from_slice(&nonce),
|
||||
Payload {
|
||||
msg: ciphertext,
|
||||
aad,
|
||||
},
|
||||
)
|
||||
.map_err(|_| CryptoError::AeadDecrypt)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AeadKey {
|
||||
fn drop(&mut self) {
|
||||
self.key.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
impl zeroize::ZeroizeOnDrop for AeadKey {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -155,4 +236,50 @@ mod tests {
|
||||
}
|
||||
assert_eq!(seen.len(), 10_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aead_key_explicit_nonce_roundtrip() {
|
||||
let k = AeadKey::new([7u8; 32]);
|
||||
let ct = k.seal(42, b"hello datagram", b"aad");
|
||||
let pt = k.open(42, &ct, b"aad").expect("open at same counter");
|
||||
assert_eq!(pt, b"hello datagram");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aead_key_wrong_counter_or_aad_fails() {
|
||||
let k = AeadKey::new([9u8; 32]);
|
||||
let ct = k.seal(5, b"msg", b"aad");
|
||||
assert!(k.open(6, &ct, b"aad").is_err(), "wrong counter must fail");
|
||||
assert!(k.open(5, &ct, b"other").is_err(), "wrong aad must fail");
|
||||
let mut tampered = ct.clone();
|
||||
tampered[0] ^= 1;
|
||||
assert!(k.open(5, &tampered, b"aad").is_err(), "tamper must fail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aead_key_matches_session_nonce_scheme() {
|
||||
// An AeadKey at counter c must reproduce what an AeadSession produces at counter c.
|
||||
let key = [3u8; 32];
|
||||
let mut s = AeadSession::new(key);
|
||||
let from_session = s.seal(b"x", b"a"); // counter 0, then advances
|
||||
let from_key = AeadKey::new(key).seal(0, b"x", b"a");
|
||||
assert_eq!(from_session, from_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_parts_preserves_key_and_counter() {
|
||||
let mut s = AeadSession::new([5u8; 32]);
|
||||
let _ = s.seal(b"a", b"");
|
||||
let _ = s.seal(b"b", b""); // counter now 2
|
||||
let (key, counter) = s.into_parts();
|
||||
assert_eq!(counter, 2);
|
||||
// The recovered key, used at the next counter, matches a fresh session advanced to 2.
|
||||
let expect = {
|
||||
let mut s2 = AeadSession::new([5u8; 32]);
|
||||
let _ = s2.seal(b"a", b"");
|
||||
let _ = s2.seal(b"b", b"");
|
||||
s2.seal(b"c", b"d")
|
||||
};
|
||||
assert_eq!(key.seal(counter, b"c", b"d"), expect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ pub mod aead;
|
||||
pub mod kdf;
|
||||
pub mod kem;
|
||||
|
||||
pub use aead::AeadSession;
|
||||
pub use aead::{AeadKey, AeadSession};
|
||||
pub use kdf::{derive_session_keys, SessionKeys};
|
||||
pub use kem::{HybridCiphertext, HybridPrivateKey, HybridPublicKey, HybridSharedSecret};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user