auto-detect enrolled key, --email only needed for enroll/remove

This commit is contained in:
2026-03-20 03:07:29 +09:00
parent 626a51d2b8
commit ff9e980c5c
3 changed files with 43 additions and 46 deletions

View File

@@ -6,9 +6,6 @@ mod ipc;
mod log; mod log;
mod storage; mod storage;
use std::fs;
use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use zeroize::Zeroize; use zeroize::Zeroize;
@@ -42,27 +39,6 @@ struct Args {
remove: bool, remove: bool,
} }
fn config_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
PathBuf::from(home).join(".cache").join("com.bitwarden.desktop")
}
fn config_path() -> PathBuf {
config_dir().join("agent.conf")
}
fn save_email(email: &str) {
fs::create_dir_all(config_dir()).ok();
fs::write(config_path(), email).ok();
}
fn load_email() -> Option<String> {
fs::read_to_string(config_path())
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
fn user_hash(email: &str) -> String { fn user_hash(email: &str) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(email.to_lowercase().trim().as_bytes()); hasher.update(email.to_lowercase().trim().as_bytes());
@@ -72,29 +48,17 @@ fn user_hash(email: &str) -> String {
fn main() { fn main() {
let args = Args::parse(); let args = Args::parse();
let email = args
.email
.clone()
.or_else(load_email)
.unwrap_or_else(|| log::fatal("no email provided (use --email on first run)"));
let uid = user_hash(&email);
let store = get_backend(args.backend.as_deref()); let store = get_backend(args.backend.as_deref());
let prompt = get_prompter(args.askpass.as_deref()); let prompt = get_prompter(args.askpass.as_deref());
log::info(&format!("backend: {}", store.name())); log::info(&format!("backend: {}", store.name()));
if args.remove { if args.enroll {
if store.has_key(&uid) { let email = args
store.remove(&uid); .email
log::info(&format!("key removed for {email}")); .as_deref()
} else { .unwrap_or_else(|| log::fatal("--email required for enrollment"));
log::info(&format!("no key found for {email}")); let uid = user_hash(email);
}
return;
}
if !store.has_key(&uid) || args.enroll {
log::info(if !store.has_key(&uid) { log::info(if !store.has_key(&uid) {
"enrolling" "enrolling"
} else { } else {
@@ -108,7 +72,7 @@ fn main() {
.unwrap_or_else(|| log::fatal("no password provided")); .unwrap_or_else(|| log::fatal("no password provided"));
log::info(&format!("logging in as {email}")); log::info(&format!("logging in as {email}"));
let (mut key_bytes, server_uid) = auth::login(&email, &pw, &args.server, &prompt); let (mut key_bytes, server_uid) = auth::login(email, &pw, &args.server, &prompt);
log::info(&format!("authenticated, uid={server_uid}")); log::info(&format!("authenticated, uid={server_uid}"));
let auth = prompt(&format!("choose {} password:", store.name())) let auth = prompt(&format!("choose {} password:", store.name()))
@@ -123,13 +87,34 @@ fn main() {
.store(&uid, &key_bytes, &auth) .store(&uid, &key_bytes, &auth)
.unwrap_or_else(|e| log::fatal(&format!("store failed: {e}"))); .unwrap_or_else(|e| log::fatal(&format!("store failed: {e}")));
key_bytes.zeroize(); key_bytes.zeroize();
save_email(&email);
log::info(&format!("key sealed via {}", store.name())); log::info(&format!("key sealed via {}", store.name()));
log::info("wiped key from memory"); log::info("wiped key from memory");
} else { return;
log::info(&format!("key ready for {email}"));
} }
if args.remove {
let email = args
.email
.as_deref()
.unwrap_or_else(|| log::fatal("--email required for --remove"));
let uid = user_hash(email);
if store.has_key(&uid) {
store.remove(&uid);
log::info(&format!("key removed for {email}"));
} else {
log::info(&format!("no key found for {email}"));
}
return;
}
let uid = match store.find_key() {
Some(uid) => {
log::info(&format!("found key: {uid}"));
uid
}
None => log::fatal("no enrolled key found (run with --email <email> --enroll first)"),
};
let mut bridge = BiometricBridge::new(store, uid, prompt); let mut bridge = BiometricBridge::new(store, uid, prompt);
let sock = ipc::socket_path(); let sock = ipc::socket_path();
log::info(&format!("listening on {}", sock.display())); log::info(&format!("listening on {}", sock.display()));

View File

@@ -8,6 +8,7 @@ pub trait KeyStore {
fn store(&self, uid: &str, data: &[u8], auth: &str) -> Result<(), String>; fn store(&self, uid: &str, data: &[u8], auth: &str) -> Result<(), String>;
fn load(&self, uid: &str, auth: &str) -> Result<Vec<u8>, String>; fn load(&self, uid: &str, auth: &str) -> Result<Vec<u8>, String>;
fn remove(&self, uid: &str); fn remove(&self, uid: &str);
fn find_key(&self) -> Option<String>;
} }
pub fn get_backend(preferred: Option<&str>) -> Box<dyn KeyStore> { pub fn get_backend(preferred: Option<&str>) -> Box<dyn KeyStore> {

View File

@@ -114,6 +114,17 @@ impl KeyStore for PinKeyStore {
fs::remove_file(p).ok(); fs::remove_file(p).ok();
} }
} }
fn find_key(&self) -> Option<String> {
let entries = fs::read_dir(&self.dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("enc") {
return path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string());
}
}
None
}
} }
fn derive(password: &str, salt: &[u8]) -> Result<Zeroizing<Vec<u8>>, String> { fn derive(password: &str, salt: &[u8]) -> Result<Zeroizing<Vec<u8>>, String> {