use anyhow::{bail, Context as _, Result};
use clap::Args;
use std::io::Write;
use yb_core::Zeroizing;
use yb_core::{
auxiliaries::{
enable_pin_protected_management_key, generate_random_management_key, KeySwitch,
ProtectionMode,
},
errors::YbError,
list_blobs,
report::{CardReport, StorePresence},
store::{
constants::{DEFAULT_KEY_SLOT, DEFAULT_OBJECT_COUNT, DEFAULT_SUBJECT},
Store,
},
Context, MgmtAlgo, SecretOp, SlotKeyCheck,
};
use crate::cli::util::quote_name;
#[derive(Args, Debug, Default)]
pub struct FormatArgs {
#[arg(short = 'c', long = "object-count")]
pub object_count: Option<u8>,
#[arg(short = 'k', long = "key-slot")]
pub key_slot: Option<String>,
#[arg(short = 'g', long = "generate")]
pub generate: bool,
#[arg(short = 'n', long = "subject")]
pub subject: Option<String>,
#[arg(long = "protect")]
pub protect: bool,
#[arg(long = "yes")]
pub yes: bool,
#[arg(long = "post-quantum")]
pub post_quantum: bool,
#[arg(long = "plan")]
pub plan: bool,
}
impl FormatArgs {
pub fn has_no_format_flags(&self) -> bool {
!(self.generate
|| self.protect
|| self.object_count.is_some()
|| self.key_slot.is_some()
|| self.subject.is_some()
|| self.yes
|| self.plan
|| self.post_quantum)
}
}
pub fn run(ctx: &Context, args: &FormatArgs) -> Result<()> {
if args.post_quantum {
#[cfg(not(windows))]
return crate::cli::format_pq::run_flags(ctx, args);
#[cfg(windows)]
bail!("post-quantum stores are not supported on Windows");
}
run_with_output(ctx, args, &mut std::io::stdout().lock())
}
pub fn run_with_output(ctx: &Context, args: &FormatArgs, out: &mut dyn Write) -> Result<()> {
let settings = Settings::from_args(args)?;
let report = args.plan.then(|| CardReport::build(ctx, settings.slot));
if let Some(ref report) = report {
writeln!(out, "{}", report.render(None, false))?;
}
let prepared = preflight(ctx, &settings).context("nothing was changed on the YubiKey")?;
let store = StorePresence::probe(&ctx.reader, ctx.piv.as_ref());
if let Some(report) = report {
let occupied = report.slot.is_occupied();
let plan = FormatPlan {
serial: ctx.serial,
change_pin: false,
change_puk: false,
management: ManagementStep::new(ctx, &settings, &prepared)?,
erase: EraseStep::from_store(&store),
slot_key: match (settings.generate, occupied) {
(false, _) => SlotStep::Keep,
(true, false) => SlotStep::Generate,
(true, true) => SlotStep::Replace,
},
slot: settings.slot,
object_count: settings.object_count,
};
write!(out, "{}", plan.render())?;
return Ok(());
}
match EraseStep::from_store(&store) {
EraseStep::Blobs(names) => {
for name in names {
eprintln!("will be destroyed: {}", quote_name(&name));
}
}
EraseStep::Unreadable => {
eprintln!("Warning: the existing store cannot be parsed; it will be erased")
}
EraseStep::NoStore | EraseStep::Empty => {}
}
apply(ctx, &settings, prepared, Vec::new())
}
pub(crate) struct Settings {
pub object_count: u8,
pub slot: u8,
pub generate: bool,
pub subject: String,
pub protect: bool,
}
impl Settings {
fn from_args(args: &FormatArgs) -> Result<Self> {
let object_count = args.object_count.unwrap_or(DEFAULT_OBJECT_COUNT);
if !(1..=32).contains(&object_count) {
bail!("object-count must be 1–32");
}
let slot = match args.key_slot {
Some(ref s) => parse_slot(s)?,
None => DEFAULT_KEY_SLOT,
};
let standard_slots: &[u8] = &[0x9A, 0x9C, 0x9D, 0x9E];
if !standard_slots.contains(&slot) && !(0x80u8..=0x95u8).contains(&slot) {
eprintln!("Warning: slot 0x{slot:02x} is not a standard PIV key slot");
}
Ok(Self {
object_count,
slot,
generate: args.generate,
subject: args
.subject
.clone()
.unwrap_or_else(|| DEFAULT_SUBJECT.to_owned()),
protect: args.protect,
})
}
}
pub(crate) struct Prepared {
pub management_key: Zeroizing<String>,
pub management_key_in_printed: bool,
pub already_protected: bool,
pub pin: String,
}
impl Prepared {
pub fn resolve(ctx: &Context, protect: bool, pin: String) -> Result<Self> {
let management_key = ctx.management_key_for_write()?;
let management_key_in_printed = protect && ctx.management_key_in_printed()?;
let already_protected = protect && ctx.management_key_protected()?;
Ok(Self {
management_key,
management_key_in_printed,
already_protected,
pin,
})
}
}
pub(crate) enum ManagementStep {
Keep,
Protect(MgmtAlgo),
KeepProtected(MgmtAlgo),
}
impl ManagementStep {
pub fn new(ctx: &Context, settings: &Settings, prepared: &Prepared) -> Result<Self> {
if !settings.protect {
return Ok(Self::Keep);
}
let algo = ctx.piv.management_key_algorithm(&ctx.reader)?;
Ok(if prepared.already_protected {
Self::KeepProtected(algo)
} else {
Self::Protect(algo)
})
}
}
pub(crate) enum EraseStep {
NoStore,
Empty,
Blobs(Vec<String>),
Unreadable,
}
impl EraseStep {
pub fn from_store(store: &StorePresence) -> Self {
match store {
StorePresence::None => Self::NoStore,
StorePresence::Unreadable(_) => Self::Unreadable,
StorePresence::Present(store) => {
let names: Vec<String> = list_blobs(store).into_iter().map(|b| b.name).collect();
if names.is_empty() {
Self::Empty
} else {
Self::Blobs(names)
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SlotStep {
Keep,
Generate,
Replace,
}
pub(crate) struct FormatPlan {
pub serial: u32,
pub change_pin: bool,
pub change_puk: bool,
pub management: ManagementStep,
pub erase: EraseStep,
pub slot_key: SlotStep,
pub slot: u8,
pub object_count: u8,
}
impl FormatPlan {
pub fn steps(&self) -> Vec<String> {
let slot = self.slot;
let mut steps = Vec::new();
if self.change_pin {
steps.push("Change PIN".to_owned());
}
if self.change_puk {
steps.push("Change PUK".to_owned());
}
steps.push(match self.management {
ManagementStep::Keep => {
"Keep the management key (not stored on the YubiKey)".to_owned()
}
ManagementStep::Protect(algo) => format!(
"Replace the management key with a random one ({algo}), kept on the YubiKey \
and unlocked by the PIN"
),
ManagementStep::KeepProtected(algo) => {
format!("Keep the management key stored on the YubiKey ({algo})")
}
});
match &self.erase {
EraseStep::NoStore => {}
EraseStep::Empty => {
steps.push("Erase the existing store (it holds no blobs)".to_owned())
}
EraseStep::Blobs(names) => steps.push(format!(
"ERASE store — destroys {}: {}",
plural(names.len(), "blob"),
quoted_list(names)
)),
EraseStep::Unreadable => {
steps.push("ERASE the existing store (it cannot be read)".to_owned())
}
}
steps.push(format!(
"Create the store: {}, key slot 0x{slot:02x}",
plural(self.object_count.into(), "object")
));
steps.push(match self.slot_key {
SlotStep::Keep => format!("Keep existing key in slot 0x{slot:02x}"),
SlotStep::Generate => format!("Generate a new key in slot 0x{slot:02x}"),
SlotStep::Replace => format!("REPLACE the key in slot 0x{slot:02x} with a new one"),
});
steps
}
pub fn render(&self) -> String {
let mut out = format!("Plan for YubiKey {}:\n", self.serial);
for (i, step) in self.steps().iter().enumerate() {
out.push_str(&format!(" {}. {step}\n", i + 1));
}
out
}
pub fn destroys(&self) -> Option<String> {
let mut what = Vec::new();
match &self.erase {
EraseStep::Blobs(names) => what.push(format!(
"{} ({})",
plural(names.len(), "blob"),
quoted_list(names)
)),
EraseStep::Unreadable => what.push("the unreadable store".to_owned()),
EraseStep::NoStore | EraseStep::Empty => {}
}
if self.slot_key == SlotStep::Replace {
what.push(format!("the key in slot 0x{:02x}", self.slot));
}
(!what.is_empty()).then(|| what.join(" and "))
}
}
fn plural(n: usize, noun: &str) -> String {
if n == 1 {
format!("1 {noun}")
} else {
format!("{n} {noun}s")
}
}
fn quoted_list(names: &[String]) -> String {
names
.iter()
.map(|n| quote_name(n))
.collect::<Vec<_>>()
.join(", ")
}
fn preflight(ctx: &Context, settings: &Settings) -> Result<Prepared> {
let slot = settings.slot;
ctx.ensure_supported_protection()?;
ctx.enforce_default_policy(SecretOp::Format {
protect: settings.protect,
})?;
let pin = ctx.require_pin()?.ok_or_else(|| {
YbError::new("a PIN is needed to format the store")
.fix("set YB_PIN, use --pin-stdin, or run yb in a terminal")
})?;
ctx.piv.verify_pin(&ctx.reader, &pin)?;
let prepared = Prepared::resolve(ctx, settings.protect, pin)?;
if !settings.generate {
match ctx.check_slot_key(slot)? {
SlotKeyCheck::Match => {}
SlotKeyCheck::NoCertificate => {
return Err(YbError::new(format!("no certificate in slot 0x{slot:02x}"))
.fix("add --generate to create a key and its certificate")
.into());
}
SlotKeyCheck::Mismatch => {
return Err(YbError::new(format!(
"the key in slot 0x{slot:02x} does not match its certificate"
))
.fix("add --generate to replace both")
.into());
}
}
}
Ok(prepared)
}
pub(crate) fn apply(
ctx: &Context,
settings: &Settings,
prepared: Prepared,
already_done: Vec<String>,
) -> Result<()> {
let (reader, piv) = (ctx.reader.as_str(), ctx.piv.as_ref());
let slot = settings.slot;
let mut phase = PhaseB::new(ctx.quiet, already_done);
let mut management_key = prepared.management_key;
let switching = settings.protect && !prepared.already_protected;
if switching {
let algo = piv.management_key_algorithm(reader)?;
let new_key = generate_random_management_key(algo);
phase.run("storing a new management key on the YubiKey", "", || {
enable_pin_protected_management_key(
reader,
piv,
&KeySwitch {
old_key: &management_key,
old_key_in_printed: prepared.management_key_in_printed,
new_key: &new_key,
algo,
clearing_legacy_flag: ctx.protection == ProtectionMode::LegacyOrPukBlocked,
},
)
})?;
management_key = new_key;
if !ctx.quiet {
eprintln!("New management key ({algo}) kept on the YubiKey, unlocked by the PIN.");
}
} else if settings.protect && !ctx.quiet {
let algo = piv.management_key_algorithm(reader)?;
eprintln!("The management key ({algo}) is already kept on the YubiKey; keeping it.");
}
phase.run(
"erasing the store",
&format!(
"The store may be partly erased; the key in slot 0x{slot:02x} is unchanged, so \
intact blobs still decrypt. Run `yb format` again."
),
|| Store::format(reader, piv, settings.object_count, slot, &management_key),
)?;
if !switching {
ctx.complete_pending_repairs();
}
if settings.generate {
phase.run(
&format!("generating a key in slot 0x{slot:02x}"),
&format!(
"The store is empty and the key in slot 0x{slot:02x} may have been \
replaced, but its certificate could not be written or does not match. \
Do not store data. Run `yb format --generate` again."
),
|| {
piv.generate_certificate(
reader,
slot,
&settings.subject,
&management_key,
Some(&prepared.pin),
)?;
match ctx.check_slot_key(slot)? {
SlotKeyCheck::Match => Ok(()),
_ => Err(YbError::new(
"the new certificate does not match the key in the slot",
)
.into()),
}
},
)?;
}
if !ctx.quiet {
eprintln!(
"Store formatted: {} object(s), key slot 0x{slot:02x}",
settings.object_count
);
}
Ok(())
}
struct PhaseB {
quiet: bool,
done: Vec<String>,
}
impl PhaseB {
fn new(quiet: bool, done: Vec<String>) -> Self {
Self { quiet, done }
}
fn run<T>(
&mut self,
what: &str,
on_failure: &str,
step: impl FnOnce() -> Result<T>,
) -> Result<T> {
if !self.quiet {
eprintln!("{}…", capitalize(what));
}
let result = step().with_context(|| {
let done = if self.done.is_empty() {
"nothing".to_owned()
} else {
self.done.join(", ")
};
let mut msg = format!("yb format stopped while {what} (completed: {done})");
if !on_failure.is_empty() {
msg.push_str(". ");
msg.push_str(on_failure);
}
msg
});
if result.is_ok() {
self.done.push(what.to_owned());
}
result
}
}
fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}
fn parse_slot(s: &str) -> anyhow::Result<u8> {
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u8::from_str_radix(hex, 16).map_err(|_| anyhow::anyhow!("invalid key-slot: {s}"))
} else {
s.parse::<u8>()
.map_err(|_| anyhow::anyhow!("invalid key-slot: {s}"))
}
}