use std::{env, fs}; use std::process::exit; use aes_gcm_stream::Aes256GcmStreamEncryptor; use clap::Parser; use pinentry::PassphraseInput; use rand::random; use secrecy::ExposeSecret; use serde::Serialize; #[derive(Debug, Parser)] #[command(name = "pinentry-cli")] #[command(about = "PIN entry command line", long_about = None)] struct Cli { /// pinentry command location /// e.g. in MacOS maybe /usr/local/MacGPG2/libexec/pinentry-mac.app/Contents/MacOS/pinentry-mac /// , or set environment export PIN_ENTRY_CMD=pinentry-cmd #[arg(long, short = 'e')] pub pin_entry: Option, /// Description in pinentry #[arg(long, short = 'd')] pub description: Option, // Prompt in pinentry #[arg(long)] pub prompt: Option, // Encryption key, must be 32 bytes and in HEX #[arg(long, short = 'k')] pub encryption_key: String, // Disable fallback to rpassword #[arg(long, short = 'C')] pub disable_fallback_cli: Option, } #[derive(Serialize)] struct PinResult { pub success: bool, #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, #[serde(skip_serializing_if = "Option::is_none")] pub pin: Option, } impl PinResult { pub fn new_pin(key: &[u8; 32], pin: String) -> PinResult { PinResult { success: true, error: None, pin: Some(encrypt(key, pin)), } } pub fn new_error(error: String) -> PinResult { PinResult { success: false, error: Some(error), pin: None, } } } const PIN_ENTRY_ENV: &str = "PIN_ENTRY_CMD"; const PIN_ENTRY_1: &str = "/usr/local/MacGPG2/libexec/pinentry-mac.app/Contents/MacOS/pinentry-mac"; const PIN_ENTRY_DEFAULT: &str = "pinentry"; fn main() { let args = Cli::parse(); let pin_result = get_pin(args); let result = serde_json::to_string(&pin_result).unwrap(); println!("{}", &result); exit(if pin_result.success { 0 } else { -1 }); } fn get_pin_entry(args: &Cli) -> String { args.pin_entry.clone().unwrap_or_else(|| { if let Ok(pin_entry) = env::var(PIN_ENTRY_ENV) { return pin_entry; } if let Ok(m) = fs::metadata(PIN_ENTRY_1) { if m.is_file() { return PIN_ENTRY_1.to_string(); } } PIN_ENTRY_DEFAULT.to_string() }) } fn get_encryption_key(args: &Cli) -> Result<[u8; 32], PinResult> { let mut encryption_key = [0_u8; 32]; match hex::decode(&args.encryption_key) { Ok(key) => { if key.len() != 32 { return Err(PinResult::new_error(format!("Bad encryption key length, expected 32, actual: {}", key.len()))); } encryption_key.copy_from_slice(&key[..32]); } Err(e) => return Err(PinResult::new_error(format!("{}", e))) }; Ok(encryption_key) } fn get_pin(args: Cli) -> PinResult { let pin_entry = get_pin_entry(&args); let description = args.description.as_deref().unwrap_or("Please input PIN"); let prompt = args.prompt.as_deref().unwrap_or("PIN: "); let fallback_cli = !args.disable_fallback_cli.unwrap_or(false); let encryption_key = match get_encryption_key(&args) { Ok(encryption_key) => encryption_key, Err(pin_result) => return pin_result, }; if let Some(mut input) = PassphraseInput::with_binary(pin_entry) { let secret = input .with_description(&format!("{}.", description)) .with_prompt(prompt) .interact(); match secret { Ok(secret_string) => { PinResult::new_pin(&encryption_key, secret_string.expose_secret().to_string()) } Err(e) => { PinResult::new_error(format!("{}", e)) } } } else if fallback_cli { match rpassword::prompt_password(format!("{}: ", description)) { Ok(pin) => { PinResult::new_pin(&encryption_key, pin) } Err(e) => { PinResult::new_error(format!("{}", e)) } } } else { PinResult::new_error(String::from("pinentry not found and --disable-fallback-cli is turned on.")) } } fn encrypt(key: &[u8; 32], pin: String) -> String { let nonce = random::<[u8; 12]>(); let mut encrypted = vec![]; let mut encryptor = Aes256GcmStreamEncryptor::new(*key, &nonce); encrypted.extend_from_slice(&encryptor.update(pin.as_bytes())); let (c, t) = encryptor.finalize(); encrypted.extend_from_slice(&c); encrypted.extend_from_slice(&t); format!("{}.{}", hex::encode(nonce), hex::encode(&encrypted)) }