refactor(cli): personal add delegates to shared item_build builders
This commit is contained in:
@@ -1,37 +1,76 @@
|
|||||||
//! `relicario add <kind>` — create a new item of the given type.
|
//! `relicario add <kind>` — create a new item of the given type.
|
||||||
//!
|
//!
|
||||||
//! `cmd_add` does the common save / manifest upsert / commit dance. The seven
|
//! `cmd_add` resolves `title` / non-secret prompts, then delegates to the
|
||||||
//! per-type `build_*_item` helpers each return a fully-populated `Item`. The
|
//! shared builders in `commands/item_build.rs`. Group / tags / favorite are
|
||||||
//! `Document` builder is the only one that needs the unlocked vault (for the
|
//! set AFTER the build so the builders stay portable to the org vault.
|
||||||
//! attachment-cap settings + writing the encrypted blob alongside the item).
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use anyhow::Result;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
|
|
||||||
use crate::AddKind;
|
use crate::AddKind;
|
||||||
use crate::parse::{base32_decode_lenient, guess_mime, parse_month_year};
|
use crate::commands::item_build as ib;
|
||||||
use crate::prompt::{prompt, prompt_optional, prompt_or_flag, prompt_or_flag_optional, prompt_secret};
|
use crate::prompt::{prompt_or_flag, prompt_or_flag_optional};
|
||||||
|
|
||||||
pub fn cmd_add(kind: AddKind) -> Result<()> {
|
pub fn cmd_add(kind: AddKind) -> Result<()> {
|
||||||
let vault = crate::session::UnlockedVault::unlock_interactive()?;
|
let vault = crate::session::UnlockedVault::unlock_interactive()?;
|
||||||
let mut manifest = vault.load_manifest()?;
|
let mut manifest = vault.load_manifest()?;
|
||||||
|
|
||||||
let item = match kind {
|
let item = match kind {
|
||||||
AddKind::Login { title, username, url, password_prompt, password, group, tags, favorite, totp_qr } =>
|
AddKind::Login { title, username, url, password_prompt, password, group, tags, favorite, totp_qr } => {
|
||||||
build_login_item(title, username, url, password_prompt, password, group, tags, favorite, totp_qr)?,
|
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||||
AddKind::SecureNote { title, body_prompt, group, tags } =>
|
let username = prompt_or_flag_optional(username, "Username", |s| Ok(s.to_string()))?;
|
||||||
build_secure_note_item(title, body_prompt, group, tags)?,
|
let url = prompt_or_flag_optional(url, "URL", |s| Ok(s.to_string()))?;
|
||||||
AddKind::Identity { title, full_name, email, phone, date_of_birth, group, tags } =>
|
let mut item = ib::build_login(title, username, url, password, /*password_stdin*/ false, password_prompt, totp_qr)?;
|
||||||
build_identity_item(title, full_name, email, phone, date_of_birth, group, tags)?,
|
item.group = group; item.tags = tags; item.favorite = favorite;
|
||||||
AddKind::Card { title, holder, expiry, kind, group, tags } =>
|
item
|
||||||
build_card_item(title, holder, expiry, kind, group, tags)?,
|
}
|
||||||
AddKind::Key { title, label, algorithm, group, tags } =>
|
AddKind::SecureNote { title, body_prompt: _, group, tags } => {
|
||||||
build_key_item(title, label, algorithm, group, tags)?,
|
// NOTE: per the v0.8.1 spec's unified secret model, a note body is a
|
||||||
AddKind::Document { title, file, group, tags } =>
|
// multiline secret that always reads stdin to EOF; the legacy single-line
|
||||||
build_document_item(&vault, title, file, group, tags)?,
|
// `prompt("Body")` path is retired. `false` here means "print the Ctrl-D
|
||||||
AddKind::Totp { title, issuer, label, secret, period, digits, algorithm, group, tags } =>
|
// hint" (interactive default). A4 replaces `--body-prompt` with `--body-stdin`
|
||||||
build_totp_item(title, issuer, label, secret, period, digits, algorithm, group, tags)?,
|
// and threads the real flag. `body_prompt` is intentionally ignored this task.
|
||||||
|
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||||
|
let mut item = ib::build_secure_note(title, None, /*body_stdin*/ false)?;
|
||||||
|
item.group = group; item.tags = tags;
|
||||||
|
item
|
||||||
|
}
|
||||||
|
AddKind::Identity { title, full_name, email, phone, date_of_birth, group, tags } => {
|
||||||
|
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||||
|
let mut item = ib::build_identity(title, full_name, email, phone, date_of_birth)?;
|
||||||
|
item.group = group; item.tags = tags;
|
||||||
|
item
|
||||||
|
}
|
||||||
|
AddKind::Card { title, holder, expiry, kind, group, tags } => {
|
||||||
|
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||||
|
let mut item = ib::build_card(title, holder, expiry, &kind, false, false, false)?;
|
||||||
|
item.group = group; item.tags = tags;
|
||||||
|
item
|
||||||
|
}
|
||||||
|
AddKind::Key { title, label, algorithm, group, tags } => {
|
||||||
|
// public_key is None for the personal vault: the legacy `prompt_optional`
|
||||||
|
// for it was unreachable (stdin already at EOF after the key-material read).
|
||||||
|
// Org `add key` (Dev-B) supplies it via --public-key.
|
||||||
|
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||||
|
let mut item = ib::build_key(title, label, algorithm, None, false)?;
|
||||||
|
item.group = group; item.tags = tags;
|
||||||
|
item
|
||||||
|
}
|
||||||
|
AddKind::Document { title, file, group, tags } => {
|
||||||
|
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||||
|
let caps = vault.load_settings()?.attachment_caps;
|
||||||
|
let (mut item, enc) = ib::build_document(title, file, vault.key(), caps.per_attachment_max_bytes)?;
|
||||||
|
item.group = group; item.tags = tags;
|
||||||
|
let att_dir = vault.root().join("attachments").join(item.id.as_str());
|
||||||
|
std::fs::create_dir_all(&att_dir)?;
|
||||||
|
std::fs::write(att_dir.join(format!("{}.enc", enc.id.as_str())), &enc.bytes)?;
|
||||||
|
item
|
||||||
|
}
|
||||||
|
AddKind::Totp { title, issuer, label, secret, period, digits, algorithm, group, tags } => {
|
||||||
|
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||||
|
let mut item = ib::build_totp(title, issuer, label, secret, false, period, digits, &algorithm)?;
|
||||||
|
item.group = group; item.tags = tags;
|
||||||
|
item
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
vault.save_item(&item)?;
|
vault.save_item(&item)?;
|
||||||
@@ -51,263 +90,3 @@ pub fn cmd_add(kind: AddKind) -> Result<()> {
|
|||||||
eprintln!("Added: {} (id={})", item.title, item.id.as_str());
|
eprintln!("Added: {} (id={})", item.title, item.id.as_str());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn build_login_item(
|
|
||||||
title: Option<String>,
|
|
||||||
username: Option<String>,
|
|
||||||
url: Option<String>,
|
|
||||||
password_prompt: bool,
|
|
||||||
password: Option<String>,
|
|
||||||
group: Option<String>,
|
|
||||||
tags: Vec<String>,
|
|
||||||
favorite: bool,
|
|
||||||
totp_qr: Option<PathBuf>,
|
|
||||||
) -> Result<relicario_core::Item> {
|
|
||||||
use relicario_core::item_types::{LoginCore, TotpAlgorithm, TotpConfig, TotpKind};
|
|
||||||
use relicario_core::{Item, ItemCore};
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
|
||||||
let username = prompt_or_flag_optional(username, "Username", |s| Ok(s.to_string()))?;
|
|
||||||
let url = prompt_or_flag_optional(url, "URL", |s| Ok(s.to_string()))?;
|
|
||||||
let parsed_url = match url {
|
|
||||||
Some(s) => Some(url::Url::parse(&s).with_context(|| format!("invalid URL: {s}"))?),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
let password = if let Some(p) = password {
|
|
||||||
Some(Zeroizing::new(p))
|
|
||||||
} else if password_prompt {
|
|
||||||
Some(Zeroizing::new(prompt_secret("Password: ")?))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let totp = if let Some(path) = totp_qr {
|
|
||||||
let secret_b32 = crate::helpers::decode_totp_qr(&path)?;
|
|
||||||
let secret_bytes = base32_decode_lenient(&secret_b32)?;
|
|
||||||
Some(TotpConfig {
|
|
||||||
secret: Zeroizing::new(secret_bytes),
|
|
||||||
algorithm: TotpAlgorithm::Sha1,
|
|
||||||
digits: 6,
|
|
||||||
period_seconds: 30,
|
|
||||||
kind: TotpKind::Totp,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let mut item = Item::new(title, ItemCore::Login(LoginCore {
|
|
||||||
username, password, url: parsed_url, totp,
|
|
||||||
}));
|
|
||||||
item.group = group;
|
|
||||||
item.tags = tags;
|
|
||||||
item.favorite = favorite;
|
|
||||||
Ok(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_secure_note_item(
|
|
||||||
title: Option<String>,
|
|
||||||
body_prompt: bool,
|
|
||||||
group: Option<String>,
|
|
||||||
tags: Vec<String>,
|
|
||||||
) -> Result<relicario_core::Item> {
|
|
||||||
use relicario_core::item_types::SecureNoteCore;
|
|
||||||
use relicario_core::{Item, ItemCore};
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
|
||||||
let body = if body_prompt {
|
|
||||||
eprintln!("Enter note body; end with Ctrl-D on a blank line:");
|
|
||||||
let mut s = String::new();
|
|
||||||
std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?;
|
|
||||||
s
|
|
||||||
} else {
|
|
||||||
prompt("Body")?
|
|
||||||
};
|
|
||||||
let mut item = Item::new(title, ItemCore::SecureNote(SecureNoteCore {
|
|
||||||
body: Zeroizing::new(body),
|
|
||||||
}));
|
|
||||||
item.group = group;
|
|
||||||
item.tags = tags;
|
|
||||||
Ok(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_identity_item(
|
|
||||||
title: Option<String>,
|
|
||||||
full_name: Option<String>,
|
|
||||||
email: Option<String>,
|
|
||||||
phone: Option<String>,
|
|
||||||
date_of_birth: Option<String>,
|
|
||||||
group: Option<String>,
|
|
||||||
tags: Vec<String>,
|
|
||||||
) -> Result<relicario_core::Item> {
|
|
||||||
use relicario_core::item_types::IdentityCore;
|
|
||||||
use relicario_core::{Item, ItemCore};
|
|
||||||
|
|
||||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
|
||||||
let dob = match date_of_birth {
|
|
||||||
Some(s) => Some(chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d")
|
|
||||||
.with_context(|| format!("invalid date {s} (expected YYYY-MM-DD)"))?),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
let mut item = Item::new(title, ItemCore::Identity(IdentityCore {
|
|
||||||
full_name, address: None, phone, email, date_of_birth: dob,
|
|
||||||
}));
|
|
||||||
item.group = group;
|
|
||||||
item.tags = tags;
|
|
||||||
Ok(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_card_item(
|
|
||||||
title: Option<String>,
|
|
||||||
holder: Option<String>,
|
|
||||||
expiry: Option<String>,
|
|
||||||
kind: String,
|
|
||||||
group: Option<String>,
|
|
||||||
tags: Vec<String>,
|
|
||||||
) -> Result<relicario_core::Item> {
|
|
||||||
use relicario_core::item_types::{CardCore, CardKind};
|
|
||||||
use relicario_core::{Item, ItemCore};
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
|
||||||
let number = Zeroizing::new(prompt_secret("Card number: ")?);
|
|
||||||
let cvv = Zeroizing::new(prompt_secret("CVV (blank to skip): ")?);
|
|
||||||
let cvv = if cvv.is_empty() { None } else { Some(cvv) };
|
|
||||||
let pin = Zeroizing::new(prompt_secret("PIN (blank to skip): ")?);
|
|
||||||
let pin = if pin.is_empty() { None } else { Some(pin) };
|
|
||||||
|
|
||||||
let parsed_expiry = match expiry {
|
|
||||||
Some(s) => Some(parse_month_year(&s)?),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
let parsed_kind = match kind.as_str() {
|
|
||||||
"credit" => CardKind::Credit,
|
|
||||||
"debit" => CardKind::Debit,
|
|
||||||
"gift" => CardKind::Gift,
|
|
||||||
"loyalty" => CardKind::Loyalty,
|
|
||||||
"other" => CardKind::Other,
|
|
||||||
other => anyhow::bail!("unknown card kind: {other}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut item = Item::new(title, ItemCore::Card(CardCore {
|
|
||||||
number: Some(number), holder, expiry: parsed_expiry, cvv, pin, kind: parsed_kind,
|
|
||||||
}));
|
|
||||||
item.group = group;
|
|
||||||
item.tags = tags;
|
|
||||||
Ok(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_key_item(
|
|
||||||
title: Option<String>,
|
|
||||||
label: Option<String>,
|
|
||||||
algorithm: Option<String>,
|
|
||||||
group: Option<String>,
|
|
||||||
tags: Vec<String>,
|
|
||||||
) -> Result<relicario_core::Item> {
|
|
||||||
use relicario_core::item_types::KeyCore;
|
|
||||||
use relicario_core::{Item, ItemCore};
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
|
||||||
eprintln!("Paste key material; end with Ctrl-D on a blank line:");
|
|
||||||
let mut key_material = String::new();
|
|
||||||
std::io::Read::read_to_string(&mut std::io::stdin(), &mut key_material)?;
|
|
||||||
if key_material.trim().is_empty() { anyhow::bail!("key material required"); }
|
|
||||||
let public_key = prompt_optional("Public key (blank to skip)")?;
|
|
||||||
|
|
||||||
let mut item = Item::new(title, ItemCore::Key(KeyCore {
|
|
||||||
key_material: Zeroizing::new(key_material),
|
|
||||||
label, public_key, algorithm,
|
|
||||||
}));
|
|
||||||
item.group = group;
|
|
||||||
item.tags = tags;
|
|
||||||
Ok(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_document_item(
|
|
||||||
vault: &crate::session::UnlockedVault,
|
|
||||||
title: Option<String>,
|
|
||||||
file: PathBuf,
|
|
||||||
group: Option<String>,
|
|
||||||
tags: Vec<String>,
|
|
||||||
) -> Result<relicario_core::Item> {
|
|
||||||
use relicario_core::item_types::DocumentCore;
|
|
||||||
use relicario_core::{encrypt_attachment, AttachmentRef, Item, ItemCore};
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
|
||||||
let bytes = fs::read(&file)
|
|
||||||
.with_context(|| format!("failed to read {}", file.display()))?;
|
|
||||||
let caps = vault.load_settings()?.attachment_caps;
|
|
||||||
let enc = encrypt_attachment(&bytes, vault.key(), caps.per_attachment_max_bytes)?;
|
|
||||||
|
|
||||||
let filename = file.file_name()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("file path has no filename: {}", file.display()))?
|
|
||||||
.to_string_lossy()
|
|
||||||
.into_owned();
|
|
||||||
let mime_type = guess_mime(&filename);
|
|
||||||
|
|
||||||
let primary_attachment = enc.id.clone();
|
|
||||||
let mut item = Item::new(title, ItemCore::Document(DocumentCore {
|
|
||||||
filename: filename.clone(),
|
|
||||||
mime_type: mime_type.clone(),
|
|
||||||
primary_attachment: primary_attachment.clone(),
|
|
||||||
}));
|
|
||||||
item.group = group;
|
|
||||||
item.tags = tags;
|
|
||||||
item.attachments.push(AttachmentRef {
|
|
||||||
id: primary_attachment.clone(),
|
|
||||||
filename, mime_type,
|
|
||||||
size: bytes.len() as u64,
|
|
||||||
created: item.created,
|
|
||||||
});
|
|
||||||
|
|
||||||
let att_dir = vault.root().join("attachments").join(item.id.as_str());
|
|
||||||
fs::create_dir_all(&att_dir)?;
|
|
||||||
fs::write(att_dir.join(format!("{}.enc", primary_attachment.as_str())), &enc.bytes)?;
|
|
||||||
Ok(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn build_totp_item(
|
|
||||||
title: Option<String>,
|
|
||||||
issuer: Option<String>,
|
|
||||||
label: Option<String>,
|
|
||||||
secret: Option<String>,
|
|
||||||
period: u32,
|
|
||||||
digits: u8,
|
|
||||||
algorithm: String,
|
|
||||||
group: Option<String>,
|
|
||||||
tags: Vec<String>,
|
|
||||||
) -> Result<relicario_core::Item> {
|
|
||||||
use relicario_core::item_types::{TotpAlgorithm, TotpConfig, TotpCore, TotpKind};
|
|
||||||
use relicario_core::{Item, ItemCore};
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
|
||||||
let secret_b32 = match secret {
|
|
||||||
Some(s) => s,
|
|
||||||
None => prompt_secret("TOTP secret (base32): ")?,
|
|
||||||
};
|
|
||||||
let secret_bytes = base32_decode_lenient(&secret_b32)?;
|
|
||||||
let algo = match algorithm.to_ascii_lowercase().as_str() {
|
|
||||||
"sha1" => TotpAlgorithm::Sha1,
|
|
||||||
"sha256" => TotpAlgorithm::Sha256,
|
|
||||||
"sha512" => TotpAlgorithm::Sha512,
|
|
||||||
other => anyhow::bail!("unknown algorithm: {other}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut item = Item::new(title, ItemCore::Totp(TotpCore {
|
|
||||||
config: TotpConfig {
|
|
||||||
secret: Zeroizing::new(secret_bytes),
|
|
||||||
algorithm: algo,
|
|
||||||
digits,
|
|
||||||
period_seconds: period,
|
|
||||||
kind: TotpKind::Totp,
|
|
||||||
},
|
|
||||||
issuer, label,
|
|
||||||
}));
|
|
||||||
item.group = group;
|
|
||||||
item.tags = tags;
|
|
||||||
Ok(item)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -162,6 +162,128 @@ pub(crate) fn edit_totp(t: &mut relicario_core::item_types::TotpCore, history: &
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_login(
|
||||||
|
title: String, username: Option<String>, url: Option<String>,
|
||||||
|
password: Option<String>, password_stdin: bool, password_prompt: bool,
|
||||||
|
totp_qr: Option<PathBuf>,
|
||||||
|
) -> Result<Item> {
|
||||||
|
use relicario_core::item_types::{LoginCore, TotpAlgorithm, TotpConfig, TotpKind};
|
||||||
|
let parsed_url = match url {
|
||||||
|
Some(s) => Some(url::Url::parse(&s).with_context(|| format!("invalid URL: {s}"))?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let password = if let Some(p) = password {
|
||||||
|
Some(Zeroizing::new(p))
|
||||||
|
} else if password_stdin {
|
||||||
|
Some(Zeroizing::new(resolve_secret_line(true, "Password")?))
|
||||||
|
} else if password_prompt {
|
||||||
|
Some(Zeroizing::new(crate::prompt::prompt_secret("Password: ")?))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let totp = if let Some(path) = totp_qr {
|
||||||
|
let secret_b32 = crate::helpers::decode_totp_qr(&path)?;
|
||||||
|
let secret_bytes = base32_decode_lenient(&secret_b32)?;
|
||||||
|
Some(TotpConfig {
|
||||||
|
secret: Zeroizing::new(secret_bytes), algorithm: TotpAlgorithm::Sha1,
|
||||||
|
digits: 6, period_seconds: 30, kind: TotpKind::Totp,
|
||||||
|
})
|
||||||
|
} else { None };
|
||||||
|
Ok(Item::new(title, ItemCore::Login(LoginCore { username, password, url: parsed_url, totp })))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_secure_note(title: String, body: Option<String>, body_stdin: bool) -> Result<Item> {
|
||||||
|
use relicario_core::item_types::SecureNoteCore;
|
||||||
|
let body = match body {
|
||||||
|
Some(b) => b,
|
||||||
|
None => resolve_secret_multiline(body_stdin, "Enter note body; end with Ctrl-D on a blank line:")?,
|
||||||
|
};
|
||||||
|
Ok(Item::new(title, ItemCore::SecureNote(SecureNoteCore { body: Zeroizing::new(body) })))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_identity(
|
||||||
|
title: String, full_name: Option<String>, email: Option<String>,
|
||||||
|
phone: Option<String>, date_of_birth: Option<String>,
|
||||||
|
) -> Result<Item> {
|
||||||
|
use relicario_core::item_types::IdentityCore;
|
||||||
|
let dob = match date_of_birth {
|
||||||
|
Some(s) => Some(chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d")
|
||||||
|
.with_context(|| format!("invalid date {s} (expected YYYY-MM-DD)"))?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
Ok(Item::new(title, ItemCore::Identity(IdentityCore {
|
||||||
|
full_name, address: None, phone, email, date_of_birth: dob,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_card(
|
||||||
|
title: String, holder: Option<String>, expiry: Option<String>, kind: &str,
|
||||||
|
number_stdin: bool, cvv_stdin: bool, pin_stdin: bool,
|
||||||
|
) -> Result<Item> {
|
||||||
|
use relicario_core::item_types::CardCore;
|
||||||
|
let number = Zeroizing::new(resolve_secret_line(number_stdin, "Card number")?);
|
||||||
|
let cvv = resolve_secret_line(cvv_stdin, "CVV (blank to skip)")?;
|
||||||
|
let cvv = if cvv.is_empty() { None } else { Some(Zeroizing::new(cvv)) };
|
||||||
|
let pin = resolve_secret_line(pin_stdin, "PIN (blank to skip)")?;
|
||||||
|
let pin = if pin.is_empty() { None } else { Some(Zeroizing::new(pin)) };
|
||||||
|
let parsed_expiry = match expiry { Some(s) => Some(crate::parse::parse_month_year(&s)?), None => None };
|
||||||
|
Ok(Item::new(title, ItemCore::Card(CardCore {
|
||||||
|
number: Some(number), holder, expiry: parsed_expiry, cvv, pin, kind: parse_card_kind(kind)?,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_key(
|
||||||
|
title: String, label: Option<String>, algorithm: Option<String>,
|
||||||
|
public_key: Option<String>, material_stdin: bool,
|
||||||
|
) -> Result<Item> {
|
||||||
|
use relicario_core::item_types::KeyCore;
|
||||||
|
let key_material = resolve_secret_multiline(material_stdin, "Paste key material; end with Ctrl-D on a blank line:")?;
|
||||||
|
if key_material.trim().is_empty() { anyhow::bail!("key material required"); }
|
||||||
|
Ok(Item::new(title, ItemCore::Key(KeyCore {
|
||||||
|
key_material: Zeroizing::new(key_material), label, public_key, algorithm,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_totp(
|
||||||
|
title: String, issuer: Option<String>, label: Option<String>,
|
||||||
|
secret: Option<String>, secret_stdin: bool, period: u32, digits: u8, algorithm: &str,
|
||||||
|
) -> Result<Item> {
|
||||||
|
use relicario_core::item_types::{TotpConfig, TotpCore, TotpKind};
|
||||||
|
let secret_b32 = match secret {
|
||||||
|
Some(s) => s,
|
||||||
|
None => resolve_secret_line(secret_stdin, "TOTP secret (base32)")?,
|
||||||
|
};
|
||||||
|
let secret_bytes = base32_decode_lenient(&secret_b32)?;
|
||||||
|
Ok(Item::new(title, ItemCore::Totp(TotpCore {
|
||||||
|
config: TotpConfig {
|
||||||
|
secret: Zeroizing::new(secret_bytes), algorithm: parse_totp_algorithm(algorithm)?,
|
||||||
|
digits, period_seconds: period, kind: TotpKind::Totp,
|
||||||
|
},
|
||||||
|
issuer, label,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_document(
|
||||||
|
title: String, file: PathBuf, key: &Zeroizing<[u8; 32]>, max_bytes: u64,
|
||||||
|
) -> Result<(Item, EncryptedAttachment)> {
|
||||||
|
use relicario_core::item_types::DocumentCore;
|
||||||
|
use relicario_core::{encrypt_attachment, AttachmentRef};
|
||||||
|
let bytes = std::fs::read(&file).with_context(|| format!("failed to read {}", file.display()))?;
|
||||||
|
let enc = encrypt_attachment(&bytes, key, max_bytes)?;
|
||||||
|
let filename = file.file_name()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("file path has no filename: {}", file.display()))?
|
||||||
|
.to_string_lossy().into_owned();
|
||||||
|
let mime_type = crate::parse::guess_mime(&filename);
|
||||||
|
let primary_attachment = enc.id.clone();
|
||||||
|
let mut item = Item::new(title, ItemCore::Document(DocumentCore {
|
||||||
|
filename: filename.clone(), mime_type: mime_type.clone(), primary_attachment: primary_attachment.clone(),
|
||||||
|
}));
|
||||||
|
item.attachments.push(AttachmentRef {
|
||||||
|
id: primary_attachment, filename, mime_type, size: bytes.len() as u64, created: item.created,
|
||||||
|
});
|
||||||
|
Ok((item, enc))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn push_history(
|
pub(crate) fn push_history(
|
||||||
history: &mut FieldHistory,
|
history: &mut FieldHistory,
|
||||||
synthetic_key: &str,
|
synthetic_key: &str,
|
||||||
|
|||||||
Reference in New Issue
Block a user