Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
# Full OS matrix for stable only; MSRV and beta on ubuntu only
include:
# MSRV - ensures we don't use newer Rust features
- rust: "1.82"
- rust: "1.85"
os: ubuntu-latest
# Stable - primary target, all platforms
- rust: stable
Expand Down Expand Up @@ -51,7 +51,7 @@ jobs:
run: cargo fmt --check

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

- name: Run tests (all features)
Expand Down
29 changes: 26 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.1"
edition = "2021"
authors = ["cachekit Contributors"]
description = "LZ4 compression, xxHash3 integrity, AES-256-GCM encryption for byte payloads"
rust-version = "1.82"
rust-version = "1.85"
license = "MIT"
repository = "https://github.com/cachekit-io/cachekit-core"
homepage = "https://github.com/cachekit-io/cachekit-core"
Expand Down Expand Up @@ -36,20 +36,32 @@ lz4_flex = { version = "0.11", features = ["frame", "std"], optional = true }
# xxHash3-64: ~36 GB/s, sufficient for corruption detection (security via AES-GCM auth tag)
xxhash-rust = { version = "0.8", features = ["xxh3"], optional = true }


# Encryption dependencies (all optional, gated by encryption feature)
# Uses HKDF-SHA256 for key derivation (NOT Blake2b - that's only for Python cache keys)
ring = { version = "0.17", optional = true }
# ring is native-only (see [target.'cfg(not(target_arch = "wasm32"))'.dependencies])
zeroize = { version = "1.8", features = ["derive"], optional = true }
hkdf = { version = "0.12", optional = true }
sha2 = { version = "0.10", optional = true }
hmac = { version = "0.12", optional = true }
generic-array = { version = "0.14", optional = true }

# wasm32 RNG: getrandom with JS feature for wasm32-unknown-unknown targets
getrandom = { version = "0.2", features = ["js"], optional = true }

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

# Byte utilities
bytes = "1.5"
byteorder = "1.5"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# ring: hardware-accelerated AES-256-GCM for native targets only.
# Does NOT compile on wasm32-unknown-unknown (requires clang for C asm).
# Kept optional so it is only compiled when the `encryption` feature is active.
ring = { version = "0.17", optional = true }

[build-dependencies]
# C header generation (required for ffi feature)
cbindgen = "0.29"
Expand All @@ -59,6 +71,7 @@ proptest = "1.4"
serde_json = "1.0"
blake2 = "0.10"
hex = "0.4"
aes-gcm = { version = "0.10", features = ["zeroize"] }

[features]
default = ["compression", "checksum", "messagepack"]
Expand All @@ -69,18 +82,28 @@ checksum = ["dep:xxhash-rust"]
messagepack = ["dep:rmp-serde"]

# Encryption (AES-256-GCM with HKDF-SHA256 key derivation)
# Native: ring provides hardware-accelerated AES-256-GCM (target-conditional dep above)
# wasm32: aes-gcm (pure Rust) is used instead — automatically selected via cfg(target_arch)
# aes-gcm/aes/getrandom compile on native but are dead code (all usage behind wasm32 cfg);
# the compiler optimizes them out.
encryption = [
"dep:ring",
"dep:zeroize",
"dep:hkdf",
"dep:sha2",
"dep:hmac",
"dep:generic-array",
"dep:aes-gcm",
"dep:aes",
"dep:getrandom",
]

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

# wasm32 support: alias for encryption (deps now folded into encryption feature)
wasm = ["encryption"]

# Kani formal verification configuration
# Provides mathematical proofs of memory safety for unsafe code and FFI boundaries
[package.metadata.kani]
Expand Down
12 changes: 10 additions & 2 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,16 @@ deny = []
# Skip specific dependencies from multiple-version checks
# These are transitive dependencies where version duplication is unavoidable
skip = [
# getrandom 0.2.x via ring, getrandom 0.3.x via proptest/tempfile
{ crate = "getrandom@0.2.16", reason = "Transitive via ring crypto lib" },
# getrandom has 3 major versions in the dep tree:
# 0.2.x via aes-gcm/crypto-common (encryption)
# 0.3.x via proptest (dev-dependency)
# 0.4.x via tempfile/cbindgen (build-dependency)
{ crate = "getrandom@0.2", reason = "Transitive via aes-gcm crypto chain" },
{ crate = "getrandom@0.3", reason = "Transitive via proptest (dev-dependency)" },
# rand_core duplication from aes-gcm (0.6.x) vs proptest (0.9.x)
{ crate = "rand_core@0.6", reason = "Transitive via aes-gcm crypto chain" },
# libc duplication unavoidable (getrandom versions pull different libc)
{ crate = "libc@0.2", reason = "Transitive via multiple getrandom versions" },
]

# Skip crate trees entirely (e.g., frequently-updated foundational crates)
Expand Down
19 changes: 13 additions & 6 deletions src/byte_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::metrics::OperationMetrics;
use lz4_flex;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
use thiserror::Error;
#[cfg(feature = "checksum")]
Expand Down Expand Up @@ -175,14 +176,17 @@ impl ByteStorage {

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

// Time compression operation
// Time compression operation (wasm32: Instant unavailable, use 0)
#[cfg(not(target_arch = "wasm32"))]
let compression_start = Instant::now();
let original_size = data.len();

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

let compression_elapsed = compression_start.elapsed();
let compression_micros = compression_elapsed.as_micros() as u64;
#[cfg(not(target_arch = "wasm32"))]
let compression_micros = compression_start.elapsed().as_micros() as u64;
#[cfg(target_arch = "wasm32")]
let compression_micros = 0u64;
let compressed_size = envelope.compressed_data.len();

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

// Time decompression and checksum operations
// Time decompression and checksum operations (wasm32: Instant unavailable, use 0)
#[cfg(not(target_arch = "wasm32"))]
let decompress_start = Instant::now();

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

let decompress_elapsed = decompress_start.elapsed();
let decompress_micros = decompress_elapsed.as_micros() as u64;
#[cfg(not(target_arch = "wasm32"))]
let decompress_micros = decompress_start.elapsed().as_micros() as u64;
#[cfg(target_arch = "wasm32")]
let decompress_micros = 0u64;

// Calculate compression ratio from stored metadata
let compressed_size = envelope.compressed_data.len();
Expand Down
Loading
Loading