Skip to content

Commit 59f90df

Browse files
committed
fix: resolve expert panel findings for wasm32 compat
- Fold aes-gcm/getrandom into encryption feature (no separate wasm flag needed) - Enable zeroize on aes/aes-gcm deps (scrub round keys on drop) - Add cross-backend wire format test (ring <-> aes-gcm roundtrip) - Remove dead cfg guards inside native-only functions - Consistency: error messages, min length expression, doc comments
1 parent 0defd5d commit 59f90df

3 files changed

Lines changed: 67 additions & 24 deletions

File tree

‎Cargo.toml‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ generic-array = { version = "0.14", optional = true }
4949
getrandom = { version = "0.2", features = ["js"], optional = true }
5050

5151
# RustCrypto: pure-Rust AES-256-GCM for wasm32 targets (ring requires clang + C asm)
52-
aes-gcm = { version = "0.10", optional = true }
53-
aes = { version = "0.8", optional = true }
52+
aes-gcm = { version = "0.10", features = ["zeroize"], optional = true }
53+
aes = { version = "0.8", features = ["zeroize"], optional = true }
5454

5555
# Byte utilities
5656
bytes = "1.5"
@@ -71,6 +71,7 @@ proptest = "1.4"
7171
serde_json = "1.0"
7272
blake2 = "0.10"
7373
hex = "0.4"
74+
aes-gcm = { version = "0.10", features = ["zeroize"] }
7475

7576
[features]
7677
default = ["compression", "checksum", "messagepack"]
@@ -82,23 +83,26 @@ messagepack = ["dep:rmp-serde"]
8283

8384
# Encryption (AES-256-GCM with HKDF-SHA256 key derivation)
8485
# Native: ring provides hardware-accelerated AES-256-GCM (target-conditional dep above)
85-
# wasm32: aes-gcm (pure Rust) is used instead — activated by the `wasm` feature
86-
# Note: `dep:ring` is valid here because ring is declared optional in the target section;
87-
# on wasm32 targets the dep simply doesn't exist, so Cargo ignores it safely.
86+
# wasm32: aes-gcm (pure Rust) is used instead — automatically selected via cfg(target_arch)
87+
# aes-gcm/aes/getrandom compile on native but are dead code (all usage behind wasm32 cfg);
88+
# the compiler optimizes them out.
8889
encryption = [
8990
"dep:ring",
9091
"dep:zeroize",
9192
"dep:hkdf",
9293
"dep:sha2",
9394
"dep:hmac",
9495
"dep:generic-array",
96+
"dep:aes-gcm",
97+
"dep:aes",
98+
"dep:getrandom",
9599
]
96100

97101
# C FFI layer (generates include/cachekit.h)
98102
ffi = []
99103

100-
# wasm32 support: getrandom with JS bindings for RNG + RustCrypto AES-GCM for encryption
101-
wasm = ["dep:getrandom", "dep:aes-gcm", "dep:aes"]
104+
# wasm32 support: alias for encryption (deps now folded into encryption feature)
105+
wasm = ["encryption"]
102106

103107
# Kani formal verification configuration
104108
# Provides mathematical proofs of memory safety for unsafe code and FFI boundaries

‎src/encryption/core.rs‎

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ static GLOBAL_INSTANCE_COUNTER: LazyLock<AtomicU64> = LazyLock::new(|| {
7878

7979
// ── wasm32: thread_local Cell<u64> seeded from getrandom ────────────────────
8080

81-
/// Per-thread encryptor instance counter for wasm32 (no atomics available).
82-
///
83-
/// wasm32-unknown-unknown lacks threads, so a thread-local Cell<u64> is safe.
84-
/// Seeded from `getrandom` (which uses the JS crypto API via WASM).
81+
// Per-thread encryptor instance counter for wasm32 (no atomics available).
82+
//
83+
// wasm32-unknown-unknown lacks threads, so a thread-local Cell<u64> is safe.
84+
// Seeded from `getrandom` (which uses the JS crypto API via WASM).
8585
#[cfg(target_arch = "wasm32")]
8686
thread_local! {
8787
static WASM_INSTANCE_COUNTER: std::cell::Cell<u64> = {
@@ -347,8 +347,7 @@ impl ZeroKnowledgeEncryptor {
347347
key: &[u8],
348348
aad: &[u8],
349349
) -> Result<Vec<u8>, EncryptionError> {
350-
// Time encryption operation (wasm32: Instant unavailable, use 0)
351-
#[cfg(not(target_arch = "wasm32"))]
350+
// Time encryption operation
352351
let encryption_start = Instant::now();
353352

354353
// Validate key length
@@ -381,10 +380,7 @@ impl ZeroKnowledgeEncryptor {
381380
result.extend_from_slice(&ciphertext);
382381

383382
// Update metrics for observability
384-
#[cfg(not(target_arch = "wasm32"))]
385383
let encryption_micros = encryption_start.elapsed().as_micros() as u64;
386-
#[cfg(target_arch = "wasm32")]
387-
let encryption_micros = 0u64;
388384
if let Ok(mut metrics) = self.last_metrics.lock() {
389385
*metrics = OperationMetrics::new()
390386
.with_encryption(encryption_micros, self.hardware_acceleration_detected);
@@ -409,8 +405,7 @@ impl ZeroKnowledgeEncryptor {
409405
key: &[u8],
410406
aad: &[u8],
411407
) -> Result<Vec<u8>, EncryptionError> {
412-
// Time decryption operation (wasm32: Instant unavailable, use 0)
413-
#[cfg(not(target_arch = "wasm32"))]
408+
// Time decryption operation
414409
let decryption_start = Instant::now();
415410

416411
// Validate key length
@@ -454,10 +449,7 @@ impl ZeroKnowledgeEncryptor {
454449
plaintext.truncate(decrypted_len);
455450

456451
// Update metrics for observability
457-
#[cfg(not(target_arch = "wasm32"))]
458452
let decryption_micros = decryption_start.elapsed().as_micros() as u64;
459-
#[cfg(target_arch = "wasm32")]
460-
let decryption_micros = 0u64;
461453
if let Ok(mut metrics) = self.last_metrics.lock() {
462454
*metrics = OperationMetrics::new()
463455
.with_encryption(decryption_micros, self.hardware_acceleration_detected);
@@ -559,10 +551,10 @@ impl ZeroKnowledgeEncryptor {
559551
return Err(EncryptionError::InvalidKeyLength(key.len()));
560552
}
561553

562-
// Minimum: nonce(12) + tag(16) = 28 bytes
563-
if ciphertext.len() < 28 {
554+
// Minimum: nonce(12) + tag(16)
555+
if ciphertext.len() < 12 + 16 {
564556
return Err(EncryptionError::InvalidCiphertext(
565-
"ciphertext too short (minimum 28 bytes: 12 nonce + 16 tag)".into(),
557+
"Ciphertext too short".into(),
566558
));
567559
}
568560

‎tests/wasm32_compat_tests.rs‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,50 @@ mod byte_storage_roundtrip {
5757
assert_eq!("cbor", format);
5858
}
5959
}
60+
61+
/// Verify that aes-gcm (wasm32 backend) produces wire-format-compatible
62+
/// output that ring (native backend) can decrypt, and vice versa.
63+
#[cfg(feature = "encryption")]
64+
#[test]
65+
fn cross_backend_wire_format_compatibility() {
66+
use aes_gcm::{
67+
aead::{Aead, KeyInit, Payload},
68+
Aes256Gcm, Nonce as AesGcmNonce,
69+
};
70+
use cachekit_core::ZeroKnowledgeEncryptor;
71+
72+
let key = [0x42u8; 32];
73+
let nonce_bytes = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
74+
let plaintext = b"cross-backend wire format test";
75+
let aad = b"test_aad_domain";
76+
77+
// Encrypt with aes-gcm (simulating wasm32 path)
78+
let cipher = Aes256Gcm::new_from_slice(&key).unwrap();
79+
let nonce = AesGcmNonce::from_slice(&nonce_bytes);
80+
let ct = cipher
81+
.encrypt(nonce, Payload { msg: &plaintext[..], aad: &aad[..] })
82+
.unwrap();
83+
84+
// Build wire format: nonce(12) || ciphertext || tag(16)
85+
let mut wire = Vec::new();
86+
wire.extend_from_slice(&nonce_bytes);
87+
wire.extend_from_slice(&ct);
88+
89+
// Decrypt with ring (native path) via ZeroKnowledgeEncryptor
90+
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
91+
let decrypted = encryptor.decrypt_aes_gcm(&wire, &key, aad).unwrap();
92+
assert_eq!(decrypted, plaintext);
93+
94+
// Also test the reverse: ring encrypts, aes-gcm decrypts
95+
let ring_ciphertext = encryptor.encrypt_aes_gcm(plaintext, &key, aad).unwrap();
96+
97+
// Extract nonce and ciphertext+tag from ring output
98+
let ring_nonce = &ring_ciphertext[..12];
99+
let ring_ct_tag = &ring_ciphertext[12..];
100+
101+
let nonce2 = AesGcmNonce::from_slice(ring_nonce);
102+
let decrypted2 = cipher
103+
.decrypt(nonce2, Payload { msg: ring_ct_tag, aad: &aad[..] })
104+
.unwrap();
105+
assert_eq!(decrypted2, plaintext);
106+
}

0 commit comments

Comments
 (0)