use eyre::{Result, WrapErr, eyre};
use rand::{RngCore, rngs::OsRng};
use std::{
fs,
path::{Path, PathBuf},
};
#[derive(Debug, clap::Args)]
pub(crate) struct KeyArgs {
#[arg(short, long, conflicts_with_all = ["key_file", "ephemeral_key"])]
pub(crate) key_seed: Option<String>,
#[arg(long, conflicts_with_all = ["key_seed", "ephemeral_key"])]
pub(crate) key_file: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["key_seed", "key_file"])]
pub(crate) ephemeral_key: bool,
}
pub(crate) fn resolve_key_seed(args: &KeyArgs) -> Result<[u8; 32]> {
if let Some(hex_seed) = &args.key_seed {
let seed_bytes = crate::parse_32_bytes(hex_seed, "key seed")?;
tracing::info!("Using signing key from --key-seed");
return Ok(seed_bytes);
}
if let Some(key_path) = &args.key_file {
return load_key_file_bytes(key_path);
}
if args.ephemeral_key {
tracing::warn!("Using ephemeral key (will be lost on restart)");
let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes);
return Ok(bytes);
}
Err(eyre!(
"No key source specified. Use one of:\n \
--key-file <PATH> Load key from file\n \
--key-seed <HEX> Key from hex seed\n \
--ephemeral-key Random key (lost on restart)"
))
}
pub(crate) fn signer_from_seed(seed: &[u8; 32]) -> subduction_crypto::signer::memory::MemorySigner {
subduction_crypto::signer::memory::MemorySigner::from_bytes(seed)
}
pub(crate) fn keyhive_signer_from_seed(
seed: &[u8; 32],
) -> keyhive_crypto::signer::memory::MemorySigner {
ed25519_dalek::SigningKey::from_bytes(seed).into()
}
fn load_key_file_bytes(path: &Path) -> Result<[u8; 32]> {
let contents =
fs::read(path).wrap_err_with(|| format!("Failed to read key file: {}", path.display()))?;
let seed_bytes = parse_key_file_contents(&contents, path)?;
tracing::info!("Loaded signing key from {}", path.display());
Ok(seed_bytes)
}
fn parse_key_file_contents(contents: &[u8], path: &Path) -> Result<[u8; 32]> {
let trimmed = String::from_utf8_lossy(contents);
let trimmed = trimmed.trim();
if trimmed.len() == 64 && trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
let mut seed = [0u8; 32];
hex::decode_to_slice(trimmed, &mut seed)
.wrap_err_with(|| format!("Invalid hex in key file: {}", path.display()))?;
return Ok(seed);
}
if contents.len() == 32 {
let mut seed = [0u8; 32];
seed.copy_from_slice(contents);
return Ok(seed);
}
Err(eyre!(
"Key file {} must contain either 64 hex characters or 32 raw bytes (found {} bytes)",
path.display(),
contents.len()
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signers_from_same_seed_have_matching_identities() {
use keyhive_crypto::verifiable::Verifiable;
let seed = [42u8; 32];
let signer = signer_from_seed(&seed);
let kh_signer = keyhive_signer_from_seed(&seed);
let subduction_vk = signer.verifying_key();
let keyhive_vk = kh_signer.verifying_key();
assert_eq!(
subduction_vk.as_bytes(),
keyhive_vk.as_bytes(),
"subduction and keyhive signers must derive the same public key"
);
}
}