diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index dec59b0..a54b7cd 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -657,7 +657,119 @@ fn commit_paths(vault: &crate::session::UnlockedVault, message: &str, paths: &[& Ok(()) } -fn cmd_get(_query: String, _show: bool, _copy: bool) -> Result<()> { bail!("not yet implemented"); } +fn cmd_get(query: String, show: bool, copy: bool) -> Result<()> { + use relicario_core::ItemCore; + use zeroize::Zeroizing; + + let vault = crate::session::UnlockedVault::unlock_interactive()?; + let manifest = vault.load_manifest()?; + let entry = resolve_query(&manifest, &query)?; + let item = vault.load_item(&entry.id)?; + + println!("ID: {}", item.id.as_str()); + println!("Title: {}", item.title); + println!("Type: {:?}", item.r#type); + if let Some(g) = &item.group { println!("Group: {g}"); } + if !item.tags.is_empty() { println!("Tags: {}", item.tags.join(", ")); } + println!("Created: {}", crate::helpers::iso8601(item.created)); + println!("Modified: {}", crate::helpers::iso8601(item.modified)); + if let Some(t) = item.trashed_at { println!("Trashed: {}", crate::helpers::iso8601(t)); } + println!(); + + let primary_secret: Option> = match &item.core { + ItemCore::Login(l) => { + if let Some(u) = &l.username { println!("Username: {u}"); } + if let Some(u) = &l.url { println!("URL: {u}"); } + if let Some(p) = &l.password { Some(p.clone()) } else { None } + } + ItemCore::SecureNote(n) => { + if show { println!("Body:\n{}", n.body.as_str()); } + else { println!("Body: ********"); } + None + } + ItemCore::Identity(i) => { + if let Some(v) = &i.full_name { println!("Name: {v}"); } + if let Some(v) = &i.email { println!("Email: {v}"); } + if let Some(v) = &i.phone { println!("Phone: {v}"); } + if let Some(v) = &i.date_of_birth { println!("DOB: {v}"); } + None + } + ItemCore::Card(c) => { + if let Some(h) = &c.holder { println!("Holder: {h}"); } + if let Some(e) = &c.expiry { println!("Expiry: {:02}/{}", e.month, e.year); } + println!("Kind: {:?}", c.kind); + c.number.clone() + } + ItemCore::Key(k) => { + if let Some(l) = &k.label { println!("Label: {l}"); } + if let Some(a) = &k.algorithm { println!("Algo: {a}"); } + if let Some(pk) = &k.public_key { println!("Pubkey: {pk}"); } + Some(k.key_material.clone()) + } + ItemCore::Document(d) => { + println!("Filename: {}", d.filename); + println!("MIME: {}", d.mime_type); + None + } + ItemCore::Totp(t) => { + if let Some(i) = &t.issuer { println!("Issuer: {i}"); } + if let Some(l) = &t.label { println!("Label: {l}"); } + println!("Period: {}s", t.config.period_seconds); + println!("Digits: {}", t.config.digits); + None + } + }; + + if let Some(secret) = primary_secret { + if show { + println!("Secret: {}", secret.as_str()); + } else { + println!("Secret: ******** (use --show to reveal, --copy to clipboard)"); + } + if copy { + copy_to_clipboard_then_clear(&secret)?; + eprintln!("Copied to clipboard (auto-clears in 30s)."); + } + } + + Ok(()) +} + +fn resolve_query<'a>( + manifest: &'a relicario_core::Manifest, + query: &str, +) -> Result<&'a relicario_core::ManifestEntry> { + if let Some(entry) = manifest.items.values().find(|e| e.id.as_str() == query) { + return Ok(entry); + } + let hits: Vec<_> = manifest.search(query); + match hits.len() { + 0 => anyhow::bail!("no item matches `{query}`"), + 1 => Ok(hits[0]), + _ => { + let titles: Vec<&str> = hits.iter().map(|e| e.title.as_str()).collect(); + anyhow::bail!("ambiguous — {} matches: {}", hits.len(), titles.join(", ")) + } + } +} + +fn copy_to_clipboard_then_clear(secret: &zeroize::Zeroizing) -> Result<()> { + use arboard::Clipboard; + let mut cb = Clipboard::new().context("failed to access clipboard")?; + cb.set_text(secret.as_str().to_string()).context("failed to write clipboard")?; + let cleared_copy = zeroize::Zeroizing::new(secret.as_str().to_owned()); + // Unconditional clear (audit M6): spawn a detached thread that waits 30s + // and then rewrites the clipboard with empty string. Even if the user + // copies something else in the interim, we still overwrite once. + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(30)); + if let Ok(mut cb) = Clipboard::new() { + let _ = cb.set_text(String::new()); + drop(cleared_copy); // zeroize the detached copy + } + }); + Ok(()) +} fn cmd_list(_t: Option, _g: Option, _tag: Option, _trashed: bool) -> Result<()> { bail!("not yet implemented"); } fn cmd_edit(_query: String) -> Result<()> { bail!("not yet implemented"); } fn cmd_rm(_query: String) -> Result<()> { bail!("not yet implemented"); }