feat: v0.1.0, init commit
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.idea/
|
||||
|
||||
# ---> macOS
|
||||
# General
|
||||
.DS_Store
|
||||
|
||||
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "pinentry-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
aes-gcm-stream = "0.2.3"
|
||||
clap = { version = "4.5.8", features = ["derive"] }
|
||||
hex = "0.4.3"
|
||||
pinentry = "0.5.0"
|
||||
rand = "0.8.5"
|
||||
rpassword = "7.3.1"
|
||||
secrecy = "0.8.0"
|
||||
serde = { version = "1.0.203", features = ["serde_derive"] }
|
||||
serde_json = "1.0.118"
|
||||
zeroize = "1.8.1"
|
||||
150
src/main.rs
Normal file
150
src/main.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
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<String>,
|
||||
/// Description in pinentry
|
||||
#[arg(long, short = 'd')]
|
||||
pub description: Option<String>,
|
||||
// Prompt in pinentry
|
||||
#[arg(long)]
|
||||
pub prompt: Option<String>,
|
||||
// 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<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PinResult {
|
||||
pub success: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pin: Option<String>,
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user