mirror of
https://github.com/morgan9e/bitwarden-desktop-agent
synced 2026-04-14 00:04:06 +09:00
auto-detect enrolled key, --email only needed for enroll/remove
This commit is contained in:
77
src/main.rs
77
src/main.rs
@@ -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()));
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user