Skip to main content
QIMEM implements a lineage-based key rotation system that enables cryptographic agility while maintaining backward compatibility for decryption.

Key Lifecycle States

Key Metadata Structure

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyMetadata {
    /// Key identifier.
    pub key_id: Uuid,
    /// Lineage identifier.
    pub lineage_id: Uuid,
    /// Rotation version.
    pub version: i32,
    /// Whether key is active for encryption.
    pub active: bool,
}
Source: src/keystore/mod.rs:19-29

Field Semantics

Unique identifier for this specific key version.
  • Generated via Uuid::new_v4() on creation or rotation
  • Used as primary lookup key in storage
  • Stored in Envelope.key_id field after encryption
Identifier shared across all versions in a rotation chain.
  • Equal to the first key’s key_id in the lineage
  • Enables querying all versions of a rotated key
  • Immutable across rotations
Example lineage:
Key 1: key_id=A, lineage_id=A, version=1  (root)
Key 2: key_id=B, lineage_id=A, version=2  (rotated)
Key 3: key_id=C, lineage_id=A, version=3  (rotated)
Monotonically increasing version number within the lineage.
  • Root key starts at version=1
  • Incremented by 1 on each rotation
  • Enables auditing rotation history
Whether this key version can be used for new encryptions.
  • Only the latest version in a lineage is active
  • Inactive keys can still decrypt old envelopes
  • Attempting to encrypt with inactive key returns QimemError::KeyInactive

Key Material Structure

#[derive(Debug, Clone)]
pub struct KeyMaterial {
    /// Key identifier.
    pub key_id: Uuid,
    /// Encryption key bytes.
    pub material: Zeroizing<Vec<u8>>,
    /// Active status.
    pub active: bool,
}
Source: src/keystore/mod.rs:32-40
Security: The material field uses zeroize::Zeroizing to ensure key bytes are securely erased from memory when dropped. See Security for details.

Key Store Operations

The KeyStore trait defines the key lifecycle contract:
pub trait KeyStore: Send + Sync {
    /// Create a new root key.
    fn create_key(&self) -> Result<KeyMetadata>;
    /// Retrieve key material by key id.
    fn get_key(&self, key_id: Uuid) -> Result<KeyMaterial>;
    /// Rotate a key and return the new active version.
    fn rotate_key(&self, key_id: Uuid) -> Result<KeyMetadata>;
}
Source: src/keystore/mod.rs:43-50

Operation Details

Creates a new root key (the first key in a lineage).Process:
  1. Generate new UUID for key_id
  2. Set lineage_id = key_id (root property)
  3. Set version = 1
  4. Set active = true
  5. Generate 32 random bytes for key material via OsRng
  6. Store key and return metadata
Implementation (InMemoryKeyStore):
fn create_key(&self) -> Result<KeyMetadata> {
    let key_id = Uuid::new_v4();
    let lineage_id = key_id;
    let metadata = KeyMetadata {
        key_id,
        lineage_id,
        version: 1,
        active: true,
    };
    let stored = StoredKey {
        metadata: metadata.clone(),
        material: generate_key_material().to_vec(),
    };
    self.keys.write()?.insert(key_id, stored);
    self.lineages.write()?.insert(lineage_id, key_id);
    Ok(metadata)
}
Source: src/keystore/memory.rs:23-45

Rotation Design

From the README:
Key rotation design
  • Rotation creates a new active key record and deactivates the old key.
  • Old keys remain available for decrypt compatibility.
  • Inactive keys are rejected for encryption.
Source: README.md:32-35

Why Rotation Matters

Compliance

Many security standards (PCI-DSS, HIPAA, etc.) require periodic key rotation.

Blast Radius

Limits the amount of data encrypted with a single key, reducing impact of key compromise.

Forward Secrecy

Old data remains encrypted with old keys even after rotation.

Zero Downtime

Old envelopes decrypt seamlessly during and after rotation.

Storage Backend Comparison

Stateless storage using RwLock<HashMap<Uuid, StoredKey>>.Structure:
#[derive(Debug, Default)]
pub struct InMemoryKeyStore {
    keys: RwLock<HashMap<Uuid, StoredKey>>,
    lineages: RwLock<HashMap<Uuid, Uuid>>,
}
Source: src/keystore/memory.rs:16-20Properties:
  • Keys lost on process restart
  • No persistence between CLI invocations
  • Suitable for testing and stateless API mode
  • Lock contention on high concurrency
Use Case: Development, testing, ephemeral environments

Key Material Generation

All backends use the same secure key generation:
pub(crate) fn generate_key_material() -> Zeroizing<Vec<u8>> {
    let mut key = vec![0_u8; 32];
    OsRng.fill_bytes(&mut key);
    Zeroizing::new(key)
}
Source: src/keystore/mod.rs:52-56
32 bytes (256 bits) is the required key size for AES-256-GCM. ChaCha20-Poly1305 also uses 256-bit keys.

Error Handling

ErrorWhenRecovery
KeyNotFound(Uuid)get_key() or rotate_key() on unknown keyEnsure key was created
KeyInactive(Uuid)Attempting to encrypt with inactive keyRotate key or use active version
Config(String)Lock poisoning (in-memory) or connection errors (Postgres)Restart or fix database
Source: src/error.rs:20-37

Best Practices

1

Rotate Regularly

Establish a rotation schedule (e.g., every 90 days) based on compliance requirements.
2

Never Delete Old Keys

Inactive keys must remain available to decrypt historical envelopes.
3

Track Lineage

Use lineage_id to query all versions of a key for auditing.
4

Use Postgres in Production

The in-memory store loses keys on restart. Use stateful mode for persistence.
5

Monitor Active Keys

Alert if multiple keys in the same lineage are active (indicates rotation bug).

See Also

Envelope Format

How key_id is embedded in envelopes

Security

Key material protection with zeroize

API Reference

Full KeyStore trait documentation

Build docs developers (and LLMs) love