use anyhow::{bail, Result};
use std::fmt;
pub const GET_METADATA_MGMT: [u8; 5] = [0x00, 0xF7, 0x00, 0x9B, 0x00];
pub const GET_METADATA_PIN: [u8; 5] = [0x00, 0xF7, 0x00, 0x80, 0x00];
pub const GET_METADATA_PUK: [u8; 5] = [0x00, 0xF7, 0x00, 0x81, 0x00];
pub(crate) fn get_metadata_apdu(slot: u8) -> [u8; 5] {
[0x00, 0xF7, 0x00, slot, 0x00]
}
const TAG_ALGORITHM: u8 = 0x01;
const TAG_POLICY: u8 = 0x02;
const TAG_ORIGIN: u8 = 0x03;
const TAG_IS_DEFAULT: u8 = 0x05;
const TAG_RETRIES: u8 = 0x06;
const TOUCH_ALWAYS: u8 = 0x02;
const TOUCH_CACHED: u8 = 0x03;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MgmtAlgo {
Tdes,
Aes128,
Aes192,
Aes256,
}
impl MgmtAlgo {
pub fn from_id(id: u8) -> Option<Self> {
match id {
0x03 => Some(Self::Tdes),
0x08 => Some(Self::Aes128),
0x0A => Some(Self::Aes192),
0x0C => Some(Self::Aes256),
_ => None,
}
}
pub fn id(self) -> u8 {
match self {
Self::Tdes => 0x03,
Self::Aes128 => 0x08,
Self::Aes192 => 0x0A,
Self::Aes256 => 0x0C,
}
}
pub fn key_len(self) -> usize {
match self {
Self::Aes128 => 16,
Self::Tdes | Self::Aes192 => 24,
Self::Aes256 => 32,
}
}
pub fn block_size(self) -> usize {
match self {
Self::Tdes => 8,
Self::Aes128 | Self::Aes192 | Self::Aes256 => 16,
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name.to_ascii_uppercase().replace('-', "").as_str() {
"TDES" | "3DES" => Some(Self::Tdes),
"AES128" => Some(Self::Aes128),
"AES192" => Some(Self::Aes192),
"AES256" => Some(Self::Aes256),
_ => None,
}
}
pub fn check_key_len(self, key: &[u8]) -> Result<()> {
if key.len() != self.key_len() {
bail!(
"management key is {} bytes but the card uses {} ({} bytes)",
key.len(),
self,
self.key_len()
);
}
Ok(())
}
}
impl fmt::Display for MgmtAlgo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Tdes => "3DES",
Self::Aes128 => "AES-128",
Self::Aes192 => "AES-192",
Self::Aes256 => "AES-256",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TouchPolicy {
Never,
Always,
Cached,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MgmtMetadata {
pub algo: MgmtAlgo,
pub touch: TouchPolicy,
pub is_default: bool,
}
impl MgmtMetadata {
pub fn touch_required(&self) -> bool {
self.touch != TouchPolicy::Never
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CredentialMetadata {
pub is_default: bool,
pub retries_total: u8,
pub retries_left: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyOrigin {
Generated,
Imported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SlotMetadata {
pub algorithm: u8,
pub origin: Option<KeyOrigin>,
}
pub fn key_algorithm_name(id: u8) -> String {
match id {
0x06 => "RSA 1024".to_owned(),
0x07 => "RSA 2048".to_owned(),
0x05 => "RSA 3072".to_owned(),
0x16 => "RSA 4096".to_owned(),
0x11 => "EC P-256".to_owned(),
0x14 => "EC P-384".to_owned(),
0xE0 => "Ed25519".to_owned(),
0xE1 => "X25519".to_owned(),
other => format!("algorithm 0x{other:02x}"),
}
}
pub const ALGO_ECCP256: u8 = 0x11;
fn is_default_flag(tlv: &std::collections::HashMap<u8, Vec<u8>>) -> bool {
tlv.get(&TAG_IS_DEFAULT).map(Vec::as_slice) == Some(&[0x01])
}
pub(crate) fn parse_credential_metadata(data: &[u8]) -> Result<CredentialMetadata> {
let tlv = crate::auxiliaries::parse_tlv_flat(data);
let (retries_total, retries_left) = match tlv.get(&TAG_RETRIES).map(Vec::as_slice) {
Some([total, left]) => (*total, *left),
_ => bail!("GET METADATA (PIN/PUK): missing retry counters"),
};
Ok(CredentialMetadata {
is_default: is_default_flag(&tlv),
retries_total,
retries_left,
})
}
pub(crate) fn parse_slot_metadata(data: &[u8]) -> Result<SlotMetadata> {
let tlv = crate::auxiliaries::parse_tlv_flat(data);
let algorithm = match tlv.get(&TAG_ALGORITHM).map(Vec::as_slice) {
Some([id]) => *id,
_ => bail!("GET METADATA (key slot): missing algorithm"),
};
let origin = match tlv.get(&TAG_ORIGIN).map(Vec::as_slice) {
Some([0x01]) => Some(KeyOrigin::Generated),
Some([0x02]) => Some(KeyOrigin::Imported),
_ => None,
};
Ok(SlotMetadata { algorithm, origin })
}
pub(crate) fn parse_mgmt_metadata(data: &[u8]) -> Result<MgmtMetadata> {
let tlv = crate::auxiliaries::parse_tlv_flat(data);
let id = match tlv.get(&TAG_ALGORITHM).map(Vec::as_slice) {
Some([id]) => *id,
_ => bail!("GET METADATA (management key): missing algorithm"),
};
let algo = MgmtAlgo::from_id(id)
.ok_or_else(|| anyhow::anyhow!("unsupported management key algorithm 0x{id:02x}"))?;
let touch = match tlv.get(&TAG_POLICY).map(Vec::as_slice) {
Some([_, TOUCH_ALWAYS]) => TouchPolicy::Always,
Some([_, TOUCH_CACHED]) => TouchPolicy::Cached,
_ => TouchPolicy::Never,
};
Ok(MgmtMetadata {
algo,
touch,
is_default: is_default_flag(&tlv),
})
}
pub(crate) fn parse_retries_remaining(data: &[u8]) -> Option<u8> {
let tlv = crate::auxiliaries::parse_tlv_flat(data);
match tlv.get(&TAG_RETRIES).map(Vec::as_slice) {
Some([_total, remaining]) => Some(*remaining),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ids_round_trip() {
for algo in [
MgmtAlgo::Tdes,
MgmtAlgo::Aes128,
MgmtAlgo::Aes192,
MgmtAlgo::Aes256,
] {
assert_eq!(MgmtAlgo::from_id(algo.id()), Some(algo));
}
assert_eq!(MgmtAlgo::from_id(0x07), None);
}
#[test]
fn tdes_and_aes192_share_key_length() {
assert_eq!(MgmtAlgo::Tdes.key_len(), MgmtAlgo::Aes192.key_len());
assert_ne!(MgmtAlgo::Tdes.block_size(), MgmtAlgo::Aes192.block_size());
}
#[test]
fn from_name_accepts_fixture_spellings() {
assert_eq!(MgmtAlgo::from_name("TDES"), Some(MgmtAlgo::Tdes));
assert_eq!(MgmtAlgo::from_name("3des"), Some(MgmtAlgo::Tdes));
assert_eq!(MgmtAlgo::from_name("AES-192"), Some(MgmtAlgo::Aes192));
assert_eq!(MgmtAlgo::from_name("aes256"), Some(MgmtAlgo::Aes256));
assert_eq!(MgmtAlgo::from_name("RSA2048"), None);
}
#[test]
fn check_key_len_message() {
let err = MgmtAlgo::Aes192.check_key_len(&[0u8; 16]).unwrap_err();
assert_eq!(
err.to_string(),
"management key is 16 bytes but the card uses AES-192 (24 bytes)"
);
assert!(MgmtAlgo::Aes192.check_key_len(&[0u8; 24]).is_ok());
}
#[test]
fn parse_metadata_aes192_default() {
let data = [0x01, 0x01, 0x0A, 0x02, 0x02, 0x00, 0x01, 0x05, 0x01, 0x01];
let md = parse_mgmt_metadata(&data).unwrap();
assert_eq!(md.algo, MgmtAlgo::Aes192);
assert!(!md.touch_required());
assert!(md.is_default);
}
#[test]
fn parse_metadata_touch_policy() {
let always = [0x01, 0x01, 0x03, 0x02, 0x02, 0x00, 0x02];
assert_eq!(
parse_mgmt_metadata(&always).unwrap().touch,
TouchPolicy::Always
);
let cached = [0x01, 0x01, 0x03, 0x02, 0x02, 0x00, 0x03];
assert!(parse_mgmt_metadata(&cached).unwrap().touch_required());
}
#[test]
fn parse_metadata_rejects_unknown_algorithm() {
let data = [0x01, 0x01, 0x07];
let err = parse_mgmt_metadata(&data).unwrap_err();
assert!(err
.to_string()
.contains("unsupported management key algorithm 0x07"));
}
#[test]
fn parse_metadata_requires_algorithm() {
assert!(parse_mgmt_metadata(&[0x05, 0x01, 0x01]).is_err());
}
#[test]
fn parse_credential_and_slot_metadata() {
let md = parse_credential_metadata(&[0x05, 0x01, 0x01, 0x06, 0x02, 0x03, 0x02]).unwrap();
assert_eq!(
md,
CredentialMetadata {
is_default: true,
retries_total: 3,
retries_left: 2
}
);
assert!(parse_credential_metadata(&[0x05, 0x01, 0x01]).is_err());
let md = parse_slot_metadata(&[0x01, 0x01, 0x11, 0x03, 0x01, 0x01]).unwrap();
assert_eq!(md.algorithm, ALGO_ECCP256);
assert_eq!(md.origin, Some(KeyOrigin::Generated));
assert_eq!(key_algorithm_name(0x07), "RSA 2048");
}
#[test]
fn parse_retries() {
let data = [0x05, 0x01, 0x00, 0x06, 0x02, 0x03, 0x00];
assert_eq!(parse_retries_remaining(&data), Some(0));
assert_eq!(parse_retries_remaining(&[]), None);
}
}