phantom_protocol/crypto/
aes_session.rs1#[cfg(feature = "fips")]
18use aws_lc_rs::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM};
19#[cfg(not(feature = "fips"))]
20use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM};
21use std::sync::atomic::{AtomicU64, Ordering};
22
23pub const AES_GCM_OVERHEAD: usize = 16;
25
26pub struct AesSession {
29 send_key: LessSafeKey,
31 recv_key: LessSafeKey,
33 send_counter: AtomicU64,
35 recv_counter: AtomicU64,
37 nonce_prefix: [u8; 4],
39}
40
41impl AesSession {
42 pub fn from_shared_secret(shared_secret: &[u8; 32]) -> Result<Self, crate::CoreError> {
47 Self::build(shared_secret, false)
48 }
49
50 pub fn from_shared_secret_peer(shared_secret: &[u8; 32]) -> Result<Self, crate::CoreError> {
55 Self::build(shared_secret, true)
56 }
57
58 fn build(shared_secret: &[u8; 32], swap: bool) -> Result<Self, crate::CoreError> {
59 let key_a = zeroize::Zeroizing::new(crate::crypto::kdf::derive_key_32(
65 "phantom-aes-send-v1",
66 shared_secret,
67 ));
68 let key_b = zeroize::Zeroizing::new(crate::crypto::kdf::derive_key_32(
69 "phantom-aes-recv-v1",
70 shared_secret,
71 ));
72
73 let (send_bytes, recv_bytes) = if swap { (key_b, key_a) } else { (key_a, key_b) };
74
75 let send_unbound = UnboundKey::new(&AES_256_GCM, &*send_bytes)
76 .map_err(|_| crate::CoreError::CryptoError("Invalid key".into()))?;
77 let recv_unbound = UnboundKey::new(&AES_256_GCM, &*recv_bytes)
78 .map_err(|_| crate::CoreError::CryptoError("Invalid key".into()))?;
79
80 let prefix_bytes = crate::crypto::kdf::derive_key_32("phantom-nonce-pfx-v1", shared_secret);
81 let mut nonce_prefix = [0u8; 4];
82 nonce_prefix.copy_from_slice(&prefix_bytes[..4]);
83
84 Ok(Self {
85 send_key: LessSafeKey::new(send_unbound),
86 recv_key: LessSafeKey::new(recv_unbound),
87 send_counter: AtomicU64::new(0),
88 recv_counter: AtomicU64::new(0),
89 nonce_prefix,
90 })
91 }
92
93 #[inline]
95 pub fn encrypt_in_place(&self, aad: &[u8], buf: &mut Vec<u8>) -> Result<(), EncryptError> {
96 let counter = self.send_counter.fetch_add(1, Ordering::Relaxed);
97 let nonce = self.make_nonce(counter);
98 self.send_key
99 .seal_in_place_append_tag(nonce, Aad::from(aad), buf)
100 .map_err(|_| EncryptError::EncryptionFailed)?;
101 Ok(())
102 }
103
104 #[inline]
106 pub fn encrypt(&self, aad: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, EncryptError> {
107 let mut buf = Vec::with_capacity(plaintext.len() + AES_GCM_OVERHEAD);
108 buf.extend_from_slice(plaintext);
109 self.encrypt_in_place(aad, &mut buf)?;
110 Ok(buf)
111 }
112
113 #[inline]
115 pub fn decrypt_in_place<'a>(
116 &self,
117 aad: &[u8],
118 buf: &'a mut [u8],
119 ) -> Result<&'a mut [u8], EncryptError> {
120 let counter = self.recv_counter.fetch_add(1, Ordering::Relaxed);
121 let nonce = self.make_nonce(counter);
122 self.recv_key
123 .open_in_place(nonce, Aad::from(aad), buf)
124 .map_err(|_| EncryptError::DecryptionFailed)
125 }
126
127 #[inline]
129 pub fn decrypt(&self, aad: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, EncryptError> {
130 let mut buf = ciphertext.to_vec();
131 let plaintext = self.decrypt_in_place(aad, &mut buf)?;
132 let len = plaintext.len();
133 buf.truncate(len);
134 Ok(buf)
135 }
136
137 #[inline(always)]
138 fn make_nonce(&self, counter: u64) -> Nonce {
139 let mut n = [0u8; 12];
140 n[..4].copy_from_slice(&self.nonce_prefix);
141 n[4..12].copy_from_slice(&counter.to_be_bytes());
142 Nonce::assume_unique_for_key(n)
143 }
144}
145
146#[derive(Debug, Clone, Copy)]
148pub enum EncryptError {
149 EncryptionFailed,
150 DecryptionFailed,
151}
152
153impl std::fmt::Display for EncryptError {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 Self::EncryptionFailed => write!(f, "Encryption failed"),
157 Self::DecryptionFailed => write!(f, "Decryption / authentication failed"),
158 }
159 }
160}
161
162impl std::error::Error for EncryptError {}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn round_trip() {
170 let secret = [0xABu8; 32];
171 let session_a = AesSession::from_shared_secret(&secret).unwrap();
174 let session_b = AesSession::from_shared_secret_peer(&secret).unwrap();
175
176 let msg = b"Hello, PQC world!";
177 let ct = session_a.encrypt(&[], msg).expect("Encryption failed");
178
179 let pt = session_b.decrypt(&[], &ct).expect("Decryption failed");
181 assert_eq!(&pt, msg);
182 }
183
184 #[test]
185 fn throughput_smoke() {
186 use std::time::Instant;
187
188 let session = AesSession::from_shared_secret(&[0xAB; 32]).unwrap();
189 let data = vec![0u8; 64 * 1024];
190 let iters = 50_000;
191
192 let start = Instant::now();
193 for _ in 0..iters {
194 let enc = session.encrypt(&[], &data).expect("Encryption failed");
195 std::hint::black_box(enc);
196 }
197 let elapsed = start.elapsed();
198
199 let total_mb = (data.len() * iters) as f64 / 1024.0 / 1024.0;
200 let throughput = total_mb / elapsed.as_secs_f64();
201 let backend = if cfg!(feature = "fips") {
202 "aws-lc-rs"
203 } else {
204 "ring"
205 };
206 eprintln!("{backend} AES-256-GCM: {throughput:.0} MiB/s");
207 }
209}