use crate::piv::session::PcscSession;
use crate::piv::PivBackend;
use crate::store::constants::OBJECT_ID_ZERO;
use anyhow::{bail, Result};
const MAX_SLOTS: usize = 256;
const PROBE_MAX: usize = 4095;
pub fn measure_free_nvm(reader: &str, mgmt_key: &str, verbose: bool) -> Result<usize> {
let mut session = PcscSession::open(reader)?;
let mut empty_slots = Vec::new();
for i in 0..MAX_SLOTS {
let id = OBJECT_ID_ZERO + i as u32;
if session.try_get_data_size(id)?.unwrap_or(0) == 0 {
empty_slots.push(i);
}
}
if verbose {
eprintln!(
" measure_free_nvm: {} empty slots out of {MAX_SLOTS}.",
empty_slots.len()
);
}
let mut filled: Vec<usize> = Vec::new();
let measured = (|| -> Result<usize> {
let mut total = 0usize;
for &slot in &empty_slots {
let id = OBJECT_ID_ZERO + slot as u32;
filled.push(slot);
let size = dichotomy_fill(&mut session, mgmt_key, id)?;
if verbose {
eprintln!(" slot {slot:3}: {size} bytes");
}
if size == 0 {
if verbose {
eprintln!(" NVM full — stopping early.");
}
break;
}
total += size;
}
Ok(total)
})();
let left: Vec<String> = filled
.iter()
.filter(|&&slot| {
let id = OBJECT_ID_ZERO + slot as u32;
let emptied = session
.authenticate_management_key(mgmt_key)
.and_then(|()| session.try_put_data(id, &[]));
!matches!(emptied, Ok(true))
})
.map(|&slot| format!("0x{:06X}", OBJECT_ID_ZERO + slot as u32))
.collect();
if !left.is_empty() {
bail!(
"the NVM probe could not empty object(s) {}; they use YubiKey memory \
until removed (a PIV reset removes them)",
left.join(", ")
);
}
let total = measured?;
if verbose {
eprintln!(" measure_free_nvm: total free = {total} bytes.");
}
Ok(total)
}
fn dichotomy_fill(session: &mut PcscSession, mgmt_key: &str, id: u32) -> Result<usize> {
let mut lo = 1usize;
let mut hi = PROBE_MAX;
let mut committed = 0usize;
while lo <= hi {
let mid = (lo + hi) / 2;
session.authenticate_management_key(mgmt_key)?;
if session.try_put_data(id, &vec![0xEEu8; mid])? {
committed = mid;
lo = mid + 1;
} else {
if committed > 0 {
session.authenticate_management_key(mgmt_key)?;
session.try_put_data(id, &vec![0xEEu8; committed])?;
}
hi = mid - 1;
}
}
Ok(committed)
}
const YUBIKEY_NVM_BYTES: usize = 51_200;
pub struct NvmUsage {
pub store_bytes: usize,
pub other_bytes: usize,
pub free_bytes: usize,
}
const KNOWN_PIV_OBJECTS: &[u32] = &[
0x5F_C102, 0x5F_C100, 0x5F_C101, 0x5F_C105, 0x5F_C10A, 0x5F_C10B, 0x5F_C10C, 0x5F_C103, 0x5F_C106, 0x5F_C108, 0x5F_C109, 0x5F_C10D, 0x5F_C10E, 0x5F_C10F, 0x5F_C110, 0x5F_C111, 0x5F_C112, 0x5F_C113, 0x5F_C114, 0x5F_C115, 0x5F_C116, 0x5F_C117, 0x5F_C118, 0x5F_C119, 0x5F_C11A, 0x5F_C11B, 0x5F_C11C, 0x5F_C11D, 0x5F_C11E, 0x5F_C11F, 0x5F_C120, 0x5F_C121, ];
pub fn scan_store_memory(reader: &str, piv: &dyn PivBackend) -> Result<NvmUsage> {
let _lock = piv.exclusive(reader)?;
let ids = match crate::store::Store::from_device(reader, piv) {
Ok(store) => store_object_ids(&store),
Err(_) => std::collections::HashSet::new(),
};
scan_nvm(reader, piv, &ids)
}
fn store_object_ids(store: &crate::store::Store) -> std::collections::HashSet<u32> {
let mut ids: std::collections::HashSet<u32> = store
.objects
.iter()
.filter(|o| o.is_head())
.flat_map(|h| store.chunk_chain(h.index()))
.map(|i| store.object_id(i))
.collect();
if store.kind.is_post_quantum() {
ids.insert(OBJECT_ID_ZERO);
}
ids
}
pub fn scan_nvm(
reader: &str,
piv: &dyn PivBackend,
store_object_ids: &std::collections::HashSet<u32>,
) -> Result<NvmUsage> {
let _lock = piv.exclusive(reader)?;
let mut store_bytes = 0usize;
let mut other_bytes = 0usize;
for i in 0u32..256 {
let id = OBJECT_ID_ZERO + i;
if let Some(size) = piv.object_size(reader, id)? {
if store_object_ids.contains(&id) {
store_bytes += size;
}
}
}
for &id in KNOWN_PIV_OBJECTS {
if let Some(size) = piv.object_size(reader, id)? {
other_bytes += size;
}
}
let used = store_bytes + other_bytes;
let free_bytes = YUBIKEY_NVM_BYTES.saturating_sub(used);
Ok(NvmUsage {
store_bytes,
other_bytes,
free_bytes,
})
}