use std::collections::HashMap;
#[derive(Clone, Debug, Default)]
pub struct MdsEntry {
pub description: String,
pub icon: Option<String>,
pub status: Option<String>,
#[allow(dead_code)]
pub certificate_number: Option<String>,
pub effective_date: Option<String>,
#[allow(dead_code)]
pub authenticator_version: Option<u64>,
pub protocol_family: Option<String>,
pub mds_versions: Vec<String>,
}
impl MdsEntry {
pub fn certification_label(&self) -> Option<String> {
let s = self.status.as_deref()?;
let pretty = match s {
"FIDO_CERTIFIED" => "FIDO Certified".to_string(),
"FIDO_CERTIFIED_L1" => "FIDO Certified L1".to_string(),
"FIDO_CERTIFIED_L1plus" => "FIDO Certified L1+".to_string(),
"FIDO_CERTIFIED_L2" => "FIDO Certified L2".to_string(),
"FIDO_CERTIFIED_L2plus" => "FIDO Certified L2+".to_string(),
"FIDO_CERTIFIED_L3" => "FIDO Certified L3".to_string(),
"FIDO_CERTIFIED_L3plus" => "FIDO Certified L3+".to_string(),
"NOT_FIDO_CERTIFIED" => "Not FIDO Certified".to_string(),
other => other.replace('_', " "),
};
Some(pretty)
}
pub fn certification_level(&self) -> Option<&'static str> {
Some(match self.status.as_deref()? {
"FIDO_CERTIFIED" => "Certified",
"FIDO_CERTIFIED_L1" => "L1",
"FIDO_CERTIFIED_L1plus" => "L1+",
"FIDO_CERTIFIED_L2" => "L2",
"FIDO_CERTIFIED_L2plus" => "L2+",
"FIDO_CERTIFIED_L3" => "L3",
"FIDO_CERTIFIED_L3plus" => "L3+",
_ => return None,
})
}
pub fn is_advisory(&self) -> bool {
matches!(
self.status.as_deref(),
Some(
"USER_VERIFICATION_BYPASS"
| "ATTESTATION_KEY_COMPROMISE"
| "USER_KEY_REMOTE_COMPROMISE"
| "USER_KEY_PHYSICAL_COMPROMISE"
| "REVOKED"
)
)
}
}
#[derive(Default)]
pub struct MdsDb {
by_aaguid: HashMap<String, MdsEntry>,
}
impl MdsDb {
pub fn load_bundled() -> Self {
let mut db = MdsDb::default();
for path in external_mds_paths() {
if let Ok(text) = std::fs::read_to_string(&path) {
if db.merge_json(&text) > 0 {
return db;
}
}
}
db.merge_json(BUNDLED_MDS_JSON);
db
}
pub fn get(&self, aaguid: &[u8; 16]) -> Option<&MdsEntry> {
if aaguid.iter().all(|&b| b == 0) {
return None;
}
self.by_aaguid
.get(&super::aaguid::format_aaguid_pub(aaguid))
}
fn merge_json(&mut self, json: &str) -> usize {
let parsed: Result<Vec<RawEntry>, _> = serde_json::from_str(json);
let Ok(entries) = parsed else {
return 0;
};
let mut n = 0;
for r in entries {
let key = r.aaguid.trim().to_lowercase();
if key.is_empty() {
continue;
}
self.by_aaguid.insert(
key.clone(),
MdsEntry {
description: r.description.unwrap_or_default(),
icon: r.icon,
status: r.status,
certificate_number: r.certificate_number,
effective_date: r.effective_date,
authenticator_version: r.authenticator_version,
protocol_family: r.protocol_family,
mds_versions: r.mds_versions.unwrap_or_default(),
},
);
n += 1;
}
n
}
}
#[derive(serde::Deserialize)]
struct RawEntry {
aaguid: String,
#[serde(default)]
description: Option<String>,
#[serde(default)]
icon: Option<String>,
#[serde(default)]
status: Option<String>,
#[serde(default, rename = "certificateNumber")]
certificate_number: Option<String>,
#[serde(default, rename = "effectiveDate")]
effective_date: Option<String>,
#[serde(default, rename = "authenticatorVersion")]
authenticator_version: Option<u64>,
#[serde(default, rename = "protocolFamily")]
protocol_family: Option<String>,
#[serde(default, rename = "versions")]
mds_versions: Option<Vec<String>>,
}
static BUNDLED_MDS_JSON: &str = include_str!("../../assets/mds_data.json");
fn external_mds_paths() -> Vec<std::path::PathBuf> {
use std::path::PathBuf;
let mut out: Vec<PathBuf> = Vec::new();
if let Ok(p) = std::env::var("KEYROOST_MDS_FILE") {
if !p.is_empty() {
out.push(PathBuf::from(p));
}
}
let cfg: Option<PathBuf> = if cfg!(test) {
keyroost_keyring::config_dir().and_then(|d| d.parent().map(PathBuf::from))
} else if cfg!(target_os = "windows") {
std::env::var_os("APPDATA").map(PathBuf::from)
} else if cfg!(target_os = "macos") {
std::env::var_os("HOME")
.map(|h| PathBuf::from(h).join("Library").join("Application Support"))
} else {
std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
};
if let Some(dir) = cfg {
out.push(dir.join("keyroost").join("mds_data.json"));
}
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
out.push(dir.join("mds_data.json"));
}
}
out
}
pub fn decode_icon(data_uri: &str) -> Option<egui::ColorImage> {
let b64 = data_uri
.strip_prefix("data:image/png;base64,")
.or_else(|| data_uri.strip_prefix("data:image/png;charset=utf-8;base64,"))?;
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD
.decode(b64.trim())
.ok()?;
let decoder = png::Decoder::new(std::io::Cursor::new(bytes));
let mut reader = decoder.read_info().ok()?;
let mut buf = vec![0u8; reader.output_buffer_size()?];
let frame = reader.next_frame(&mut buf).ok()?;
let (w, h) = (frame.width as usize, frame.height as usize);
let data = &buf[..frame.buffer_size()];
let rgba: Vec<u8> = match frame.color_type {
png::ColorType::Rgba => data.to_vec(),
png::ColorType::Rgb => data
.as_chunks::<3>()
.0
.iter()
.flat_map(|p| [p[0], p[1], p[2], 255])
.collect(),
png::ColorType::GrayscaleAlpha => data
.as_chunks::<2>()
.0
.iter()
.flat_map(|p| [p[0], p[0], p[0], p[1]])
.collect(),
png::ColorType::Grayscale => data.iter().flat_map(|&g| [g, g, g, 255]).collect(),
_ => return None,
};
if rgba.len() != w * h * 4 {
return None;
}
Some(egui::ColorImage::from_rgba_unmultiplied([w, h], &rgba))
}