90 lines
2.7 KiB
Rust
90 lines
2.7 KiB
Rust
//! Common helpers shared by all commands.
|
|
|
|
use std::io::{self, Write};
|
|
use std::path::PathBuf;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use anyhow::{anyhow, Context, Result};
|
|
|
|
use levcs_core::Repository;
|
|
use levcs_identity::keychain::Keychain;
|
|
use levcs_identity::keys::SecretKey;
|
|
|
|
pub fn now_micros() -> i64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_micros() as i64)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
pub fn open_repo() -> Result<Repository> {
|
|
let cwd = std::env::current_dir()?;
|
|
Ok(Repository::discover(&cwd)?)
|
|
}
|
|
|
|
pub fn keychain_path() -> PathBuf {
|
|
Keychain::default_path()
|
|
}
|
|
|
|
pub fn load_keychain() -> Result<Keychain> {
|
|
Ok(Keychain::load_or_default(&keychain_path())?)
|
|
}
|
|
|
|
pub fn save_keychain(kc: &Keychain) -> Result<()> {
|
|
Ok(kc.save(&keychain_path())?)
|
|
}
|
|
|
|
pub fn read_passphrase(prompt: &str) -> Result<String> {
|
|
eprint!("{prompt}");
|
|
io::stderr().flush().ok();
|
|
// Best-effort no-echo: rely on terminal raw mode if available; otherwise
|
|
// fall back to plain readline. This is intentionally simple to avoid
|
|
// pulling another crate; users on headless CI should pass keys via
|
|
// unencrypted slots or a future agent.
|
|
let mut buf = String::new();
|
|
io::stdin().read_line(&mut buf)?;
|
|
if buf.ends_with('\n') {
|
|
buf.pop();
|
|
if buf.ends_with('\r') {
|
|
buf.pop();
|
|
}
|
|
}
|
|
Ok(buf)
|
|
}
|
|
|
|
/// Resolve a key label and load the secret. If `label` is None, default to
|
|
/// `"personal"`, then to the only key in the chain if there is exactly one.
|
|
pub fn load_secret(label: Option<&str>) -> Result<(String, SecretKey)> {
|
|
let kc = load_keychain()?;
|
|
let chosen = match label {
|
|
Some(l) => l.to_string(),
|
|
None => {
|
|
if kc.entry("personal").is_some() {
|
|
"personal".into()
|
|
} else if kc.keys.len() == 1 {
|
|
kc.keys[0].label.clone()
|
|
} else if kc.keys.is_empty() {
|
|
return Err(anyhow!(
|
|
"no keys in keychain at {:?}; run `levcs key generate <label>`",
|
|
keychain_path()
|
|
));
|
|
} else {
|
|
return Err(anyhow!("multiple keys in keychain; pass --key <label>"));
|
|
}
|
|
}
|
|
};
|
|
let entry = kc
|
|
.entry(&chosen)
|
|
.ok_or_else(|| anyhow!("unknown key label: {chosen}"))?
|
|
.clone();
|
|
let sk = if entry.private.is_some() {
|
|
kc.secret(&chosen, || Ok(String::new()))
|
|
.context("loading plaintext secret")?
|
|
} else {
|
|
let pp = read_passphrase(&format!("passphrase for key '{chosen}': "))?;
|
|
kc.secret(&chosen, || Ok(pp.clone()))
|
|
.context("decrypting secret")?
|
|
};
|
|
Ok((chosen, sk))
|
|
}
|