lair_keystore_api/
lair_store.rs

1//! Items related to securely persisting keystore secrets (e.g. to disk).
2
3use crate::types::SharedSizedLockedArray;
4use crate::*;
5use futures::future::BoxFuture;
6use one_err::OneErr;
7use std::future::Future;
8use std::sync::{Arc, Mutex};
9
10fn is_false(b: impl std::borrow::Borrow<bool>) -> bool {
11    !b.borrow()
12}
13
14/// Helper traits for store types - you probably don't need these unless
15/// you are implementing new lair core instance logic.
16pub mod traits {
17    use super::*;
18
19    /// Defines a lair storage mechanism.
20    pub trait AsLairStore: 'static + Send + Sync {
21        /// Return the context key for both encryption and decryption
22        /// of secret data within the store that is NOT deep_locked.
23        fn get_bidi_ctx_key(&self) -> SharedSizedLockedArray<32>;
24
25        /// List the entries tracked by the lair store.
26        fn list_entries(
27            &self,
28        ) -> BoxFuture<'static, LairResult<Vec<LairEntryInfo>>>;
29
30        /// Write a new entry to the lair store.
31        /// Should error if the tag already exists.
32        fn write_entry(
33            &self,
34            entry: LairEntry,
35        ) -> BoxFuture<'static, LairResult<()>>;
36
37        /// Get an entry from the lair store by tag.
38        fn get_entry_by_tag(
39            &self,
40            tag: Arc<str>,
41        ) -> BoxFuture<'static, LairResult<LairEntry>>;
42
43        /// Get an entry from the lair store by ed25519 pub key.
44        fn get_entry_by_ed25519_pub_key(
45            &self,
46            ed25519_pub_key: Ed25519PubKey,
47        ) -> BoxFuture<'static, LairResult<LairEntry>>;
48
49        /// Get an entry from the lair store by x25519 pub key.
50        fn get_entry_by_x25519_pub_key(
51            &self,
52            x25519_pub_key: X25519PubKey,
53        ) -> BoxFuture<'static, LairResult<LairEntry>>;
54    }
55
56    /// Defines a factory that produces lair storage mechanism instances.
57    pub trait AsLairStoreFactory: 'static + Send + Sync {
58        /// Open a store connection with given config / passphrase.
59        fn connect_to_store(
60            &self,
61            unlock_secret: SharedSizedLockedArray<32>,
62        ) -> BoxFuture<'static, LairResult<LairStore>>;
63    }
64}
65use traits::*;
66
67/// Public information associated with a given seed.
68#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
69#[serde(rename_all = "camelCase")]
70pub struct SeedInfo {
71    /// The ed25519 signature public key derived from this seed.
72    pub ed25519_pub_key: Ed25519PubKey,
73
74    /// The x25519 encryption public key derived from this seed.
75    pub x25519_pub_key: X25519PubKey,
76
77    /// Flag indicating if this seed is allowed to be exported.
78    #[serde(skip_serializing_if = "is_false", default)]
79    pub exportable: bool,
80}
81
82/// The 32 byte blake2b digest of the der encoded tls certificate.
83pub type CertDigest = BinDataSized<32>;
84
85/// Public information associated with a given tls certificate.
86#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
87#[serde(rename_all = "camelCase")]
88pub struct CertInfo {
89    /// The random sni that was generated for this certificate.
90    pub sni: Arc<str>,
91
92    /// The 32 byte blake2b digest of the der encoded tls certificate.
93    pub digest: CertDigest,
94
95    /// The der-encoded tls certificate bytes.
96    pub cert: BinData,
97}
98
99/// The type and tag of this lair entry.
100#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
101#[serde(tag = "type", rename_all = "camelCase")]
102#[non_exhaustive]
103pub enum LairEntryInfo {
104    /// This entry is type 'Seed' (see LairEntryInner).
105    Seed {
106        /// User-supplied tag for this seed.
107        tag: Arc<str>,
108
109        /// The seed info associated with this seed.
110        seed_info: SeedInfo,
111    },
112
113    /// This entry is type 'DeepLockedSeed' (see LairEntryInner).
114    DeepLockedSeed {
115        /// User-supplied tag for this seed.
116        tag: Arc<str>,
117
118        /// The seed info associated with this seed
119        seed_info: SeedInfo,
120    },
121
122    /// This entry is type 'TlsCert' (see LairEntryInner).
123    WkaTlsCert {
124        /// User-supplied tag for this seed.
125        tag: Arc<str>,
126
127        /// The certificate info.
128        cert_info: CertInfo,
129    },
130}
131
132/// Data type for secret seed
133pub type Seed = SecretDataSized<32, 49>;
134
135/// The raw lair entry inner types that can be stored. This is generally
136/// wrapped by an `Arc`. See the typedef [LairEntry].
137#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
138#[serde(tag = "type", rename_all = "camelCase")]
139#[non_exhaustive]
140pub enum LairEntryInner {
141    /// This seed can be
142    /// - derived
143    /// - used for ed25519 signatures
144    /// - used for x25519 encryption
145    ///
146    /// The secretstream seed uses the base passphrase-derived secret
147    /// for decryption.
148    Seed {
149        /// User-supplied tag for this seed.
150        tag: Arc<str>,
151
152        /// The seed info associated with this seed.
153        seed_info: SeedInfo,
154
155        /// The actual seed, encrypted with context key.
156        seed: Seed,
157    },
158
159    /// As 'Seed' but requires an additional access-time passphrase.
160    DeepLockedSeed {
161        /// User-supplied tag for this seed.
162        tag: Arc<str>,
163
164        /// The seed info associated with this seed.
165        seed_info: SeedInfo,
166
167        /// Salt for argon2id encrypted seed.
168        salt: BinDataSized<16>,
169
170        /// Argon2id ops limit used when encrypting this seed.
171        ops_limit: u32,
172
173        /// Argon2id mem limit used when encrypting this seed.
174        mem_limit: u32,
175
176        /// The actual seed, encrypted with deep passphrase.
177        seed: Seed,
178    },
179
180    /// This tls cert and private key can be used to establish tls cryptography
181    /// The secretstream priv_key uses the base passphrase-derived secret
182    /// for decryption.
183    WkaTlsCert {
184        /// User-supplied tag for this tls certificate.
185        tag: Arc<str>,
186
187        /// The certificate info.
188        cert_info: CertInfo,
189
190        /// The certificate private key, encrypted with context key.
191        priv_key: SecretData,
192    },
193}
194
195impl LairEntryInner {
196    /// Encode this LairEntry as bytes.
197    pub fn encode(&self) -> LairResult<Box<[u8]>> {
198        use serde::Serialize;
199        let mut se =
200            rmp_serde::encode::Serializer::new(Vec::new()).with_struct_map();
201        self.serialize(&mut se).map_err(one_err::OneErr::new)?;
202        Ok(se.into_inner().into_boxed_slice())
203    }
204
205    /// Decode a LairEntry from bytes.
206    pub fn decode(bytes: &[u8]) -> LairResult<LairEntryInner> {
207        let item: LairEntryInner =
208            rmp_serde::from_read(bytes).map_err(one_err::OneErr::new)?;
209        Ok(item)
210    }
211
212    /// Get the tag associated with this entry.
213    pub fn tag(&self) -> Arc<str> {
214        match self {
215            Self::Seed { tag, .. } => tag.clone(),
216            Self::DeepLockedSeed { tag, .. } => tag.clone(),
217            Self::WkaTlsCert { tag, .. } => tag.clone(),
218        }
219    }
220}
221
222/// An actual LairEntry. Unlike [LairEntryInfo], this type contains the
223/// actual secrets associated with the keystore entry.
224pub type LairEntry = Arc<LairEntryInner>;
225
226/// A handle to a running lair keystore backend persistance instance.
227/// Allows storing, listing, and retrieving keystore secrets.
228#[derive(Clone)]
229pub struct LairStore(pub Arc<dyn AsLairStore>);
230
231impl LairStore {
232    /// Return the context key for both encryption and decryption
233    /// of secret data within the store that is NOT deep_locked.
234    pub fn get_bidi_ctx_key(&self) -> SharedSizedLockedArray<32> {
235        AsLairStore::get_bidi_ctx_key(&*self.0)
236    }
237
238    /// Inject a pre-generated seed,
239    /// and associate it with the given tag, returning the
240    /// seed_info derived from the generated seed.
241    pub fn insert_seed(
242        &self,
243        seed: SharedSizedLockedArray<32>,
244        tag: Arc<str>,
245        exportable: bool,
246    ) -> impl Future<Output = LairResult<SeedInfo>> + 'static + Send {
247        let inner = self.0.clone();
248        async move {
249            // derive the ed25519 signature keypair from this seed
250            let mut ed_pk = [0; sodoken::sign::PUBLICKEYBYTES];
251            let mut ed_sk = sodoken::SizedLockedArray::<
252                { sodoken::sign::SECRETKEYBYTES },
253            >::new()?;
254            sodoken::sign::seed_keypair(
255                &mut ed_pk,
256                &mut ed_sk.lock(),
257                &seed.lock().unwrap().lock(),
258            )?;
259
260            // derive the x25519 encryption keypair from this seed
261            let mut x_pk = [0; sodoken::crypto_box::XSALSA_PUBLICKEYBYTES];
262            let mut x_sk = sodoken::SizedLockedArray::<
263                { sodoken::crypto_box::XSALSA_SECRETKEYBYTES },
264            >::new()?;
265            sodoken::crypto_box::xsalsa_seed_keypair(
266                &mut x_pk,
267                &mut x_sk.lock(),
268                &seed.lock().unwrap().lock(),
269            )?;
270
271            // encrypt the seed with our bidi context key
272            let key = inner.get_bidi_ctx_key();
273            let seed = SecretDataSized::encrypt(key, seed).await?;
274
275            // populate our seed info with the derived public keys
276            let seed_info = SeedInfo {
277                ed25519_pub_key: ed_pk.into(),
278                x25519_pub_key: x_pk.into(),
279                exportable,
280            };
281
282            // construct the entry for the keystore
283            let entry = LairEntryInner::Seed {
284                tag,
285                seed_info: seed_info.clone(),
286                seed,
287            };
288
289            // write the entry to the store
290            inner.write_entry(Arc::new(entry)).await?;
291
292            // return the seed info
293            Ok(seed_info)
294        }
295    }
296
297    /// Generate a new cryptographically secure random seed,
298    /// and associate it with the given tag, returning the
299    /// seed_info derived from the generated seed.
300    pub fn new_seed(
301        &self,
302        tag: Arc<str>,
303        exportable: bool,
304    ) -> impl Future<Output = LairResult<SeedInfo>> + 'static + Send {
305        let this = self.clone();
306        async move {
307            // generate a new random seed
308            let mut seed = sodoken::SizedLockedArray::<32>::new()?;
309            sodoken::random::randombytes_buf(&mut *seed.lock())?;
310
311            let seed = Arc::new(Mutex::new(seed));
312
313            this.insert_seed(seed, tag, exportable).await
314        }
315    }
316
317    /// Inject a pre-generated seed,
318    /// and associate it with the given tag, returning the
319    /// seed_info derived from the generated seed.
320    /// This seed is deep_locked, meaning it needs an additional
321    /// runtime passphrase to be decrypted / used.
322    pub fn insert_deep_locked_seed(
323        &self,
324        seed: SharedSizedLockedArray<32>,
325        tag: Arc<str>,
326        ops_limit: u32,
327        mem_limit: u32,
328        mut deep_lock_passphrase: sodoken::SizedLockedArray<64>,
329        exportable: bool,
330    ) -> impl Future<Output = LairResult<SeedInfo>> + 'static + Send {
331        let inner = self.0.clone();
332        async move {
333            // derive the ed25519 signature keypair from this seed
334            let mut ed_pk = [0; sodoken::sign::PUBLICKEYBYTES];
335            let mut ed_sk = sodoken::SizedLockedArray::<
336                { sodoken::sign::SECRETKEYBYTES },
337            >::new()?;
338            sodoken::sign::seed_keypair(
339                &mut ed_pk,
340                &mut ed_sk.lock(),
341                &seed.lock().unwrap().lock(),
342            )?;
343
344            // derive the x25519 encryption keypair from this seed
345            let mut x_pk = [0; sodoken::crypto_box::XSALSA_PUBLICKEYBYTES];
346            let mut x_sk = sodoken::SizedLockedArray::<
347                { sodoken::crypto_box::XSALSA_SECRETKEYBYTES },
348            >::new()?;
349            sodoken::crypto_box::xsalsa_seed_keypair(
350                &mut x_pk,
351                &mut x_sk.lock(),
352                &seed.lock().unwrap().lock(),
353            )?;
354
355            // generate the deep lock key from the passphrase
356            let (salt, key) = tokio::task::spawn_blocking({
357                move || -> std::io::Result<([u8; sodoken::argon2::ARGON2_ID_SALTBYTES], sodoken::SizedLockedArray<32>)> {
358                    // generate the salt for the pwhash deep locking
359                    let mut salt = [0; sodoken::argon2::ARGON2_ID_SALTBYTES];
360                    sodoken::random::randombytes_buf(&mut salt)?;
361
362                    let mut key = sodoken::SizedLockedArray::<32>::new()?;
363                    sodoken::argon2::blocking_argon2id(
364                        &mut *key.lock(),
365                        &*deep_lock_passphrase.lock(),
366                        &salt,
367                        ops_limit,
368                        mem_limit,
369                    )?;
370
371                    Ok((salt, key))
372                }
373            })
374            .await
375            .map_err(OneErr::new)??;
376
377            // encrypt the seed with the deep lock key
378            let seed =
379                SecretDataSized::encrypt(Arc::new(Mutex::new(key)), seed)
380                    .await?;
381
382            // populate our seed info with the derived public keys
383            let seed_info = SeedInfo {
384                ed25519_pub_key: ed_pk.into(),
385                x25519_pub_key: x_pk.into(),
386                exportable,
387            };
388
389            // construct the entry for the keystore
390            let entry = LairEntryInner::DeepLockedSeed {
391                tag,
392                seed_info: seed_info.clone(),
393                salt: salt.into(),
394                ops_limit,
395                mem_limit,
396                seed,
397            };
398
399            // write the entry to the store
400            inner.write_entry(Arc::new(entry)).await?;
401
402            // return the seed info
403            Ok(seed_info)
404        }
405    }
406
407    /// Generate a new cryptographically secure random seed,
408    /// and associate it with the given tag, returning the
409    /// seed_info derived from the generated seed.
410    /// This seed is deep_locked, meaning it needs an additional
411    /// runtime passphrase to be decrypted / used.
412    pub fn new_deep_locked_seed(
413        &self,
414        tag: Arc<str>,
415        ops_limit: u32,
416        mem_limit: u32,
417        deep_lock_passphrase: sodoken::SizedLockedArray<64>,
418        exportable: bool,
419    ) -> impl Future<Output = LairResult<SeedInfo>> + 'static + Send {
420        let this = self.clone();
421        async move {
422            // generate a new random seed
423            let mut seed = sodoken::SizedLockedArray::<32>::new()?;
424            sodoken::random::randombytes_buf(&mut *seed.lock())?;
425
426            let seed = Arc::new(Mutex::new(seed));
427
428            this.insert_deep_locked_seed(
429                seed,
430                tag,
431                ops_limit,
432                mem_limit,
433                deep_lock_passphrase,
434                exportable,
435            )
436            .await
437        }
438    }
439
440    /// Generate a new cryptographically secure random wka tls cert,
441    /// and associate it with the given tag, returning the
442    /// cert_info derived from the generated cert.
443    pub fn new_wka_tls_cert(
444        &self,
445        tag: Arc<str>,
446    ) -> impl Future<Output = LairResult<CertInfo>> + 'static + Send {
447        let inner = self.0.clone();
448        async move {
449            use crate::internal::tls::*;
450
451            // generate the random well-known-authority signed certificate.
452            let TlsCertGenResult {
453                sni,
454                priv_key,
455                cert,
456                digest,
457            } = tls_cert_self_signed_new().await?;
458
459            // encrypt the private key with our context secret
460            let key = inner.get_bidi_ctx_key();
461            let priv_key = SecretData::encrypt(key, priv_key).await?;
462
463            // populate the certificate info
464            let cert_info = CertInfo {
465                sni,
466                digest: digest.into(),
467                cert: cert.into(),
468            };
469
470            // construct the entry for the keystore
471            let entry = LairEntryInner::WkaTlsCert {
472                tag,
473                cert_info: cert_info.clone(),
474                priv_key,
475            };
476
477            // write the entry to the store
478            inner.write_entry(Arc::new(entry)).await?;
479
480            // return the cert info
481            Ok(cert_info)
482        }
483    }
484
485    /// List the entries tracked by the lair store.
486    pub fn list_entries(
487        &self,
488    ) -> impl Future<Output = LairResult<Vec<LairEntryInfo>>> + 'static + Send
489    {
490        AsLairStore::list_entries(&*self.0)
491    }
492
493    /// Get an entry from the lair store by tag.
494    pub fn get_entry_by_tag(
495        &self,
496        tag: Arc<str>,
497    ) -> impl Future<Output = LairResult<LairEntry>> + 'static + Send {
498        AsLairStore::get_entry_by_tag(&*self.0, tag)
499    }
500
501    /// Get an entry from the lair store by ed25519 pub key.
502    pub fn get_entry_by_ed25519_pub_key(
503        &self,
504        ed25519_pub_key: Ed25519PubKey,
505    ) -> impl Future<Output = LairResult<LairEntry>> + 'static + Send {
506        AsLairStore::get_entry_by_ed25519_pub_key(&*self.0, ed25519_pub_key)
507    }
508
509    /// Get an entry from the lair store by x25519 pub key.
510    pub fn get_entry_by_x25519_pub_key(
511        &self,
512        x25519_pub_key: X25519PubKey,
513    ) -> impl Future<Output = LairResult<LairEntry>> + 'static + Send {
514        AsLairStore::get_entry_by_x25519_pub_key(&*self.0, x25519_pub_key)
515    }
516}
517
518/// A factory abstraction allowing connecting to a lair keystore persistance
519/// backend with an unlock secret (generally derived from a user passphrase).
520#[derive(Clone)]
521pub struct LairStoreFactory(pub Arc<dyn AsLairStoreFactory>);
522
523impl LairStoreFactory {
524    /// Connect to an existing store with the given unlock_secret.
525    pub fn connect_to_store(
526        &self,
527        unlock_secret: sodoken::SizedLockedArray<32>,
528    ) -> impl Future<Output = LairResult<LairStore>> + 'static + Send {
529        AsLairStoreFactory::connect_to_store(
530            &*self.0,
531            Arc::new(Mutex::new(unlock_secret)),
532        )
533    }
534}