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