use aes::{
Aes256,
cipher::{
Array,
BlockCipherDecrypt as _,
BlockCipherEncrypt as _,
KeyInit as _,
},
};
use ciborium::Value;
use hmac_sha256::{
HMAC,
Hash,
};
use p256::{
EncodedPoint,
PublicKey,
SecretKey,
ecdh::diffie_hellman,
elliptic_curve::{
generic_array::GenericArray,
rand_core::OsRng,
sec1::{
FromEncodedPoint as _,
ToEncodedPoint as _,
},
},
};
use zeroize::Zeroize as _;
use crate::{
cbor,
error::{
Error,
Result,
},
hid::Transport,
};
pub const CLIENT_PIN_COMMAND: u8 = 0x06;
pub const GET_PIN_RETRIES: u8 = 0x01;
pub const GET_KEY_AGREEMENT: u8 = 0x02;
pub const SET_PIN: u8 = 0x03;
pub const CHANGE_PIN: u8 = 0x04;
pub const GET_PIN_TOKEN: u8 = 0x05;
pub const PROTOCOL_V1: u64 = 1;
const AES_BLOCK: usize = 16;
fn aes256_cbc_encrypt(key: &[u8; 32], plaintext: &[u8]) -> Vec<u8> {
assert!(
!plaintext.is_empty() && plaintext.len().is_multiple_of(AES_BLOCK),
"AES-CBC plaintext must be a non-empty multiple of 16"
);
let cipher = Aes256::new(&Array::from(*key));
let mut out = Vec::<u8>::with_capacity(plaintext.len());
let mut prev = [0_u8; AES_BLOCK];
for chunk in plaintext.chunks_exact(AES_BLOCK) {
let mut block = [0_u8; AES_BLOCK];
for ((dst, lhs), rhs) in block.iter_mut().zip(chunk.iter()).zip(prev.iter()) {
*dst = *lhs ^ *rhs;
}
let mut arr = Array::from(block);
cipher.encrypt_block(&mut arr);
let bytes: [u8; AES_BLOCK] = arr.into();
out.extend_from_slice(&bytes);
prev = bytes;
}
out
}
fn aes256_cbc_decrypt(key: &[u8; 32], ciphertext: &[u8]) -> Vec<u8> {
assert!(
!ciphertext.is_empty() && ciphertext.len().is_multiple_of(AES_BLOCK),
"AES-CBC ciphertext must be a non-empty multiple of 16"
);
let cipher = Aes256::new(&Array::from(*key));
let mut out = Vec::<u8>::with_capacity(ciphertext.len());
let mut prev = [0_u8; AES_BLOCK];
for chunk in ciphertext.chunks_exact(AES_BLOCK) {
let mut in_block = [0_u8; AES_BLOCK];
in_block.copy_from_slice(chunk);
let mut arr = Array::from(in_block);
cipher.decrypt_block(&mut arr);
let decrypted: [u8; AES_BLOCK] = arr.into();
let mut plain = [0_u8; AES_BLOCK];
for ((dst, lhs), rhs) in plain.iter_mut().zip(decrypted.iter()).zip(prev.iter()) {
*dst = *lhs ^ *rhs;
}
out.extend_from_slice(&plain);
prev = in_block;
}
out
}
pub fn get_pin_retries(transport: &mut Transport) -> Result<u8> {
let request = Value::Map(vec![
(Value::Integer(1.into()), Value::Integer(PROTOCOL_V1.into())),
(
Value::Integer(2.into()),
Value::Integer(GET_PIN_RETRIES.into()),
),
]);
let mut payload = Vec::<u8>::with_capacity(1 + 16);
payload.push(CLIENT_PIN_COMMAND);
payload.extend(cbor::encode(&request)?);
let response = transport.transact(&payload, None)?;
let map = cbor::decode(&response)?;
let raw = cbor::get_int_field(&map, 3)
.and_then(Value::as_integer)
.map(i128::from)
.ok_or(Error::Pin("getPinRetries response missing pinRetries"))?;
u8::try_from(raw).map_err(|_| Error::Pin("getPinRetries value out of u8 range"))
}
pub struct PinSession {
shared_secret: [u8; 32],
platform_pubkey: PublicKey,
}
impl PinSession {
pub fn establish(transport: &mut Transport) -> Result<Self> {
let platform_secret = SecretKey::random(&mut OsRng);
let platform_pubkey = platform_secret.public_key();
let request = Value::Map(vec![
(Value::Integer(1.into()), Value::Integer(PROTOCOL_V1.into())),
(
Value::Integer(2.into()),
Value::Integer(GET_KEY_AGREEMENT.into()),
),
]);
let mut payload = vec![CLIENT_PIN_COMMAND];
payload.extend(cbor::encode(&request)?);
let response = transport.transact(&payload, None)?;
let map = cbor::decode(&response)?;
let cose = cbor::get_int_field(&map, 1).ok_or(Error::Pin(
"getKeyAgreement response missing key agreement field",
))?;
let authenticator_pk = parse_cose_p256_pubkey(cose)?;
let shared = diffie_hellman(
platform_secret.to_nonzero_scalar(),
authenticator_pk.as_affine(),
);
let shared_secret = Hash::hash(shared.raw_secret_bytes());
Ok(Self {
shared_secret,
platform_pubkey,
})
}
pub fn get_pin_token(&self, transport: &mut Transport, pin: &str) -> Result<PinToken> {
let mut digest = Hash::hash(pin.as_bytes());
let mut pin_hash = [0_u8; 16];
pin_hash.copy_from_slice(&digest[..16]);
digest.zeroize();
let mut pin_hash_enc = aes256_cbc_encrypt(&self.shared_secret, &pin_hash);
pin_hash.zeroize();
let mut payload = vec![CLIENT_PIN_COMMAND];
payload.extend(cbor::encode(&Value::Map(vec![
(Value::Integer(1.into()), Value::Integer(PROTOCOL_V1.into())),
(
Value::Integer(2.into()),
Value::Integer(GET_PIN_TOKEN.into()),
),
(
Value::Integer(3.into()),
encode_platform_cose_pubkey(&self.platform_pubkey)?,
),
(Value::Integer(6.into()), Value::Bytes(pin_hash_enc.clone())),
]))?);
pin_hash_enc.zeroize();
let response = transport.transact(&payload, None)?;
let map = cbor::decode(&response)?;
let encrypted = cbor::get_int_field(&map, 2)
.and_then(Value::as_bytes)
.ok_or(Error::Pin("getPinToken response missing token"))?;
if encrypted.is_empty() || !encrypted.len().is_multiple_of(16) || encrypted.len() > 48 {
return Err(Error::Pin(
"encrypted pinUvAuthToken length must be 16, 32, or 48 bytes",
));
}
let bytes = aes256_cbc_decrypt(&self.shared_secret, encrypted);
Ok(PinToken { bytes })
}
#[must_use]
pub const fn shared_secret(&self) -> &[u8; 32] {
&self.shared_secret
}
pub fn platform_cose_pubkey(&self) -> Result<Value> {
encode_platform_cose_pubkey(&self.platform_pubkey)
}
pub fn aes_cbc_encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
if plaintext.is_empty() || !plaintext.len().is_multiple_of(16) {
return Err(Error::Pin(
"AES-CBC plaintext must be a non-empty multiple of 16",
));
}
Ok(aes256_cbc_encrypt(&self.shared_secret, plaintext))
}
pub fn aes_cbc_decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(16) {
return Err(Error::Pin(
"AES-CBC ciphertext must be a non-empty multiple of 16",
));
}
Ok(aes256_cbc_decrypt(&self.shared_secret, ciphertext))
}
}
impl Drop for PinSession {
fn drop(&mut self) {
self.shared_secret.zeroize();
}
}
pub struct PinToken {
bytes: Vec<u8>,
}
impl PinToken {
#[must_use]
pub fn auth_param(&self, message: &[u8]) -> [u8; 16] {
let tag = HMAC::mac(message, self.bytes.as_slice());
let mut out = [0_u8; 16];
out.copy_from_slice(&tag[..16]);
out
}
}
impl Drop for PinToken {
fn drop(&mut self) {
self.bytes.zeroize();
}
}
const COSE_KTY_EC2: i128 = 2;
const COSE_ALG_ECDH_ES_HKDF_256: i128 = -25;
const COSE_CRV_P256: i128 = 1;
fn parse_cose_p256_pubkey(cose: &Value) -> Result<PublicKey> {
let kty = cbor::get_int_field(cose, 1)
.and_then(Value::as_integer)
.map(i128::from)
.ok_or(Error::Pin("COSE key missing kty"))?;
if kty != COSE_KTY_EC2 {
return Err(Error::Pin("COSE key kty is not EC2"));
}
let alg = cbor::get_int_field(cose, 3)
.and_then(Value::as_integer)
.map(i128::from)
.ok_or(Error::Pin("COSE key missing alg"))?;
if alg != COSE_ALG_ECDH_ES_HKDF_256 {
return Err(Error::Pin("COSE key alg is not ECDH-ES+HKDF-256"));
}
let crv = cbor::get_int_field(cose, -1)
.and_then(Value::as_integer)
.map(i128::from)
.ok_or(Error::Pin("COSE key missing crv"))?;
if crv != COSE_CRV_P256 {
return Err(Error::Pin("COSE key crv is not P-256"));
}
let x_field = cbor::get_int_field(cose, -2)
.and_then(Value::as_bytes)
.ok_or(Error::Pin("COSE key missing x coordinate"))?;
let y_field = cbor::get_int_field(cose, -3)
.and_then(Value::as_bytes)
.ok_or(Error::Pin("COSE key missing y coordinate"))?;
if x_field.len() != 32 || y_field.len() != 32 {
return Err(Error::Pin("COSE key coordinates wrong length"));
}
let encoded = EncodedPoint::from_affine_coordinates(
GenericArray::from_slice(x_field),
GenericArray::from_slice(y_field),
false,
);
Option::<PublicKey>::from(PublicKey::from_encoded_point(&encoded))
.ok_or(Error::Pin("authenticator pubkey is not on curve"))
}
fn encode_platform_cose_pubkey(pubkey: &PublicKey) -> Result<Value> {
let encoded = pubkey.to_encoded_point(false);
let x = encoded
.x()
.ok_or(Error::Pin("platform pubkey missing x"))?
.to_vec();
let y = encoded
.y()
.ok_or(Error::Pin("platform pubkey missing y"))?
.to_vec();
Ok(Value::Map(vec![
(Value::Integer(1.into()), Value::Integer(2.into())),
(Value::Integer(3.into()), Value::Integer((-25_i32).into())),
(Value::Integer((-1_i32).into()), Value::Integer(1.into())),
(Value::Integer((-2_i32).into()), Value::Bytes(x)),
(Value::Integer((-3_i32).into()), Value::Bytes(y)),
]))
}
#[must_use]
pub fn hmac_truncated(shared_secret: &[u8; 32], data: &[u8]) -> [u8; 16] {
let tag = HMAC::mac(data, shared_secret);
let mut out = [0_u8; 16];
out.copy_from_slice(&tag[..16]);
out
}