lair_keystore_api 0.2.3

secret lair private keystore API library
Documentation
//! An in-process keystore that manages the entire lair server life-cycle
//! without needing to call out to an external process.

use crate::*;
use std::future::Future;

/// An in-process keystore that manages the entire lair server life-cycle
/// without needing to call out to an external process.
#[derive(Clone)]
pub struct InProcKeystore {
    config: LairServerConfig,
    passphrase: sodoken::BufRead,
    srv_hnd: crate::lair_server::LairServer,
}

impl InProcKeystore {
    /// Construct a new InProcKeystore instance.
    /// The internal server will already be "interactively" unlocked.
    pub fn new<P>(
        config: LairServerConfig,
        store_factory: LairStoreFactory,
        passphrase: P,
    ) -> impl Future<Output = LairResult<Self>> + 'static + Send
    where
        P: Into<sodoken::BufRead> + 'static + Send,
    {
        async move {
            let passphrase = passphrase.into();

            // set up our server handler
            let srv_hnd = crate::lair_server::spawn_lair_server_task(
                config.clone(),
                "lair-keystore-in-proc".into(),
                crate::LAIR_VER.into(),
                store_factory,
                passphrase.clone(),
            )
            .await?;

            Ok(Self {
                config,
                passphrase,
                srv_hnd,
            })
        }
    }

    /// Get a handle to the LairStore instantiated by this server,
    /// may error if a store has not yet been created.
    pub fn store(
        &self,
    ) -> impl Future<Output = LairResult<LairStore>> + 'static + Send {
        self.srv_hnd.store()
    }

    /// Get the config used by the LairServer held by this InProcKeystore.
    pub fn get_config(&self) -> LairServerConfig {
        self.config.clone()
    }

    /// Get a new LairClient connection to this InProcKeystore server.
    /// This new connection will already have verified the server identity
    /// via "hello" request as well as unlocked the connection.
    pub fn new_client(
        &self,
    ) -> impl Future<Output = LairResult<LairClient>> + 'static + Send {
        let srv_pub_key = self.config.get_server_pub_key();
        let passphrase = self.passphrase.clone();
        let srv_hnd = self.srv_hnd.clone();
        async move {
            let srv_pub_key = srv_pub_key?;

            // note, for now it greatly simplifies the implementation
            // to just have the single server implementation expecting
            // to process async read/write channels. This increases
            // overhead for the in-process implementation, so, someday
            // we could make a shortcut for this use-case.

            // create a new duplex to simulate networking code
            let (srv, cli) = tokio::io::duplex(4096);

            // split into read/write halves
            let (srv_recv, srv_send) = tokio::io::split(srv);
            let (cli_recv, cli_send) = tokio::io::split(cli);

            // get the server accept future
            let srv_fut = srv_hnd.accept(srv_send, srv_recv);

            // get the client wrap future
            let cli_fut =
                crate::lair_client::async_io::new_async_io_lair_client(
                    cli_send,
                    cli_recv,
                    srv_pub_key.cloned_inner().into(),
                );

            // await both futures at the same time so they can
            // exchange information
            let (_, cli_hnd) =
                futures::future::try_join(srv_fut, cli_fut).await?;

            // verify server identity
            cli_hnd.hello(srv_pub_key).await?;

            // unlock the connection
            cli_hnd.unlock(passphrase).await?;

            Ok(cli_hnd)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;

    #[tokio::test(flavor = "multi_thread")]
    async fn in_proc_happy_path() {
        // set up a passphrase
        let passphrase = sodoken::BufRead::from(&b"passphrase"[..]);

        // create the config for the test server
        // the path is immaterial since we'll be using an in-memory store
        let config = Arc::new(
            hc_seed_bundle::PwHashLimits::Interactive
                .with_exec(|| {
                    LairServerConfigInner::new("/", passphrase.clone())
                })
                .await
                .unwrap(),
        );

        // create an in-process keystore with an in-memory store
        let keystore = InProcKeystore::new(
            config,
            crate::mem_store::create_mem_store_factory(),
            passphrase.clone(),
        )
        .await
        .unwrap();

        let config = keystore.get_config();
        println!("{}", config);

        // create a client connection to the keystore
        let client = keystore.new_client().await.unwrap();

        // create a new seed
        let seed_info_ref = client
            .new_seed("test-tag".into(), None, false)
            .await
            .unwrap();

        // list keystore contents
        let mut entry_list = client.list_entries().await.unwrap();

        assert_eq!(1, entry_list.len());
        match entry_list.remove(0) {
            LairEntryInfo::Seed { tag, seed_info } => {
                assert_eq!("test-tag", &*tag);
                assert_eq!(seed_info, seed_info_ref);
            }
            oth => panic!("unexpected: {:?}", oth),
        }

        // create a new deep-locked seed
        let _seed_info_ref_deep = hc_seed_bundle::PwHashLimits::Interactive
            .with_exec(|| {
                client.new_seed(
                    "test-tag-deep".into(),
                    Some(sodoken::BufRead::from(&b"deep"[..])),
                    false,
                )
            })
            .await
            .unwrap();

        println!("{:#?}", client.list_entries().await.unwrap());

        let seed_info_ref2 = client
            .new_seed("test-tag-2".into(), None, false)
            .await
            .unwrap();

        let (nonce, cipher) = client
            .crypto_box_xsalsa_by_pub_key(
                seed_info_ref.x25519_pub_key.clone(),
                seed_info_ref2.x25519_pub_key.clone(),
                None,
                b"hello"[..].into(),
            )
            .await
            .unwrap();

        let msg = client
            .crypto_box_xsalsa_open_by_pub_key(
                seed_info_ref.x25519_pub_key,
                seed_info_ref2.x25519_pub_key,
                None,
                nonce,
                cipher,
            )
            .await
            .unwrap();

        assert_eq!(b"hello", &*msg);
    }
}