Skip to main content

coven_crypto/
keys.rs

1//! Every opened key remains available by its store or circle key id (§12.2).
2
3use std::collections::BTreeMap;
4use std::fmt;
5
6use coven_foundation::id_source::KeyId;
7use sha2::{Digest, Sha256};
8use subtle::ConstantTimeEq;
9use uuid::Uuid;
10use zeroize::Zeroizing;
11
12use crate::{derivation, randomness, wire};
13use crate::{CircleId, CryptoError, DerivedKeys, EncryptionKey, MaterialError, SecretBytes};
14
15/// A store's identified 32-byte key (§12), erased on drop.
16/// Cloning supplies an independent unlocked snapshot to in-memory custody.
17#[derive(Clone)]
18pub struct StoreKey {
19    id: KeyId,
20    bytes: Zeroizing<[u8; 32]>,
21}
22
23impl StoreKey {
24    /// The encoded key a box or custody holds: its id, then its 32 bytes.
25    pub const ENCODED_LEN: usize = 16 + 32;
26
27    /// Make random key material for an identity supplied by the id source.
28    pub fn generate(id: KeyId) -> Result<Self, CryptoError> {
29        Ok(Self { id, bytes: randomness::secret_bytes()? })
30    }
31
32    /// Import a store key opened from custody or a member's sealed copy (§12.1).
33    pub fn from_bytes(id: KeyId, bytes: [u8; 32]) -> Self {
34        let bytes = Zeroizing::new(bytes);
35        Self { id, bytes }
36    }
37
38    /// The random identity named by the entry introducing this key (§12.1).
39    pub fn id(&self) -> KeyId {
40        self.id
41    }
42
43    /// SHA-256 of the exact key bytes, committed inside its introducing entry (§12.1).
44    pub fn hash(&self) -> [u8; 32] {
45        Sha256::digest(self.bytes.as_ref()).into()
46    }
47
48    /// Check the hash committed by an authorized introduction before accepting this key.
49    pub fn check_hash(&self, expected: &[u8; 32]) -> Result<(), MaterialError> {
50        if !bool::from(self.hash().ct_eq(expected)) {
51            return Err(MaterialError::StoreKeyConflict(self.id));
52        }
53        Ok(())
54    }
55
56    /// Derive separate encryption and authorship keys.
57    pub fn derive(&self) -> DerivedKeys {
58        DerivedKeys::new(&self.bytes)
59    }
60
61    pub(crate) fn encode_into(&self, bytes: &mut Vec<u8>) {
62        bytes.extend_from_slice(self.id().0.as_bytes());
63        bytes.extend_from_slice(self.bytes.as_ref());
64    }
65
66    pub(crate) fn decode(bytes: &mut &[u8]) -> Result<Self, MaterialError> {
67        Ok(Self::from_encoded(&Zeroizing::new(wire::array::<{ Self::ENCODED_LEN }>(bytes)?)))
68    }
69
70    /// Read the encoding [`Self::encode_into`] writes.
71    pub(crate) fn from_encoded(encoded: &[u8; Self::ENCODED_LEN]) -> Self {
72        let id = KeyId(Uuid::from_bytes(std::array::from_fn(|i| encoded[i])));
73        Self::from_bytes(id, std::array::from_fn(|i| encoded[16 + i]))
74    }
75}
76
77impl fmt::Debug for StoreKey {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        f.debug_struct("StoreKey").field("id", &self.id).finish_non_exhaustive()
80    }
81}
82
83/// One circle's identified 32-byte key, introduced by creation or rotation (§12.1).
84/// Cloning supplies an independent unlocked snapshot to in-memory custody.
85#[derive(Clone, Debug)]
86pub struct CircleKey {
87    circle: CircleId,
88    key: StoreKey,
89}
90
91impl CircleKey {
92    /// The encoded key a box or custody holds: its circle, then its store-key
93    /// encoding.
94    pub const ENCODED_LEN: usize = 16 + StoreKey::ENCODED_LEN;
95
96    /// Make a circle key using the operating system's randomness.
97    pub fn generate(circle: CircleId, id: KeyId) -> Result<Self, CryptoError> {
98        Ok(Self { circle, key: StoreKey::generate(id)? })
99    }
100
101    /// Import a circle key opened from custody or a member's sealed copy.
102    pub fn from_bytes(circle: CircleId, id: KeyId, bytes: [u8; 32]) -> Self {
103        Self { circle, key: StoreKey::from_bytes(id, bytes) }
104    }
105
106    /// The circle whose rows this key protects; file keys travel inside those rows.
107    pub fn circle(&self) -> CircleId {
108        self.circle
109    }
110
111    /// The random identity named by the entry introducing this key.
112    pub fn id(&self) -> KeyId {
113        self.key.id()
114    }
115
116    /// SHA-256 of the exact key bytes, committed inside its introducing entry (§12.1).
117    pub fn hash(&self) -> [u8; 32] {
118        self.key.hash()
119    }
120
121    /// Check the hash committed by an authorized introduction before accepting this key.
122    pub fn check_hash(&self, expected: &[u8; 32]) -> Result<(), MaterialError> {
123        if !bool::from(self.key.hash().ct_eq(expected)) {
124            return Err(MaterialError::CircleKeyConflict { circle: self.circle, key: self.id() });
125        }
126        Ok(())
127    }
128
129    /// Derive separate encryption and authorship keys.
130    pub fn derive(&self) -> DerivedKeys {
131        self.key.derive()
132    }
133
134    pub(crate) fn encode_into(&self, bytes: &mut Vec<u8>) {
135        bytes.extend_from_slice(self.circle.0.as_bytes());
136        self.key.encode_into(bytes);
137    }
138
139    pub(crate) fn decode(bytes: &mut &[u8]) -> Result<Self, MaterialError> {
140        Ok(Self::from_encoded(&Zeroizing::new(wire::array::<{ Self::ENCODED_LEN }>(bytes)?)))
141    }
142
143    /// Read the encoding [`Self::encode_into`] writes.
144    pub(crate) fn from_encoded(encoded: &[u8; Self::ENCODED_LEN]) -> Self {
145        Self {
146            circle: CircleId(Uuid::from_bytes(std::array::from_fn(|i| encoded[i]))),
147            key: StoreKey::from_encoded(&Zeroizing::new(std::array::from_fn(|i| encoded[16 + i]))),
148        }
149    }
150}
151
152/// The first bytes of a keyring's custody encoding: its magic and version.
153const KEYRING_MAGIC: &[u8] = b"CVKR\x01";
154
155/// Every store and circle key this device has opened, including older keys (§12.2).
156/// Cloning lets in-memory custody keep its copy while returning an unlocked one.
157#[derive(Clone, Debug)]
158pub struct StoreKeyring {
159    stores: BTreeMap<KeyId, StoreKey>,
160    circles: BTreeMap<(CircleId, KeyId), CircleKey>,
161}
162
163impl StoreKeyring {
164    /// Start with one opened store key. A keyring always holds a store key.
165    pub fn new(key: StoreKey) -> Self {
166        Self { stores: BTreeMap::from([(key.id(), key)]), circles: BTreeMap::new() }
167    }
168
169    /// Keep an opened store key; a different key at the same id is an error.
170    pub fn insert_store_key(&mut self, key: StoreKey) -> Result<(), MaterialError> {
171        if let Some(existing) = self.stores.get(&key.id()) {
172            if !bool::from(existing.bytes.as_ref().ct_eq(key.bytes.as_ref())) {
173                return Err(MaterialError::StoreKeyConflict(key.id()));
174            }
175        } else {
176            self.stores.insert(key.id(), key);
177        }
178        Ok(())
179    }
180
181    /// Keep an opened circle key, retaining its earlier keys too (§15.5).
182    pub fn insert_circle_key(&mut self, key: CircleKey) -> Result<(), MaterialError> {
183        let id = (key.circle(), key.id());
184        if let Some(existing) = self.circles.get(&id) {
185            if !bool::from(existing.key.bytes.as_ref().ct_eq(key.key.bytes.as_ref())) {
186                return Err(MaterialError::CircleKeyConflict { circle: id.0, key: id.1 });
187            }
188        } else {
189            self.circles.insert(id, key);
190        }
191        Ok(())
192    }
193
194    /// An opened store key by identity, so older writes still open (§12.2).
195    pub fn store_key(&self, key: KeyId) -> Result<&StoreKey, MaterialError> {
196        self.stores.get(&key).ok_or(MaterialError::UnknownStoreKey(key))
197    }
198
199    /// All held store-key identities, including keys preceding a member's join.
200    pub fn store_key_ids(&self) -> impl Iterator<Item = KeyId> + '_ {
201        self.stores.keys().copied()
202    }
203
204    /// All held keys for a circle, including concurrent and historical rotations.
205    pub fn circle_key_ids(&self, circle: CircleId) -> impl Iterator<Item = KeyId> + '_ {
206        self.circles.keys().filter_map(move |(owner, key)| (*owner == circle).then_some(*key))
207    }
208
209    /// An opened circle key by its circle and identity (§15.5).
210    pub fn circle_key(&self, circle: CircleId, key: KeyId) -> Result<&CircleKey, MaterialError> {
211        self.circles.get(&(circle, key)).ok_or(MaterialError::UnknownCircleKey { circle, key })
212    }
213
214    /// Encode every opened key for custody. The returned bytes remain secret.
215    pub fn to_secret_bytes(&self) -> SecretBytes {
216        // Allocate once: growing a secret Vec would abandon an unerased copy.
217        let mut bytes = Zeroizing::new(Vec::with_capacity(
218            KEYRING_MAGIC.len()
219                + 8
220                + self.stores.len() * StoreKey::ENCODED_LEN
221                + 8
222                + self.circles.len() * CircleKey::ENCODED_LEN,
223        ));
224        bytes.extend_from_slice(KEYRING_MAGIC);
225        bytes.extend_from_slice(&(self.stores.len() as u64).to_le_bytes());
226        for key in self.stores.values() {
227            key.encode_into(&mut bytes);
228        }
229        bytes.extend_from_slice(&(self.circles.len() as u64).to_le_bytes());
230        for key in self.circles.values() {
231            key.encode_into(&mut bytes);
232        }
233        SecretBytes(bytes)
234    }
235
236    /// Decode custody bytes, rejecting malformed, duplicate or empty keyrings.
237    pub fn from_secret_bytes(mut bytes: &[u8]) -> Result<Self, MaterialError> {
238        wire::prefix(&mut bytes, KEYRING_MAGIC)?;
239        let count = wire::number(&mut bytes)?;
240        if count == 0 || count > (bytes.len() / StoreKey::ENCODED_LEN) as u64 {
241            return Err(MaterialError::Encoding);
242        }
243        let mut stores = BTreeMap::new();
244        for _ in 0..count {
245            let key = StoreKey::decode(&mut bytes)?;
246            if stores.insert(key.id(), key).is_some() {
247                return Err(MaterialError::Encoding);
248            }
249        }
250        let count = wire::number(&mut bytes)?;
251        if count > (bytes.len() / CircleKey::ENCODED_LEN) as u64 {
252            return Err(MaterialError::Encoding);
253        }
254        let mut circles = BTreeMap::new();
255        for _ in 0..count {
256            let key = CircleKey::decode(&mut bytes)?;
257            if circles.insert((key.circle(), key.id()), key).is_some() {
258                return Err(MaterialError::Encoding);
259            }
260        }
261        wire::end(bytes)?;
262        Ok(Self { stores, circles })
263    }
264}
265
266/// The one-time invite secret lets a person ask to join, not open the store (§13.2).
267pub struct InviteSecret(Zeroizing<[u8; 32]>);
268
269impl InviteSecret {
270    /// Make an invite secret using the operating system's randomness.
271    pub fn generate() -> Result<Self, CryptoError> {
272        Ok(Self(randomness::secret_bytes()?))
273    }
274
275    /// Import the secret carried by a scanned invite (§13.2).
276    pub fn from_bytes(bytes: [u8; 32]) -> Self {
277        Self(Zeroizing::new(bytes))
278    }
279
280    /// Encode the secret for the invite code, keeping its allocation zeroizing.
281    pub fn to_secret_bytes(&self) -> SecretBytes {
282        SecretBytes::new(self.0.to_vec())
283    }
284
285    /// Derive the join request's encryption key with its own HKDF label (Appendix D).
286    pub fn join_request_key(&self) -> EncryptionKey {
287        EncryptionKey(derivation::derive_label(&self.0, derivation::JOIN_REQUEST))
288    }
289}
290
291impl fmt::Debug for InviteSecret {
292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        f.write_str("InviteSecret([REDACTED])")
294    }
295}
296
297#[cfg(test)]
298#[path = "keys_tests.rs"]
299mod tests;