//! `relicario get` — print a single item, masking secrets unless `--show`. use anyhow::{Context, Result}; pub 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()?; crate::helpers::refresh_groups_cache(vault.root(), &manifest); let entry = super::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(t) = &l.totp { if show { println!("TOTP: {}", data_encoding::BASE32.encode(&t.secret)); } else { println!("TOTP: **** (use --show to reveal)"); } } l.password.clone() } 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 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(()) }