mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
fmt: Run cargo fmt
This commit is contained in:
+20
-3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
@@ -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,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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user