use crate::*;
use std::future::Future;
use std::sync::Arc;
const PID_FILE_NAME: &str = "pid_file";
const STORE_FILE_NAME: &str = "store_file";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum LairServerSignatureFallback {
None,
#[serde(rename_all = "camelCase")]
Command {
program: std::path::PathBuf,
args: Option<Vec<String>>,
},
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LairServerConfigInner {
pub connection_url: url::Url,
pub pid_file: std::path::PathBuf,
pub store_file: std::path::PathBuf,
pub signature_fallback: LairServerSignatureFallback,
pub runtime_secrets_salt: BinDataSized<16>,
pub runtime_secrets_mem_limit: u32,
pub runtime_secrets_ops_limit: u32,
pub runtime_secrets_context_key: SecretDataSized<32, 49>,
pub runtime_secrets_id_seed: SecretDataSized<32, 49>,
}
impl std::fmt::Display for LairServerConfigInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = serde_yaml::to_string(&self).map_err(|_| std::fmt::Error)?;
let mut lines = Vec::new();
for (id, line) in s.split('\n').enumerate() {
if id > 0 {
if line.starts_with("connectionUrl:") {
lines.push("");
lines.push("# The connection url for communications between server / client.");
lines.push("# - `unix:///path/to/unix/socket?k=Yada`");
lines.push(
"# - `named_pipe:\\\\.\\pipe\\my_pipe_name?k=Yada`",
);
lines.push("# - (not yet supported) `tcp://127.0.0.1:12345?k=Yada`");
} else if line.starts_with("pidFile:") {
lines.push("");
lines.push("# The pid file for managing a running lair-keystore process");
} else if line.starts_with("storeFile:") {
lines.push("");
lines.push(
"# The sqlcipher store file for persisting secrets",
);
} else if line.starts_with("signatureFallback:") {
lines.push("");
lines.push(
"# Configuration for managing sign_by_pub_key fallback",
);
lines.push("# in case the pub key does not exist in the lair store.");
lines.push("# - `signatureFallback: none`");
lines.push("# - ```");
lines.push("# signatureFallback: !command");
lines.push("# # 'program' will resolve to a path, specifying 'echo'");
lines.push("# # will try to run './echo', probably not what you want.");
lines.push("# program: \"./my-executable\"");
lines.push("# # args are optional");
lines.push("# args:");
lines.push("# - test-arg1");
lines.push("# - test-arg2");
lines.push("# ```");
} else if line.starts_with("runtimeSecretsSalt:") {
lines.push("");
lines.push("# -- cryptographic secrets --");
lines.push("# If you modify the data below, you risk loosing access to your keys.");
}
}
lines.push(line);
}
f.write_str(&lines.join("\n"))
}
}
impl LairServerConfigInner {
pub fn from_bytes(bytes: &[u8]) -> LairResult<Self> {
serde_yaml::from_slice(bytes).map_err(one_err::OneErr::new)
}
pub fn new<P>(
root_path: P,
passphrase: sodoken::BufRead,
) -> impl Future<Output = LairResult<Self>> + 'static + Send
where
P: AsRef<std::path::Path>,
{
let root_path = root_path.as_ref().to_owned();
let limits = hc_seed_bundle::PwHashLimits::current();
async move {
let mut pid_file = root_path.clone();
pid_file.push(PID_FILE_NAME);
let mut store_file = root_path.clone();
store_file.push(STORE_FILE_NAME);
let pw_hash = <sodoken::BufWriteSized<64>>::new_mem_locked()?;
sodoken::hash::blake2b::hash(pw_hash.clone(), passphrase).await?;
let salt = <sodoken::BufWriteSized<16>>::new_no_lock();
sodoken::random::bytes_buf(salt.clone()).await?;
let ops_limit = limits.as_ops_limit();
let mem_limit = limits.as_mem_limit();
let pre_secret = <sodoken::BufWriteSized<32>>::new_mem_locked()?;
sodoken::hash::argon2id::hash(
pre_secret.clone(),
pw_hash,
salt.clone(),
ops_limit,
mem_limit,
)
.await?;
let ctx_secret = <sodoken::BufWriteSized<32>>::new_mem_locked()?;
sodoken::kdf::derive_from_key(
ctx_secret.clone(),
42,
*b"CtxSecKy",
pre_secret.clone(),
)?;
let id_secret = <sodoken::BufWriteSized<32>>::new_mem_locked()?;
sodoken::kdf::derive_from_key(
id_secret.clone(),
142,
*b"IdnSecKy",
pre_secret,
)?;
let context_key = <sodoken::BufWriteSized<32>>::new_mem_locked()?;
sodoken::random::bytes_buf(context_key.clone()).await?;
let id_seed = <sodoken::BufWriteSized<32>>::new_mem_locked()?;
sodoken::random::bytes_buf(id_seed.clone()).await?;
let id_pk = <sodoken::BufWriteSized<32>>::new_no_lock();
let id_sk = <sodoken::BufWriteSized<32>>::new_mem_locked()?;
use sodoken::crypto_box::curve25519xchacha20poly1305::*;
seed_keypair(id_pk.clone(), id_sk, id_seed.clone()).await?;
let context_key = SecretDataSized::encrypt(
ctx_secret.to_read_sized(),
context_key.to_read_sized(),
)
.await?;
let id_seed = SecretDataSized::encrypt(
id_secret.to_read_sized(),
id_seed.to_read_sized(),
)
.await?;
let id_pk: BinDataSized<32> =
id_pk.try_unwrap_sized().unwrap().into();
#[cfg(windows)]
let connection_url = {
let id = nanoid::nanoid!();
url::Url::parse(&format!(
"named-pipe:\\\\.\\pipe\\{}?k={}",
id, id_pk
))
.unwrap()
};
#[cfg(not(windows))]
let connection_url = {
let mut con_path = root_path.clone();
con_path.push("socket");
url::Url::parse(&format!(
"unix://{}?k={}",
con_path.to_str().unwrap(),
id_pk
))
.unwrap()
};
let config = LairServerConfigInner {
connection_url,
pid_file,
store_file,
signature_fallback: LairServerSignatureFallback::None,
runtime_secrets_salt: salt.try_unwrap_sized().unwrap().into(),
runtime_secrets_mem_limit: mem_limit,
runtime_secrets_ops_limit: ops_limit,
runtime_secrets_context_key: context_key,
runtime_secrets_id_seed: id_seed,
};
Ok(config)
}
}
pub fn get_connection_scheme(&self) -> &str {
self.connection_url.scheme()
}
pub fn get_connection_path(&self) -> std::path::PathBuf {
get_connection_path(&self.connection_url)
}
pub fn get_server_pub_key(&self) -> LairResult<BinDataSized<32>> {
get_server_pub_key_from_connection_url(&self.connection_url)
}
}
pub fn get_connection_path(url: &url::Url) -> std::path::PathBuf {
#[cfg(windows)]
{
std::path::PathBuf::from(url.path())
}
#[cfg(not(windows))]
{
url.to_file_path().expect("can decode as file path")
}
}
pub fn get_server_pub_key_from_connection_url(
url: &url::Url,
) -> LairResult<BinDataSized<32>> {
for (k, v) in url.query_pairs() {
if k == "k" {
return v.parse();
}
}
Err("no server_pub_key on connection_url".into())
}
pub type LairServerConfig = Arc<LairServerConfigInner>;
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn test_config_yaml() {
let passphrase = sodoken::BufRead::from(&b"passphrase"[..]);
let mut srv = hc_seed_bundle::PwHashLimits::Minimum
.with_exec(|| {
LairServerConfigInner::new("/tmp/my/path", passphrase)
})
.await
.unwrap();
println!("-- server config start --");
println!("{}", &srv);
println!("-- server config end --");
assert_eq!(
std::path::PathBuf::from("/tmp/my/path").as_path(),
srv.pid_file.parent().unwrap(),
);
srv.signature_fallback = LairServerSignatureFallback::Command {
program: std::path::Path::new("./my-executable").into(),
args: Some(vec!["test-arg1".into(), "test-arg2".into()]),
};
println!("-- server config start --");
println!("{}", &srv);
println!("-- server config end --");
}
}