Skip to main content

quiver_core/
keyring.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! The key-supply seam between the storage engine and `quiver-crypto`.
3//!
4//! A [`KeyRing`] tells the [`Store`](crate::Store) which [`PageCodec`] seals
5//! which bytes: one **catalog** codec for the engine-wide structures (the
6//! manifest and the write-ahead log), and a **per-collection** codec for each
7//! collection's segments and index artifacts. Splitting the codec by collection
8//! is what makes **crypto-shredding** possible (ADR-0010): when a collection's
9//! data is sealed under its own data-encryption key (DEK), destroying that one
10//! small key renders the collection's durable bytes unrecoverable even if the
11//! ciphertext survives in a backup.
12//!
13//! This module defines the seam and the trivial [`SingleCodecKeyRing`], which
14//! preserves the pre-envelope behaviour — one codec for everything, either the
15//! plaintext [`PlainCodec`] when encryption-at-rest is off or a single AEAD codec
16//! when a key is configured without the per-collection envelope. The envelope
17//! key-ring that wraps per-collection DEKs under a master key lives in
18//! `quiver-crypto`, so the engine itself stays free of key management.
19
20use crate::error::Result;
21use crate::ids::CollectionId;
22use crate::page::{PageCodec, PlainCodec};
23
24/// Supplies the page codecs the storage engine seals data with, and manages the
25/// per-collection key lifecycle that crypto-shredding relies on.
26///
27/// Implementations are shared for the lifetime of a [`Store`](crate::Store), so
28/// they must be `Send + Sync`.
29pub trait KeyRing: Send + Sync {
30    /// The codec for engine-wide structures: the manifest and the write-ahead
31    /// log.
32    fn catalog_codec(&self) -> &dyn PageCodec;
33
34    /// The codec for one collection's segments and index artifacts.
35    ///
36    /// # Errors
37    /// Fails if the collection's key material is unavailable — for an envelope
38    /// key-ring that means it was crypto-shredded, so the data is intentionally
39    /// unrecoverable.
40    fn collection_codec(&self, collection: CollectionId) -> Result<Box<dyn PageCodec>>;
41
42    /// Provision key material for a new collection. Idempotent, and a no-op for
43    /// key-rings without per-collection keys.
44    ///
45    /// # Errors
46    /// Fails if key material cannot be generated or persisted.
47    fn provision_collection(&self, collection: CollectionId) -> Result<()>;
48
49    /// Crypto-shred a collection: destroy its key material so its sealed data can
50    /// never be decrypted again. A no-op for key-rings without per-collection
51    /// keys, where reclaiming the files is the only erasure.
52    ///
53    /// # Errors
54    /// Fails if the key material cannot be destroyed.
55    fn shred_collection(&self, collection: CollectionId) -> Result<()>;
56}
57
58/// A [`KeyRing`] that seals everything — catalog and every collection — with one
59/// shared codec.
60///
61/// This is the pre-envelope behaviour: [`PlainCodec`] when encryption-at-rest is
62/// disabled, or a single AEAD codec when a key is configured without the
63/// per-collection envelope. It holds no per-collection keys, so `provision` and
64/// `shred` are no-ops — a dropped collection is erased only by reclaiming its
65/// files.
66pub struct SingleCodecKeyRing {
67    codec: Box<dyn PageCodec>,
68}
69
70impl SingleCodecKeyRing {
71    /// Wrap a single codec as a key-ring.
72    #[must_use]
73    pub fn new(codec: Box<dyn PageCodec>) -> Self {
74        Self { codec }
75    }
76
77    /// A plaintext key-ring — encryption-at-rest disabled.
78    #[must_use]
79    pub fn plaintext() -> Self {
80        Self::new(Box::new(PlainCodec))
81    }
82}
83
84impl KeyRing for SingleCodecKeyRing {
85    fn catalog_codec(&self) -> &dyn PageCodec {
86        self.codec.as_ref()
87    }
88
89    fn collection_codec(&self, _collection: CollectionId) -> Result<Box<dyn PageCodec>> {
90        Ok(self.codec.clone_box())
91    }
92
93    fn provision_collection(&self, _collection: CollectionId) -> Result<()> {
94        Ok(())
95    }
96
97    fn shred_collection(&self, _collection: CollectionId) -> Result<()> {
98        Ok(())
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::page::PAGE_SIZE;
106
107    #[test]
108    fn single_codec_keyring_shares_one_codec() {
109        let kr = SingleCodecKeyRing::plaintext();
110        // Catalog and every collection resolve to a codec with the same block
111        // size (the plaintext identity codec here).
112        assert_eq!(kr.catalog_codec().block_size(), PAGE_SIZE);
113        let c0 = kr.collection_codec(CollectionId(0)).unwrap();
114        let c1 = kr.collection_codec(CollectionId(1)).unwrap();
115        assert_eq!(c0.block_size(), PAGE_SIZE);
116        assert_eq!(c1.block_size(), PAGE_SIZE);
117    }
118
119    #[test]
120    fn single_codec_keyring_provision_and_shred_are_noops() {
121        let kr = SingleCodecKeyRing::plaintext();
122        // No per-collection keys: provisioning and shredding always succeed and
123        // leave the codec available.
124        kr.provision_collection(CollectionId(7)).unwrap();
125        kr.shred_collection(CollectionId(7)).unwrap();
126        assert!(kr.collection_codec(CollectionId(7)).is_ok());
127    }
128}