Merge pull request #7 from str4d/pretty-cli
Implement no-command pretty CLI
This commit is contained in:
+190
-1
@@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user