4.8 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | ||
|---|---|---|---|---|---|---|---|---|---|
| key-caching-ttl | Implement TTL-based key cache with LRU eviction for SecretService | completed |
|
moderate | medium | component | implementation |
Description
The SecretServiceHandle currently re-derives keys from the seed on every call. The spec (after update) requires a TTL-based key cache with LRU eviction, cleared on Lock. This is the resolution of OQ-SVC-04.
The current SecretServiceInner holds:
mnemonic: Option<Mnemonic>seed: Option<Seed>unlocked: bool
It needs to add:
cache: HashMap<String, CachedKey>where the key is the derivation path stringcache_ttl: Duration(default 1 hour, configurable)- LRU eviction when cache exceeds a configurable max size
Design considerations:
- The cache key is the derivation path string (e.g.,
m/74'/0'/0'/0'). This means caching at the path level — if you derive the same path multiple times, you get the cached key (within TTL). - The cached value must hold the derived key material zeroize-protected, not just the public key.
- TTL is checked on access, not via a background timer. Expired entries are evicted lazily.
Lockclears the cache entirely and zeroizes all cached entries.- Cache hits avoid re-derivation from seed, which is the main performance win.
- The cache must be behind
RwLock(already used forSecretServiceInner).
Cache entry structure:
#[derive(Zeroize)]
#[zeroize(drop)]
struct CachedKey {
derived_at: Instant,
key_type: KeyType,
private_key: Vec<u8>, // Zeroize-protected
public_key: Vec<u8>,
}
Cache configuration:
pub struct CacheConfig {
pub ttl: Duration, // Default: 1 hour
pub max_entries: usize, // Default: 64
}
Behavior:
- On
derive_*call: check cache. If hit andInstant::now() - derived_at < ttl, return cached. If expired, evict and re-derive. If miss, derive and insert. - On
Lock: zeroize all cache entries, clear the HashMap, zeroize seed and mnemonic (existing behavior). - On
Encrypt/Decrypt: the encryption key atPATHS::ENCRYPTIONis also cached (same path, same behavior).
Implementation:
Add a cache module to alknet-secret/src/cache.rs implementing KeyCache with get, insert, evict_expired, clear (zeroize on clear).
Wire into SecretServiceInner behind the existing RwLock.
Acceptance Criteria
cache.rsmodule added toalknet-secret/src/withKeyCachestructCachedKeystruct with Zeroize-derived private key,derived_at: Instant,key_type,public_keyCacheConfigstruct withttl: Duration(default 1 hour) andmax_entries: usize(default 64)KeyCache::get(path: &str) -> Option<&CachedKey>returns cached entry if within TTLKeyCache::insert(path: &str, key: CachedKey)inserts, evicts LRU if over max_entriesKeyCache::evict_expired()removes entries past TTL, zeroizing themKeyCache::clear()zeroizes all entries and clears the HashMapSecretServiceInnergains acache: KeyCachefield (behind RwLock)SecretServiceHandle::new()accepts optionalCacheConfig(defaults applied)derive_ed25519,derive_encryption_key,derive_ethereum_keycheck cache before re-derivingLockclears the cache (zeroizes all cached entries)- Unit test: cache hit avoids re-derivation
- Unit test: cache miss derives and caches
- Unit test: expired entry is evicted on access and re-derived
- Unit test: LRU eviction when cache exceeds max_entries
- Unit test: Lock clears all cache entries and zeroizes them
- Unit test: encrypt/decrypt uses cached encryption key
References
- docs/architecture/secret-service.md — Key caching subsection (after spec update)
- docs/architecture/decisions/038-seed-lifecycle-memory-security.md — Zeroize requirement
- OQ-SVC-04 — Resolved: yes, cache with TTL default 1 hour
- crates/alknet-secret/src/service.rs — SecretServiceInner (to add cache)
- crates/alknet-secret/src/lib.rs — Module re-exports
Notes
This task depends on
derivedkey-zeroize-securitybecauseCachedKeyneeds the same zeroize discipline thatDerivedKeygets. IfDerivedKeybecomes non-Clone,CachedKeyis a separate internal type that holds the same data but is managed within the cache.
The LRU implementation can use
std::collections::HashMap+ a doubly-linked list (orlrucrate for simplicity). Given the max_entries default of 64, even a simple scan-and-evict approach is fine. Preferlrucrate for correctness and simplicity.
Cache configuration should be exposed through
SecretService::new()or aSecretServiceBuilderpattern, not throughSecretServiceHandle::new(). The handle wraps an already-configured service.
Summary
To be filled on completion