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
27 changes: 24 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,55 @@ concurrency:

jobs:
test:
name: ${{ matrix.rust }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.rust == 'beta' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
# 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"
os: ubuntu-latest
# Stable - primary target, all platforms
- rust: stable
os: ubuntu-latest
- rust: stable
os: macos-latest
- rust: stable
os: windows-latest
# Beta - early warning (allowed to fail)
- rust: beta
os: ubuntu-latest
steps:
- uses: actions/checkout@v5

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@master
with:
components: rustfmt, clippy
toolchain: ${{ matrix.rust }}
components: ${{ matrix.rust != '1.80' && 'rustfmt, clippy' || '' }}

- name: Cache cargo registry
uses: Swatinem/rust-cache@v2
with:
cache-all-crates: true

# Formatting and clippy only on stable (lints evolve between versions)
- name: Check formatting
if: matrix.rust == 'stable'
run: cargo fmt --check

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

- name: Run tests (all features)
run: cargo test --all-features

- name: Run FFI tests
if: matrix.rust == 'stable'
run: cargo test --features ffi

security:
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[package]
name = "cachekit-core"
version = "0.1.0"
edition = "2024"
edition = "2021"
authors = ["cachekit Contributors"]
description = "LZ4 compression, xxHash3 integrity, AES-256-GCM encryption for byte payloads"
rust-version = "1.85"
rust-version = "1.82"
license = "MIT"
repository = "https://github.com/cachekit-io/cachekit-core"
homepage = "https://github.com/cachekit-io/cachekit-core"
Expand Down
4 changes: 2 additions & 2 deletions examples/bench_throughput.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ fn bench_size(size: usize, iterations: usize) {
// AES-256-GCM Encrypt
#[cfg(feature = "encryption")]
{
use ring::aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM};
let key_bytes = [0u8; 32];
let unbound = UnboundKey::new(&AES_256_GCM, &key_bytes).unwrap();
let key = LessSafeKey::new(unbound);
Expand All @@ -75,7 +75,7 @@ fn bench_size(size: usize, iterations: usize) {
// AES-256-GCM Decrypt
#[cfg(feature = "encryption")]
{
use ring::aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM};
let key_bytes = [0u8; 32];
let unbound = UnboundKey::new(&AES_256_GCM, &key_bytes).unwrap();
let key = LessSafeKey::new(unbound);
Expand Down
2 changes: 1 addition & 1 deletion src/encryption/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

use crate::metrics::OperationMetrics;
use ring::{
aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey},
aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM},
rand::{SecureRandom, SystemRandom},
};
use std::sync::atomic::{AtomicU64, Ordering};
Expand Down
8 changes: 8 additions & 0 deletions src/encryption/key_derivation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
//! to prevent key confusion attacks and ensure cryptographic isolation between different
//! uses of the same master key.

// Zeroize derive macro generates code that triggers false positive unused_assignments
// lint in Rust 1.92+ for #[zeroize(skip)] fields. The TenantKeys.tenant_id field IS read.
#![allow(unused_assignments)]

use hkdf::Hkdf;
use sha2::Sha256;
use thiserror::Error;
Expand Down Expand Up @@ -153,12 +157,16 @@ pub fn derive_tenant_keys(
///
/// Note: `Clone` is intentionally NOT derived to prevent key material from proliferating
/// in memory. Each `TenantKeys` instance is zeroized on drop via `ZeroizeOnDrop`.
// Allow unused_assignments: Zeroize derive macro generates assignment code for #[zeroize(skip)]
// fields that triggers false positive in Rust 1.92+. The tenant_id field IS read in tests/fuzz.
#[allow(unused_assignments)]
#[derive(Debug, Zeroize, ZeroizeOnDrop)]
pub struct TenantKeys {
pub encryption_key: [u8; 32],
pub authentication_key: [u8; 32],
pub cache_key_salt: [u8; 32],
#[zeroize(skip)]
#[allow(unused_assignments)] // False positive: field IS read, Zeroize derive triggers lint
pub tenant_id: String,
}

Expand Down
8 changes: 8 additions & 0 deletions src/encryption/key_rotation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
//! - Write only with new key (migration forward)
//! - Key version bytes in ciphertext header track which key was used

// Zeroize derive macro generates code that triggers false positive unused_assignments
// lint in Rust 1.92+ for #[zeroize(skip)] fields. The KeyRotationState.rotation_active field IS read.
#![allow(unused_assignments)]

use std::convert::TryInto;
use zeroize::{Zeroize, ZeroizeOnDrop};

Expand Down Expand Up @@ -114,6 +118,9 @@ impl RotationAwareHeader {
/// let state = KeyRotationState::new([0u8; 32]);
/// let cloned = state.clone(); // ERROR: Clone not implemented
/// ```
// Allow unused_assignments: Zeroize derive macro generates assignment code for #[zeroize(skip)]
// fields that triggers false positive in Rust 1.92+. The rotation_active field IS read.
#[allow(unused_assignments)]
#[derive(Debug, Zeroize, ZeroizeOnDrop)]
pub struct KeyRotationState {
/// Old key for reading legacy ciphertext (backward compatibility during migration)
Expand All @@ -122,6 +129,7 @@ pub struct KeyRotationState {
pub new_key: [u8; 32],
/// Indicates if rotation is currently active (old_key exists)
#[zeroize(skip)]
#[allow(unused_assignments)] // False positive: field IS read, Zeroize derive triggers lint
pub rotation_active: bool,
}

Expand Down
4 changes: 2 additions & 2 deletions src/encryption/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub mod key_rotation;

// Re-exports for convenience
pub use core::{EncryptionError, ZeroKnowledgeEncryptor};
pub use key_derivation::{KeyDerivationError, derive_domain_key};
pub use key_derivation::{derive_domain_key, KeyDerivationError};
pub use key_rotation::{KeyRotationState, RotationAwareHeader};

// RotationAwareHeader is the canonical encryption header
Expand Down Expand Up @@ -59,7 +59,7 @@ mod tests {
assert_eq!(decoded.key_fingerprint, [0x12; 16]);
assert_eq!(decoded.domain, *b"ench");
assert_eq!(decoded.key_version, 0); // Non-rotated data
// Verify algorithm is always AES-256-GCM (byte value 0)
// Verify algorithm is always AES-256-GCM (byte value 0)
assert_eq!(bytes[1], 0);
}

Expand Down
6 changes: 3 additions & 3 deletions src/ffi/byte_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use std::slice;
/// - Pointers remain valid for duration of call
///
/// Function is panic-safe and will never unwind across FFI boundary.
#[unsafe(no_mangle)]
#[no_mangle]
pub unsafe extern "C" fn cachekit_compress(
input: *const u8,
input_len: usize,
Expand Down Expand Up @@ -133,7 +133,7 @@ pub unsafe extern "C" fn cachekit_compress(
/// - Pointers remain valid for duration of call
///
/// Function is panic-safe and will never unwind across FFI boundary.
#[unsafe(no_mangle)]
#[no_mangle]
pub unsafe extern "C" fn cachekit_decompress(
input: *const u8,
input_len: usize,
Expand Down Expand Up @@ -220,7 +220,7 @@ pub unsafe extern "C" fn cachekit_decompress(
///
/// # Safety
/// This is a pure computation with no memory access. Always safe to call.
#[unsafe(no_mangle)]
#[no_mangle]
pub extern "C" fn cachekit_compressed_bound(input_len: usize) -> usize {
// LZ4 worst case: input_len + (input_len / 255) + 16
// See: https://github.com/lz4/lz4/blob/dev/lib/lz4.h#L166
Expand Down
14 changes: 7 additions & 7 deletions src/ffi/encryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const TAG_SIZE: usize = 16;
const CIPHERTEXT_OVERHEAD: usize = NONCE_SIZE + TAG_SIZE; // 28 bytes

#[cfg(feature = "encryption")]
use crate::encryption::{ZeroKnowledgeEncryptor, derive_domain_key};
use crate::encryption::{derive_domain_key, ZeroKnowledgeEncryptor};
#[cfg(feature = "encryption")]
use crate::ffi::error::CachekitError;
#[cfg(feature = "encryption")]
Expand Down Expand Up @@ -77,7 +77,7 @@ use std::slice;
/// **RECOMMENDATION**: Store the encryptor handle in a global/singleton and reuse it.
/// Each handle supports 2^32 (~4 billion) encryptions before requiring a new key.
#[cfg(feature = "encryption")]
#[unsafe(no_mangle)]
#[no_mangle]
pub unsafe extern "C" fn cachekit_encryptor_new(
error_out: *mut CachekitError,
) -> *mut CachekitEncryptor {
Expand Down Expand Up @@ -115,7 +115,7 @@ pub unsafe extern "C" fn cachekit_encryptor_new(
/// - `handle` must not be used after this call
/// - Function is panic-safe and will never unwind across FFI boundary
#[cfg(feature = "encryption")]
#[unsafe(no_mangle)]
#[no_mangle]
pub unsafe extern "C" fn cachekit_encryptor_free(handle: *mut CachekitEncryptor) {
let _ = catch_unwind(|| {
// from_opaque_ptr handles null check and validity tracking
Expand Down Expand Up @@ -144,7 +144,7 @@ pub unsafe extern "C" fn cachekit_encryptor_free(handle: *mut CachekitEncryptor)
/// - `handle` must remain valid for duration of call
/// - Function is panic-safe and will never unwind across FFI boundary
#[cfg(feature = "encryption")]
#[unsafe(no_mangle)]
#[no_mangle]
pub unsafe extern "C" fn cachekit_encryptor_get_counter(handle: *const CachekitEncryptor) -> u64 {
let result = catch_unwind(|| {
// as_ref handles null check and validity tracking
Expand Down Expand Up @@ -195,7 +195,7 @@ pub unsafe extern "C" fn cachekit_encryptor_get_counter(handle: *const CachekitE
///
/// Function is panic-safe and will never unwind across FFI boundary.
#[cfg(feature = "encryption")]
#[unsafe(no_mangle)]
#[no_mangle]
pub unsafe extern "C" fn cachekit_encrypt(
handle: *mut CachekitEncryptor,
key: *const u8,
Expand Down Expand Up @@ -324,7 +324,7 @@ pub unsafe extern "C" fn cachekit_encrypt(
///
/// Function is panic-safe and will never unwind across FFI boundary.
#[cfg(feature = "encryption")]
#[unsafe(no_mangle)]
#[no_mangle]
pub unsafe extern "C" fn cachekit_decrypt(
handle: *const CachekitEncryptor,
key: *const u8,
Expand Down Expand Up @@ -438,7 +438,7 @@ pub unsafe extern "C" fn cachekit_decrypt(
///
/// Function is panic-safe and will never unwind across FFI boundary.
#[cfg(feature = "encryption")]
#[unsafe(no_mangle)]
#[no_mangle]
pub unsafe extern "C" fn cachekit_derive_key(
master: *const u8,
master_len: usize,
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ pub use byte_storage::{ByteStorage, StorageEnvelope};
pub mod encryption;
#[cfg(feature = "encryption")]
pub use encryption::{
EncryptionError, EncryptionHeader, KeyDerivationError, KeyDomain, KeyRotationState,
RotationAwareHeader, ZeroKnowledgeEncryptor, derive_domain_key,
derive_domain_key, EncryptionError, EncryptionHeader, KeyDerivationError, KeyDomain,
KeyRotationState, RotationAwareHeader, ZeroKnowledgeEncryptor,
};

// C FFI layer (feature-gated)
Expand Down
9 changes: 5 additions & 4 deletions tests/encryption_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
mod common;

use cachekit_core::encryption::core::{EncryptionError, ZeroKnowledgeEncryptor};
use cachekit_core::encryption::key_derivation::{KeyDerivationError, derive_domain_key};
use cachekit_core::encryption::key_derivation::{derive_domain_key, KeyDerivationError};
use common::fixtures::*;

// Local test constants only used in encryption tests
Expand Down Expand Up @@ -882,10 +882,11 @@ mod security_tests {
let max_timing = *timings.iter().max().unwrap();
let diff = (max_timing - min_timing) as f64 / min_timing as f64;

// Relaxed threshold for CI environments (noisy neighbors, CPU throttling)
// Real timing leaks would show 2-10x differences, not ~100%
// Relaxed threshold for CI environments (noisy neighbors, CPU throttling, VMs)
// macOS CI runners especially noisy - seen 178% variance
// Real timing leaks would show 2-10x (200-1000%) differences
assert!(
diff < 1.5,
diff < 2.0,
"Key-dependent timing difference too large: {:.1}% - possible timing leak",
diff * 100.0
);
Expand Down
Loading