Skip to content

Recipe 9: Encrypting Data at Rest With a Lease-Scoped Key

Situation

You want encrypted storage, but you also want a strong operational property:

decryption capability should exist only while the program is alive and actively renewing access.

Conventional secret management often leaves long-lived keys in:

  • config
  • environment variables
  • secret stores with large blast radii

In a lease-based system, you can scope the key itself as a leased resource with a TTL.

What You Build

An EncryptedLease pattern:

  • Ciphertext is stored in a “data lease” (memory or block storage).
  • The data-encryption key (DEK) is stored only in a separate short-TTL “key lease”.
  • The program renews the key lease while it wants the ciphertext to be decryptable.

When key lease expires, ciphertext becomes inert.

Building Blocks

  • grafos_securestore::{KeyEpochManager, EncryptedBlobStore} for envelope encryption with epoch-based key rotation — source
  • MemBuilder for the key lease
  • BlockBuilder (or MemBuilder) for the data lease
  • grafos_sync::watch() if you need to distribute key epoch metadata

Related API docs:

Threat Model Notes

This pattern is strong against:

  • post-mortem recovery of fabric memory or disks after the program stops renewing keys

It does not protect against:

  • an attacker who compromises the process while the key lease is active
  • a fabric implementation that does not securely zero key memory on reclaim

Be explicit about these assumptions in production documentation.

Design

Envelope Encryption via KeyEpochManager

KeyEpochManager manages a sequence of key epochs, each with its own DEK and TTL. The active epoch’s DEK is used for new encrypts; old epochs remain readable until they expire.

EncryptedBlobStore wraps a crypto backend and the key manager, providing put/get/remove for encrypted blobs. Under the hood it performs envelope encryption: each blob is encrypted with the active epoch’s DEK, and the epoch ID is stored alongside the ciphertext.

Key Epochs and Rotation

use grafos_securestore::{KeyEpochManager, MockCryptoBackend, EpochStatus};
let mut key_mgr = KeyEpochManager::new(Box::new(MockCryptoBackend::new()));
let now = 1_000_000u64;
let old_epoch_id = key_mgr.create_epoch(now, /* ttl_secs */ 300)?;
// Later, rotate to a new epoch:
key_mgr.rotate(now + 200, 300)?;
// Old epoch transitions: Active -> Rotating -> Expired
let status = key_mgr.get_epoch(old_epoch_id).map(|e| e.status);
assert_eq!(status, Some(EpochStatus::Rotating));

When an epoch expires, its DEK is gone. Ciphertext encrypted under that epoch becomes inert — fail-closed.

Renewal

Renew the key lease before TTL expiry (e.g. at 60-80% of TTL). If renewal fails, treat it as “key is going away” and stop decrypting.

Walkthrough (Implementation Sketch)

1. Create Key Manager and Blob Store

use grafos_securestore::{KeyEpochManager, EncryptedBlobStore, MockCryptoBackend, BlobId};
let mut key_mgr = KeyEpochManager::new(Box::new(MockCryptoBackend::new()));
let now = 1_000_000u64;
key_mgr.create_epoch(now, 300)?; // 5-minute TTL
let mut store = EncryptedBlobStore::new(key_mgr);

2. Encrypt and Store

let blob_info = store.put(BlobId(42), b"sensitive payload")?;

Internally: fetches the active epoch’s DEK, generates a nonce, AEAD-encrypts the plaintext, stores (epoch_id, nonce, ciphertext).

3. Decrypt and Read

let plaintext = store.get(&blob_info)?;

Looks up the epoch ID stored with the blob, retrieves the DEK (if the epoch is still active or rotating), and decrypts. If the epoch has expired, this returns an error — fail-closed.

4. Key Rotation

let now = now + 200; // some time later
store.key_manager_mut().rotate(now, 300)?;

New writes use the new epoch. Old blobs remain readable until their epoch expires.

5. Deletion

store.remove(&blob_info.blob_id)?;

Failure Modes

  • LeaseExpired for key lease: key is gone; reads fail; data remains as ciphertext.
  • Disconnected: treat as transient; if you can’t renew, assume key will expire and fail closed.

Observability

Track:

  • key lease renewals
  • key epoch rotations
  • decrypt failures due to expired key

Variations

  • Shamir splitting across multiple key leases (more complex)
  • Split key material by audience and require multiple capabilities