pub use keyroost_resolve::{enumerate, CapState, Caps, Device, DeviceId, DeviceKind};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum CapTab {
#[default]
Overview,
Fido2,
Oath,
Pgp,
Piv,
Otp,
}
pub fn tab_cap(t: CapTab) -> Option<Caps> {
match t {
CapTab::Overview => None,
CapTab::Fido2 => Some(Caps::FIDO2),
CapTab::Oath => Some(Caps::OATH),
CapTab::Pgp => Some(Caps::PGP),
CapTab::Piv => Some(Caps::PIV),
CapTab::Otp => Some(Caps::OTP),
}
}
pub trait DeviceView {
fn title(&self) -> &str;
fn tabs(&self) -> Vec<CapTab>;
fn tab_unverified(&self, t: CapTab) -> bool;
}
impl DeviceView for Device {
fn title(&self) -> &str {
self.name.as_deref().unwrap_or(&self.model)
}
fn tab_unverified(&self, t: CapTab) -> bool {
tab_cap(t).is_some_and(|c| self.cap_state(c) == CapState::Unverified)
}
fn tabs(&self) -> Vec<CapTab> {
if self.kind == DeviceKind::Token || self.kind == DeviceKind::ProgToken {
return Vec::new();
}
let mut v = vec![CapTab::Overview];
if self.caps.has(Caps::FIDO2) {
v.push(CapTab::Fido2);
}
if self.caps.has(Caps::OATH) {
v.push(CapTab::Oath);
}
if self.caps.has(Caps::PGP) {
v.push(CapTab::Pgp);
}
if self.caps.has(Caps::PIV) {
v.push(CapTab::Piv);
}
if self.caps.has(Caps::OTP) {
v.push(CapTab::Otp);
}
v
}
}
use keyroost_resolve::{KeyLabel, NameSource, Naming};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NameStoreChoice {
Computer,
Key,
}
pub fn store_choices(d: &Device) -> Vec<NameStoreChoice> {
let mut v = vec![NameStoreChoice::Computer];
let has_storage = matches!(
d.naming.on_key,
KeyLabel::Absent | KeyLabel::Present(_) | KeyLabel::Unreadable
);
if d.caps.has(Caps::FIDO2) && d.hid_path.is_some() && has_storage {
v.push(NameStoreChoice::Key);
}
v
}
pub fn default_store(d: &Device) -> NameStoreChoice {
if d.naming.source == Some(NameSource::Key) {
NameStoreChoice::Key
} else {
NameStoreChoice::Computer
}
}
pub fn lacks_key_storage(d: &Device) -> bool {
d.naming.on_key == KeyLabel::Unsupported
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NameSave {
Unchanged,
Local(Option<String>),
OnKey {
name: Option<String>,
question: Option<String>,
},
}
pub const NAME_IS_ON_THE_KEY: &str =
"This key's name is stored on the key. Rename it there, or clear the name first.";
pub fn plan_name_save(
naming: &Naming,
choice: NameStoreChoice,
input: &str,
) -> Result<NameSave, &'static str> {
let on_key = match &naming.on_key {
KeyLabel::Present(l) if naming.source == Some(NameSource::Key) => Some(l.as_str()),
_ => None,
};
if input.is_empty() {
return Ok(match on_key {
Some(old) => NameSave::OnKey {
name: None,
question: Some(format!("Remove the name \"{old}\" stored on the key?")),
},
None => NameSave::Local(None),
});
}
match (choice, on_key) {
(NameStoreChoice::Computer, Some(_)) => Err(NAME_IS_ON_THE_KEY),
(NameStoreChoice::Key, Some(old)) if old == input => Ok(NameSave::Unchanged),
(NameStoreChoice::Key, old) => Ok(NameSave::OnKey {
name: Some(input.to_string()),
question: old.map(|old| {
format!("Replace the name \"{old}\" stored on the key with \"{input}\"?")
}),
}),
(NameStoreChoice::Computer, None) => {
if naming.source == Some(NameSource::Computer) && naming.plain.as_deref() == Some(input)
{
Ok(NameSave::Unchanged)
} else {
Ok(NameSave::Local(Some(input.to_string())))
}
}
}
}
pub fn write_back_offer(d: &Device, dismissed: bool) -> Option<&str> {
if dismissed || !store_choices(d).contains(&NameStoreChoice::Key) {
return None;
}
d.naming.missing_on_key.as_deref()
}
pub fn has_key_record(keyring: &keyroost_keyring::Keyring, serials: &[&str], name: &str) -> bool {
serials.iter().any(|s| {
keyring
.records_for(s)
.iter()
.any(|r| r.name == name && r.stored == keyroost_keyring::NameStore::Key)
})
}
pub fn key_name_row_text(label: &str) -> String {
if keyroost_keyring::validate_name(label).is_ok() {
format!("key name: \"{label}\"")
} else {
"key name (not shown)".to_string()
}
}
pub fn keep_name_default(array: &keyroost_ctap::large_blobs::LargeBlobArray) -> bool {
array.label().is_some()
}
#[cfg(test)]
mod tests {
use super::*;
fn dev(fido: bool, hid: bool, on_key: KeyLabel) -> Device {
let mut caps = Caps::default();
if fido {
caps.insert(Caps::FIDO2);
}
Device {
id: "serial:12345678".into(),
name: None,
vendor: "Vendor".into(),
model: "Model".into(),
serial: "12345678".into(),
transport: "USB".into(),
firmware: String::new(),
caps,
unverified: Caps::default(),
kind: DeviceKind::Key,
hid_path: hid.then(|| "/dev/hidraw0".into()),
reader: None,
hid_serial: None,
naming: Naming {
on_key,
..Naming::default()
},
}
}
fn on_key(name: &str) -> Naming {
Naming {
plain: Some(name.into()),
source: Some(NameSource::Key),
selectable: true,
on_key: KeyLabel::Present(name.into()),
missing_on_key: None,
}
}
use NameStoreChoice::{Computer, Key};
#[test]
fn store_choices_fido_with_large_blob_offers_both() {
for l in [
KeyLabel::Absent,
KeyLabel::Present("Desk".into()),
KeyLabel::Unreadable,
] {
assert_eq!(store_choices(&dev(true, true, l)), vec![Computer, Key]);
}
}
#[test]
fn store_choices_fido_without_large_blob_is_this_computer_only() {
let d = dev(true, true, KeyLabel::Unsupported);
assert_eq!(store_choices(&d), vec![Computer]);
assert!(lacks_key_storage(&d));
for l in [KeyLabel::NotRead, KeyLabel::ReadFailed] {
let d = dev(true, true, l);
assert_eq!(store_choices(&d), vec![Computer]);
assert!(!lacks_key_storage(&d));
}
}
#[test]
fn store_choices_card_only_is_this_computer_only() {
let d = dev(true, false, KeyLabel::NotRead);
assert_eq!(store_choices(&d), vec![Computer]);
assert!(!lacks_key_storage(&d));
}
#[test]
fn store_choices_molto2_is_this_computer_only() {
let mut d = dev(false, false, KeyLabel::NotRead);
d.kind = DeviceKind::Token;
assert_eq!(store_choices(&d), vec![Computer]);
assert!(!lacks_key_storage(&d));
}
#[test]
fn default_store_follows_current_place() {
let mut d = dev(true, true, KeyLabel::Absent);
assert_eq!(default_store(&d), Computer);
d.naming = Naming::local(Some("Desk"));
assert_eq!(default_store(&d), Computer);
d.naming = on_key("Desk");
assert_eq!(default_store(&d), Key);
}
#[test]
fn plan_name_save_follows_the_cli_rules() {
let unnamed = Naming {
on_key: KeyLabel::Absent,
..Naming::default()
};
let local = Naming::local(Some("Desk"));
let key = on_key("Desk");
assert_eq!(
plan_name_save(&unnamed, Computer, "A"),
Ok(NameSave::Local(Some("A".into())))
);
assert_eq!(
plan_name_save(&unnamed, Key, "A"),
Ok(NameSave::OnKey {
name: Some("A".into()),
question: None
})
);
assert_eq!(
plan_name_save(&local, Key, "Desk"),
Ok(NameSave::OnKey {
name: Some("Desk".into()),
question: None
})
);
assert_eq!(
plan_name_save(&local, Computer, "Desk"),
Ok(NameSave::Unchanged)
);
assert_eq!(plan_name_save(&key, Key, "Desk"), Ok(NameSave::Unchanged));
assert_eq!(
plan_name_save(&key, Key, "Lab"),
Ok(NameSave::OnKey {
name: Some("Lab".into()),
question: Some("Replace the name \"Desk\" stored on the key with \"Lab\"?".into())
})
);
assert_eq!(
plan_name_save(&key, Computer, "Lab"),
Err(NAME_IS_ON_THE_KEY)
);
for c in [Computer, Key] {
assert_eq!(
plan_name_save(&key, c, ""),
Ok(NameSave::OnKey {
name: None,
question: Some("Remove the name \"Desk\" stored on the key?".into())
})
);
assert_eq!(plan_name_save(&local, c, ""), Ok(NameSave::Local(None)));
}
let unreadable = Naming {
on_key: KeyLabel::Unreadable,
..Naming::default()
};
assert_eq!(
plan_name_save(&unreadable, Key, "A"),
Ok(NameSave::OnKey {
name: Some("A".into()),
question: None
})
);
}
#[test]
fn write_back_offer_needs_a_missing_name_and_storage() {
let mut d = dev(true, true, KeyLabel::Absent);
assert_eq!(write_back_offer(&d, false), None);
d.naming.missing_on_key = Some("Desk".into());
assert_eq!(write_back_offer(&d, false), Some("Desk"));
assert_eq!(write_back_offer(&d, true), None, "dismissed");
d.naming.on_key = KeyLabel::ReadFailed;
assert_eq!(write_back_offer(&d, false), None, "storage not known");
let mut card = dev(true, false, KeyLabel::Absent);
card.naming.missing_on_key = Some("Desk".into());
assert_eq!(write_back_offer(&card, false), None, "no HID path");
}
#[test]
fn has_key_record_needs_this_keys_on_key_record_of_that_name() {
use keyroost_keyring::{Keyring, NameStore, RecordMeta};
let mut k = Keyring::default();
let serials = ["12345678", "ABCDEF01"];
assert!(!has_key_record(&k, &serials, "Desk"));
k.set_name(
"ABCDEF01",
"Desk",
NameStore::Computer,
RecordMeta::default(),
)
.unwrap();
assert!(!has_key_record(&k, &serials, "Desk"), "a computer record");
k.set_name("ABCDEF01", "Desk", NameStore::Key, RecordMeta::default())
.unwrap();
assert!(has_key_record(&k, &serials, "Desk"), "under the HID serial");
assert!(!has_key_record(&k, &serials, "Lab"));
assert!(!has_key_record(&k, &["99999999"], "Desk"), "another key");
}
#[test]
fn key_name_row_text_hides_names_that_fail_the_rules() {
assert_eq!(key_name_row_text("Desk"), "key name: \"Desk\"");
assert_eq!(key_name_row_text("a\u{202E}b"), "key name (not shown)");
}
#[test]
fn keep_name_defaults_on_only_with_a_name() {
use keyroost_ctap::device_label::DeviceLabel;
use keyroost_ctap::large_blobs::LargeBlobArray;
let empty = LargeBlobArray::parse(&[0x80]).unwrap();
assert!(!keep_name_default(&empty));
let named = empty
.with_label(
Some(&DeviceLabel {
label: "Desk".into(),
writer: None,
}),
[0; 12],
)
.unwrap();
assert!(keep_name_default(&named));
}
}