Skip to content

Commit 0defd5d

Browse files
committed
chore: add RustCrypto aes-gcm backend for wasm32 encryption
ring doesn't compile on wasm32-unknown-unknown (requires clang for C asm). Move ring to a target-conditional dep (native only), add aes-gcm from RustCrypto for wasm32. Both produce identical AES-256-GCM wire format: [nonce(12)][ciphertext][auth_tag(16)] Native targets continue using ring for hardware-accelerated performance. wasm32 targets use aes-gcm (pure Rust) when wasm+encryption features active. key_derivation.rs unchanged — already uses RustCrypto hkdf/sha2, compiles on both targets without modification.
1 parent 087a4fb commit 0defd5d

2 files changed

Lines changed: 114 additions & 4 deletions

File tree

‎Cargo.toml‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,9 @@ lz4_flex = { version = "0.11", features = ["frame", "std"], optional = true }
3636
# xxHash3-64: ~36 GB/s, sufficient for corruption detection (security via AES-GCM auth tag)
3737
xxhash-rust = { version = "0.8", features = ["xxh3"], optional = true }
3838

39-
4039
# Encryption dependencies (all optional, gated by encryption feature)
4140
# Uses HKDF-SHA256 for key derivation (NOT Blake2b - that's only for Python cache keys)
42-
ring = { version = "0.17", optional = true }
41+
# ring is native-only (see [target.'cfg(not(target_arch = "wasm32"))'.dependencies])
4342
zeroize = { version = "1.8", features = ["derive"], optional = true }
4443
hkdf = { version = "0.12", optional = true }
4544
sha2 = { version = "0.10", optional = true }
@@ -49,10 +48,20 @@ generic-array = { version = "0.14", optional = true }
4948
# wasm32 RNG: getrandom with JS feature for wasm32-unknown-unknown targets
5049
getrandom = { version = "0.2", features = ["js"], optional = true }
5150

51+
# 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 }
54+
5255
# Byte utilities
5356
bytes = "1.5"
5457
byteorder = "1.5"
5558

59+
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
60+
# ring: hardware-accelerated AES-256-GCM for native targets only.
61+
# Does NOT compile on wasm32-unknown-unknown (requires clang for C asm).
62+
# Kept optional so it is only compiled when the `encryption` feature is active.
63+
ring = { version = "0.17", optional = true }
64+
5665
[build-dependencies]
5766
# C header generation (required for ffi feature)
5867
cbindgen = "0.29"
@@ -72,6 +81,10 @@ checksum = ["dep:xxhash-rust"]
7281
messagepack = ["dep:rmp-serde"]
7382

7483
# Encryption (AES-256-GCM with HKDF-SHA256 key derivation)
84+
# 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.
7588
encryption = [
7689
"dep:ring",
7790
"dep:zeroize",
@@ -84,8 +97,8 @@ encryption = [
8497
# C FFI layer (generates include/cachekit.h)
8598
ffi = []
8699

87-
# wasm32 support: getrandom with JS bindings for RNG on wasm32-unknown-unknown
88-
wasm = ["dep:getrandom"]
100+
# wasm32 support: getrandom with JS bindings for RNG + RustCrypto AES-GCM for encryption
101+
wasm = ["dep:getrandom", "dep:aes-gcm", "dep:aes"]
89102

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

‎src/encryption/core.rs‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,20 @@
2020
//! for a total of 2^96 unique nonces - far exceeding any practical usage.
2121
2222
use crate::metrics::OperationMetrics;
23+
24+
// Native: ring for AES-256-GCM (hardware-accelerated, requires clang)
25+
#[cfg(not(target_arch = "wasm32"))]
2326
use ring::{
2427
aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM},
2528
rand::{SecureRandom, SystemRandom},
2629
};
30+
31+
// wasm32: RustCrypto aes-gcm (pure Rust, compiles on wasm32-unknown-unknown)
32+
#[cfg(target_arch = "wasm32")]
33+
use aes_gcm::{
34+
aead::{Aead, KeyInit, Payload},
35+
Aes256Gcm, Nonce as AesGcmNonce,
36+
};
2737
#[cfg(not(target_arch = "wasm32"))]
2838
use std::sync::atomic::{AtomicU64, Ordering};
2939
#[cfg(not(target_arch = "wasm32"))]
@@ -330,6 +340,7 @@ impl ZeroKnowledgeEncryptor {
330340
///
331341
/// # Returns
332342
/// Encrypted data in format: `[nonce(12)][ciphertext+auth_tag]`
343+
#[cfg(not(target_arch = "wasm32"))]
333344
pub fn encrypt_aes_gcm(
334345
&self,
335346
plaintext: &[u8],
@@ -391,6 +402,7 @@ impl ZeroKnowledgeEncryptor {
391402
///
392403
/// # Returns
393404
/// Decrypted plaintext data
405+
#[cfg(not(target_arch = "wasm32"))]
394406
pub fn decrypt_aes_gcm(
395407
&self,
396408
ciphertext: &[u8],
@@ -489,6 +501,91 @@ impl ZeroKnowledgeEncryptor {
489501
.into(),
490502
))
491503
}
504+
505+
/// Encrypt data using AES-256-GCM with authenticated additional data (wasm32)
506+
///
507+
/// Uses RustCrypto's `aes-gcm` crate (pure Rust, compiles on wasm32-unknown-unknown).
508+
/// Produces identical wire format to the native `ring` path:
509+
/// `[nonce(12)][ciphertext][auth_tag(16)]`
510+
#[cfg(target_arch = "wasm32")]
511+
pub fn encrypt_aes_gcm(
512+
&self,
513+
plaintext: &[u8],
514+
key: &[u8],
515+
aad: &[u8],
516+
) -> Result<Vec<u8>, EncryptionError> {
517+
if key.len() != 32 {
518+
return Err(EncryptionError::InvalidKeyLength(key.len()));
519+
}
520+
521+
let nonce_bytes = self.generate_nonce()?;
522+
523+
let cipher = Aes256Gcm::new_from_slice(key)
524+
.map_err(|e| EncryptionError::EncryptionFailed(format!("key error: {e}")))?;
525+
526+
let nonce = AesGcmNonce::from_slice(&nonce_bytes);
527+
528+
// encrypt() returns ciphertext || tag (no nonce) — same layout as ring's seal_in_place_append_tag
529+
let ciphertext_with_tag = cipher
530+
.encrypt(nonce, Payload { msg: plaintext, aad })
531+
.map_err(|e| EncryptionError::EncryptionFailed(format!("AES-GCM encrypt failed: {e}")))?;
532+
533+
// Wire format: nonce(12) || ciphertext || tag(16) — identical to native ring output
534+
let mut result = Vec::with_capacity(12 + ciphertext_with_tag.len());
535+
result.extend_from_slice(&nonce_bytes);
536+
result.extend_from_slice(&ciphertext_with_tag);
537+
538+
// Metrics: Instant unavailable on wasm32
539+
if let Ok(mut metrics) = self.last_metrics.lock() {
540+
*metrics = OperationMetrics::new().with_encryption(0u64, false);
541+
}
542+
543+
Ok(result)
544+
}
545+
546+
/// Decrypt data using AES-256-GCM with authenticated additional data (wasm32)
547+
///
548+
/// Uses RustCrypto's `aes-gcm` crate (pure Rust, compiles on wasm32-unknown-unknown).
549+
/// Expects identical wire format to the native `ring` path:
550+
/// `[nonce(12)][ciphertext][auth_tag(16)]`
551+
#[cfg(target_arch = "wasm32")]
552+
pub fn decrypt_aes_gcm(
553+
&self,
554+
ciphertext: &[u8],
555+
key: &[u8],
556+
aad: &[u8],
557+
) -> Result<Vec<u8>, EncryptionError> {
558+
if key.len() != 32 {
559+
return Err(EncryptionError::InvalidKeyLength(key.len()));
560+
}
561+
562+
// Minimum: nonce(12) + tag(16) = 28 bytes
563+
if ciphertext.len() < 28 {
564+
return Err(EncryptionError::InvalidCiphertext(
565+
"ciphertext too short (minimum 28 bytes: 12 nonce + 16 tag)".into(),
566+
));
567+
}
568+
569+
let nonce_bytes = &ciphertext[..12];
570+
let encrypted_data = &ciphertext[12..]; // ciphertext + tag
571+
572+
let cipher = Aes256Gcm::new_from_slice(key)
573+
.map_err(|e| EncryptionError::DecryptionFailed(format!("key error: {e}")))?;
574+
575+
let nonce = AesGcmNonce::from_slice(nonce_bytes);
576+
577+
// decrypt() verifies auth tag and returns plaintext
578+
let plaintext = cipher
579+
.decrypt(nonce, Payload { msg: encrypted_data, aad })
580+
.map_err(|_| EncryptionError::AuthenticationFailed)?;
581+
582+
// Metrics: Instant unavailable on wasm32
583+
if let Ok(mut metrics) = self.last_metrics.lock() {
584+
*metrics = OperationMetrics::new().with_encryption(0u64, false);
585+
}
586+
587+
Ok(plaintext)
588+
}
492589
}
493590

494591
// Note: Default is intentionally NOT implemented.

0 commit comments

Comments
 (0)