Skip to main content

corium_transactor/
keys.rs

1//! Per-database storage encryption.
2//!
3//! Encryption is a property of one database, and the node's storage service is
4//! shared by all of them, so every database that has a key manifest gets its
5//! own view of that service: an [`EncryptedBlobStore`] decorator over the
6//! shared backend and a [`LogCipher`] over its transaction log. A database
7//! with no manifest keeps reading and writing plaintext, forever — that is
8//! fixed at creation and is what keeps existing databases working untouched.
9//!
10//! Keys are unwrapped here, at open and at rotation, and never on a blob read
11//! or a log append: everything below holds an already-unwrapped snapshot.
12
13use std::collections::BTreeMap;
14use std::sync::Arc;
15use std::time::SystemTime;
16
17use async_trait::async_trait;
18use corium_crypt::{KeyId, Keyring, SecretKey};
19use corium_log::{LogCipher, LogError};
20use corium_store::{
21    BlobId, BlobIdStream, BlobStore, EncryptedBlobStore, KeyManifest, RootStore, StoreError,
22};
23
24use crate::backend::NodeStore;
25
26/// One database's view of the node's storage service.
27///
28/// Root records are cleartext under both arms — they hold no user data, and
29/// `RootStore::compare_and_set` compares bytes — so only blob access differs.
30pub enum DbStore {
31    /// An unencrypted database: the shared backend, unchanged.
32    Plain(Arc<NodeStore>),
33    /// An encrypted database: plaintext in, deterministic ciphertext out,
34    /// blob ids the digests of the stored objects.
35    Encrypted(EncryptedBlobStore<Arc<NodeStore>>),
36}
37
38impl DbStore {
39    /// The undecorated backend, for operations that work on stored bytes:
40    /// listing, deletion, timestamps, and root records.
41    #[must_use]
42    pub fn raw(&self) -> &Arc<NodeStore> {
43        match self {
44            Self::Plain(store) => store,
45            Self::Encrypted(store) => store.inner(),
46        }
47    }
48
49    /// The storage-key epoch new blobs are written under, when encrypted.
50    #[must_use]
51    pub fn storage_epoch(&self) -> Option<u32> {
52        match self {
53            Self::Plain(_) => None,
54            Self::Encrypted(store) => Some(store.current_epoch()),
55        }
56    }
57}
58
59#[async_trait]
60impl BlobStore for DbStore {
61    async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
62        match self {
63            Self::Plain(store) => store.put(bytes).await,
64            Self::Encrypted(store) => store.put(bytes).await,
65        }
66    }
67
68    async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
69        match self {
70            Self::Plain(store) => store.get(id).await,
71            Self::Encrypted(store) => store.get(id).await,
72        }
73    }
74
75    async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
76        match self {
77            Self::Plain(store) => store.contains(id).await,
78            Self::Encrypted(store) => store.contains(id).await,
79        }
80    }
81
82    async fn put_if_absent(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
83        match self {
84            Self::Plain(store) => store.put_if_absent(bytes).await,
85            Self::Encrypted(store) => store.put_if_absent(bytes).await,
86        }
87    }
88
89    async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
90        self.raw().delete(id).await
91    }
92
93    async fn list(&self) -> Result<BlobIdStream, StoreError> {
94        self.raw().list().await
95    }
96
97    async fn modified_at(&self, id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
98        self.raw().modified_at(id).await
99    }
100}
101
102#[async_trait]
103impl RootStore for DbStore {
104    async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
105        self.raw().get_root(name).await
106    }
107
108    async fn cas_root(
109        &self,
110        name: &str,
111        expected: Option<&[u8]>,
112        new: &[u8],
113    ) -> Result<(), StoreError> {
114        self.raw().cas_root(name, expected, new).await
115    }
116
117    async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
118        self.raw().delete_root(name).await
119    }
120
121    async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
122        self.raw().list_roots(prefix).await
123    }
124}
125
126/// A database's resolved storage-encryption state.
127pub struct DbCrypto {
128    /// The store every read and publication for this database goes through.
129    pub store: Arc<DbStore>,
130    /// The cipher the database's log seals record payloads with, when
131    /// encrypted. The open log holds this same handle, so a rotation installs
132    /// a new key snapshot into it rather than reopening anything.
133    pub cipher: Option<Arc<LogCipher>>,
134}
135
136/// Failure resolving a database's storage keys.
137#[derive(Debug, thiserror::Error)]
138pub enum KeyWiringError {
139    /// The database is encrypted and this process holds no keyring.
140    #[error(
141        "database {db:?} is encrypted under key {kek}; \
142         start this process with --storage-key naming it"
143    )]
144    NoKeyring {
145        /// Database whose manifest was found.
146        db: String,
147        /// Key-encryption key the manifest names.
148        kek: KeyId,
149    },
150    /// The manifest carries no epoch new writes could use.
151    #[error("database {0:?} has a key manifest with no active storage-key epoch")]
152    NoActiveEpoch(String),
153    /// Store or key-resolution failure.
154    #[error(transparent)]
155    Store(#[from] StoreError),
156    /// The log cipher rejected the resolved keys.
157    #[error(transparent)]
158    Log(#[from] LogError),
159}
160
161/// The lineage bytes authenticated into every log record of `db`.
162///
163/// A record's AAD binds this, so a record cannot be opened as if it belonged
164/// to another database. The database name is that identity today; a restore
165/// under a different name therefore mints its own keys and re-seals, which is
166/// what backup format 2 will have to arrange.
167#[must_use]
168pub fn log_lineage(db: &str) -> Vec<u8> {
169    db.as_bytes().to_vec()
170}
171
172/// Resolves `manifest` into the store and log cipher `db` operates through.
173///
174/// `manifest` is `None` for an unencrypted database, which needs no keyring
175/// even when the process has one.
176///
177/// # Errors
178///
179/// Returns [`KeyWiringError`] when the database is encrypted and this process
180/// has no keyring, when the manifest names no active epoch, or when any epoch
181/// cannot be unwrapped — so a misconfigured process fails at open, naming the
182/// key, rather than at its first read.
183pub async fn resolve_db_crypto(
184    db: &str,
185    store: &Arc<NodeStore>,
186    manifest: Option<&KeyManifest>,
187    keyring: Option<&Arc<dyn Keyring>>,
188) -> Result<DbCrypto, KeyWiringError> {
189    let Some(manifest) = manifest.filter(|manifest| !manifest.storage_keys.is_empty()) else {
190        return Ok(DbCrypto {
191            store: Arc::new(DbStore::Plain(Arc::clone(store))),
192            cipher: None,
193        });
194    };
195    let Some(keyring) = keyring else {
196        return Err(KeyWiringError::NoKeyring {
197            db: db.to_owned(),
198            kek: manifest.kek.clone(),
199        });
200    };
201    let (epoch, keys) = unwrap_active(db, manifest, keyring.as_ref()).await?;
202    Ok(DbCrypto {
203        store: Arc::new(DbStore::Encrypted(EncryptedBlobStore::new(
204            Arc::clone(store),
205            epoch,
206            keys.clone(),
207        )?)),
208        cipher: Some(Arc::new(LogCipher::new(log_lineage(db), epoch, keys)?)),
209    })
210}
211
212/// Rebuilds `crypto` from a manifest that has just changed.
213///
214/// The blob decorator is reconstructed, and the log cipher — held by the open
215/// log — takes the new snapshot in place, so a rotation takes effect on the
216/// next append with no restart and no log reopen.
217///
218/// # Errors
219///
220/// Returns [`KeyWiringError`] under the same conditions as
221/// [`resolve_db_crypto`]. `crypto` is left untouched unless every epoch
222/// resolves.
223pub async fn reload_db_crypto(
224    db: &str,
225    crypto: &DbCrypto,
226    manifest: &KeyManifest,
227    keyring: Option<&Arc<dyn Keyring>>,
228) -> Result<Arc<DbStore>, KeyWiringError> {
229    let Some(keyring) = keyring else {
230        return Err(KeyWiringError::NoKeyring {
231            db: db.to_owned(),
232            kek: manifest.kek.clone(),
233        });
234    };
235    let (epoch, keys) = unwrap_active(db, manifest, keyring.as_ref()).await?;
236    let store = Arc::new(DbStore::Encrypted(EncryptedBlobStore::new(
237        Arc::clone(crypto.store.raw()),
238        epoch,
239        keys.clone(),
240    )?));
241    if let Some(cipher) = &crypto.cipher {
242        cipher.install(epoch, keys)?;
243    }
244    Ok(store)
245}
246
247async fn unwrap_active(
248    db: &str,
249    manifest: &KeyManifest,
250    keyring: &dyn Keyring,
251) -> Result<(u32, BTreeMap<u32, SecretKey>), KeyWiringError> {
252    let epoch = manifest
253        .active_storage_epoch()
254        .ok_or_else(|| KeyWiringError::NoActiveEpoch(db.to_owned()))?;
255    Ok((epoch, manifest.unwrap_storage_keys(keyring).await?))
256}