Implement no-command pretty CLI

This commit is contained in:
Jack Grigg
2021-01-03 21:16:56 +00:00
parent a617cc91fb
commit c5a2b7ee5a
+190 -1
View File
@@ -1,9 +1,11 @@
use age_plugin::run_state_machine; use age_plugin::run_state_machine;
use dialoguer::{Confirm, Select};
use gumdrop::Options; use gumdrop::Options;
use log::warn; use log::warn;
use yubikey_piv::{ use yubikey_piv::{
certificate::PublicKeyInfo, certificate::PublicKeyInfo,
key::{RetiredSlotId, SlotId}, key::{RetiredSlotId, SlotId},
policy::{PinPolicy, TouchPolicy},
Key, Readers, Key, Readers,
}; };
@@ -276,7 +278,194 @@ fn main() -> Result<(), Error> {
} else if opts.list_all { } else if opts.list_all {
list(true) list(true)
} else { } else {
// TODO: CLI identity generation eprintln!("✨ Let's get your YubiKey set up for age! ✨");
eprintln!("");
eprintln!("This tool can create a new age identity in a free slot of your YubiKey.");
eprintln!("It will generate an identity file that you can use with an age client,");
eprintln!("along with the corresponding recipient.");
eprintln!("");
eprintln!("If you are already using a YubiKey with age, you can select an existing");
eprintln!("slot to recreate its corresponding identity file and recipient.");
eprintln!("");
eprintln!("When asked below to select an option, use the up/down arrow keys to");
eprintln!("make your choice, or press [Esc] or [q] to quit.");
eprintln!("");
if Readers::open()?.iter()?.len() == 0 {
eprintln!("⏳ Please insert the YubiKey you want to set up.");
};
let mut readers = yubikey::wait_for_readers()?;
// Filter out readers we can't connect to.
let readers_list: Vec<_> = readers
.iter()?
.filter_map(|reader| match reader.open() {
Ok(_) => Some(reader),
Err(e) => {
use std::error::Error;
let reason = if let Some(inner) = e.source() {
format!("{}: {}", e, inner)
} else {
e.to_string()
};
warn!("Ignoring {}: {}", reader.name(), reason);
None
}
})
.collect();
let reader_names = readers_list
.iter()
.map(|reader| {
reader
.open()
.map(|yk| format!("{} (Serial: {})", reader.name(), yk.serial()))
})
.collect::<Result<Vec<_>, _>>()?;
let mut yubikey = match Select::new()
.with_prompt("🔑 Select a YubiKey")
.items(&reader_names)
.default(0)
.interact_opt()?
{
Some(yk) => readers_list[yk].open()?,
None => return Ok(()),
};
let keys = Key::list(&mut yubikey)?;
// Identify slots that we can't allow the user to select.
let slot_details: Vec<_> = USABLE_SLOTS
.iter()
.map(|&slot| {
keys.iter()
.find(|key| key.slot() == SlotId::Retired(slot))
.map(|key| match key.certificate().subject_pki() {
PublicKeyInfo::EcP256(pubkey) => {
p256::Recipient::from_pubkey(*pubkey).map(|_| {
// Cache the details we need to display to the user.
let (_, cert) =
x509_parser::parse_x509_certificate(key.certificate().as_ref())
.unwrap();
let (name, _) = util::extract_name(&cert, true).unwrap();
let created = cert.validity().not_before.to_rfc2822();
format!("{}, created: {}", name, created)
})
}
_ => None,
})
})
.collect();
let slots: Vec<_> = slot_details
.iter()
.enumerate()
.map(|(i, occupied)| {
// Use 1-indexing in the UI for niceness
let i = i + 1;
match occupied {
Some(Some(name)) => format!("Slot {} ({})", i, name),
Some(None) => format!("Slot {} (Unusable)", i),
None => format!("Slot {} (Empty)", i),
}
})
.collect();
let (stub, recipient, created) = {
let (slot_index, slot) = loop {
match Select::new()
.with_prompt("🕳️ Select a slot for your age identity")
.items(&slots)
.default(0)
.interact_opt()?
{
Some(slot) => {
if let Some(None) = slot_details[slot] {
} else {
break (slot + 1, USABLE_SLOTS[slot]);
}
}
None => return Ok(()),
}
};
if let Some(key) = keys.iter().find(|key| key.slot() == SlotId::Retired(slot)) {
let recipient = match key.certificate().subject_pki() {
PublicKeyInfo::EcP256(pubkey) => {
p256::Recipient::from_pubkey(*pubkey).expect("We checked this above")
}
_ => unreachable!(),
};
if Confirm::new()
.with_prompt(&format!("Use existing identity in slot {}?", slot_index))
.interact()?
{
let stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient);
let (_, cert) =
x509_parser::parse_x509_certificate(key.certificate().as_ref()).unwrap();
let created = cert.validity().not_before.to_rfc2822();
(stub, recipient, created)
} else {
return Ok(());
}
} else {
let pin_policy = match Select::new()
.with_prompt("🔤 Select a PIN policy")
.items(&[
"Always (A PIN is required for every decryption, if set)",
"Once (A PIN is required once per session, if set)",
"Never (A PIN is NOT required to decrypt)",
])
.default(1)
.interact_opt()?
{
Some(0) => PinPolicy::Always,
Some(1) => PinPolicy::Once,
Some(2) => PinPolicy::Never,
Some(_) => unreachable!(),
None => return Ok(()),
};
let touch_policy = match Select::new()
.with_prompt("👆 Select a touch policy")
.items(&[
"Always (A physical touch is required for every decryption)",
"Cached (A physical touch is required for decryption, and is cached for 15 seconds)",
"Never (A physical touch is NOT required to decrypt)",
])
.default(0)
.interact_opt()?
{
Some(0) => TouchPolicy::Always,
Some(1) => TouchPolicy::Cached,
Some(2) => TouchPolicy::Never,
Some(_) => unreachable!(),
None => return Ok(()),
};
if Confirm::new()
.with_prompt(&format!("Generate new identity in slot {}?", slot_index))
.interact()?
{
eprintln!();
builder::IdentityBuilder::new(Some(slot))
.with_name(opts.name)
.with_pin_policy(Some(pin_policy))
.with_touch_policy(Some(touch_policy))
.force(opts.force)
.build(&mut yubikey)?
} else {
return Ok(());
}
}
};
util::print_identity(stub, recipient, &created);
Ok(()) Ok(())
} }
} }