lair_keystore_api 0.2.3

secret lair private keystore API library
Documentation
//! Items related to securely persisting keystore secrets (e.g. to disk).

use crate::*;
use futures::future::BoxFuture;
use std::future::Future;
use std::sync::Arc;

fn is_false(b: impl std::borrow::Borrow<bool>) -> bool {
    !b.borrow()
}

/// Helper traits for store types - you probably don't need these unless
/// you are implementing new lair core instance logic.
pub mod traits {
    use super::*;

    /// Defines a lair storage mechanism.
    pub trait AsLairStore: 'static + Send + Sync {
        /// Return the context key for both encryption and decryption
        /// of secret data within the store that is NOT deep_locked.
        fn get_bidi_ctx_key(&self) -> sodoken::BufReadSized<32>;

        /// List the entries tracked by the lair store.
        fn list_entries(
            &self,
        ) -> BoxFuture<'static, LairResult<Vec<LairEntryInfo>>>;

        /// Write a new entry to the lair store.
        /// Should error if the tag already exists.
        fn write_entry(
            &self,
            entry: LairEntry,
        ) -> BoxFuture<'static, LairResult<()>>;

        /// Get an entry from the lair store by tag.
        fn get_entry_by_tag(
            &self,
            tag: Arc<str>,
        ) -> BoxFuture<'static, LairResult<LairEntry>>;

        /// Get an entry from the lair store by ed25519 pub key.
        fn get_entry_by_ed25519_pub_key(
            &self,
            ed25519_pub_key: Ed25519PubKey,
        ) -> BoxFuture<'static, LairResult<LairEntry>>;

        /// Get an entry from the lair store by x25519 pub key.
        fn get_entry_by_x25519_pub_key(
            &self,
            x25519_pub_key: X25519PubKey,
        ) -> BoxFuture<'static, LairResult<LairEntry>>;
    }

    /// Defines a factory that produces lair storage mechanism instances.
    pub trait AsLairStoreFactory: 'static + Send + Sync {
        /// Open a store connection with given config / passphrase.
        fn connect_to_store(
            &self,
            unlock_secret: sodoken::BufReadSized<32>,
        ) -> BoxFuture<'static, LairResult<LairStore>>;
    }
}
use traits::*;

/// Public information associated with a given seed.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SeedInfo {
    /// The ed25519 signature public key derived from this seed.
    pub ed25519_pub_key: Ed25519PubKey,

    /// The x25519 encryption public key derived from this seed.
    pub x25519_pub_key: X25519PubKey,

    /// Flag indicating if this seed is allowed to be exported.
    #[serde(skip_serializing_if = "is_false", default)]
    pub exportable: bool,
}

/// The 32 byte blake2b digest of the der encoded tls certificate.
pub type CertDigest = BinDataSized<32>;

/// Public information associated with a given tls certificate.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CertInfo {
    /// The random sni that was generated for this certificate.
    pub sni: Arc<str>,

    /// The 32 byte blake2b digest of the der encoded tls certificate.
    pub digest: CertDigest,

    /// The der-encoded tls certificate bytes.
    pub cert: BinData,
}

/// The type and tag of this lair entry.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
#[non_exhaustive]
pub enum LairEntryInfo {
    /// This entry is type 'Seed' (see LairEntryInner).
    Seed {
        /// User-supplied tag for this seed.
        tag: Arc<str>,

        /// The seed info associated with this seed.
        seed_info: SeedInfo,
    },

    /// This entry is type 'DeepLockedSeed' (see LairEntryInner).
    DeepLockedSeed {
        /// User-supplied tag for this seed.
        tag: Arc<str>,

        /// The seed info associated with this seed
        seed_info: SeedInfo,
    },

    /// This entry is type 'TlsCert' (see LairEntryInner).
    WkaTlsCert {
        /// User-supplied tag for this seed.
        tag: Arc<str>,

        /// The certificate info.
        cert_info: CertInfo,
    },
}

/// The raw lair entry inner types that can be stored. This is generally
/// wrapped by an `Arc`. See the typedef [LairEntry].
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
#[non_exhaustive]
pub enum LairEntryInner {
    /// This seed can be
    /// - derived
    /// - used for ed25519 signatures
    /// - used for x25519 encryption
    /// The secretstream seed uses the base passphrase-derived secret
    /// for decryption.
    Seed {
        /// User-supplied tag for this seed.
        tag: Arc<str>,

        /// The seed info associated with this seed.
        seed_info: SeedInfo,

        /// The actual seed, encrypted with context key.
        seed: SecretDataSized<32, 49>,
    },

    /// As 'Seed' but requires an additional access-time passphrase.
    DeepLockedSeed {
        /// User-supplied tag for this seed.
        tag: Arc<str>,

        /// The seed info associated with this seed.
        seed_info: SeedInfo,

        /// Salt for argon2id encrypted seed.
        salt: BinDataSized<16>,

        /// Argon2id ops limit used when encrypting this seed.
        ops_limit: u32,

        /// Argon2id mem limit used when encrypting this seed.
        mem_limit: u32,

        /// The actual seed, encrypted with deep passphrase.
        seed: SecretDataSized<32, 49>,
    },

    /// This tls cert and private key can be used to establish tls cryptography
    /// The secretstream priv_key uses the base passphrase-derived secret
    /// for decryption.
    WkaTlsCert {
        /// User-supplied tag for this tls certificate.
        tag: Arc<str>,

        /// The certificate info.
        cert_info: CertInfo,

        /// The certificate private key, encrypted with context key.
        priv_key: SecretData,
    },
}

impl LairEntryInner {
    /// Encode this LairEntry as bytes.
    pub fn encode(&self) -> LairResult<Box<[u8]>> {
        use serde::Serialize;
        let mut se =
            rmp_serde::encode::Serializer::new(Vec::new()).with_struct_map();
        self.serialize(&mut se).map_err(one_err::OneErr::new)?;
        Ok(se.into_inner().into_boxed_slice())
    }

    /// Decode a LairEntry from bytes.
    pub fn decode(bytes: &[u8]) -> LairResult<LairEntryInner> {
        let item: LairEntryInner =
            rmp_serde::from_read(bytes).map_err(one_err::OneErr::new)?;
        Ok(item)
    }

    /// Get the tag associated with this entry.
    pub fn tag(&self) -> Arc<str> {
        match self {
            Self::Seed { tag, .. } => tag.clone(),
            Self::DeepLockedSeed { tag, .. } => tag.clone(),
            Self::WkaTlsCert { tag, .. } => tag.clone(),
        }
    }
}

/// An actual LairEntry. Unlike [LairEntryInfo], this type contains the
/// actual secrets associated with the keystore entry.
pub type LairEntry = Arc<LairEntryInner>;

/// A handle to a running lair keystore backend persistance instance.
/// Allows storing, listing, and retrieving keystore secrets.
#[derive(Clone)]
pub struct LairStore(pub Arc<dyn AsLairStore>);

impl LairStore {
    /// Return the context key for both encryption and decryption
    /// of secret data within the store that is NOT deep_locked.
    pub fn get_bidi_ctx_key(&self) -> sodoken::BufReadSized<32> {
        AsLairStore::get_bidi_ctx_key(&*self.0)
    }

    /// Inject a pre-generated seed,
    /// and associate it with the given tag, returning the
    /// seed_info derived from the generated seed.
    pub fn insert_seed(
        &self,
        seed: sodoken::BufReadSized<32>,
        tag: Arc<str>,
        exportable: bool,
    ) -> impl Future<Output = LairResult<SeedInfo>> + 'static + Send {
        let inner = self.0.clone();
        async move {
            // derive the ed25519 signature keypair from this seed
            let ed_pk = sodoken::BufWriteSized::new_no_lock();
            let ed_sk = sodoken::BufWriteSized::new_mem_locked()?;
            sodoken::sign::seed_keypair(ed_pk.clone(), ed_sk, seed.clone())
                .await?;

            // derive the x25519 encryption keypair from this seed
            let x_pk = sodoken::BufWriteSized::new_no_lock();
            let x_sk = sodoken::BufWriteSized::new_mem_locked()?;
            sodoken::crypto_box::curve25519xchacha20poly1305::seed_keypair(
                x_pk.clone(),
                x_sk,
                seed.clone(),
            )
            .await?;

            // encrypt the seed with our bidi context key
            let key = inner.get_bidi_ctx_key();
            let seed = SecretDataSized::encrypt(key, seed).await?;

            // populate our seed info with the derived public keys
            let seed_info = SeedInfo {
                ed25519_pub_key: ed_pk.try_unwrap_sized().unwrap().into(),
                x25519_pub_key: x_pk.try_unwrap_sized().unwrap().into(),
                exportable,
            };

            // construct the entry for the keystore
            let entry = LairEntryInner::Seed {
                tag,
                seed_info: seed_info.clone(),
                seed,
            };

            // write the entry to the store
            inner.write_entry(Arc::new(entry)).await?;

            // return the seed info
            Ok(seed_info)
        }
    }

    /// Generate a new cryptographically secure random seed,
    /// and associate it with the given tag, returning the
    /// seed_info derived from the generated seed.
    pub fn new_seed(
        &self,
        tag: Arc<str>,
        exportable: bool,
    ) -> impl Future<Output = LairResult<SeedInfo>> + 'static + Send {
        let this = self.clone();
        async move {
            // generate a new random seed
            let seed = sodoken::BufWriteSized::new_mem_locked()?;
            sodoken::random::bytes_buf(seed.clone()).await?;

            this.insert_seed(seed.to_read_sized(), tag, exportable)
                .await
        }
    }

    /// Inject a pre-generated seed,
    /// and associate it with the given tag, returning the
    /// seed_info derived from the generated seed.
    /// This seed is deep_locked, meaning it needs an additional
    /// runtime passphrase to be decrypted / used.
    pub fn insert_deep_locked_seed(
        &self,
        seed: sodoken::BufReadSized<32>,
        tag: Arc<str>,
        ops_limit: u32,
        mem_limit: u32,
        deep_lock_passphrase: sodoken::BufReadSized<64>,
        exportable: bool,
    ) -> impl Future<Output = LairResult<SeedInfo>> + 'static + Send {
        let inner = self.0.clone();
        async move {
            // derive the ed25519 signature keypair from this seed
            let ed_pk = sodoken::BufWriteSized::new_no_lock();
            let ed_sk = sodoken::BufWriteSized::new_mem_locked()?;
            sodoken::sign::seed_keypair(ed_pk.clone(), ed_sk, seed.clone())
                .await?;

            // derive the x25519 encryption keypair from this seed
            let x_pk = sodoken::BufWriteSized::new_no_lock();
            let x_sk = sodoken::BufWriteSized::new_mem_locked()?;
            sodoken::crypto_box::curve25519xchacha20poly1305::seed_keypair(
                x_pk.clone(),
                x_sk,
                seed.clone(),
            )
            .await?;

            // generate the salt for the pwhash deep locking
            let salt = <sodoken::BufWriteSized<16>>::new_no_lock();
            sodoken::random::bytes_buf(salt.clone()).await?;

            // generate the deep lock key from the passphrase
            let key = <sodoken::BufWriteSized<32>>::new_mem_locked()?;
            sodoken::hash::argon2id::hash(
                key.clone(),
                deep_lock_passphrase,
                salt.clone(),
                ops_limit,
                mem_limit,
            )
            .await?;

            // encrypt the seed with the deep lock key
            let seed =
                SecretDataSized::encrypt(key.to_read_sized(), seed).await?;

            // populate our seed info with the derived public keys
            let seed_info = SeedInfo {
                ed25519_pub_key: ed_pk.try_unwrap_sized().unwrap().into(),
                x25519_pub_key: x_pk.try_unwrap_sized().unwrap().into(),
                exportable,
            };

            // construct the entry for the keystore
            let entry = LairEntryInner::DeepLockedSeed {
                tag,
                seed_info: seed_info.clone(),
                salt: salt.try_unwrap_sized().unwrap().into(),
                ops_limit,
                mem_limit,
                seed,
            };

            // write the entry to the store
            inner.write_entry(Arc::new(entry)).await?;

            // return the seed info
            Ok(seed_info)
        }
    }

    /// Generate a new cryptographically secure random seed,
    /// and associate it with the given tag, returning the
    /// seed_info derived from the generated seed.
    /// This seed is deep_locked, meaning it needs an additional
    /// runtime passphrase to be decrypted / used.
    pub fn new_deep_locked_seed(
        &self,
        tag: Arc<str>,
        ops_limit: u32,
        mem_limit: u32,
        deep_lock_passphrase: sodoken::BufReadSized<64>,
        exportable: bool,
    ) -> impl Future<Output = LairResult<SeedInfo>> + 'static + Send {
        let this = self.clone();
        async move {
            // generate a new random seed
            let seed = sodoken::BufWriteSized::new_mem_locked()?;
            sodoken::random::bytes_buf(seed.clone()).await?;

            this.insert_deep_locked_seed(
                seed.to_read_sized(),
                tag,
                ops_limit,
                mem_limit,
                deep_lock_passphrase,
                exportable,
            )
            .await
        }
    }

    /// Generate a new cryptographically secure random wka tls cert,
    /// and associate it with the given tag, returning the
    /// cert_info derived from the generated cert.
    pub fn new_wka_tls_cert(
        &self,
        tag: Arc<str>,
    ) -> impl Future<Output = LairResult<CertInfo>> + 'static + Send {
        let inner = self.0.clone();
        async move {
            use crate::internal::tls::*;

            // generate the random well-known-authority signed certificate.
            let TlsCertGenResult {
                sni,
                priv_key,
                cert,
                digest,
            } = tls_cert_self_signed_new().await?;

            // encrypt the private key with our context secret
            let key = inner.get_bidi_ctx_key();
            let priv_key = SecretData::encrypt(key, priv_key).await?;

            // populate the certificate info
            let cert_info = CertInfo {
                sni,
                digest: digest.into(),
                cert: cert.into(),
            };

            // construct the entry for the keystore
            let entry = LairEntryInner::WkaTlsCert {
                tag,
                cert_info: cert_info.clone(),
                priv_key,
            };

            // write the entry to the store
            inner.write_entry(Arc::new(entry)).await?;

            // return the cert info
            Ok(cert_info)
        }
    }

    /// List the entries tracked by the lair store.
    pub fn list_entries(
        &self,
    ) -> impl Future<Output = LairResult<Vec<LairEntryInfo>>> + 'static + Send
    {
        AsLairStore::list_entries(&*self.0)
    }

    /// Get an entry from the lair store by tag.
    pub fn get_entry_by_tag(
        &self,
        tag: Arc<str>,
    ) -> impl Future<Output = LairResult<LairEntry>> + 'static + Send {
        AsLairStore::get_entry_by_tag(&*self.0, tag)
    }

    /// Get an entry from the lair store by ed25519 pub key.
    pub fn get_entry_by_ed25519_pub_key(
        &self,
        ed25519_pub_key: Ed25519PubKey,
    ) -> impl Future<Output = LairResult<LairEntry>> + 'static + Send {
        AsLairStore::get_entry_by_ed25519_pub_key(&*self.0, ed25519_pub_key)
    }

    /// Get an entry from the lair store by x25519 pub key.
    pub fn get_entry_by_x25519_pub_key(
        &self,
        x25519_pub_key: X25519PubKey,
    ) -> impl Future<Output = LairResult<LairEntry>> + 'static + Send {
        AsLairStore::get_entry_by_x25519_pub_key(&*self.0, x25519_pub_key)
    }
}

/// A factory abstraction allowing connecting to a lair keystore persistance
/// backend with an unlock secret (generally derived from a user passphrase).
#[derive(Clone)]
pub struct LairStoreFactory(pub Arc<dyn AsLairStoreFactory>);

impl LairStoreFactory {
    /// Connect to an existing store with the given unlock_secret.
    pub fn connect_to_store(
        &self,
        unlock_secret: sodoken::BufReadSized<32>,
    ) -> impl Future<Output = LairResult<LairStore>> + 'static + Send {
        AsLairStoreFactory::connect_to_store(&*self.0, unlock_secret)
    }
}