Skip to main content

spacedb_store/
collection.rs

1//! [`Collection<K, V>`] — the encrypted typed table.
2//!
3//! A `Collection` is a [`crate::Table`] whose values are sealed under a
4//! per-collection DEK ([`crate::crypto`]): the engine only ever stores
5//! `nonce ‖ ciphertext`, so a host that holds the bytes (a replica on a
6//! stranger's machine, in later milestones) stores something it cannot read.
7//!
8//! The DEK is wrapped under the vault key and persisted in the reserved
9//! `_dek_wrappings` table; the `Collection` caches that **ciphertext** wrapping.
10//! On every row operation it fetches the vault key through the [`KeyProvider`]
11//! seam and unwraps the DEK — so a vault that locks mid-session (cold-gate)
12//! immediately blocks reads and writes, rather than being bypassed by a cached
13//! plaintext key.
14//!
15//! Keys are **not** encrypted (the engine needs them in the clear to index and
16//! range-scan); only values are. Key privacy, where needed, is achieved by
17//! hashing the key before it reaches the store (the ADR 0005 `blake3(rp_origin)`
18//! pattern) — a caller concern, not this layer's.
19
20use std::marker::PhantomData;
21use std::sync::Arc;
22
23use serde::{de::DeserializeOwned, Serialize};
24use zeroize::Zeroizing;
25
26use crate::codec::{decode_value, encode_value, KeyDecode, KeyEncode};
27use crate::crypto::{open_row, seal_row, unwrap_dek, wrap_fresh_dek, KeyProvider, WrappedDek, KEY_LEN};
28use crate::engine::{Durability, KvEngine, Readable, WriteTx};
29use crate::error::{StoreError, StoreResult};
30use crate::table::Table;
31
32/// The reserved table that stores each collection's wrapped DEK, keyed by
33/// collection name. Collection names may not collide with reserved (`_`-prefixed)
34/// tables.
35pub const DEK_WRAPPINGS_TABLE: &str = "_dek_wrappings";
36
37fn wrappings_table() -> Table<String, WrappedDek> {
38    Table::new(DEK_WRAPPINGS_TABLE)
39}
40
41/// An encrypted, typed collection. Rows are AEAD-sealed under a per-collection
42/// DEK; see the module docs for the trust model.
43pub struct Collection<K, V> {
44    name: String,
45    schema_version: u32,
46    /// The DEK wrapped under the vault key — ciphertext, safe to hold in memory.
47    wrapped_dek: WrappedDek,
48    key_provider: Arc<dyn KeyProvider>,
49    _types: PhantomData<fn() -> (K, V)>,
50}
51
52impl<K, V> std::fmt::Debug for Collection<K, V> {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        // Deliberately omits the key provider and the wrapped DEK.
55        f.debug_struct("Collection")
56            .field("name", &self.name)
57            .field("schema_version", &self.schema_version)
58            .finish_non_exhaustive()
59    }
60}
61
62impl<K, V> Collection<K, V>
63where
64    K: KeyEncode + KeyDecode,
65    V: Serialize + DeserializeOwned,
66{
67    /// Open an **existing** collection. Errors with
68    /// [`StoreError::CollectionNotFound`] if no DEK wrapping has been provisioned.
69    pub fn open<E: KvEngine>(
70        engine: &E,
71        key_provider: Arc<dyn KeyProvider>,
72        name: impl Into<String>,
73        schema_version: u32,
74    ) -> StoreResult<Self> {
75        let name = Self::checked_name(name)?;
76        let r = engine.begin_read()?;
77        let wrapped = wrappings_table()
78            .get(&r, &name)?
79            .ok_or_else(|| StoreError::CollectionNotFound(name.clone()))?;
80        Ok(Self::assemble(name, schema_version, wrapped, key_provider))
81    }
82
83    /// Open a collection, provisioning a fresh DEK on first use. The check and the
84    /// create happen in one write transaction, so a collection is never
85    /// double-provisioned with conflicting DEKs by a concurrent opener.
86    pub fn open_or_create<E: KvEngine>(
87        engine: &E,
88        key_provider: Arc<dyn KeyProvider>,
89        name: impl Into<String>,
90        schema_version: u32,
91    ) -> StoreResult<Self> {
92        let name = Self::checked_name(name)?;
93        let table = wrappings_table();
94
95        let mut w = engine.begin_write(Durability::Immediate)?;
96        if let Some(existing) = table.get(&w, &name)? {
97            // Already provisioned — nothing to write. Drop the txn (no commit).
98            drop(w);
99            return Ok(Self::assemble(name, schema_version, existing, key_provider));
100        }
101
102        // First use: generate + wrap a fresh DEK under the vault key.
103        let vault_key = key_provider.vault_key()?;
104        let (wrapped, _dek) = wrap_fresh_dek(&vault_key, &name)?;
105        table.put(&mut w, &name, &wrapped)?;
106        w.commit()?;
107
108        Ok(Self::assemble(name, schema_version, wrapped, key_provider))
109    }
110
111    fn assemble(
112        name: String,
113        schema_version: u32,
114        wrapped_dek: WrappedDek,
115        key_provider: Arc<dyn KeyProvider>,
116    ) -> Self {
117        Self {
118            name,
119            schema_version,
120            wrapped_dek,
121            key_provider,
122            _types: PhantomData,
123        }
124    }
125
126    fn checked_name(name: impl Into<String>) -> StoreResult<String> {
127        let name = name.into();
128        if name.starts_with('_') {
129            return Err(StoreError::ReservedName(name));
130        }
131        Ok(name)
132    }
133
134    /// The collection's name (its table name).
135    pub fn name(&self) -> &str {
136        &self.name
137    }
138
139    /// The schema version bound into every row's AAD.
140    pub fn schema_version(&self) -> u32 {
141        self.schema_version
142    }
143
144    /// Fetch the vault key (cold-gated) and unwrap this collection's DEK. Done
145    /// per operation so a mid-session lock takes effect immediately.
146    fn dek(&self) -> StoreResult<Zeroizing<[u8; KEY_LEN]>> {
147        let vault_key = self.key_provider.vault_key()?;
148        Ok(unwrap_dek(&vault_key, &self.name, &self.wrapped_dek)?)
149    }
150
151    /// Fetch and decrypt the value for `key`, or `None` if absent. A missing row
152    /// returns `None` **without** touching the vault — only a present row requires
153    /// an unlock to decrypt.
154    pub fn get(&self, tx: &impl Readable, key: &K) -> StoreResult<Option<V>> {
155        let key_bytes = key.encode();
156        let sealed = match tx.get_raw(&self.name, &key_bytes)? {
157            Some(bytes) => bytes,
158            None => return Ok(None),
159        };
160        let dek = self.dek()?;
161        let plain = open_row(&dek, &self.name, &key_bytes, self.schema_version, &sealed)?;
162        Ok(Some(decode_value(&plain)?))
163    }
164
165    /// Encrypt and store `value` under `key`.
166    pub fn put(&self, tx: &mut impl WriteTx, key: &K, value: &V) -> StoreResult<()> {
167        let key_bytes = key.encode();
168        let dek = self.dek()?;
169        let sealed = seal_row(
170            &dek,
171            &self.name,
172            &key_bytes,
173            self.schema_version,
174            &encode_value(value)?,
175        )?;
176        tx.put_raw(&self.name, &key_bytes, &sealed)
177    }
178
179    /// Remove `key`. Returns `true` if a value was present. No key material is
180    /// needed to delete a ciphertext row.
181    pub fn delete(&self, tx: &mut impl WriteTx, key: &K) -> StoreResult<bool> {
182        tx.delete_raw(&self.name, &key.encode())
183    }
184
185    /// Decrypt and return the `(key, value)` pairs in `[lo, hi)`, in ascending
186    /// logical key order. The DEK is unwrapped once for the whole scan.
187    pub fn range(&self, tx: &impl Readable, lo: &K, hi: &K) -> StoreResult<Vec<(K, V)>> {
188        let raw = tx.range_raw(&self.name, &lo.encode(), &hi.encode())?;
189        if raw.is_empty() {
190            return Ok(Vec::new());
191        }
192        let dek = self.dek()?;
193        raw.into_iter()
194            .map(|(key_bytes, sealed)| {
195                let plain = open_row(&dek, &self.name, &key_bytes, self.schema_version, &sealed)?;
196                Ok((K::decode(&key_bytes)?, decode_value(&plain)?))
197            })
198            .collect()
199    }
200}