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}