use crate::*;
use futures::stream::StreamExt;
use std::future::Future;
mod raw_ipc;
#[derive(Clone)]
pub struct IpcKeystoreServer {
config: LairServerConfig,
srv_hnd: crate::lair_server::LairServer,
}
impl IpcKeystoreServer {
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 con_recv = raw_ipc::ipc_bind(config.clone()).await?;
let srv_hnd = crate::lair_server::spawn_lair_server_task(
config.clone(),
"lair-keystore-ipc".into(),
crate::LAIR_VER.into(),
store_factory,
passphrase.into(),
)
.await?;
{
let srv_hnd = srv_hnd.clone();
tokio::task::spawn(async move {
let srv_hnd = &srv_hnd;
con_recv.for_each_concurrent(4096, |incoming| async move {
let (send, recv) = match incoming {
Err(e) => {
tracing::error!("Error accepting incoming ipc connection: {:?}", e);
return;
}
Ok(r) => r,
};
if let Err(e) = srv_hnd.accept(send, recv).await {
tracing::error!("Error accepting incoming ipc connection: {:?}", e);
}
}).await;
tracing::error!(
"IpcKeystoreServer ipc_raw con recv loop ended!"
);
});
}
Ok(Self { config, srv_hnd })
}
}
pub fn store(
&self,
) -> impl Future<Output = LairResult<LairStore>> + 'static + Send {
self.srv_hnd.store()
}
pub fn get_config(&self) -> LairServerConfig {
self.config.clone()
}
}
pub struct IpcKeystoreClientOptions {
pub connection_url: url::Url,
pub passphrase: sodoken::BufRead,
pub exact_client_server_version_match: bool,
}
pub fn ipc_keystore_connect<P>(
connection_url: url::Url,
passphrase: P,
) -> impl Future<Output = LairResult<LairClient>> + 'static + Send
where
P: Into<sodoken::BufRead> + 'static + Send,
{
let passphrase = passphrase.into();
ipc_keystore_connect_options(IpcKeystoreClientOptions {
connection_url,
passphrase,
exact_client_server_version_match: false,
})
}
pub fn ipc_keystore_connect_options(
opts: IpcKeystoreClientOptions,
) -> impl Future<Output = LairResult<LairClient>> + 'static + Send {
async move {
let server_pub_key =
get_server_pub_key_from_connection_url(&opts.connection_url)?;
let (send, recv) =
raw_ipc::ipc_connect(opts.connection_url.clone()).await?;
let cli_hnd = crate::lair_client::async_io::new_async_io_lair_client(
send,
recv,
server_pub_key.cloned_inner().into(),
)
.await?;
let ver = cli_hnd.hello(server_pub_key).await?;
priv_check_hello_ver(&opts, &ver)?;
cli_hnd.unlock(opts.passphrase).await?;
Ok(cli_hnd)
}
}
fn priv_check_hello_ver(
opts: &IpcKeystoreClientOptions,
server_version: &str,
) -> LairResult<()> {
if opts.exact_client_server_version_match
&& server_version != crate::LAIR_VER
{
return Err(format!(
"Invalid lair server version, this client requires '{}', but got '{}'.",
crate::LAIR_VER,
server_version,
).into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
async fn connect(tmp_dir: &tempdir::TempDir) -> crate::LairClient {
let passphrase = sodoken::BufRead::from(&b"passphrase"[..]);
let config = Arc::new(
hc_seed_bundle::PwHashLimits::Minimum
.with_exec(|| {
LairServerConfigInner::new(
tmp_dir.path(),
passphrase.clone(),
)
})
.await
.unwrap(),
);
let keystore = IpcKeystoreServer::new(
config,
crate::mem_store::create_mem_store_factory(),
passphrase.clone(),
)
.await
.unwrap();
let config = keystore.get_config();
println!("{}", config);
ipc_keystore_connect(config.connection_url.clone(), passphrase)
.await
.unwrap()
}
#[tokio::test(flavor = "multi_thread")]
async fn ipc_happy_path() {
let tmp_dir = tempdir::TempDir::new("lair_ipc_keystore_test").unwrap();
let tmp_dir2 =
tempdir::TempDir::new("lair_ipc_keystore_test2").unwrap();
let client = connect(&tmp_dir).await;
let client2 = connect(&tmp_dir2).await;
let seed_info_ref = client
.new_seed("test-tag".into(), None, true)
.await
.unwrap();
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),
}
let entry = client.get_entry("test-tag".into()).await.unwrap();
match entry {
LairEntryInfo::Seed { tag, seed_info } => {
assert_eq!("test-tag", &*tag);
assert_eq!(seed_info, seed_info_ref);
}
oth => panic!("unexpected: {:?}", oth),
}
let sig = client
.sign_by_pub_key(
seed_info_ref.ed25519_pub_key.clone(),
None,
b"hello".to_vec().into(),
)
.await
.unwrap();
assert!(seed_info_ref
.ed25519_pub_key
.verify_detached(sig, &b"hello"[..])
.await
.unwrap());
let _seed_info_ref_deep = hc_seed_bundle::PwHashLimits::Minimum
.with_exec(|| {
client.new_seed(
"test-tag-deep".into(),
Some(sodoken::BufRead::from(&b"deep"[..])),
false,
)
})
.await
.unwrap();
let cert_info =
client.new_wka_tls_cert("test-cert".into()).await.unwrap();
println!("{:#?}", cert_info);
let priv_key = client
.get_wka_tls_cert_priv_key("test-cert".into())
.await
.unwrap();
println!("got priv key: {} bytes", priv_key.len());
println!("{:#?}", client.list_entries().await.unwrap());
let (nonce, cipher) = client
.secretbox_xsalsa_by_tag(
"test-tag".into(),
None,
b"hello".to_vec().into(),
)
.await
.unwrap();
let msg = client
.secretbox_xsalsa_open_by_tag(
"test-tag".into(),
None,
nonce,
cipher,
)
.await
.unwrap();
assert_eq!(b"hello", &*msg);
let seed_info_ref2 = client2
.new_seed("test-tag2".into(), None, true)
.await
.unwrap();
let (nonce, cipher) = client
.export_seed_by_tag(
"test-tag".into(),
seed_info_ref.x25519_pub_key.clone(),
seed_info_ref2.x25519_pub_key.clone(),
None,
)
.await
.unwrap();
let imported_seed_info = client2
.import_seed(
seed_info_ref.x25519_pub_key.clone(),
seed_info_ref2.x25519_pub_key.clone(),
None,
nonce,
cipher,
"test-tag".into(),
true,
)
.await
.unwrap();
assert_eq!(seed_info_ref, imported_seed_info);
}
}