Skip to content

Commit a18730c

Browse files
authored
chore: add wasm32-unknown-unknown compatibility (#20)
* chore: add wasm32 conditional timing for Instant compatibility * chore: add wasm32 fallback for AtomicU64 and SystemRandom * 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. * 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 * chore: apply cargo fmt * chore: bump MSRV from 1.82 to 1.85 cbindgen 0.29.2 pulls clap 4.6.0 which requires edition2024 (stabilized in Rust 1.85, Feb 2025). Pinning transitive deps is fragile; bumping MSRV is the correct fix. * ci: update MSRV job from 1.82 to 1.85 * chore: update cargo-deny skips for getrandom version changes aes-gcm brings in getrandom 0.2.x (via crypto-common), proptest uses 0.3.x, and tempfile/cbindgen uses 0.4.x. Updated skip entries to use semver ranges instead of exact versions.
1 parent 9f8abda commit a18730c

6 files changed

Lines changed: 375 additions & 36 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
# Full OS matrix for stable only; MSRV and beta on ubuntu only
2020
include:
2121
# MSRV - ensures we don't use newer Rust features
22-
- rust: "1.82"
22+
- rust: "1.85"
2323
os: ubuntu-latest
2424
# Stable - primary target, all platforms
2525
- rust: stable
@@ -51,7 +51,7 @@ jobs:
5151
run: cargo fmt --check
5252

5353
- name: Run clippy
54-
if: matrix.rust != '1.82'
54+
if: matrix.rust != '1.85'
5555
run: cargo clippy --all-features -- -D warnings
5656

5757
- name: Run tests (all features)

Cargo.toml

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.1.1"
44
edition = "2021"
55
authors = ["cachekit Contributors"]
66
description = "LZ4 compression, xxHash3 integrity, AES-256-GCM encryption for byte payloads"
7-
rust-version = "1.82"
7+
rust-version = "1.85"
88
license = "MIT"
99
repository = "https://github.com/cachekit-io/cachekit-core"
1010
homepage = "https://github.com/cachekit-io/cachekit-core"
@@ -36,20 +36,32 @@ 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 }
4645
hmac = { version = "0.12", optional = true }
4746
generic-array = { version = "0.14", optional = true }
4847

48+
# wasm32 RNG: getrandom with JS feature for wasm32-unknown-unknown targets
49+
getrandom = { version = "0.2", features = ["js"], optional = true }
50+
51+
# RustCrypto: pure-Rust AES-256-GCM for wasm32 targets (ring requires clang + C asm)
52+
aes-gcm = { version = "0.10", features = ["zeroize"], optional = true }
53+
aes = { version = "0.8", features = ["zeroize"], optional = true }
54+
4955
# Byte utilities
5056
bytes = "1.5"
5157
byteorder = "1.5"
5258

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+
5365
[build-dependencies]
5466
# C header generation (required for ffi feature)
5567
cbindgen = "0.29"
@@ -59,6 +71,7 @@ proptest = "1.4"
5971
serde_json = "1.0"
6072
blake2 = "0.10"
6173
hex = "0.4"
74+
aes-gcm = { version = "0.10", features = ["zeroize"] }
6275

6376
[features]
6477
default = ["compression", "checksum", "messagepack"]
@@ -69,18 +82,28 @@ checksum = ["dep:xxhash-rust"]
6982
messagepack = ["dep:rmp-serde"]
7083

7184
# Encryption (AES-256-GCM with HKDF-SHA256 key derivation)
85+
# Native: ring provides hardware-accelerated AES-256-GCM (target-conditional dep above)
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.
7289
encryption = [
7390
"dep:ring",
7491
"dep:zeroize",
7592
"dep:hkdf",
7693
"dep:sha2",
7794
"dep:hmac",
7895
"dep:generic-array",
96+
"dep:aes-gcm",
97+
"dep:aes",
98+
"dep:getrandom",
7999
]
80100

81101
# C FFI layer (generates include/cachekit.h)
82102
ffi = []
83103

104+
# wasm32 support: alias for encryption (deps now folded into encryption feature)
105+
wasm = ["encryption"]
106+
84107
# Kani formal verification configuration
85108
# Provides mathematical proofs of memory safety for unsafe code and FFI boundaries
86109
[package.metadata.kani]

deny.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,16 @@ deny = []
7070
# Skip specific dependencies from multiple-version checks
7171
# These are transitive dependencies where version duplication is unavoidable
7272
skip = [
73-
# getrandom 0.2.x via ring, getrandom 0.3.x via proptest/tempfile
74-
{ crate = "getrandom@0.2.16", reason = "Transitive via ring crypto lib" },
73+
# getrandom has 3 major versions in the dep tree:
74+
# 0.2.x via aes-gcm/crypto-common (encryption)
75+
# 0.3.x via proptest (dev-dependency)
76+
# 0.4.x via tempfile/cbindgen (build-dependency)
77+
{ crate = "getrandom@0.2", reason = "Transitive via aes-gcm crypto chain" },
78+
{ crate = "getrandom@0.3", reason = "Transitive via proptest (dev-dependency)" },
79+
# rand_core duplication from aes-gcm (0.6.x) vs proptest (0.9.x)
80+
{ crate = "rand_core@0.6", reason = "Transitive via aes-gcm crypto chain" },
81+
# libc duplication unavoidable (getrandom versions pull different libc)
82+
{ crate = "libc@0.2", reason = "Transitive via multiple getrandom versions" },
7583
]
7684

7785
# Skip crate trees entirely (e.g., frequently-updated foundational crates)

src/byte_storage.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::metrics::OperationMetrics;
1010
use lz4_flex;
1111
use serde::{Deserialize, Serialize};
1212
use std::sync::{Arc, Mutex};
13+
#[cfg(not(target_arch = "wasm32"))]
1314
use std::time::Instant;
1415
use thiserror::Error;
1516
#[cfg(feature = "checksum")]
@@ -175,14 +176,17 @@ impl ByteStorage {
175176

176177
let format = format.unwrap_or_else(|| self.default_format.clone());
177178

178-
// Time compression operation
179+
// Time compression operation (wasm32: Instant unavailable, use 0)
180+
#[cfg(not(target_arch = "wasm32"))]
179181
let compression_start = Instant::now();
180182
let original_size = data.len();
181183

182184
let envelope = StorageEnvelope::new(data.to_vec(), format)?;
183185

184-
let compression_elapsed = compression_start.elapsed();
185-
let compression_micros = compression_elapsed.as_micros() as u64;
186+
#[cfg(not(target_arch = "wasm32"))]
187+
let compression_micros = compression_start.elapsed().as_micros() as u64;
188+
#[cfg(target_arch = "wasm32")]
189+
let compression_micros = 0u64;
186190
let compressed_size = envelope.compressed_data.len();
187191

188192
// Serialize envelope with MessagePack
@@ -220,14 +224,17 @@ impl ByteStorage {
220224
let envelope: StorageEnvelope = rmp_serde::from_slice(envelope_bytes)
221225
.map_err(|e| ByteStorageError::DeserializationFailed(e.to_string()))?;
222226

223-
// Time decompression and checksum operations
227+
// Time decompression and checksum operations (wasm32: Instant unavailable, use 0)
228+
#[cfg(not(target_arch = "wasm32"))]
224229
let decompress_start = Instant::now();
225230

226231
// Extract and validate data (all security checks happen inside extract())
227232
let data = envelope.extract()?;
228233

229-
let decompress_elapsed = decompress_start.elapsed();
230-
let decompress_micros = decompress_elapsed.as_micros() as u64;
234+
#[cfg(not(target_arch = "wasm32"))]
235+
let decompress_micros = decompress_start.elapsed().as_micros() as u64;
236+
#[cfg(target_arch = "wasm32")]
237+
let decompress_micros = 0u64;
231238

232239
// Calculate compression ratio from stored metadata
233240
let compressed_size = envelope.compressed_data.len();

0 commit comments

Comments
 (0)