use std::fs;
use std::io::{self, Write};
use chrono::{NaiveDate, Utc};
use wecanencrypt::card::{
is_card_connected, update_primary_expiry_on_card, update_subkeys_expiry_on_card,
verify_user_pin,
};
use wecanencrypt::parse_cert_bytes;
fn read_line(prompt: &str) -> String {
print!("{}", prompt);
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
input.trim().to_string()
}
fn read_password(prompt: &str) -> String {
print!("{}", prompt);
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
input.trim().to_string()
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <path_to_public_key>", args[0]);
eprintln!("\nExample:");
eprintln!(" cargo run --features card --example upexp -- mykey.pub");
std::process::exit(1);
}
let key_path = &args[1];
let public_key = match fs::read(key_path) {
Ok(data) => data,
Err(e) => {
eprintln!(
"Error: Failed to read public key file '{}': {}",
key_path, e
);
std::process::exit(1);
}
};
let info = match parse_cert_bytes(&public_key, true) {
Ok(info) => info,
Err(e) => {
eprintln!("Error: Failed to parse public key: {}", e);
std::process::exit(1);
}
};
println!("\n=== Key Information ===\n");
println!("Primary key: {}", info.fingerprint);
println!("Key expiring on: {:?}", info.expiration_time);
if !info.subkeys.is_empty() {
println!("\nSubkeys:");
for subkey in &info.subkeys {
println!(
" {} - expires: {:?}, type: {:?}",
subkey.fingerprint, subkey.expiration_time, subkey.key_type
);
}
}
println!();
let confirm = read_line("If all looks good, enter YES: ");
if confirm.to_uppercase() != "YES" {
println!("Aborted.");
std::process::exit(0);
}
let date_str = read_line("\nEnter new expiry date (YYYY-MM-DD): ");
let expiry_seconds = match parse_expiry_date(&date_str) {
Ok(secs) => secs,
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
};
println!("\nNow connect the YubiKey and make sure you have only one YubiKey inserted.");
let _ = read_line("Press Enter when ready...");
if !is_card_connected() {
eprintln!("Error: No smart card detected. Please insert a YubiKey.");
std::process::exit(1);
}
let pin = read_password("Enter your YubiKey PIN: ");
let pin_bytes = pin.as_bytes();
println!("\nVerifying the user PIN...");
match verify_user_pin(pin_bytes, None) {
Ok(true) => println!("User PIN verified.\n"),
Ok(false) => {
eprintln!("Error: PIN verification returned false.");
std::process::exit(1);
}
Err(e) => {
eprintln!(
"Error: User PIN failed. Double check before you lock your card.\n{}",
e
);
std::process::exit(1);
}
}
println!("Updating primary key expiry. Touch the YubiKey when flashing...");
let updated_with_primary =
match update_primary_expiry_on_card(&public_key, expiry_seconds, pin_bytes) {
Ok(data) => {
println!("Primary key expiry updated.");
data
}
Err(e) => {
eprintln!("Error: Failed to update primary key expiry: {}", e);
std::process::exit(1);
}
};
let subkey_fps: Vec<&str> = info
.subkeys
.iter()
.map(|s| s.fingerprint.as_str())
.collect();
if !subkey_fps.is_empty() {
println!("\nUpdating subkey expiry. Touch the YubiKey when flashing...");
let updated_full = match update_subkeys_expiry_on_card(
&updated_with_primary,
&subkey_fps,
expiry_seconds,
pin_bytes,
) {
Ok(data) => {
println!("Subkey expiry updated.");
data
}
Err(e) => {
eprintln!("Error: Failed to update subkey expiry: {}", e);
std::process::exit(1);
}
};
let output_path = format!("updated_{}.pub", info.fingerprint);
match fs::write(&output_path, &updated_full) {
Ok(_) => println!("\nUpdated key saved as {}", output_path),
Err(e) => {
eprintln!("Error: Failed to write updated key: {}", e);
std::process::exit(1);
}
}
} else {
let output_path = format!("updated_{}.pub", info.fingerprint);
match fs::write(&output_path, &updated_with_primary) {
Ok(_) => println!("\nUpdated key saved as {}", output_path),
Err(e) => {
eprintln!("Error: Failed to write updated key: {}", e);
std::process::exit(1);
}
}
}
println!("\nDone!");
}
fn parse_expiry_date(date_str: &str) -> Result<u64, String> {
let target_date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
.map_err(|e| format!("Invalid date format. Use YYYY-MM-DD: {}", e))?;
let target_datetime = target_date
.and_hms_opt(0, 0, 0)
.ok_or("Failed to create datetime")?
.and_utc();
let now = Utc::now();
if target_datetime <= now {
return Err("Expiry date must be in the future".to_string());
}
let duration = target_datetime.signed_duration_since(now);
let seconds_until_expiry = duration.num_seconds() as u64 + 86400;
Ok(seconds_until_expiry)
}