Skip to content

Recipe 20: Time-Bound Secret Vault

Situation

You need secrets to exist only for a bounded time window. Operationally, “remember to revoke” is weak.

The lease model gives you a natural tool:

  • secrets live in short TTL key leases
  • ciphertext can be durable

This recipe is the “unexpectedly cool” security consequence of leases: temporal scoping becomes an enforceable property of the runtime, not a policy checkbox.

What You Build

A vault that:

  • manages encryption key epochs via KeyEpochManager
  • encrypts/decrypts blobs via EncryptedBlobStore
  • rotates keys with automatic old-epoch expiry
  • enforces fail-closed semantics: expired epoch = no decryption

Building Blocks

  • grafos_securestore::{KeyEpochManager, EncryptedBlobStore, EpochStatus}source
  • grafos_securestore::{EpochId, BlobId, BlobInfo, CryptoBackend}source

Related API docs:

Design

KeyEpochManager Manages the Epoch Lifecycle

KeyEpochManager holds encryption keys scoped to epochs. Each epoch has:

  • a TTL (key material dies when the epoch expires)
  • a status: Active -> Rotating -> Expired
  • a MemRegionLocator describing where the key lives in leased memory

Only one epoch is Active at a time. Creating a new epoch transitions the previous one to Rotating. Calling expire_old(now) zeroes key material for expired epochs — fail closed, enforced by zeroize.

EncryptedBlobStore for Encrypt/Decrypt

EncryptedBlobStore uses the active epoch’s key to encrypt blobs. Each blob records its epoch ID, nonce, and AAD. At decryption time, the store resolves the key by epoch ID. If the epoch is expired or missing, decryption fails with SecureStoreError.

Access Control (Capabilities)

If you integrate capability tokens:

  • every read requires a token scoped to the vault resource id and operation
  • tokens should be audience-bound (to the client identity) and time-bounded
  • revocation broadcasts can kill tokens early

Even with transport-level authentication, keep capability checks as defense-in-depth.

Auditability

Emit structured events:

  • epoch created / rotated / expired
  • epoch renewal failed
  • decrypt attempts failed due to expired epoch

Walkthrough

1. Initialize the Vault

use grafos_securestore::{
KeyEpochManager, EncryptedBlobStore, MockCryptoBackend, BlobId,
};
let crypto = Box::new(MockCryptoBackend::new()); // Use AesGcmBackend in production
let mut key_mgr = KeyEpochManager::new(crypto);
// Create the first epoch with a 1-hour TTL
let epoch_id = key_mgr.create_epoch(now, 3600)?;
// Wrap in a blob store
let mut vault = EncryptedBlobStore::new(key_mgr);

2. Encrypt a Secret

let blob_info = vault.put(BlobId(1), b"my secret data")?;
// blob_info contains epoch_id, nonce, AAD — needed for decryption

3. Decrypt While Key is Active

let plaintext = vault.get(&blob_info)?;
assert_eq!(plaintext, b"my secret data");

4. Rotate Keys

// Create a new epoch — old one becomes Rotating
let new_epoch = vault.key_manager_mut().rotate(now, 3600)?;
// Old epoch keys still accessible for decryption during rotation window
let plaintext = vault.get(&blob_info)?;
assert_eq!(plaintext, b"my secret data");

5. Expire Old Epoch

// Time passes... old epoch TTL expires
vault.key_manager_mut().expire_old(now + 7200);
// Old epoch key material is zeroed. Decryption fails closed:
let result = vault.get(&blob_info);
assert!(result.is_err()); // SecureStoreError::EpochExpired

6. Check Epoch Status

use grafos_securestore::EpochStatus;
let info = vault.key_manager().get_epoch(epoch_id);
if let Some(info) = info {
match info.status {
EpochStatus::Active => { /* current epoch */ }
EpochStatus::Rotating => { /* old epoch, still decryptable */ }
EpochStatus::Expired => { /* key zeroed, no decryption */ }
}
}

Failure Modes

  • Key epoch expires unexpectedly: fail closed on reads. get() returns EpochExpired.
  • Disconnected: if you cannot verify/renew key lease, assume expiry is imminent and fail closed.
  • Partial rotation: entries encrypted under old epoch remain decryptable until expire_old() is called. Keep epoch metadata until all active entries are rewrapped or expired.

Variations

  • split DEK material across multiple key leases (requires quorum to decrypt)
  • per-tenant epochs for multi-tenant isolation
  • durable “vault index” in block storage mapping entry id -> ciphertext locator + epoch
  • use renew_active(duration_secs) to extend the active epoch’s TTL before it expires