use crate::metrics::OperationMetrics;
#[cfg(not(target_arch = "wasm32"))]
use ring::{
aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM},
rand::{SecureRandom, SystemRandom},
};
#[cfg(target_arch = "wasm32")]
use aes_gcm::{
aead::{Aead, KeyInit, Payload},
Aes256Gcm, Nonce as AesGcmNonce,
};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::{Arc, LazyLock, Mutex};
#[cfg(target_arch = "wasm32")]
use std::sync::{Arc, Mutex};
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
use thiserror::Error;
#[cfg(not(target_arch = "wasm32"))]
static GLOBAL_INSTANCE_COUNTER: LazyLock<AtomicU64> = LazyLock::new(|| {
let rng = SystemRandom::new();
let mut random_seed = [0u8; 4];
rng.fill(&mut random_seed)
.expect("SystemRandom::fill failed during GLOBAL_INSTANCE_COUNTER initialization");
let seed = u32::from_be_bytes(random_seed) as u64;
AtomicU64::new(seed << 32)
});
#[cfg(target_arch = "wasm32")]
thread_local! {
static WASM_INSTANCE_COUNTER: std::cell::Cell<u64> = {
let mut seed_bytes = [0u8; 4];
getrandom::getrandom(&mut seed_bytes)
.expect("getrandom failed during WASM_INSTANCE_COUNTER initialization");
let seed = u32::from_be_bytes(seed_bytes) as u64;
std::cell::Cell::new(seed << 32)
};
}
fn next_instance_id() -> u64 {
#[cfg(not(target_arch = "wasm32"))]
{
GLOBAL_INSTANCE_COUNTER.fetch_add(1, Ordering::SeqCst)
}
#[cfg(target_arch = "wasm32")]
{
WASM_INSTANCE_COUNTER.with(|c| {
let id = c.get();
c.set(id.wrapping_add(1));
id
})
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use std::arch::is_x86_feature_detected;
#[derive(Error, Debug)]
pub enum EncryptionError {
#[error("Invalid key length: expected 32 bytes, got {0}")]
InvalidKeyLength(usize),
#[error("Invalid nonce length: expected 12 bytes, got {0}")]
InvalidNonceLength(usize),
#[error("Encryption failed: {0}")]
EncryptionFailed(String),
#[error("Decryption failed: {0}")]
DecryptionFailed(String),
#[error("Random number generation failed")]
RngFailure,
#[error("Invalid ciphertext format: {0}")]
InvalidCiphertext(String),
#[error("Invalid encryption header: {0}")]
InvalidHeader(String),
#[error("Unsupported encryption version: {0}")]
UnsupportedVersion(u8),
#[error("Unsupported encryption algorithm: {0}")]
UnsupportedAlgorithm(u8),
#[error("Authentication verification failed")]
AuthenticationFailed,
#[error("Nonce counter exhausted - key rotation required")]
NonceCounterExhausted,
#[error("Key rotation not yet implemented")]
NotImplemented(String),
}
pub struct ZeroKnowledgeEncryptor {
hardware_acceleration_detected: bool,
#[cfg(not(target_arch = "wasm32"))]
nonce_counter: AtomicU64,
#[cfg(target_arch = "wasm32")]
nonce_counter: std::cell::Cell<u64>,
instance_id: u64,
last_metrics: Arc<Mutex<OperationMetrics>>,
}
impl ZeroKnowledgeEncryptor {
pub fn new() -> Result<Self, EncryptionError> {
let hardware_acceleration_detected = Self::detect_hardware_acceleration();
let instance_id = next_instance_id();
Ok(Self {
hardware_acceleration_detected,
#[cfg(not(target_arch = "wasm32"))]
nonce_counter: AtomicU64::new(0),
#[cfg(target_arch = "wasm32")]
nonce_counter: std::cell::Cell::new(0),
instance_id,
last_metrics: Arc::new(Mutex::new(OperationMetrics::new())),
})
}
pub fn get_instance_id(&self) -> u64 {
self.instance_id
}
fn detect_hardware_acceleration() -> bool {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
#[cfg(target_feature = "aes")]
return true;
#[cfg(not(target_feature = "aes"))]
{
if is_x86_feature_detected!("aes") {
return true;
}
false
}
}
#[cfg(target_arch = "aarch64")]
{
#[cfg(target_feature = "aes")]
{
true
}
#[cfg(not(target_feature = "aes"))]
{
return cfg!(target_feature = "neon");
}
}
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
false
}
pub fn hardware_acceleration_enabled(&self) -> bool {
self.hardware_acceleration_detected
}
fn generate_nonce(&self) -> Result<[u8; 12], EncryptionError> {
#[cfg(not(target_arch = "wasm32"))]
let counter = self.nonce_counter.fetch_add(1, Ordering::SeqCst);
#[cfg(target_arch = "wasm32")]
let counter = {
let c = self.nonce_counter.get();
self.nonce_counter.set(c.wrapping_add(1));
c
};
if counter >= u32::MAX as u64 {
return Err(EncryptionError::NonceCounterExhausted);
}
let mut nonce_bytes = [0u8; 12];
nonce_bytes[0..8].copy_from_slice(&self.instance_id.to_be_bytes());
nonce_bytes[8..12].copy_from_slice(&(counter as u32).to_be_bytes());
Ok(nonce_bytes)
}
pub fn get_nonce_counter(&self) -> u64 {
#[cfg(not(target_arch = "wasm32"))]
{
self.nonce_counter.load(Ordering::SeqCst)
}
#[cfg(target_arch = "wasm32")]
{
self.nonce_counter.get()
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn encrypt_aes_gcm(
&self,
plaintext: &[u8],
key: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, EncryptionError> {
let encryption_start = Instant::now();
if key.len() != 32 {
return Err(EncryptionError::InvalidKeyLength(key.len()));
}
let unbound_key = UnboundKey::new(&AES_256_GCM, key)
.map_err(|_| EncryptionError::EncryptionFailed("Invalid key".into()))?;
let aead_key = LessSafeKey::new(unbound_key);
let nonce_bytes = self.generate_nonce()?;
let nonce = Nonce::assume_unique_for_key(nonce_bytes);
let mut ciphertext = Vec::from(plaintext);
aead_key
.seal_in_place_append_tag(nonce, Aad::from(aad), &mut ciphertext)
.map_err(|e| {
EncryptionError::EncryptionFailed(format!("AES-GCM encryption failed: {:?}", e))
})?;
let mut result = Vec::with_capacity(12 + ciphertext.len());
result.extend_from_slice(&nonce_bytes);
result.extend_from_slice(&ciphertext);
let encryption_micros = encryption_start.elapsed().as_micros() as u64;
if let Ok(mut metrics) = self.last_metrics.lock() {
*metrics = OperationMetrics::new()
.with_encryption(encryption_micros, self.hardware_acceleration_detected);
}
Ok(result)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn decrypt_aes_gcm(
&self,
ciphertext: &[u8],
key: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, EncryptionError> {
let decryption_start = Instant::now();
if key.len() != 32 {
return Err(EncryptionError::InvalidKeyLength(key.len()));
}
if ciphertext.len() < 12 + 16 {
return Err(EncryptionError::InvalidCiphertext(
"Ciphertext too short".into(),
));
}
let nonce_bytes: [u8; 12] = ciphertext[..12]
.try_into()
.map_err(|_| EncryptionError::InvalidNonceLength(ciphertext.len().min(12)))?;
let nonce = Nonce::assume_unique_for_key(nonce_bytes);
let encrypted_data = &ciphertext[12..];
let unbound_key = UnboundKey::new(&AES_256_GCM, key)
.map_err(|_| EncryptionError::DecryptionFailed("Invalid key".into()))?;
let aead_key = LessSafeKey::new(unbound_key);
let mut plaintext = Vec::from(encrypted_data);
let decrypted_len = aead_key
.open_in_place(nonce, Aad::from(aad), &mut plaintext)
.map_err(|_e| {
EncryptionError::AuthenticationFailed
})?
.len();
plaintext.truncate(decrypted_len);
let decryption_micros = decryption_start.elapsed().as_micros() as u64;
if let Ok(mut metrics) = self.last_metrics.lock() {
*metrics = OperationMetrics::new()
.with_encryption(decryption_micros, self.hardware_acceleration_detected);
}
Ok(plaintext)
}
#[cfg(all(test, not(target_arch = "wasm32")))]
pub fn generate_key(&self) -> Result<[u8; 32], EncryptionError> {
let rng = SystemRandom::new();
let mut key = [0u8; 32];
rng.fill(&mut key)
.map_err(|_| EncryptionError::RngFailure)?;
Ok(key)
}
pub fn get_last_metrics(&self) -> OperationMetrics {
self.last_metrics
.lock()
.map(|metrics| metrics.clone())
.unwrap_or_else(|_| OperationMetrics::new())
}
pub fn rotate_key(&mut self, _new_master_key: &[u8]) -> Result<(), EncryptionError> {
Err(EncryptionError::NotImplemented(
"Key rotation will be implemented in a future release with gradual migration support"
.into(),
))
}
#[cfg(target_arch = "wasm32")]
pub fn encrypt_aes_gcm(
&self,
plaintext: &[u8],
key: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, EncryptionError> {
if key.len() != 32 {
return Err(EncryptionError::InvalidKeyLength(key.len()));
}
let nonce_bytes = self.generate_nonce()?;
let cipher = Aes256Gcm::new_from_slice(key)
.map_err(|e| EncryptionError::EncryptionFailed(format!("key error: {e}")))?;
let nonce = AesGcmNonce::from_slice(&nonce_bytes);
let ciphertext_with_tag = cipher
.encrypt(
nonce,
Payload {
msg: plaintext,
aad,
},
)
.map_err(|e| {
EncryptionError::EncryptionFailed(format!("AES-GCM encrypt failed: {e}"))
})?;
let mut result = Vec::with_capacity(12 + ciphertext_with_tag.len());
result.extend_from_slice(&nonce_bytes);
result.extend_from_slice(&ciphertext_with_tag);
if let Ok(mut metrics) = self.last_metrics.lock() {
*metrics = OperationMetrics::new().with_encryption(0u64, false);
}
Ok(result)
}
#[cfg(target_arch = "wasm32")]
pub fn decrypt_aes_gcm(
&self,
ciphertext: &[u8],
key: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, EncryptionError> {
if key.len() != 32 {
return Err(EncryptionError::InvalidKeyLength(key.len()));
}
if ciphertext.len() < 12 + 16 {
return Err(EncryptionError::InvalidCiphertext(
"Ciphertext too short".into(),
));
}
let nonce_bytes = &ciphertext[..12];
let encrypted_data = &ciphertext[12..];
let cipher = Aes256Gcm::new_from_slice(key)
.map_err(|e| EncryptionError::DecryptionFailed(format!("key error: {e}")))?;
let nonce = AesGcmNonce::from_slice(nonce_bytes);
let plaintext = cipher
.decrypt(
nonce,
Payload {
msg: encrypted_data,
aad,
},
)
.map_err(|_| EncryptionError::AuthenticationFailed)?;
if let Ok(mut metrics) = self.last_metrics.lock() {
*metrics = OperationMetrics::new().with_encryption(0u64, false);
}
Ok(plaintext)
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
#[test]
fn test_encrypt_decrypt_roundtrip() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let key = encryptor.generate_key().unwrap();
let plaintext = b"Hello, zero-knowledge world!";
let aad = b"domain_separation_context";
let ciphertext = encryptor.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
let decrypted = encryptor.decrypt_aes_gcm(&ciphertext, &key, aad).unwrap();
assert_eq!(plaintext, decrypted.as_slice());
}
#[test]
fn test_different_keys_fail() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let key1 = encryptor.generate_key().unwrap();
let key2 = encryptor.generate_key().unwrap();
let plaintext = b"secret data";
let aad = b"context";
let ciphertext = encryptor.encrypt_aes_gcm(plaintext, &key1, aad).unwrap();
let result = encryptor.decrypt_aes_gcm(&ciphertext, &key2, aad);
assert!(matches!(result, Err(EncryptionError::AuthenticationFailed)));
}
#[test]
fn test_different_aad_fails() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let key = encryptor.generate_key().unwrap();
let plaintext = b"secret data";
let aad1 = b"context1";
let aad2 = b"context2";
let ciphertext = encryptor.encrypt_aes_gcm(plaintext, &key, aad1).unwrap();
let result = encryptor.decrypt_aes_gcm(&ciphertext, &key, aad2);
assert!(matches!(result, Err(EncryptionError::AuthenticationFailed)));
}
#[test]
fn test_invalid_key_length() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let short_key = [0u8; 16]; let plaintext = b"test";
let aad = b"context";
let result = encryptor.encrypt_aes_gcm(plaintext, &short_key, aad);
assert!(matches!(result, Err(EncryptionError::InvalidKeyLength(16))));
}
#[test]
fn test_corrupted_ciphertext() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let key = encryptor.generate_key().unwrap();
let plaintext = b"secret data";
let aad = b"context";
let mut ciphertext = encryptor.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
ciphertext[20] ^= 1;
let result = encryptor.decrypt_aes_gcm(&ciphertext, &key, aad);
assert!(matches!(result, Err(EncryptionError::AuthenticationFailed)));
}
#[test]
fn test_nonce_uniqueness() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let key = encryptor.generate_key().unwrap();
let plaintext = b"test data";
let aad = b"context";
let ciphertext1 = encryptor.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
let ciphertext2 = encryptor.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
assert_ne!(ciphertext1, ciphertext2);
let decrypted1 = encryptor.decrypt_aes_gcm(&ciphertext1, &key, aad).unwrap();
let decrypted2 = encryptor.decrypt_aes_gcm(&ciphertext2, &key, aad).unwrap();
assert_eq!(decrypted1, plaintext);
assert_eq!(decrypted2, plaintext);
}
#[test]
fn test_metrics_collection_on_encrypt() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let key = encryptor.generate_key().unwrap();
let plaintext = b"test data for encryption metrics";
let aad = b"context";
encryptor.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
let metrics = encryptor.get_last_metrics();
assert!(metrics.encryption_time_micros.is_some());
}
#[test]
fn test_metrics_collection_on_decrypt() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let key = encryptor.generate_key().unwrap();
let plaintext = b"test data for decryption metrics";
let aad = b"context";
let ciphertext = encryptor.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
encryptor.decrypt_aes_gcm(&ciphertext, &key, aad).unwrap();
let metrics = encryptor.get_last_metrics();
assert!(metrics.encryption_time_micros.is_some());
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_nonce_exhaustion_at_boundary() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let key = [0x42u8; 32];
let plaintext = b"test";
let aad = b"test_domain";
encryptor
.nonce_counter
.store(u32::MAX as u64 - 2, Ordering::SeqCst);
let result1 = encryptor.encrypt_aes_gcm(plaintext, &key, aad);
assert!(result1.is_ok(), "Encryption at u32::MAX-2 should succeed");
let result2 = encryptor.encrypt_aes_gcm(plaintext, &key, aad);
assert!(result2.is_ok(), "Encryption at u32::MAX-1 should succeed");
let result3 = encryptor.encrypt_aes_gcm(plaintext, &key, aad);
assert!(
matches!(result3, Err(EncryptionError::NonceCounterExhausted)),
"Encryption at u32::MAX should fail with NonceCounterExhausted: {:?}",
result3
);
let final_counter = encryptor.get_nonce_counter();
assert_eq!(
final_counter,
u32::MAX as u64 + 1,
"Counter should be at u32::MAX + 1 after 3 attempts"
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_counter_no_wraparound_after_exhaustion() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let key = [0x42u8; 32];
let plaintext = b"test";
let aad = b"test_domain";
encryptor
.nonce_counter
.store(u32::MAX as u64, Ordering::SeqCst);
let result1 = encryptor.encrypt_aes_gcm(plaintext, &key, aad);
assert!(
matches!(result1, Err(EncryptionError::NonceCounterExhausted)),
"First encryption at u32::MAX should fail"
);
let counter_after_first = encryptor.get_nonce_counter();
assert!(
counter_after_first > u32::MAX as u64,
"Counter must NOT wrap after exhaustion (got {}, expected > {})",
counter_after_first,
u32::MAX
);
let result2 = encryptor.encrypt_aes_gcm(plaintext, &key, aad);
assert!(
matches!(result2, Err(EncryptionError::NonceCounterExhausted)),
"Second encryption after exhaustion should also fail"
);
let result3 = encryptor.encrypt_aes_gcm(plaintext, &key, aad);
assert!(
matches!(result3, Err(EncryptionError::NonceCounterExhausted)),
"Third encryption after exhaustion should also fail"
);
let final_counter = encryptor.get_nonce_counter();
assert!(
final_counter > counter_after_first,
"Counter should continue incrementing past exhaustion"
);
println!(
"Counter after exhaustion: {} (properly stays above u32::MAX = {})",
final_counter,
u32::MAX
);
}
#[test]
fn test_multi_handle_nonce_collision_detection() {
use std::collections::HashSet;
let enc1 = ZeroKnowledgeEncryptor::new().unwrap();
let enc2 = ZeroKnowledgeEncryptor::new().unwrap();
assert_ne!(
enc1.get_instance_id(),
enc2.get_instance_id(),
"Instance IDs must be unique"
);
let shared_key = [0xABu8; 32];
let aad = b"test_domain";
let plaintext = b"secret data";
let mut all_nonces: HashSet<[u8; 12]> = HashSet::new();
for _ in 0..100 {
let ciphertext = enc1.encrypt_aes_gcm(plaintext, &shared_key, aad).unwrap();
let nonce: [u8; 12] = ciphertext[..12].try_into().unwrap();
assert!(
all_nonces.insert(nonce),
"Nonce collision detected within same encryptor (impossible!)"
);
}
for _ in 0..100 {
let ciphertext = enc2.encrypt_aes_gcm(plaintext, &shared_key, aad).unwrap();
let nonce: [u8; 12] = ciphertext[..12].try_into().unwrap();
assert!(
all_nonces.insert(nonce),
"SECURITY VIOLATION: Nonce collision detected! \
This should be impossible with deterministic instance_id. \
Nonce: {:02x?}",
nonce
);
}
println!(
"Collected {} unique nonces across 2 encryptors (deterministically unique)",
all_nonces.len()
);
}
#[test]
fn test_nonce_structure_verification() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let key = [0xCDu8; 32];
let aad = b"structure_test";
let plaintext = b"test";
let ct1 = encryptor.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
let nonce1: [u8; 12] = ct1[..12].try_into().unwrap();
let instance_id_bytes = &nonce1[..8];
let instance_id = u64::from_be_bytes(instance_id_bytes.try_into().unwrap());
let counter = u32::from_be_bytes(nonce1[8..12].try_into().unwrap());
assert_eq!(
instance_id,
encryptor.get_instance_id(),
"Nonce instance_id must match encryptor's instance_id"
);
assert_eq!(counter, 0, "First counter should be 0");
println!(
"Nonce structure: instance_id={}, counter={}",
instance_id, counter
);
let ct2 = encryptor.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
let nonce2: [u8; 12] = ct2[..12].try_into().unwrap();
let counter2 = u32::from_be_bytes(nonce2[8..12].try_into().unwrap());
assert_eq!(counter2, 1, "Second counter should be 1");
assert_ne!(nonce1, nonce2, "Nonces must be unique");
let instance_id2 = u64::from_be_bytes(nonce2[..8].try_into().unwrap());
assert_eq!(
instance_id, instance_id2,
"Instance ID must be constant within an encryptor"
);
}
#[test]
fn test_deterministic_instance_id_uniqueness() {
use crate::encryption::key_derivation::key_fingerprint;
let key = [0xEFu8; 32];
let key_fp = key_fingerprint(&key);
let enc1 = ZeroKnowledgeEncryptor::new().unwrap();
let enc2 = ZeroKnowledgeEncryptor::new().unwrap();
let enc3 = ZeroKnowledgeEncryptor::new().unwrap();
let id1 = enc1.get_instance_id();
let id2 = enc2.get_instance_id();
let id3 = enc3.get_instance_id();
assert!(id1 < id2, "Instance IDs must be monotonically increasing");
assert!(id2 < id3, "Instance IDs must be monotonically increasing");
assert_ne!(id1, id2, "Instance IDs must be unique");
assert_ne!(id2, id3, "Instance IDs must be unique");
assert_ne!(id1, id3, "Instance IDs must be unique");
println!("Key fingerprint: {:02x?}", key_fp);
println!(
"Instance IDs: {} < {} < {} (monotonic, deterministic)",
id1, id2, id3
);
}
#[test]
fn test_encryptor_nonce_space_isolation() {
let enc1 = ZeroKnowledgeEncryptor::new().unwrap();
let enc2 = ZeroKnowledgeEncryptor::new().unwrap();
let enc3 = ZeroKnowledgeEncryptor::new().unwrap();
let key = [0x11u8; 32];
let aad = b"instance_test";
let plaintext = b"test";
let ct1 = enc1.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
let ct2 = enc2.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
let ct3 = enc3.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
let nonce1: [u8; 12] = ct1[..12].try_into().unwrap();
let nonce2: [u8; 12] = ct2[..12].try_into().unwrap();
let nonce3: [u8; 12] = ct3[..12].try_into().unwrap();
let id1_bytes = &nonce1[..8];
let id2_bytes = &nonce2[..8];
let id3_bytes = &nonce3[..8];
assert_eq!(&nonce1[8..12], &[0, 0, 0, 0], "Counter should be 0");
assert_eq!(&nonce2[8..12], &[0, 0, 0, 0], "Counter should be 0");
assert_eq!(&nonce3[8..12], &[0, 0, 0, 0], "Counter should be 0");
assert_ne!(
id1_bytes, id2_bytes,
"Instance IDs must be unique (enc1 vs enc2)"
);
assert_ne!(
id1_bytes, id3_bytes,
"Instance IDs must be unique (enc1 vs enc3)"
);
assert_ne!(
id2_bytes, id3_bytes,
"Instance IDs must be unique (enc2 vs enc3)"
);
assert_eq!(
u64::from_be_bytes(id1_bytes.try_into().unwrap()),
enc1.get_instance_id()
);
assert_eq!(
u64::from_be_bytes(id2_bytes.try_into().unwrap()),
enc2.get_instance_id()
);
assert_eq!(
u64::from_be_bytes(id3_bytes.try_into().unwrap()),
enc3.get_instance_id()
);
println!("Instance IDs are deterministically unique:");
println!(
" enc1: {:02x?} (instance_id={})",
id1_bytes,
enc1.get_instance_id()
);
println!(
" enc2: {:02x?} (instance_id={})",
id2_bytes,
enc2.get_instance_id()
);
println!(
" enc3: {:02x?} (instance_id={})",
id3_bytes,
enc3.get_instance_id()
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_concurrent_nonce_exhaustion() {
use std::sync::Arc;
use std::thread;
let encryptor = Arc::new(ZeroKnowledgeEncryptor::new().unwrap());
let key = [0x5au8; 32];
let plaintext = b"concurrent test";
let aad = b"concurrent_domain";
let start_counter = u32::MAX as u64 - 50;
encryptor
.nonce_counter
.store(start_counter, Ordering::SeqCst);
let mut handles = vec![];
let success_count = Arc::new(std::sync::atomic::AtomicU64::new(0));
let exhaustion_count = Arc::new(std::sync::atomic::AtomicU64::new(0));
for _ in 0..10 {
let encryptor = Arc::clone(&encryptor);
let success = Arc::clone(&success_count);
let exhausted = Arc::clone(&exhaustion_count);
let handle = thread::spawn(move || {
for _ in 0..10 {
match encryptor.encrypt_aes_gcm(plaintext, &key, aad) {
Ok(_) => {
success.fetch_add(1, Ordering::SeqCst);
}
Err(EncryptionError::NonceCounterExhausted) => {
exhausted.fetch_add(1, Ordering::SeqCst);
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("Thread should complete");
}
let total_success = success_count.load(Ordering::SeqCst);
let total_exhausted = exhaustion_count.load(Ordering::SeqCst);
assert_eq!(
total_success + total_exhausted,
100,
"All 100 operations must complete (success + exhaustion)"
);
assert_eq!(
total_success, 50,
"Exactly 50 operations should succeed (started 50 from limit)"
);
assert_eq!(
total_exhausted, 50,
"Exactly 50 operations should fail with exhaustion"
);
println!(
"✓ Concurrent exhaustion: {} successes, {} exhaustions (total 100)",
total_success, total_exhausted
);
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
#[kani::proof]
#[kani::unwind(3)]
fn verify_key_length_validation() {
let key_len: usize = kani::any();
kani::assume(key_len < 100);
let is_valid_length = key_len == 32;
let would_accept = key_len == 32;
assert_eq!(is_valid_length, would_accept);
}
#[kani::proof]
#[kani::unwind(3)]
fn verify_ciphertext_minimum_size() {
let ciphertext_len: usize = kani::any();
kani::assume(ciphertext_len < 100);
const MIN_CIPHERTEXT_SIZE: usize = 12 + 16;
let is_too_small = ciphertext_len < MIN_CIPHERTEXT_SIZE;
let would_reject = ciphertext_len < MIN_CIPHERTEXT_SIZE;
assert_eq!(is_too_small, would_reject);
}
#[kani::proof]
#[kani::unwind(3)]
fn verify_nonce_extraction_safety() {
let ciphertext_len: usize = kani::any();
kani::assume(ciphertext_len >= 12 + 16 && ciphertext_len < 100);
let nonce_end = 12;
assert!(nonce_end <= ciphertext_len);
let data_start = 12;
assert!(data_start <= ciphertext_len);
}
#[kani::proof]
#[kani::unwind(3)]
fn verify_aad_domain_separation() {
let aad1_len: usize = kani::any();
let aad2_len: usize = kani::any();
kani::assume(aad1_len < 100);
kani::assume(aad2_len < 100);
if aad1_len != aad2_len {
let are_different = aad1_len != aad2_len;
assert!(are_different);
}
}
#[kani::proof]
#[kani::unwind(3)]
fn verify_ciphertext_format_structure() {
let plaintext_len: usize = kani::any();
kani::assume(plaintext_len < 200);
const NONCE_SIZE: usize = 12;
const TAG_SIZE: usize = 16;
let expected_ciphertext_len = NONCE_SIZE + plaintext_len + TAG_SIZE;
assert!(expected_ciphertext_len >= NONCE_SIZE);
assert!(expected_ciphertext_len >= plaintext_len);
assert!(expected_ciphertext_len >= TAG_SIZE);
}
#[kani::proof]
#[kani::unwind(3)]
fn verify_tamper_detection_logic() {
let was_modified: bool = kani::any();
if was_modified {
let should_fail_auth = true;
assert!(should_fail_auth);
}
}
}