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