ant_quic/
token_v2.rs

1//! Token v2: bind address-validation tokens to (PeerId || CID || nonce).
2//!
3//! This module provides secure token encryption using AES-256-GCM with proper
4//! authenticated encryption. Tokens are bound to specific peer IDs and connection
5//! IDs to prevent token replay and spoofing attacks.
6//!
7//! Security features:
8//! - AES-256-GCM authenticated encryption
9//! - 12-byte nonces for uniqueness
10//! - Authentication tags to prevent tampering
11//! - Proper nonce handling to avoid reuse
12//!
13//! Not wired into transport yet; used by tests and for upcoming integration.
14#![allow(missing_docs)]
15
16// This module requires at least one crypto provider
17// It will only be compiled when ring or aws-lc-rs features are enabled
18
19use rand::RngCore;
20
21use crate::{nat_traversal_api::PeerId, shared::ConnectionId};
22
23use aws_lc_rs::aead::{AES_256_GCM, Aad, LessSafeKey, NONCE_LEN, Nonce, UnboundKey};
24
25/// A 256-bit key used for encrypting and authenticating retry tokens.
26/// Used with AES-256-GCM for authenticated encryption of token contents.
27#[derive(Clone)]
28pub struct TokenKey(pub [u8; 32]);
29
30/// The decoded contents of a retry token after successful decryption and validation.
31/// Contains the peer identity, connection ID, and nonce used for address validation.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct RetryTokenDecoded {
34    /// The peer ID that the token was issued for.
35    pub peer_id: PeerId,
36    /// The connection ID associated with this token.
37    pub cid: ConnectionId,
38    /// A unique nonce to prevent replay attacks.
39    pub nonce: u128,
40}
41
42/// Generate a random token key for testing purposes.
43/// Fills a 32-byte array with random data from the provided RNG.
44pub fn test_key_from_rng(rng: &mut dyn RngCore) -> TokenKey {
45    let mut k = [0u8; 32];
46    rng.fill_bytes(&mut k);
47    TokenKey(k)
48}
49
50/// Encode a retry token containing peer ID, connection ID, and a fresh nonce.
51/// Encrypts the token contents using AES-256-GCM with the provided key.
52/// Returns the encrypted token as bytes, including authentication tag and nonce.
53pub fn encode_retry_token_with_rng<R: RngCore>(
54    key: &TokenKey,
55    peer_id: &PeerId,
56    cid: &ConnectionId,
57    rng: &mut R,
58) -> Vec<u8> {
59    let mut nonce_bytes = [0u8; 12]; // AES-GCM standard nonce length is 12 bytes
60    rng.fill_bytes(&mut nonce_bytes);
61
62    let mut pt = Vec::with_capacity(32 + 1 + crate::MAX_CID_SIZE + 12);
63    pt.extend_from_slice(&peer_id.0);
64    pt.push(cid.len() as u8);
65    pt.extend_from_slice(&cid[..]);
66    pt.extend_from_slice(&nonce_bytes); // Include nonce in plaintext for binding
67    seal(&key.0, &nonce_bytes, &pt)
68}
69
70pub fn encode_retry_token(key: &TokenKey, peer_id: &PeerId, cid: &ConnectionId) -> Vec<u8> {
71    encode_retry_token_with_rng(key, peer_id, cid, &mut rand::thread_rng())
72}
73
74/// Decode and validate a retry token, returning the contained peer information.
75/// Decrypts the token using the provided key and validates the contents.
76/// Returns None if decryption fails or the token format is invalid.
77pub fn decode_retry_token(key: &TokenKey, token: &[u8]) -> Option<RetryTokenDecoded> {
78    // Use last 12 bytes (nonce suffix) for AEAD open
79    let (ct, nonce_suffix) = token.split_at(token.len().checked_sub(12)?);
80    let mut nonce12 = [0u8; 12];
81    nonce12.copy_from_slice(nonce_suffix);
82    let plaintext = open(&key.0, &nonce12, ct).ok()?;
83    if plaintext.len() < 32 + 1 + 12 {
84        return None;
85    } // Expect 12-byte nonce in plaintext
86    let mut off = 0usize;
87    let mut pid = [0u8; 32];
88    pid.copy_from_slice(&plaintext[off..off + 32]);
89    off += 32;
90    let cid_len = plaintext[off] as usize;
91    off += 1;
92    if plaintext.len() < off + cid_len + 12 {
93        return None;
94    }
95    let mut cid_buf = [0u8; crate::MAX_CID_SIZE];
96    cid_buf[..cid_len].copy_from_slice(&plaintext[off..off + cid_len]);
97    let cid = ConnectionId::new(&cid_buf[..cid_len]);
98    off += cid_len;
99    let mut nonce_arr = [0u8; 12];
100    nonce_arr.copy_from_slice(&plaintext[off..off + 12]);
101    let mut nonce_bytes_16 = [0u8; 16];
102    nonce_bytes_16[..12].copy_from_slice(&nonce_arr);
103    let nonce = u128::from_le_bytes(nonce_bytes_16); // Convert 12 bytes to u128 (pad with zeros)
104    Some(RetryTokenDecoded {
105        peer_id: PeerId(pid),
106        cid,
107        nonce,
108    })
109}
110
111/// Validate a retry/validation token against the expected peer and connection ID.
112/// Returns `true` if the token decodes and matches both identifiers.
113pub fn validate_token(
114    key: &TokenKey,
115    token: &[u8],
116    expected_peer: &PeerId,
117    expected_cid: &ConnectionId,
118) -> bool {
119    match decode_retry_token(key, token) {
120        Some(dec) => dec.peer_id == *expected_peer && dec.cid == *expected_cid,
121        None => false,
122    }
123}
124
125/// Encrypt plaintext using AES-256-GCM with the provided key and nonce.
126/// Returns the ciphertext with authentication tag and nonce suffix.
127#[allow(clippy::expect_used, clippy::let_unit_value)]
128fn seal(key: &[u8; 32], nonce: &[u8; 12], pt: &[u8]) -> Vec<u8> {
129    let unbound_key = UnboundKey::new(&AES_256_GCM, key).expect("invalid key length");
130    let key = LessSafeKey::new(unbound_key);
131
132    // Store nonce bytes for later use before creating Nonce object
133    let nonce_bytes = *nonce;
134
135    // Use 12-byte nonce for AES-GCM encryption
136    let nonce = Nonce::try_assume_unique_for_key(&nonce_bytes).expect("invalid nonce length");
137
138    let mut in_out = pt.to_vec();
139    key.seal_in_place_append_tag(nonce, Aad::empty(), &mut in_out)
140        .expect("encryption failed");
141
142    // Append the full 12-byte nonce as suffix (standard for QUIC tokens)
143    in_out.extend_from_slice(&nonce_bytes);
144    in_out
145}
146
147/// Decrypt ciphertext using AES-256-GCM with the provided key and nonce suffix.
148/// Returns the decrypted plaintext on success, or error if decryption fails.
149///
150/// Security: Uses the same 12-byte nonce as encryption to maintain consistency.
151/// The nonce is extracted from the token suffix and used directly for decryption.
152/// Authentication failure (tampered ciphertext) will result in an error.
153fn open(key: &[u8; 32], nonce12: &[u8; 12], ct_without_suffix: &[u8]) -> Result<Vec<u8>, ()> {
154    let unbound_key = UnboundKey::new(&AES_256_GCM, key).map_err(|_| ())?;
155    let key = LessSafeKey::new(unbound_key);
156
157    // Use the 12-byte nonce directly (same as encryption)
158    // This ensures nonce consistency between encrypt/decrypt operations
159    let nonce = Nonce::try_assume_unique_for_key(nonce12).map_err(|_| ())?;
160
161    let mut in_out = ct_without_suffix.to_vec();
162    key.open_in_place(nonce, Aad::empty(), &mut in_out)
163        .map_err(|_| ())?;
164
165    // Remove the authentication tag (16 bytes) from the end
166    in_out.truncate(in_out.len() - 16);
167    Ok(in_out)
168}