diff --git a/.github/workflows/context7-refresh.yml b/.github/workflows/context7-refresh.yml new file mode 100644 index 0000000..6de577d --- /dev/null +++ b/.github/workflows/context7-refresh.yml @@ -0,0 +1,39 @@ +# Context7 serves this repo's docs to coding agents, and re-indexes on its own +# schedule, which can lag a release by weeks. This asks it to re-index when a +# release is published, so agents read the docs for the version users install. +# Scope and agent rules live in context7.json. +# Endpoint: https://context7.com/docs/integrations/github-actions +name: Context7 Refresh + +on: + release: + types: [published] + workflow_dispatch: + +permissions: {} + +jobs: + refresh: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Ask Context7 to re-index /cachekit-io/cachekit-core + env: + CONTEXT7_API_KEY: ${{ secrets.CONTEXT7_API_KEY }} + run: | + if [ -z "$CONTEXT7_API_KEY" ]; then + echo "::error::The CONTEXT7_API_KEY secret is not available to this repository." + exit 1 + fi + status=$(curl -sS --max-time 60 -o "$RUNNER_TEMP/context7-response.json" -w '%{http_code}' \ + -X POST https://context7.com/api/v1/refresh \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $CONTEXT7_API_KEY" \ + --data '{"libraryName": "/cachekit-io/cachekit-core"}') + cat "$RUNNER_TEMP/context7-response.json" + echo + case "$status" in + 202) echo "::warning::Context7 has not finalized this library yet (HTTP 202). Re-run this workflow later." ;; + 2??) echo "Context7 accepted the refresh (HTTP $status)." ;; + *) echo "::error::Context7 refresh failed with HTTP $status."; exit 1 ;; + esac diff --git a/README.md b/README.md index 4bab124..f1138f7 100644 --- a/README.md +++ b/README.md @@ -78,24 +78,28 @@ assert_eq!(data.as_slice(), retrieved.as_slice()); ```rust use cachekit_core::{ByteStorage, ZeroKnowledgeEncryptor, derive_domain_key}; +use zeroize::Zeroizing; // add the zeroize crate to your Cargo.toml // Derive tenant-isolated key from master secret -let master_key = [0u8; 32]; // Use secure key in production! -let tenant_key = derive_domain_key( - &master_key, +// From your secret manager or CACHEKIT_MASTER_KEY, hex-decoded to 32 raw bytes. +// Never hard-code it, and never pass the hex string's bytes. +// Zeroizing wipes each key from memory when it is dropped. +let master_key = Zeroizing::new(load_master_key_from_secret_manager()?); +let tenant_key = Zeroizing::new(derive_domain_key( + master_key.as_slice(), "cache", // domain separation b"tenant-12345", // tenant isolation -)?; +)?); // Encrypt sensitive data -let encryptor = ZeroKnowledgeEncryptor::new(); +let encryptor = ZeroKnowledgeEncryptor::new()?; let plaintext = b"sensitive user data"; let aad = b"tenant-12345"; // Additional authenticated data -let ciphertext = encryptor.encrypt_aes_gcm(plaintext, &tenant_key, aad)?; +let ciphertext = encryptor.encrypt_aes_gcm(plaintext, tenant_key.as_slice(), aad)?; // Decrypt (fails if AAD doesn't match) -let decrypted = encryptor.decrypt_aes_gcm(&ciphertext, &tenant_key, aad)?; +let decrypted = encryptor.decrypt_aes_gcm(&ciphertext, tenant_key.as_slice(), aad)?; assert_eq!(plaintext.as_slice(), decrypted.as_slice()); ``` @@ -121,7 +125,7 @@ fn cache_sensitive_data( let tenant_key = derive_domain_key(master_key, "cache", tenant_id.as_bytes())?; // Step 3: Encrypt compressed envelope - let encryptor = ZeroKnowledgeEncryptor::new(); + let encryptor = ZeroKnowledgeEncryptor::new()?; let ciphertext = encryptor.encrypt_aes_gcm( &compressed, &tenant_key, diff --git a/context7.json b/context7.json new file mode 100644 index 0000000..a405303 --- /dev/null +++ b/context7.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://context7.com/schema/context7.json", + "excludeFolders": [ + ".github", + "benches", + "fuzz", + "scripts", + "supply-chain", + "tests" + ], + "excludeFiles": [ + "CHANGELOG.md" + ], + "rules": [ + "Read the CacheKit master key from the CACHEKIT_MASTER_KEY environment variable or a secret manager. Never hard-code a key in source, not even a placeholder such as [0u8; 32].", + "cachekit-core reads no environment variable. Hex-decode the key (for example from `CACHEKIT_MASTER_KEY` or a secret manager) to exactly 32 raw bytes before `derive_domain_key`. Never pass the hex string's bytes: that derives a key no CacheKit SDK derives, so cross-SDK decryption fails.", + "Install the official packages: `cachekit` on PyPI, `@cachekit-io/cachekit` on npm, and `cachekit-rs` on crates.io (imported as `cachekit`). The crates.io crate named `cachekit` is an unrelated project.", + "Applications normally use a CacheKit SDK. cachekit-core is the shared byte-storage and encryption layer underneath them." + ] +} diff --git a/src/lib.rs b/src/lib.rs index 93a3e43..5738f39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,17 +40,25 @@ //! //! ```rust,ignore //! use cachekit_core::{ZeroKnowledgeEncryptor, derive_domain_key}; -//! -//! // Derive tenant-isolated key -//! let master_key = [0u8; 32]; // Use secure key in production! -//! let tenant_key = derive_domain_key(&master_key, "cache", b"tenant-123").unwrap(); -//! -//! // Encrypt -//! let encryptor = ZeroKnowledgeEncryptor::new(); -//! let ciphertext = encryptor.encrypt_aes_gcm(b"secret", &tenant_key, b"tenant-123").unwrap(); -//! -//! // Decrypt -//! let plaintext = encryptor.decrypt_aes_gcm(&ciphertext, &tenant_key, b"tenant-123").unwrap(); +//! use zeroize::Zeroizing; // add the zeroize crate to your Cargo.toml +//! +//! fn main() -> Result<(), Box> { +//! // Derive tenant-isolated key +//! // From your secret manager or CACHEKIT_MASTER_KEY, hex-decoded to 32 raw bytes. +//! // Never hard-code it, and never pass the hex string's bytes. +//! // Zeroizing wipes each key from memory when it is dropped. +//! let master_key = Zeroizing::new(load_master_key_from_secret_manager()?); +//! let tenant_key = Zeroizing::new(derive_domain_key(master_key.as_slice(), "cache", b"tenant-123")?); +//! +//! // Encrypt +//! let encryptor = ZeroKnowledgeEncryptor::new()?; +//! let ciphertext = encryptor.encrypt_aes_gcm(b"secret", tenant_key.as_slice(), b"tenant-123")?; +//! +//! // Decrypt +//! let plaintext = encryptor.decrypt_aes_gcm(&ciphertext, tenant_key.as_slice(), b"tenant-123")?; +//! assert_eq!(plaintext, b"secret"); +//! Ok(()) +//! } //! ``` //! //! ## Security Properties