use clap::Parser;
use spectre::*;
use std::process;
use std::str::FromStr;
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Parser, Debug)]
#[command(name = "spectre")]
#[command(author = "Spectre Password Manager")]
#[command(version = VERSION)]
#[command(about = "Stateless password management solution", long_about = None)]
struct Args {
site_name: Option<String>,
#[arg(short = 'u', long, env = "SPECTRE_USERNAME")]
user_name: Option<String>,
#[arg(short = 'U', long)]
user_name_update: Option<String>,
#[arg(short = 's', long)]
secret_fd: Option<i32>,
#[arg(short = 'S', long)]
secret: Option<String>,
#[arg(short = 't', long, default_value = "long")]
result_type: String,
#[arg(short = 'P', long)]
result_param: Option<String>,
#[arg(short = 'c', long, default_value = "1")]
counter: u32,
#[arg(short = 'a', long, env = "SPECTRE_ALGORITHM")]
algorithm: Option<u32>,
#[arg(short = 'p', long, default_value = "auth")]
purpose: String,
#[arg(short = 'C', long)]
context: Option<String>,
#[arg(short = 'f', long, env = "SPECTRE_FORMAT")]
format: Option<String>,
#[arg(short = 'F', long)]
format_fixed: Option<String>,
#[arg(short = 'R', long, default_value = "1")]
redacted: String,
#[arg(short = 'v', long, action = clap::ArgAction::Count)]
verbose: u8,
#[arg(short = 'q', long, action = clap::ArgAction::Count)]
quiet: u8,
#[arg(short = 'n', long)]
no_newline: bool,
}
struct Operation {
user_name: String,
user_secret: String,
site_name: String,
result_type: SpectreResultType,
result_param: Option<String>,
counter: SpectreCounter,
algorithm: SpectreAlgorithm,
purpose: SpectreKeyPurpose,
context: Option<String>,
format: SpectreFormat,
redacted: bool,
no_newline: bool,
allow_password_update: bool,
verbosity: i8,
}
fn main() {
let args = Args::parse();
if let Err(e) = run(args) {
eprintln!("Error: {}", e);
process::exit(1);
}
}
fn run(args: Args) -> Result<()> {
let verbosity = args.verbose as i8 - args.quiet as i8;
let (user_name_arg, allow_password_update) = if let Some(name) = args.user_name_update {
(Some(name), true)
} else {
(args.user_name, false)
};
let user_name = if let Some(name) = user_name_arg {
name
} else {
prompt_line("Your full name:")?
};
if user_name.is_empty() {
return Err(SpectreError::MissingField("user name".to_string()));
}
let mut user_secret = if let Some(secret) = args.secret {
if verbosity >= 0 {
eprintln!("Warning: Passing secrets via command-line is insecure!");
}
secret
} else if let Some(fd) = args.secret_fd {
read_fd(fd).map_err(SpectreError::Io)?
} else {
prompt_password("Your personal secret: ")?
};
if user_secret.is_empty() {
return Err(SpectreError::MissingField("personal secret".to_string()));
}
let site_name = if let Some(name) = args.site_name {
name
} else {
prompt_line("Site Domain:")?
};
if site_name.is_empty() {
return Err(SpectreError::MissingField("site name".to_string()));
}
let result_type = SpectreResultType::from_str(&args.result_type)?;
let purpose = SpectreKeyPurpose::from_str(&args.purpose)?;
let format_str = args.format_fixed.as_ref().or(args.format.as_ref());
let format = if let Some(f) = format_str {
SpectreFormat::parse(f)
.ok_or_else(|| SpectreError::InvalidFileFormat(f.clone()))?
} else {
SPECTRE_FORMAT_DEFAULT
};
let redacted = parse_bool(&args.redacted);
let algorithm = args.algorithm.unwrap_or(SPECTRE_ALGORITHM_CURRENT);
if !(SPECTRE_ALGORITHM_FIRST..=SPECTRE_ALGORITHM_LAST).contains(&algorithm) {
return Err(SpectreError::InvalidAlgorithm(algorithm));
}
let mut operation = Operation {
user_name,
user_secret: user_secret.clone(),
site_name,
result_type,
result_param: args.result_param,
counter: args.counter,
algorithm,
purpose,
context: args.context,
format,
redacted,
no_newline: args.no_newline,
allow_password_update,
verbosity,
};
execute_operation(&mut operation)?;
zero_string(&mut user_secret);
zero_string(&mut operation.user_secret);
Ok(())
}
fn execute_operation(op: &mut Operation) -> Result<()> {
let user_key = spectre_user_key(&op.user_name, &op.user_secret, op.algorithm)?;
let identicon = spectre_identicon(&op.user_name, &op.user_secret)?;
let identicon_render = spectre_identicon_render(identicon);
let file_path = if op.format != SpectreFormat::None {
spectre_user_path(&op.user_name, op.format)
} else {
None
};
let mut user = if let Some(ref path) = file_path {
match spectre_marshal_read(path) {
Ok((_, Some(mut user))) => {
if let Err(SpectreError::UserSecretMismatch) = spectre_marshal_auth(&mut user, &op.user_secret) {
if op.allow_password_update {
eprintln!("Personal secret mismatch. Please confirm old secret to update.");
let old_secret = prompt_password("Old personal secret: ")?;
spectre_marshal_auth(&mut user, &old_secret)?;
let new_key = spectre_user_key(&op.user_name, &op.user_secret, op.algorithm)?;
user.key_id = new_key.key_id;
user.identicon = identicon;
} else {
return Err(SpectreError::UserSecretMismatch);
}
}
user
}
_ => {
SpectreMarshalledUser::new(
op.user_name.clone(),
identicon,
user_key.key_id,
op.algorithm,
)
}
}
} else {
SpectreMarshalledUser::new(
op.user_name.clone(),
identicon,
user_key.key_id,
op.algorithm,
)
};
user.redacted = op.redacted;
let mut site = if let Some(existing_site) = user.find_site(&op.site_name).cloned() {
existing_site
} else {
SpectreMarshalledSite::new(
op.site_name.clone(),
op.result_type,
op.counter,
op.algorithm,
)
};
if op.result_type != SpectreResultType::None {
match op.purpose {
SpectreKeyPurpose::Authentication => {
site.result_type = op.result_type;
site.counter = op.counter;
}
SpectreKeyPurpose::Identification => {
site.login_type = op.result_type;
}
SpectreKeyPurpose::Recovery => {
let keyword = op.context.as_deref().unwrap_or("");
let mut question = if let Some(q) = site.find_question(keyword).cloned() {
q
} else {
SpectreMarshalledQuestion::new(keyword.to_string(), op.result_type)
};
question.question_type = op.result_type;
site.add_question(question);
}
}
}
if op.verbosity >= 0 {
eprintln!("{}'s {} for {}:", user.user_name,
match op.purpose {
SpectreKeyPurpose::Authentication => "password",
SpectreKeyPurpose::Identification => "login",
SpectreKeyPurpose::Recovery => "answer",
},
site.site_name);
eprintln!("[ {} ]", identicon_render);
}
if op.result_type.is_stateful() && op.result_param.is_some() {
let plaintext = op.result_param.as_ref().unwrap();
let state = spectre_site_state(
&user_key,
&op.site_name,
op.result_type,
plaintext,
op.counter,
op.purpose,
op.context.as_deref(),
)?;
match op.purpose {
SpectreKeyPurpose::Authentication => {
site.result_state = Some(state.clone());
}
SpectreKeyPurpose::Identification => {
site.login_state = Some(state.clone());
}
SpectreKeyPurpose::Recovery => {
let keyword = op.context.as_deref().unwrap_or("");
if let Some(question) = site.find_question_mut(keyword) {
question.state = Some(state.clone());
}
}
}
op.result_param = Some(state);
}
let result = spectre_site_result(
&user_key,
&op.site_name,
op.result_type,
op.result_param.as_deref(),
op.counter,
op.purpose,
op.context.as_deref(),
)?;
print!("{}", result);
if !op.no_newline {
println!();
}
site.uses += 1;
site.last_used = chrono::Utc::now();
user.last_used = chrono::Utc::now();
user.add_site(site);
if let Some(path) = file_path
&& op.format != SpectreFormat::None {
spectre_marshal_write(&path, op.format, &user)?;
if op.verbosity >= 1 {
eprintln!("Saved to: {}", path.display());
}
}
Ok(())
}