fmt: Run cargo fmt

This commit is contained in:
Maxim Muravev
2026-07-27 20:41:50 +03:00
parent d321a57b6b
commit e54442803c
36 changed files with 1087 additions and 443 deletions
+20 -3
View File
@@ -40,12 +40,26 @@ pub struct Apdu {
impl Apdu {
/// A command that expects a response (`Le = 0` short form).
pub fn read(cla: u8, ins: u8, p1: u8, p2: u8, data: &[u8]) -> Self {
Self { cla, ins, p1, p2, data: data.to_vec(), le: Some(0) }
Self {
cla,
ins,
p1,
p2,
data: data.to_vec(),
le: Some(0),
}
}
/// A pure write (no `Le`), for commands that answer with only a status word.
pub fn write(cla: u8, ins: u8, p1: u8, p2: u8, data: &[u8]) -> Self {
Self { cla, ins, p1, p2, data: data.to_vec(), le: None }
Self {
cla,
ins,
p1,
p2,
data: data.to_vec(),
le: None,
}
}
/// Encode to the wire. Short-form Lc/Le when the body is ≤255 bytes,
@@ -135,7 +149,10 @@ mod tests {
#[test]
fn encodes_case2_no_data_with_le() {
// LIST: header + single Le byte (0 = 256).
assert_eq!(Apdu::read(0x00, 0xA1, 0, 0, &[]).encode(), vec![0x00, 0xA1, 0, 0, 0x00]);
assert_eq!(
Apdu::read(0x00, 0xA1, 0, 0, &[]).encode(),
vec![0x00, 0xA1, 0, 0, 0x00]
);
}
#[test]
+41 -12
View File
@@ -12,7 +12,7 @@
#![allow(dead_code)]
use crate::error::PFError;
use crate::hal::apdu::{tlv, Apdu, CLA_ISO};
use crate::hal::apdu::{Apdu, CLA_ISO, tlv};
use crate::hal::transport::ccid::CcidSession;
use ring::rand::{SecureRandom, SystemRandom};
use ring::{hmac, pbkdf2};
@@ -245,7 +245,12 @@ pub fn clear_code(session: &CcidSession) -> Result<(), PFError> {
/// Add or overwrite a credential.
pub fn put(session: &CcidSession, cred: &NewCredential) -> Result<(), PFError> {
let id = build_cred_id(cred.issuer.as_deref(), &cred.account, cred.oath_type, cred.period);
let id = build_cred_id(
cred.issuer.as_deref(),
&cred.account,
cred.oath_type,
cred.period,
);
let mut key_tlv = vec![cred.oath_type.wire() | cred.algorithm.wire(), cred.digits];
key_tlv.extend_from_slice(&cred.secret);
@@ -318,7 +323,9 @@ pub fn calculate_all(session: &CcidSession) -> Result<Vec<Account>, PFError> {
pending = Some((id, OathType::Totp));
continue;
}
let Some((id, _)) = pending.take() else { continue };
let Some((id, _)) = pending.take() else {
continue;
};
let (issuer, account, period) = parse_cred_id(&id);
let (oath_type, state) = match tag {
TAG_NO_RESPONSE => (OathType::Hotp, CodeState::Hotp),
@@ -327,7 +334,10 @@ pub fn calculate_all(session: &CcidSession) -> Result<Vec<Account>, PFError> {
let value_str = format_response(value)?;
(
OathType::Totp,
CodeState::Code { value: value_str, period },
CodeState::Code {
value: value_str,
period,
},
)
}
_ => continue,
@@ -380,7 +390,9 @@ fn time_counter(period: u32) -> u64 {
/// Handles both truncated (`0x76`: `[digits, 4-byte int]`) and full
/// (`0x75`: `[digits, full HMAC]`, truncated host-side) forms.
fn format_response(value: &[u8]) -> Result<String, PFError> {
let digits = *value.first().ok_or_else(|| PFError::Device("Short code response".into()))?;
let digits = *value
.first()
.ok_or_else(|| PFError::Device("Short code response".into()))?;
let body = &value[1..];
let num = if body.len() == 4 {
u32::from_be_bytes([body[0], body[1], body[2], body[3]]) & 0x7FFF_FFFF
@@ -402,7 +414,12 @@ fn format_response(value: &[u8]) -> Result<String, PFError> {
/// Build the Yubico credential id: `[<period>/]<issuer>:<account>`, with the
/// period prefix only for non-30 s TOTP credentials.
pub fn build_cred_id(issuer: Option<&str>, account: &str, oath_type: OathType, period: u32) -> String {
pub fn build_cred_id(
issuer: Option<&str>,
account: &str,
oath_type: OathType,
period: u32,
) -> String {
let base = match issuer {
Some(i) if !i.is_empty() => format!("{i}:{account}"),
_ => account.to_string(),
@@ -476,8 +493,8 @@ pub fn parse_otpauth(uri: &str) -> Result<NewCredential, String> {
}
}
let secret = base32_decode(&secret_b32.ok_or("Missing secret")?)
.ok_or("Invalid base32 secret")?;
let secret =
base32_decode(&secret_b32.ok_or("Missing secret")?).ok_or("Invalid base32 secret")?;
if secret.is_empty() {
return Err("Empty secret".into());
}
@@ -576,7 +593,10 @@ mod tests {
#[test]
fn base32_decodes_known_vectors() {
assert_eq!(base32_decode("JBSWY3DPEHPK3PXP").unwrap(), b"Hello!\xde\xad\xbe\xef");
assert_eq!(
base32_decode("JBSWY3DPEHPK3PXP").unwrap(),
b"Hello!\xde\xad\xbe\xef"
);
assert_eq!(base32_decode("").unwrap(), Vec::<u8>::new());
// Lowercase, spaces and padding tolerated.
assert_eq!(base32_decode("nb sw y3=dp").unwrap(), b"hello");
@@ -585,15 +605,24 @@ mod tests {
#[test]
fn cred_id_roundtrips() {
assert_eq!(build_cred_id(Some("GitHub"), "alice", OathType::Totp, 30), "GitHub:alice");
assert_eq!(build_cred_id(Some("AWS"), "bob", OathType::Totp, 60), "60/AWS:bob");
assert_eq!(
build_cred_id(Some("GitHub"), "alice", OathType::Totp, 30),
"GitHub:alice"
);
assert_eq!(
build_cred_id(Some("AWS"), "bob", OathType::Totp, 60),
"60/AWS:bob"
);
assert_eq!(build_cred_id(None, "solo", OathType::Hotp, 30), "solo");
assert_eq!(
parse_cred_id("GitHub:alice"),
(Some("GitHub".into()), "alice".into(), 30)
);
assert_eq!(parse_cred_id("60/AWS:bob"), (Some("AWS".into()), "bob".into(), 60));
assert_eq!(
parse_cred_id("60/AWS:bob"),
(Some("AWS".into()), "bob".into(), 60)
);
assert_eq!(parse_cred_id("solo"), (None, "solo".into(), 30));
// A slash that isn't a period prefix stays part of the account.
assert_eq!(parse_cred_id("a/b:c"), (Some("a/b".into()), "c".into(), 30));
+95 -29
View File
@@ -12,7 +12,7 @@
#![allow(dead_code)]
use crate::error::PFError;
use crate::hal::apdu::{tlv, Apdu, CLA_ISO};
use crate::hal::apdu::{Apdu, CLA_ISO, tlv};
use crate::hal::transport::ccid::CcidSession;
pub const OPENPGP_AID: &[u8] = &[0xD2, 0x76, 0x00, 0x01, 0x24, 0x01];
@@ -142,7 +142,11 @@ pub const GENERATE_ALGOS: &[(&str, u8)] = &[
/// Build the algorithm-attribute bytes for a slot + choice, or `None` if the
/// combination is unsupported (e.g. Ed25519 on the encryption slot).
pub fn algo_attr(slot: PgpSlot, choice: u8) -> Option<Vec<u8>> {
let ec_id = if slot == PgpSlot::Dec { ALGO_ECDH } else { ALGO_ECDSA };
let ec_id = if slot == PgpSlot::Dec {
ALGO_ECDH
} else {
ALGO_ECDSA
};
let ec = |oid: &[u8]| {
let mut v = vec![ec_id];
v.extend_from_slice(oid);
@@ -214,7 +218,13 @@ pub fn open() -> Result<CcidSession, PFError> {
}
fn get_data(session: &CcidSession, tag: u16) -> Result<Vec<u8>, PFError> {
session.transceive_full(&Apdu::read(CLA_ISO, INS_GET_DATA, (tag >> 8) as u8, tag as u8, &[]))
session.transceive_full(&Apdu::read(
CLA_ISO,
INS_GET_DATA,
(tag >> 8) as u8,
tag as u8,
&[],
))
}
pub fn get_version(session: &CcidSession) -> Result<[u8; 3], PFError> {
@@ -255,14 +265,14 @@ pub fn read_info(session: &CcidSession) -> Result<PgpInfo, PFError> {
let key_info = tlv::find(disc, 0xDE).unwrap_or(&[]);
let mut keys = Vec::new();
for (i, slot) in [PgpSlot::Sig, PgpSlot::Dec, PgpSlot::Aut].into_iter().enumerate() {
for (i, slot) in [PgpSlot::Sig, PgpSlot::Dec, PgpSlot::Aut]
.into_iter()
.enumerate()
{
let attr = tlv::find(disc, slot.attr_tag() as u32).unwrap_or(&[]);
let fp = fps.get(i * 20..i * 20 + 20).unwrap_or(&[]);
let present = fp.iter().any(|&b| b != 0)
|| key_info
.get(i * 2 + 1)
.map(|&b| b != 0)
.unwrap_or(false);
let present =
fp.iter().any(|&b| b != 0) || key_info.get(i * 2 + 1).map(|&b| b != 0).unwrap_or(false);
let touch = tlv::find(disc, slot.uif_tag() as u32)
.and_then(|u| u.first())
.map(|&b| b != 0)
@@ -284,8 +294,12 @@ pub fn read_info(session: &CcidSession) -> Result<PgpInfo, PFError> {
let sex = tlv::find(ch, 0x5F35)
.and_then(|v| v.first().copied())
.unwrap_or(0x39);
let login = get_data(session, 0x5E).map(|v| str_of(&v)).unwrap_or_default();
let url = get_data(session, 0x5F50).map(|v| str_of(&v)).unwrap_or_default();
let login = get_data(session, 0x5E)
.map(|v| str_of(&v))
.unwrap_or_default();
let url = get_data(session, 0x5F50)
.map(|v| str_of(&v))
.unwrap_or_default();
Ok(PgpInfo {
version,
@@ -303,22 +317,41 @@ pub fn read_info(session: &CcidSession) -> Result<PgpInfo, PFError> {
}
fn str_of(v: &[u8]) -> String {
String::from_utf8_lossy(v).trim_end_matches('\0').to_string()
String::from_utf8_lossy(v)
.trim_end_matches('\0')
.to_string()
}
// ── PIN management ──────────────────────────────────────────────────────────
pub fn verify_pin(session: &CcidSession, reference: u8, pin: &str) -> Result<(), PFError> {
session.transceive_full(&Apdu::write(CLA_ISO, INS_VERIFY, 0x00, reference, pin.as_bytes()))?;
session.transceive_full(&Apdu::write(
CLA_ISO,
INS_VERIFY,
0x00,
reference,
pin.as_bytes(),
))?;
Ok(())
}
/// Change PW1 (`ref=PW1`) or PW3 (`ref=PW3`). The device splits old/new at the
/// stored PIN length, so send `old ‖ new` concatenated.
pub fn change_pin(session: &CcidSession, reference: u8, old: &str, new: &str) -> Result<(), PFError> {
pub fn change_pin(
session: &CcidSession,
reference: u8,
old: &str,
new: &str,
) -> Result<(), PFError> {
let mut body = old.as_bytes().to_vec();
body.extend_from_slice(new.as_bytes());
session.transceive_full(&Apdu::write(CLA_ISO, INS_CHANGE_REF, 0x00, reference, &body))?;
session.transceive_full(&Apdu::write(
CLA_ISO,
INS_CHANGE_REF,
0x00,
reference,
&body,
))?;
Ok(())
}
@@ -332,7 +365,13 @@ pub fn unblock_with_rc(session: &CcidSession, rc: &str, new_pw1: &str) -> Result
/// Unblock PW1 using a verified admin PIN (call `verify_pin(PW3)` first).
pub fn unblock_with_admin(session: &CcidSession, new_pw1: &str) -> Result<(), PFError> {
session.transceive_full(&Apdu::write(CLA_ISO, INS_RESET_RETRY, 0x02, PW1, new_pw1.as_bytes()))?;
session.transceive_full(&Apdu::write(
CLA_ISO,
INS_RESET_RETRY,
0x02,
PW1,
new_pw1.as_bytes(),
))?;
Ok(())
}
@@ -371,7 +410,11 @@ pub fn set_cardholder(
}
pub fn set_touch(session: &CcidSession, slot: PgpSlot, on: bool) -> Result<(), PFError> {
put_data(session, slot.uif_tag(), &[if on { 0x01 } else { 0x00 }, 0x20])
put_data(
session,
slot.uif_tag(),
&[if on { 0x01 } else { 0x00 }, 0x20],
)
}
pub fn set_algo_attr(session: &CcidSession, slot: PgpSlot, attr: &[u8]) -> Result<(), PFError> {
@@ -379,13 +422,15 @@ pub fn set_algo_attr(session: &CcidSession, slot: PgpSlot, attr: &[u8]) -> Resul
}
/// Set the slot's algorithm then GENERATE a key (returns the `7F49` public key).
pub fn generate(
session: &CcidSession,
slot: PgpSlot,
attr: &[u8],
) -> Result<Vec<u8>, PFError> {
pub fn generate(session: &CcidSession, slot: PgpSlot, attr: &[u8]) -> Result<Vec<u8>, PFError> {
set_algo_attr(session, slot, attr)?;
session.transceive_full(&Apdu::read(CLA_ISO, INS_GENERATE, 0x80, 0x00, &[slot.crt(), 0x00]))
session.transceive_full(&Apdu::read(
CLA_ISO,
INS_GENERATE,
0x80,
0x00,
&[slot.crt(), 0x00],
))
}
// ── Factory reset (block both PINs → TERMINATE → ACTIVATE) ───────────────────
@@ -393,7 +438,13 @@ pub fn generate(
pub fn reset(session: &CcidSession) -> Result<(), PFError> {
for reference in [PW1, PW3] {
for _ in 0..10 {
match session.transceive(&Apdu::write(CLA_ISO, INS_VERIFY, 0x00, reference, b"00000000")) {
match session.transceive(&Apdu::write(
CLA_ISO,
INS_VERIFY,
0x00,
reference,
b"00000000",
)) {
Ok((_, sw)) if sw.0 == 0x6983 => break,
Ok(_) => continue,
Err(e) => return Err(e),
@@ -411,8 +462,14 @@ mod tests {
#[test]
fn algo_attr_bytes() {
assert_eq!(algo_attr(PgpSlot::Sig, 0).unwrap(), vec![0x01, 0x08, 0x00, 0x00, 0x20, 0x00]);
assert_eq!(algo_attr(PgpSlot::Sig, 2).unwrap(), vec![0x01, 0x10, 0x00, 0x00, 0x20, 0x00]);
assert_eq!(
algo_attr(PgpSlot::Sig, 0).unwrap(),
vec![0x01, 0x08, 0x00, 0x00, 0x20, 0x00]
);
assert_eq!(
algo_attr(PgpSlot::Sig, 2).unwrap(),
vec![0x01, 0x10, 0x00, 0x00, 0x20, 0x00]
);
// P-256: ECDSA on SIG, ECDH on DEC, same OID.
assert_eq!(algo_attr(PgpSlot::Sig, 3).unwrap()[0], ALGO_ECDSA);
assert_eq!(algo_attr(PgpSlot::Dec, 3).unwrap()[0], ALGO_ECDH);
@@ -424,9 +481,18 @@ mod tests {
#[test]
fn labels_from_attrs() {
assert_eq!(algo_label(&[0x01, 0x10, 0x00, 0x00, 0x20, 0x00]), "RSA-4096");
assert_eq!(algo_label(&[0x13, 0x2B, 0x81, 0x04, 0x00, 0x22]), "ECC P-384");
assert_eq!(algo_label(&[0x16, 0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01]), "Ed25519");
assert_eq!(
algo_label(&[0x01, 0x10, 0x00, 0x00, 0x20, 0x00]),
"RSA-4096"
);
assert_eq!(
algo_label(&[0x13, 0x2B, 0x81, 0x04, 0x00, 0x22]),
"ECC P-384"
);
assert_eq!(
algo_label(&[0x16, 0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01]),
"Ed25519"
);
}
#[test]
+31 -8
View File
@@ -14,7 +14,7 @@
#![allow(dead_code)]
use crate::error::PFError;
use crate::hal::apdu::{tlv, Apdu, CLA_ISO};
use crate::hal::apdu::{Apdu, CLA_ISO, tlv};
use crate::hal::transport::ccid::CcidSession;
use ring::rand::{SecureRandom, SystemRandom};
@@ -94,7 +94,11 @@ pub struct SlotInfo {
impl SlotInfo {
fn empty(slot: u8) -> Self {
Self { slot, kind: SlotType::Empty, touch: false }
Self {
slot,
kind: SlotType::Empty,
touch: false,
}
}
pub fn configured(&self) -> bool {
self.kind != SlotType::Empty
@@ -263,8 +267,12 @@ pub fn modhex_decode(s: &str) -> Option<Vec<u8>> {
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(s.len() / 2);
for pair in bytes.chunks(2) {
let hi = MODHEX.iter().position(|&c| c == pair[0].to_ascii_lowercase())? as u8;
let lo = MODHEX.iter().position(|&c| c == pair[1].to_ascii_lowercase())? as u8;
let hi = MODHEX
.iter()
.position(|&c| c == pair[0].to_ascii_lowercase())? as u8;
let lo = MODHEX
.iter()
.position(|&c| c == pair[1].to_ascii_lowercase())? as u8;
out.push((hi << 4) | lo);
}
Some(out)
@@ -447,7 +455,11 @@ fn pad_challenge(challenge: &[u8]) -> Result<[u8; CHALLENGE_FRAME], PFError> {
let mut frame = [0u8; CHALLENGE_FRAME];
frame[..challenge.len()].copy_from_slice(challenge);
if challenge.len() < CHALLENGE_FRAME {
let pad = if *challenge.last().unwrap() == 0x7F { 0x00 } else { 0x7F };
let pad = if *challenge.last().unwrap() == 0x7F {
0x00
} else {
0x7F
};
frame[challenge.len()..].fill(pad);
}
Ok(frame)
@@ -475,8 +487,16 @@ mod tests {
// Every builder must produce a frame that CRCs to the X.25 residual.
assert_eq!(crc16(&build_chalresp(&[0x11; 20], false, &NO_ACC)), 0xF0B8);
assert_eq!(crc16(&build_hotp(&[0xAB; 20], true, true, &NO_ACC)), 0xF0B8);
assert_eq!(crc16(&build_static(&[0x04, 0x05, 0x06], false, &NO_ACC)), 0xF0B8);
assert_eq!(crc16(&build_yubico_otp(&[1; 6], &[2; 6], &[3; 16], false, &NO_ACC)), 0xF0B8);
assert_eq!(
crc16(&build_static(&[0x04, 0x05, 0x06], false, &NO_ACC)),
0xF0B8
);
assert_eq!(
crc16(&build_yubico_otp(
&[1; 6], &[2; 6], &[3; 16], false, &NO_ACC
)),
0xF0B8
);
}
#[test]
@@ -589,6 +609,9 @@ mod tests {
assert!(pad_challenge(&[]).is_err());
assert!(pad_challenge(&[0u8; CHALLENGE_FRAME + 1]).is_err());
assert!(pad_challenge(&[0u8; CHALLENGE_FRAME]).is_ok());
assert_eq!(pad_challenge(&[9u8; CHALLENGE_FRAME]).unwrap().len(), CHALLENGE_FRAME);
assert_eq!(
pad_challenge(&[9u8; CHALLENGE_FRAME]).unwrap().len(),
CHALLENGE_FRAME
);
}
}
+117 -27
View File
@@ -14,7 +14,7 @@
#![allow(dead_code)]
use crate::error::PFError;
use crate::hal::apdu::{tlv, Apdu, CLA_CHAIN, CLA_ISO};
use crate::hal::apdu::{Apdu, CLA_CHAIN, CLA_ISO, tlv};
use crate::hal::transport::ccid::CcidSession;
use cbc::cipher::{Block, BlockModeDecrypt, BlockModeEncrypt, KeyIvInit};
use ring::rand::{SecureRandom, SystemRandom};
@@ -223,18 +223,26 @@ fn get_metadata(session: &CcidSession, slot: u8) -> Result<Vec<u8>, PFError> {
}
pub fn parse_ref_status(resp: &[u8]) -> Option<RefStatus> {
let is_default = tlv::find(resp, 0x05).map(|v| v.first() == Some(&1)).unwrap_or(false);
let is_default = tlv::find(resp, 0x05)
.map(|v| v.first() == Some(&1))
.unwrap_or(false);
let retry = tlv::find(resp, 0x06)?;
if retry.len() < 2 {
return None;
}
Some(RefStatus { is_default, total: retry[0], left: retry[1] })
Some(RefStatus {
is_default,
total: retry[0],
left: retry[1],
})
}
pub fn parse_slot_meta(resp: &[u8]) -> Option<SlotMeta> {
let algo = *tlv::find(resp, 0x01)?.first()?;
let policy = tlv::find(resp, 0x02)?;
let origin = tlv::find(resp, 0x03).and_then(|v| v.first().copied()).unwrap_or(0);
let origin = tlv::find(resp, 0x03)
.and_then(|v| v.first().copied())
.unwrap_or(0);
Some(SlotMeta {
algo,
pin_policy: *policy.first().unwrap_or(&0),
@@ -248,7 +256,9 @@ fn get_data(session: &CcidSession, object_id: &[u8]) -> Result<Vec<u8>, PFError>
let mut body = Vec::new();
tlv::write(&mut body, TAG_DATA_PATH, object_id);
let resp = session.transceive_full(&Apdu::read(CLA_ISO, INS_GET_DATA, 0x3F, 0xFF, &body))?;
Ok(tlv::find(&resp, TAG_DATA_OBJECT).map(|v| v.to_vec()).unwrap_or(resp))
Ok(tlv::find(&resp, TAG_DATA_OBJECT)
.map(|v| v.to_vec())
.unwrap_or(resp))
}
/// Extract the bare DER certificate from a slot's data object (`53{70 …}`).
@@ -260,31 +270,54 @@ pub fn cert_der(object: &[u8]) -> Option<Vec<u8>> {
pub fn read_info(session: &CcidSession) -> Result<PivInfo, PFError> {
let version = get_version(session).unwrap_or([0; 3]);
let serial = get_serial(session).unwrap_or(0);
let pin = get_metadata(session, REF_PIN).ok().and_then(|r| parse_ref_status(&r));
let puk = get_metadata(session, REF_PUK).ok().and_then(|r| parse_ref_status(&r));
let pin = get_metadata(session, REF_PIN)
.ok()
.and_then(|r| parse_ref_status(&r));
let puk = get_metadata(session, REF_PUK)
.ok()
.and_then(|r| parse_ref_status(&r));
let (mgm_algo, mgm_default) = match get_metadata(session, SLOT_MGM) {
Ok(r) => (
tlv::find(&r, 0x01).and_then(|v| v.first().copied()).unwrap_or(ALGO_AES192),
tlv::find(&r, 0x05).map(|v| v.first() == Some(&1)).unwrap_or(false),
tlv::find(&r, 0x01)
.and_then(|v| v.first().copied())
.unwrap_or(ALGO_AES192),
tlv::find(&r, 0x05)
.map(|v| v.first() == Some(&1))
.unwrap_or(false),
),
Err(_) => (ALGO_AES192, false),
};
let mut slots = Vec::new();
for &slot in &PRIMARY_SLOTS {
let meta = get_metadata(session, slot).ok().and_then(|r| parse_slot_meta(&r));
let meta = get_metadata(session, slot)
.ok()
.and_then(|r| parse_slot_meta(&r));
let has_cert = get_data(session, &cert_object_id(slot))
.ok()
.and_then(|o| cert_der(&o))
.map(|d| !d.is_empty())
.unwrap_or(false);
slots.push(SlotStatus { slot, meta, has_cert });
slots.push(SlotStatus {
slot,
meta,
has_cert,
});
}
let mgm_protected = mgm_is_protected(session);
Ok(PivInfo { version, serial, pin, puk, mgm_algo, mgm_default, mgm_protected, slots })
Ok(PivInfo {
version,
serial,
pin,
puk,
mgm_algo,
mgm_default,
mgm_protected,
slots,
})
}
// ── PIN-protected management key (ykman --protect) ───────────────────────────
@@ -385,7 +418,13 @@ pub fn change_ref(
new: &str,
) -> Result<(), PFError> {
let body = change_ref_body(old, new);
session.transceive_full(&Apdu::write(CLA_ISO, INS_CHANGE_REF, 0x00, reference, &body))?;
session.transceive_full(&Apdu::write(
CLA_ISO,
INS_CHANGE_REF,
0x00,
reference,
&body,
))?;
Ok(())
}
@@ -422,7 +461,11 @@ fn aes_ecb(key: &[u8], block: &mut [u8; 16], encrypt: bool) -> Result<(), PFErro
16 => run!(aes::Aes128),
24 => run!(aes::Aes192),
32 => run!(aes::Aes256),
_ => return Err(PFError::Device("Management key must be AES (16/24/32 bytes)".into())),
_ => {
return Err(PFError::Device(
"Management key must be AES (16/24/32 bytes)".into(),
));
}
}
Ok(())
}
@@ -445,8 +488,15 @@ pub fn authenticate_mgm(session: &CcidSession, key: &[u8], algo: u8) -> Result<(
tlv::write(&mut inner, TAG_WITNESS, &[]);
inner
});
let r1 = session.transceive_full(&Apdu::read(CLA_ISO, INS_GENERAL_AUTH, algo, SLOT_MGM, &req1))?;
let outer = tlv::find(&r1, TAG_DYN_AUTH).ok_or_else(|| PFError::Device("No 7C in auth".into()))?;
let r1 = session.transceive_full(&Apdu::read(
CLA_ISO,
INS_GENERAL_AUTH,
algo,
SLOT_MGM,
&req1,
))?;
let outer =
tlv::find(&r1, TAG_DYN_AUTH).ok_or_else(|| PFError::Device("No 7C in auth".into()))?;
let enc_witness = tlv::find(outer, TAG_WITNESS)
.filter(|w| w.len() == 16)
.ok_or_else(|| PFError::Device("Bad witness".into()))?;
@@ -464,17 +514,26 @@ pub fn authenticate_mgm(session: &CcidSession, key: &[u8], algo: u8) -> Result<(
tlv::write(&mut inner, TAG_CHALLENGE, &challenge);
let mut req2 = Vec::new();
tlv::write(&mut req2, TAG_DYN_AUTH, &inner);
let r2 = session.transceive_full(&Apdu::read(CLA_ISO, INS_GENERAL_AUTH, algo, SLOT_MGM, &req2))?;
let r2 = session.transceive_full(&Apdu::read(
CLA_ISO,
INS_GENERAL_AUTH,
algo,
SLOT_MGM,
&req2,
))?;
// Verify the card's response encrypts our challenge (mutual auth).
let outer2 = tlv::find(&r2, TAG_DYN_AUTH).ok_or_else(|| PFError::Device("No 7C in auth-2".into()))?;
let outer2 =
tlv::find(&r2, TAG_DYN_AUTH).ok_or_else(|| PFError::Device("No 7C in auth-2".into()))?;
let resp = tlv::find(outer2, TAG_RESPONSE)
.filter(|r| r.len() == 16)
.ok_or_else(|| PFError::Device("Bad auth response".into()))?;
let mut expect = challenge;
aes_ecb(key, &mut expect, true)?;
if expect.as_slice() != resp {
return Err(PFError::Device("Management-key authentication failed".into()));
return Err(PFError::Device(
"Management-key authentication failed".into(),
));
}
Ok(())
}
@@ -569,7 +628,13 @@ pub fn set_mgm(session: &CcidSession, algo: u8, key: &[u8], touch: bool) -> Resu
/// Set PIN/PUK retry counts (requires mgmt AND PIN; resets PIN/PUK to defaults).
pub fn set_retries(session: &CcidSession, pin_tries: u8, puk_tries: u8) -> Result<(), PFError> {
session.transceive_full(&Apdu::read(CLA_ISO, INS_SET_RETRIES, pin_tries, puk_tries, &[]))?;
session.transceive_full(&Apdu::read(
CLA_ISO,
INS_SET_RETRIES,
pin_tries,
puk_tries,
&[],
))?;
Ok(())
}
@@ -591,7 +656,12 @@ pub fn delete_key(session: &CcidSession, slot: u8) -> Result<(), PFError> {
}
/// Import pre-built key-material TLVs into a slot (mgmt-gated). `algo` is P1.
pub fn import_key(session: &CcidSession, slot: u8, algo: u8, material: &[u8]) -> Result<(), PFError> {
pub fn import_key(
session: &CcidSession,
slot: u8,
algo: u8,
material: &[u8],
) -> Result<(), PFError> {
let apdu = Apdu {
cla: CLA_ISO,
ins: INS_IMPORT,
@@ -860,7 +930,10 @@ mod tests {
// Also accepts a bare (un-53-wrapped) body.
assert_eq!(parse_protected_mgm(&protected), Some(key.to_vec()));
// A 7-byte "key" is neither 16/24/32 → rejected.
assert_eq!(parse_protected_mgm(&[0x88, 0x09, 0x89, 0x07, 0, 0, 0, 0, 0, 0, 0]), None);
assert_eq!(
parse_protected_mgm(&[0x88, 0x09, 0x89, 0x07, 0, 0, 0, 0, 0, 0, 0]),
None
);
assert_eq!(mgm_algo_for_len(16), ALGO_AES128);
assert_eq!(mgm_algo_for_len(24), ALGO_AES192);
assert_eq!(mgm_algo_for_len(32), ALGO_AES256);
@@ -874,15 +947,26 @@ mod tests {
// mis-verify and burn a retry.
let body = change_ref_body("123456", "87654321");
assert_eq!(body.len(), 16);
assert_eq!(&body[..8], &[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0xFF, 0xFF]);
assert_eq!(&body[8..], &[0x38, 0x37, 0x36, 0x35, 0x34, 0x33, 0x32, 0x31]);
assert_eq!(
&body[..8],
&[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0xFF, 0xFF]
);
assert_eq!(
&body[8..],
&[0x38, 0x37, 0x36, 0x35, 0x34, 0x33, 0x32, 0x31]
);
}
#[test]
fn generate_template_bytes() {
// AC { 80 01 11 AA 01 03 AB 01 02 } for P-256, PIN-always, touch-always.
let t = generate_template(ALGO_ECCP256, PIN_POLICY_ALWAYS, TOUCH_POLICY_ALWAYS);
assert_eq!(t, vec![0xAC, 0x09, 0x80, 0x01, 0x11, 0xAA, 0x01, 0x03, 0xAB, 0x01, 0x02]);
assert_eq!(
t,
vec![
0xAC, 0x09, 0x80, 0x01, 0x11, 0xAA, 0x01, 0x03, 0xAB, 0x01, 0x02
]
);
// Default policies are omitted.
let t2 = generate_template(ALGO_RSA2048, PIN_POLICY_DEFAULT, TOUCH_POLICY_DEFAULT);
assert_eq!(t2, vec![0xAC, 0x03, 0x80, 0x01, 0x07]);
@@ -919,8 +1003,14 @@ mod tests {
#[test]
fn pin_padding() {
assert_eq!(pad8(b"123456"), [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0xFF, 0xFF]);
assert_eq!(pad8(b"12345678"), [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38]);
assert_eq!(
pad8(b"123456"),
[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0xFF, 0xFF]
);
assert_eq!(
pad8(b"12345678"),
[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38]
);
}
#[test]
+24 -6
View File
@@ -187,7 +187,7 @@ pub fn build_journal(
mod tests {
use super::*;
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
use ring::signature::{ECDSA_P256_SHA256_ASN1_SIGNING, EcdsaKeyPair, KeyPair};
fn entry(seq: u32, event: u8) -> Vec<u8> {
let mut e = vec![0u8; ENTRY_LEN];
@@ -233,8 +233,8 @@ mod tests {
fn checkpoint_roundtrip_verifies_and_rejects_tamper() {
let rng = SystemRandom::new();
let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng).unwrap();
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.unwrap();
let pubkey = kp.public_key().as_ref().to_vec();
let head = [0x11u8; 32];
@@ -247,10 +247,28 @@ mod tests {
msg.extend_from_slice(&challenge);
let sig = kp.sign(&rng, &msg).unwrap();
assert!(verify_checkpoint(&head, seq, sig.as_ref(), &pubkey, &challenge));
assert!(verify_checkpoint(
&head,
seq,
sig.as_ref(),
&pubkey,
&challenge
));
// A different challenge must fail (freshness) and a bad key too.
assert!(!verify_checkpoint(&head, seq, sig.as_ref(), &pubkey, &[0x23u8; 16]));
assert!(!verify_checkpoint(&head, seq + 1, sig.as_ref(), &pubkey, &challenge));
assert!(!verify_checkpoint(
&head,
seq,
sig.as_ref(),
&pubkey,
&[0x23u8; 16]
));
assert!(!verify_checkpoint(
&head,
seq + 1,
sig.as_ref(),
&pubkey,
&challenge
));
}
#[test]
+38 -25
View File
@@ -798,8 +798,12 @@ fn parse_management_info(raw: &[u8]) -> Result<ManagementInfo, String> {
0x02 if field_data.len() == 4 => {
// TAG_SERIAL is the 8-digit Yubico decimal (already serial4-masked
// by the firmware), big-endian — render it as ykman does, not hex.
let n =
u32::from_be_bytes([field_data[0], field_data[1], field_data[2], field_data[3]]);
let n = u32::from_be_bytes([
field_data[0],
field_data[1],
field_data[2],
field_data[3],
]);
info.serial = Some(n.to_string());
}
0x03 => info.usb_enabled = parse_management_u16(field_data),
@@ -1076,7 +1080,11 @@ fn build_rskey_phy_tlv(config: &AppConfigInput) -> Result<Vec<u8>, PFError> {
tlv.push(0x00);
}
if let Some(mfr) = config.manufacturer_name.as_deref().filter(|n| !n.is_empty()) {
if let Some(mfr) = config
.manufacturer_name
.as_deref()
.filter(|n| !n.is_empty())
{
let bytes = mfr.as_bytes();
if bytes.len() + 1 > 33 {
return Err(PFError::Device(
@@ -1513,7 +1521,9 @@ pub(crate) fn get_enterprise_attestation_csr() -> Result<String, String> {
fn vendor_error(status: u8, op: &str) -> String {
match status {
0x36 => "device requires a PIN — enter it".to_string(),
0x27 => "denied — no touch within the window (press the button when the LED blinks)".to_string(),
0x27 => {
"denied — no touch within the window (press the button when the LED blinks)".to_string()
}
0x30 => format!("{op}: operation not allowed (already sealed, or no OTP DEVK provisioned)"),
0x3D => "device is not locked".to_string(),
other => format!("{op} failed: status 0x{other:02x}"),
@@ -1521,11 +1531,7 @@ fn vendor_error(status: u8, op: &str) -> String {
}
/// Require a successful vendor response and unwrap its CBOR map.
fn vendor_map(
status: u8,
map: Option<Value>,
op: &str,
) -> Result<BTreeMap<Value, Value>, String> {
fn vendor_map(status: u8, map: Option<Value>, op: &str) -> Result<BTreeMap<Value, Value>, String> {
if status != 0 {
return Err(vendor_error(status, op));
}
@@ -1559,7 +1565,10 @@ fn m_bool(m: &BTreeMap<Value, Value>, k: i128) -> bool {
}
/// Read AUDIT_READ into a journal window (gated by PIN, or a touch if `pin` is None).
fn read_journal(transport: &HidTransport, pin: Option<&str>) -> Result<audit::AuditJournal, String> {
fn read_journal(
transport: &HidTransport,
pin: Option<&str>,
) -> Result<audit::AuditJournal, String> {
let (status, map) = transport
.rs_key_vendor(RSKEY_VENDOR_AUDIT_READ, None, pin)
.map_err(|e| e.to_string())?;
@@ -1598,7 +1607,11 @@ pub(crate) fn audit_verify(
let mut params = BTreeMap::new();
params.insert(Value::Integer(1), Value::Bytes(challenge.to_vec()));
let (status, map) = transport
.rs_key_vendor(RSKEY_VENDOR_AUDIT_CHECKPOINT, Some(Value::Map(params)), pin.as_deref())
.rs_key_vendor(
RSKEY_VENDOR_AUDIT_CHECKPOINT,
Some(Value::Map(params)),
pin.as_deref(),
)
.map_err(|e| e.to_string())?;
let m = vendor_map(status, map, "checkpoint")?;
@@ -1607,7 +1620,8 @@ pub(crate) fn audit_verify(
let sig = m_bytes(&m, 3).ok_or("checkpoint: missing signature")?;
let pubkey = m_bytes(&m, 4).ok_or("checkpoint: missing public key")?;
let signature_ok = audit::verify_checkpoint(&head_signed, seq_signed, &sig, &pubkey, &challenge);
let signature_ok =
audit::verify_checkpoint(&head_signed, seq_signed, &sig, &pubkey, &challenge);
let head_matches = head_signed == journal.head;
let fingerprint = audit::fingerprint(&pubkey);
let pubkey_hex = hex::encode(&pubkey);
@@ -1830,11 +1844,7 @@ pub(crate) fn lock_enable(pin: String) -> Result<String, String> {
HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?;
let blob = wrap_secret(&transport, &key)?;
let token = transport
.get_pin_token_with_permission(
&pin,
PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG,
None,
)
.get_pin_token_with_permission(&pin, PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG, None)
.map_err(|e| e.to_string())?;
transport
.authconfig_vendor(&token, RSKEY_AUT_ENABLE, Some(Value::Bytes(blob)))
@@ -1861,11 +1871,7 @@ pub(crate) fn lock_disable(pin: String, mnemonic: String) -> Result<(), String>
}
let token = transport
.get_pin_token_with_permission(
&pin,
PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG,
None,
)
.get_pin_token_with_permission(&pin, PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG, None)
.map_err(|e| e.to_string())?;
transport
.authconfig_vendor(&token, RSKEY_AUT_DISABLE, None)
@@ -1925,7 +1931,7 @@ fn certs_pem_to_der(input: &[u8]) -> Result<Vec<u8>, String> {
Err("chain is neither PEM nor DER".into())
};
}
use base64::{engine::general_purpose, Engine as _};
use base64::{Engine as _, engine::general_purpose};
let mut out = Vec::new();
let mut rest = text;
while let Some(b) = rest.find("-----BEGIN CERTIFICATE-----") {
@@ -1963,7 +1969,10 @@ pub(crate) fn att_import(
let chain = certs_pem_to_der(&chain_file)?;
if chain.is_empty() || chain.len() > 2048 {
return Err(format!("cert chain must be 1..=2048 bytes (got {})", chain.len()));
return Err(format!(
"cert chain must be 1..=2048 bytes (got {})",
chain.len()
));
}
let transport =
@@ -1974,7 +1983,11 @@ pub(crate) fn att_import(
params.insert(Value::Integer(1), Value::Bytes(blob));
params.insert(Value::Integer(2), Value::Bytes(chain));
let (status, _) = transport
.rs_key_vendor(RSKEY_VENDOR_ATT_IMPORT, Some(Value::Map(params)), pin.as_deref())
.rs_key_vendor(
RSKEY_VENDOR_ATT_IMPORT,
Some(Value::Map(params)),
pin.as_deref(),
)
.map_err(|e| e.to_string())?;
if status != 0 {
return Err(vendor_error(status, "attestation import"));
+15 -5
View File
@@ -1594,7 +1594,8 @@ impl FidoOperations for HidTransport {
// Touch-gated variants block until the button is pressed — allow ~30 s.
const VENDOR_TOUCH_TIMEOUT_MS: i32 = 32_000;
let resp = self.send_raw_with_timeout(CTAPHID_CBOR, &full_payload, VENDOR_TOUCH_TIMEOUT_MS)?;
let resp =
self.send_raw_with_timeout(CTAPHID_CBOR, &full_payload, VENDOR_TOUCH_TIMEOUT_MS)?;
if resp.is_empty() {
return Err(PFError::Device("empty vendor response".into()));
}
@@ -1627,16 +1628,25 @@ impl FidoOperations for HidTransport {
let sub_val = Value::Map(sub);
let sub_bytes = to_vec(&sub_val).map_err(|e| PFError::Io(e.to_string()))?;
let pin_auth =
self.sign_config_command(pin_token, ConfigSubCommand::VendorPrototype as u8, &sub_bytes);
let pin_auth = self.sign_config_command(
pin_token,
ConfigSubCommand::VendorPrototype as u8,
&sub_bytes,
);
let mut cfg = BTreeMap::new();
cfg.insert(
Value::Integer(ConfigParam::SubCommand as i128),
Value::Integer(ConfigSubCommand::VendorPrototype as i128),
);
cfg.insert(Value::Integer(ConfigParam::SubCommandParams as i128), sub_val);
cfg.insert(Value::Integer(ConfigParam::PinUvAuthProtocol as i128), Value::Integer(1));
cfg.insert(
Value::Integer(ConfigParam::SubCommandParams as i128),
sub_val,
);
cfg.insert(
Value::Integer(ConfigParam::PinUvAuthProtocol as i128),
Value::Integer(1),
);
cfg.insert(
Value::Integer(ConfigParam::PinUvAuthParam as i128),
Value::Bytes(pin_auth),
+11 -8
View File
@@ -9,7 +9,7 @@ use crate::{
error::PFError,
hal::{
applets::oath, applets::openpgp, applets::otp, applets::piv, fido, rescue,
transport::ccid::CcidSession, transport::DeviceHandle, types::*,
transport::DeviceHandle, transport::ccid::CcidSession, types::*,
},
};
@@ -438,7 +438,11 @@ pub fn offboard(serial: String) -> Result<crate::hal::offboard::OffboardReport,
break;
}
}
steps.push(OffboardStep { name: "otp".into(), ok, detail });
steps.push(OffboardStep {
name: "otp".into(),
ok,
detail,
});
}
steps.push(step("oath", oath_reset().map_err(|e| e.to_string())));
steps.push(step("piv", piv_reset().map_err(|e| e.to_string())));
@@ -837,13 +841,12 @@ pub fn openpgp_set_touch(admin: String, slot: openpgp::PgpSlot, on: bool) -> Res
}
/// Generate a key in a slot with the given algorithm choice (see `GENERATE_ALGOS`).
pub fn openpgp_generate(
admin: String,
slot: openpgp::PgpSlot,
choice: u8,
) -> Result<(), PFError> {
pub fn openpgp_generate(admin: String, slot: openpgp::PgpSlot, choice: u8) -> Result<(), PFError> {
let attr = openpgp::algo_attr(slot, choice).ok_or_else(|| {
PFError::Device(format!("Algorithm not supported for the {} slot", slot.label()))
PFError::Device(format!(
"Algorithm not supported for the {} slot",
slot.label()
))
})?;
let s = openpgp_admin(&admin)?;
openpgp::generate(&s, slot, &attr).map(|_| ())
+40 -9
View File
@@ -48,7 +48,11 @@ impl OffboardReport {
/// The names of the failed steps.
pub fn failures(&self) -> Vec<&str> {
self.steps.iter().filter(|s| !s.ok).map(|s| s.name.as_str()).collect()
self.steps
.iter()
.filter(|s| !s.ok)
.map(|s| s.name.as_str())
.collect()
}
/// Render the receipt as pretty JSON. `timestamp` is supplied by the caller
@@ -59,7 +63,11 @@ impl OffboardReport {
if i > 0 {
steps.push(',');
}
steps.push_str(&format!("\n {}: {}", json_str(&s.name), json_str(&s.detail)));
steps.push_str(&format!(
"\n {}: {}",
json_str(&s.name),
json_str(&s.detail)
));
}
steps.push_str("\n }");
@@ -70,10 +78,22 @@ impl OffboardReport {
out.push_str(&format!(" \"signed\": {}", self.signed));
if self.signed {
let f = |o: &Option<String>| o.clone().unwrap_or_default();
out.push_str(&format!(",\n \"fingerprint\": {}", json_str(&f(&self.fingerprint))));
out.push_str(&format!(",\n \"signed_head\": {}", json_str(&f(&self.signed_head))));
out.push_str(&format!(",\n \"signature\": {}", json_str(&f(&self.signature))));
out.push_str(&format!(",\n \"attestation_pubkey\": {}", json_str(&f(&self.pubkey))));
out.push_str(&format!(
",\n \"fingerprint\": {}",
json_str(&f(&self.fingerprint))
));
out.push_str(&format!(
",\n \"signed_head\": {}",
json_str(&f(&self.signed_head))
));
out.push_str(&format!(
",\n \"signature\": {}",
json_str(&f(&self.signature))
));
out.push_str(&format!(
",\n \"attestation_pubkey\": {}",
json_str(&f(&self.pubkey))
));
}
out.push_str("\n}\n");
out
@@ -85,7 +105,11 @@ mod tests {
use super::*;
fn step(name: &str, ok: bool) -> OffboardStep {
OffboardStep { name: name.into(), ok, detail: if ok { "ok".into() } else { "boom".into() } }
OffboardStep {
name: name.into(),
ok,
detail: if ok { "ok".into() } else { "boom".into() },
}
}
#[test]
@@ -120,8 +144,15 @@ mod tests {
assert!(j.contains("\"fingerprint\": \"abcd\""));
assert!(j.contains("\"attestation_pubkey\": \"04aa\""));
// A quote in a detail string must be escaped.
let bad = OffboardStep { name: "x".into(), ok: false, detail: "a\"b".into() };
let r2 = OffboardReport { steps: vec![bad], ..r };
let bad = OffboardStep {
name: "x".into(),
ok: false,
detail: "a\"b".into(),
};
let r2 = OffboardReport {
steps: vec![bad],
..r
};
assert!(r2.to_json("t").contains("a\\\"b"));
}
}
+9 -3
View File
@@ -12,7 +12,7 @@
use crate::error::PFError;
use crate::hal::apdu::{
Apdu, StatusWord, CLA_CHAIN, CLA_ISO, INS_GET_RESPONSE, INS_SELECT, INS_SEND_REMAINING,
Apdu, CLA_CHAIN, CLA_ISO, INS_GET_RESPONSE, INS_SELECT, INS_SEND_REMAINING, StatusWord,
};
use pcsc::{Context, Protocols, Scope, ShareMode};
@@ -42,7 +42,10 @@ impl CcidSession {
.ok_or(PFError::NoDevice)?;
let card = ctx.connect(reader, ShareMode::Shared, Protocols::ANY)?;
let mut session = Self { card, select_resp: Vec::new() };
let mut session = Self {
card,
select_resp: Vec::new(),
};
let select = Apdu::read(CLA_ISO, INS_SELECT, 0x04, 0x00, aid);
session.select_resp = session.transceive_full(&select).map_err(|e| {
PFError::Device(format!(
@@ -61,7 +64,10 @@ impl CcidSession {
return Err(PFError::Device("Truncated APDU response".into()));
}
let (data, sw) = resp.split_at(resp.len() - 2);
Ok((data.to_vec(), StatusWord(u16::from_be_bytes([sw[0], sw[1]]))))
Ok((
data.to_vec(),
StatusWord(u16::from_be_bytes([sw[0], sw[1]])),
))
}
/// Send an APDU and assemble the full response across `61xx` continuations,
+10 -15
View File
@@ -13,9 +13,8 @@ use crate::ui::screens::{
attestation::AttestationEvent, attestation::AttestationViewModel, audit::AuditViewModel,
backup::BackupViewModel, config::ConfigViewModel, home::HomeViewModel, lock::LockViewModel,
offboard::OffboardEvent, offboard::OffboardViewModel, openpgp::OpenPgpEvent,
openpgp::OpenPgpViewModel, passkeys::PasskeysEvent,
passkeys::PasskeysViewModel, piv::PivEvent, piv::PivViewModel, security::SecurityViewModel,
slots::SlotsEvent, slots::SlotsViewModel,
openpgp::OpenPgpViewModel, passkeys::PasskeysEvent, passkeys::PasskeysViewModel, piv::PivEvent,
piv::PivViewModel, security::SecurityViewModel, slots::SlotsEvent, slots::SlotsViewModel,
};
use gpui::prelude::*;
use gpui::*;
@@ -239,15 +238,13 @@ impl Render for ApplicationRoot {
Destination::Slots => {
let view = self.views_store.slots.get_or_insert_with(|| {
let view = cx.new(|cx| SlotsViewModel::new(window, cx, &self.models));
cx.subscribe_in(
&view,
window,
|_, _, event: &SlotsEvent, window, cx| match event {
cx.subscribe_in(&view, window, |_, _, event: &SlotsEvent, window, cx| {
match event {
SlotsEvent::Notification(msg) => {
window.push_notification(msg.to_string(), cx);
}
},
)
}
})
.detach();
view
});
@@ -271,15 +268,13 @@ impl Render for ApplicationRoot {
Destination::OpenPgp => {
let view = self.views_store.openpgp.get_or_insert_with(|| {
let view = cx.new(|cx| OpenPgpViewModel::new(window, cx, &self.models));
cx.subscribe_in(
&view,
window,
|_, _, event: &OpenPgpEvent, window, cx| match event {
cx.subscribe_in(&view, window, |_, _, event: &OpenPgpEvent, window, cx| {
match event {
OpenPgpEvent::Notification(msg) => {
window.push_notification(msg.to_string(), cx);
}
},
)
}
})
.detach();
view
});
+7 -14
View File
@@ -179,14 +179,12 @@ impl Render for AppSidebar {
// features, then device-wide system actions (Offboard sits just
// above About as a bottom-of-list decommission action).
.child(
SidebarGroup::new("Device").child(
SidebarMenu::new().child(self.menu_item(
cx,
"Home",
"icons/house.svg",
Destination::Home,
)),
),
SidebarGroup::new("Device").child(SidebarMenu::new().child(self.menu_item(
cx,
"Home",
"icons/house.svg",
Destination::Home,
))),
)
.child(
SidebarGroup::new("Credentials").child(
@@ -227,12 +225,7 @@ impl Render for AppSidebar {
"icons/book-open.svg",
Destination::Audit,
))
.child(self.menu_item(
cx,
"Backup",
"icons/save.svg",
Destination::Backup,
))
.child(self.menu_item(cx, "Backup", "icons/save.svg", Destination::Backup))
.child(self.menu_item(cx, "Lock", "icons/lock.svg", Destination::Lock))
.child(self.menu_item(
cx,
+21 -12
View File
@@ -26,14 +26,14 @@ const HOTPLUG_POLL_MS: u64 = 1000;
pub use crate::hal::applets::oath;
pub use crate::hal::applets::openpgp;
pub use crate::hal::fido::audit;
pub use crate::hal::fido::backup;
pub use crate::hal::fido::AttStatus;
pub use crate::hal::offboard::OffboardReport;
pub use crate::hal::applets::otp;
pub use crate::hal::io::MgmAuth;
pub use crate::hal::applets::piv;
pub use crate::hal::applets::{OathFeatures, OpenPgpFeatures, OtpFeatures, PivFeatures};
pub use crate::hal::fido::AttStatus;
pub use crate::hal::fido::audit;
pub use crate::hal::fido::backup;
pub use crate::hal::io::MgmAuth;
pub use crate::hal::offboard::OffboardReport;
pub use crate::hal::rescue::constants::{
LedColor, LedStatus, USB_CAP_FIDO2, USB_CAP_OATH, USB_CAP_OPENPGP, USB_CAP_OTP, USB_CAP_PIV,
USB_CAP_U2F,
@@ -204,10 +204,21 @@ impl DeviceRepo {
new_acc: [u8; 6],
current_acc: [u8; 6],
) -> Result<(), crate::error::PFError> {
io::otp_program_yubico(slot, public_id, private_id, key, append_cr, new_acc, current_acc)
io::otp_program_yubico(
slot,
public_id,
private_id,
key,
append_cr,
new_acc,
current_acc,
)
}
pub fn otp_delete_blocking(slot: u8, current_acc: [u8; 6]) -> Result<(), crate::error::PFError> {
pub fn otp_delete_blocking(
slot: u8,
current_acc: [u8; 6],
) -> Result<(), crate::error::PFError> {
io::otp_delete(slot, current_acc)
}
@@ -290,10 +301,7 @@ impl DeviceRepo {
io::piv_import_key(slot, key_file, auth)
}
pub fn piv_delete_cert_blocking(
slot: u8,
auth: MgmAuth,
) -> Result<(), crate::error::PFError> {
pub fn piv_delete_cert_blocking(slot: u8, auth: MgmAuth) -> Result<(), crate::error::PFError> {
io::piv_delete_cert(slot, auth)
}
@@ -396,7 +404,8 @@ impl DeviceRepo {
/// OATH feature profile of the connected firmware, if it exposes the applet.
pub fn oath_features(&self) -> Option<OathFeatures> {
let status = self.status.as_ref()?;
AnyFirmware::new(status.firmware_type.clone(), &status.info.firmware_version).oath_features()
AnyFirmware::new(status.firmware_type.clone(), &status.info.firmware_version)
.oath_features()
}
/// OTP feature profile of the connected firmware, if it exposes the applet.
+6 -2
View File
@@ -6,7 +6,7 @@ use crate::ui::models::device::oath;
use crate::ui::screens::accounts::view_model::AccountsViewModel;
use gpui::*;
use gpui_component::button::{Button, ButtonVariants};
use gpui_component::{h_flex, v_flex, ActiveTheme, Disableable, Icon, StyledExt, Theme};
use gpui_component::{ActiveTheme, Disableable, Icon, StyledExt, Theme, h_flex, v_flex};
/// Split a numeric code into two halves for readability ("123 456").
fn format_code(code: &str) -> String {
@@ -192,7 +192,11 @@ impl Render for AccountsViewModel {
.justify_center()
.gap_3()
.py_6()
.child(div().font_semibold().child("Accounts are password-protected"))
.child(
div()
.font_semibold()
.child("Accounts are password-protected"),
)
.child(
div()
.text_sm()
+29 -23
View File
@@ -6,12 +6,12 @@ use crate::ui::app::AppModels;
use crate::ui::components::applet_gate::AppletGate;
use crate::ui::components::dialog;
use crate::ui::components::dialog::{ConfirmContent, PinPromptContent};
use crate::ui::models::device::{oath, DeviceEvent, DeviceRepo, USB_CAP_OATH};
use crate::ui::components::form::{LabeledU8, select_state, selected_key};
use crate::ui::models::device::{DeviceEvent, DeviceRepo, USB_CAP_OATH, oath};
use gpui::*;
use gpui_component::button::ButtonVariants;
use crate::ui::components::form::{select_state, selected_key, LabeledU8};
use gpui_component::select::SelectState;
use gpui_component::WindowExt;
use gpui_component::button::ButtonVariants;
use gpui_component::select::SelectState;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
// Add-form dropdown options (label, key). The key is the wire value where one
@@ -94,17 +94,19 @@ impl AccountsViewModel {
}
fn start_ticker(&mut self, cx: &mut Context<Self>) {
self._ticker = Some(cx.spawn(async move |weak, cx| loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
let alive = weak.update(cx, |this, cx| {
this.now = now_unix();
if this.loaded && !this.loading && this.now / 30 != this.last_window {
this.reload(cx);
self._ticker = Some(cx.spawn(async move |weak, cx| {
loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
let alive = weak.update(cx, |this, cx| {
this.now = now_unix();
if this.loaded && !this.loading && this.now / 30 != this.last_window {
this.reload(cx);
}
cx.notify();
});
if alive.is_err() {
break;
}
cx.notify();
});
if alive.is_err() {
break;
}
}));
}
@@ -346,7 +348,10 @@ impl AccountsViewModel {
match res {
Ok(code) => {
if let Some(acc) = this.accounts.iter_mut().find(|a| a.id == id) {
acc.state = oath::CodeState::Code { value: code, period };
acc.state = oath::CodeState::Code {
value: code,
period,
};
}
}
Err(e) => cx.emit(AccountsEvent::Notification(format!("Calculate: {e}"))),
@@ -361,7 +366,8 @@ impl AccountsViewModel {
gpui_component::input::InputState::new(window, cx).placeholder("Issuer (e.g. GitHub)")
});
let account = cx.new(|cx| {
gpui_component::input::InputState::new(window, cx).placeholder("Account (e.g. you@example.com)")
gpui_component::input::InputState::new(window, cx)
.placeholder("Account (e.g. you@example.com)")
});
let secret = cx.new(|cx| {
gpui_component::input::InputState::new(window, cx)
@@ -430,8 +436,7 @@ impl AccountsViewModel {
match parsed {
Ok(cred) => {
window.close_dialog(cx);
let status =
dialog::open_status_dialog("Adding Account", window, cx);
let status = dialog::open_status_dialog("Adding Account", window, cx);
let _ = view.update(cx, |this, cx| this.execute_add(cred, status, cx));
}
Err(e) => {
@@ -530,7 +535,8 @@ impl AccountsViewModel {
this.loading = false;
match res {
Ok(_) => {
let _ = status.update(cx, |d, cx| d.set_success("Account added.".into(), cx));
let _ =
status.update(cx, |d, cx| d.set_success("Account added.".into(), cx));
this.reload(cx);
}
Err(e) => {
@@ -643,7 +649,8 @@ impl AccountsViewModel {
this.loading = false;
match res {
Ok(_) => {
let _ = status.update(cx, |d, cx| d.set_success("Account renamed.".into(), cx));
let _ =
status.update(cx, |d, cx| d.set_success("Account renamed.".into(), cx));
this.reload(cx);
}
Err(e) => {
@@ -744,9 +751,8 @@ impl AccountsViewModel {
this.loading = false;
match res {
Ok(_) => {
let _ = status.update(cx, |d, cx| {
d.set_success("OATH applet reset.".into(), cx)
});
let _ = status
.update(cx, |d, cx| d.set_success("OATH applet reset.".into(), cx));
this.accounts.clear();
this.loaded = false;
this.password = None;
+29 -5
View File
@@ -5,7 +5,7 @@ use crate::ui::components::page_view::PageView;
use crate::ui::screens::attestation::view_model::AttestationViewModel;
use gpui::*;
use gpui_component::button::{Button, ButtonVariants};
use gpui_component::{h_flex, v_flex, ActiveTheme, Disableable, Icon, StyledExt, Theme};
use gpui_component::{ActiveTheme, Disableable, Icon, StyledExt, Theme, h_flex, v_flex};
fn empty_state(heading: &str, body: String, theme: &Theme) -> AnyElement {
v_flex()
@@ -17,7 +17,13 @@ fn empty_state(heading: &str, body: String, theme: &Theme) -> AnyElement {
.border_color(theme.border)
.rounded_xl()
.child(div().font_semibold().child(heading.to_string()))
.child(div().text_sm().max_w(px(380.)).text_color(theme.muted_foreground).child(body))
.child(
div()
.text_sm()
.max_w(px(380.))
.text_color(theme.muted_foreground)
.child(body),
)
.into_any_element()
}
@@ -40,7 +46,12 @@ impl AttestationViewModel {
v_flex()
.gap_0p5()
.child(div().font_medium().child(title))
.child(div().text_sm().text_color(theme.muted_foreground).child(subtitle)),
.child(
div()
.text_sm()
.text_color(theme.muted_foreground)
.child(subtitle),
),
)
.child(btn)
}
@@ -85,7 +96,15 @@ impl Render for AttestationViewModel {
h_flex()
.gap_2()
.items_center()
.child(div().text_color(if s.installed { theme.green } else { theme.muted_foreground }).child(""))
.child(
div()
.text_color(if s.installed {
theme.green
} else {
theme.muted_foreground
})
.child(""),
)
.child(div().text_sm().child(if s.installed {
"Org attestation installed"
} else {
@@ -96,7 +115,12 @@ impl Render for AttestationViewModel {
col = col.child(
v_flex()
.gap_0p5()
.child(div().text_xs().text_color(theme.muted_foreground).child("Chain hash"))
.child(
div()
.text_xs()
.text_color(theme.muted_foreground)
.child("Chain hash"),
)
.child(div().font_family("monospace").text_xs().child(h.clone())),
);
}
+1 -1
View File
@@ -7,9 +7,9 @@ use crate::ui::components::dialog;
use crate::ui::components::dialog::StatusContent;
use crate::ui::models::device::{AttStatus, DeviceEvent, DeviceRepo, FirmwareType};
use gpui::*;
use gpui_component::WindowExt;
use gpui_component::button::ButtonVariants;
use gpui_component::input::InputState;
use gpui_component::WindowExt;
pub struct AttestationViewModel {
pub(super) device: Entity<DeviceRepo>,
+39 -9
View File
@@ -6,7 +6,7 @@ use crate::ui::models::device::audit;
use crate::ui::screens::audit::view_model::AuditViewModel;
use gpui::*;
use gpui_component::button::Button;
use gpui_component::{h_flex, v_flex, ActiveTheme, Disableable, Icon, StyledExt, Theme};
use gpui_component::{ActiveTheme, Disableable, Icon, StyledExt, Theme, h_flex, v_flex};
fn empty_state(heading: &str, body: String, theme: &Theme) -> AnyElement {
v_flex()
@@ -48,10 +48,25 @@ impl AuditViewModel {
.gap_3()
.py_1()
.text_sm()
.child(div().w(px(56.)).text_color(theme.muted_foreground).child(entry.seq.to_string()))
.child(div().w(px(72.)).text_color(theme.muted_foreground).child(format!("{:.1}s", entry.uptime_s())))
.child(
div()
.w(px(56.))
.text_color(theme.muted_foreground)
.child(entry.seq.to_string()),
)
.child(
div()
.w(px(72.))
.text_color(theme.muted_foreground)
.child(format!("{:.1}s", entry.uptime_s())),
)
.child(div().w(px(160.)).font_medium().child(entry.event_label()))
.child(div().w(px(40.)).text_color(theme.muted_foreground).child(entry.aux.to_string()))
.child(
div()
.w(px(40.))
.text_color(theme.muted_foreground)
.child(entry.aux.to_string()),
)
.child(
div()
.flex_1()
@@ -111,7 +126,10 @@ impl AuditViewModel {
j.start,
)))
.child(mono(theme, format!("epoch {}", short_hex(&j.epoch))))
.child(mono(theme, format!("head {} (chain OK)", short_hex(&j.head)))),
.child(mono(
theme,
format!("head {} (chain OK)", short_hex(&j.head)),
)),
)
.child(div().h(px(1.)).bg(theme.border))
.child(v_flex().gap_0p5().children(rows))
@@ -136,7 +154,12 @@ impl AuditViewModel {
let kv = |k: &str, val: String| {
v_flex()
.gap_0p5()
.child(div().text_xs().text_color(theme.muted_foreground).child(k.to_string()))
.child(
div()
.text_xs()
.text_color(theme.muted_foreground)
.child(k.to_string()),
)
.child(div().font_family("monospace").text_xs().child(val))
};
@@ -201,14 +224,18 @@ impl Render for AuditViewModel {
.label("Disable")
.outline()
.disabled(self.loading)
.on_click(cx.listener(|this, _, window, cx| this.open_toggle(false, window, cx))),
.on_click(
cx.listener(|this, _, window, cx| this.open_toggle(false, window, cx)),
),
),
Some(false) => Some(
Button::new("audit-enable")
.label("Enable")
.outline()
.disabled(self.loading)
.on_click(cx.listener(|this, _, window, cx| this.open_toggle(true, window, cx))),
.on_click(
cx.listener(|this, _, window, cx| this.open_toggle(true, window, cx)),
),
),
None => None,
};
@@ -218,7 +245,10 @@ impl Render for AuditViewModel {
let verify_body = self.verify_body(theme);
let (dot, status_text) = match self.enabled {
Some(true) => (theme.green, "On — recording security events to the key's flash."),
Some(true) => (
theme.green,
"On — recording security events to the key's flash.",
),
Some(false) => (
theme.muted_foreground,
"Off — journalling is opt-in; nothing is being recorded.",
+13 -4
View File
@@ -5,11 +5,11 @@ use crate::ui::app::AppModels;
use crate::ui::components::applet_gate::AppletGate;
use crate::ui::components::dialog;
use crate::ui::components::dialog::StatusContent;
use crate::ui::models::device::{audit, DeviceEvent, DeviceRepo, FirmwareType};
use crate::ui::models::device::{DeviceEvent, DeviceRepo, FirmwareType, audit};
use gpui::*;
use gpui_component::WindowExt;
use gpui_component::button::ButtonVariants;
use gpui_component::input::InputState;
use gpui_component::WindowExt;
pub struct AuditViewModel {
pub(super) device: Entity<DeviceRepo>,
@@ -67,7 +67,12 @@ impl AuditViewModel {
// ── Enable / disable journalling (PIN + touch) ──────────────────────────
pub(super) fn open_toggle(&mut self, enable: bool, window: &mut Window, cx: &mut Context<Self>) {
pub(super) fn open_toggle(
&mut self,
enable: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let pin = Self::pin_input(window, cx);
let view = cx.entity().downgrade();
let submit = {
@@ -77,7 +82,11 @@ impl AuditViewModel {
let p = (!p.is_empty()).then_some(p);
window.close_dialog(cx);
let status = dialog::open_status_dialog(
if enable { "Enabling Journalling" } else { "Disabling Journalling" },
if enable {
"Enabling Journalling"
} else {
"Disabling Journalling"
},
window,
cx,
);

Some files were not shown because too many files have changed in this diff Show More