diff --git a/Cargo.toml b/Cargo.toml index 741764a..6dd28cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,17 +6,26 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -se05x = "0.0.1" +se05x = { version = "0.0.1", features = ["serde", "builder"] } trussed = { version = "0.1.0", features = ["serde-extensions"] } trussed-auth = "0.2.2" trussed-staging = { version = "0.1.0", features = ["wrap-key-to-file", "chunked", "encrypted-chunked"] } delog = "0.1.6" embedded-hal = "0.2.7" +hkdf = { version = "0.12.3", default-features = false } +sha2 = { version = "0.10.7", default-features = false } +hex-literal = "0.4.1" +serde-byte-array = "0.1.2" +iso7816 = "0.1.1" +hmac = "0.12.1" +serde = { version = "1.0.185", default-features = false, features = ["derive"] } +rand = { version = "0.8.5", default-features = false } +littlefs2 = "0.4.0" [patch.crates-io] -se05x = { git = "https://github.com/Nitrokey/se05x.git", rev = "db1ddea25cc382355b4292352652da656abc3005"} +se05x = { git = "https://github.com/Nitrokey/se05x.git", rev = "bf696ee99bc893dda385dbbf47e0cdab9c58aba6"} trussed = { git = "https://github.com/Nitrokey/trussed", tag = "v0.1.0-nitrokey.12" } -trussed-auth = { git = "https://github.com/Nitrokey/trussed-auth", tag = "v0.2.2-nitrokey.1" } +trussed-auth = { git = "https://github.com/Nitrokey/trussed-auth", rev = "98eb1e5c1283bb68e0a1ae2f6a7d3cdfc33c3697" } trussed-staging = { git = "https://github.com/Nitrokey/trussed-staging.git", branch = "hmacsha256p256" } iso7816 = { git = "https://github.com/sosthene-nitrokey/iso7816.git", rev = "160ca3bbd8e21ec4e4ee1e0748e1eaa53a45c97f"} diff --git a/src/lib.rs b/src/lib.rs index b5d7e8c..2696ce8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,46 +1,74 @@ #![no_std] use embedded_hal::blocking::delay::DelayUs; +use rand::{CryptoRng, Rng, RngCore}; use se05x::{ - se05x::{commands::GetRandom, Se05X}, + se05x::{commands::GetRandom, ObjectId, Se05X}, t1::I2CForT1, }; use trussed::{ api::{reply, request, Request}, backend::Backend, config::MAX_MESSAGE_LENGTH, - serde_extensions::ExtensionImpl, - types::Message, + types::{Location, Message}, + Bytes, }; #[macro_use] extern crate delog; generate_macros!(); +mod trussed_auth_impl; +use trussed_auth::MAX_HW_KEY_LEN; +use trussed_auth_impl::{AuthContext, HardwareKey}; + /// Need overhead for TLV + SW bytes const BUFFER_LEN: usize = 2048; +const BACKEND_DIR: &str = "se050-bak"; + +pub enum Se05xLocation { + Persistent, + Transient, +} + +impl From for Se05xLocation { + fn from(value: Location) -> Self { + match value { + Location::Volatile => Self::Transient, + Location::External | Location::Internal => Self::Persistent, + } + } +} pub struct Se050Backend { se: Se05X, enabled: bool, failed_enable: Option, + metadata_location: Location, + hw_key: HardwareKey, } impl> Se050Backend { - pub fn new(se: Se05X) -> Self { + pub fn new( + se: Se05X, + metadata_location: Location, + hardware_key: Option>, + ) -> Self { Se050Backend { se, enabled: false, failed_enable: None, + metadata_location, + hw_key: match hardware_key { + None => HardwareKey::None, + Some(k) => HardwareKey::Raw(k), + }, } } - fn random_bytes(&mut self, count: usize) -> Result { - if count >= MAX_MESSAGE_LENGTH { - return Err(trussed::Error::MechanismParamInvalid); - } - + fn enable(&mut self) -> Result<(), trussed::Error> { if !self.enabled { + debug!("Enabling"); if let Err(e) = self.se.enable() { self.failed_enable = Some(e); } else { @@ -53,6 +81,14 @@ impl> Se050Backend { return Err(trussed::Error::FunctionFailed); } + Ok(()) + } + + fn random_bytes(&mut self, count: usize) -> Result { + if count >= MAX_MESSAGE_LENGTH { + return Err(trussed::Error::MechanismParamInvalid); + } + let mut buf = [0; BUFFER_LEN]; let res = self .se @@ -77,19 +113,29 @@ impl> Se050Backend { } } +#[derive(Default, Debug)] +pub struct Context { + auth: AuthContext, +} + impl> Backend for Se050Backend { - type Context = (); + type Context = Context; fn request( &mut self, - core_ctx: &mut trussed::types::CoreContext, - backend_ctx: &mut Self::Context, + _core_ctx: &mut trussed::types::CoreContext, + _backend_ctx: &mut Self::Context, request: &Request, - resources: &mut trussed::service::ServiceResources

, + _resources: &mut trussed::service::ServiceResources

, ) -> Result { + self.enable()?; match request { Request::RandomBytes(request::RandomBytes { count }) => self.random_bytes(*count), _ => Err(trussed::Error::RequestNotAvailable), } } } + +fn generate_object_id(rng: &mut R) -> ObjectId { + ObjectId(rng.gen_range(0x00000002u32..0x7FFF0000).to_be_bytes()) +} diff --git a/src/trussed_auth_impl.rs b/src/trussed_auth_impl.rs new file mode 100644 index 0000000..5eb64e9 --- /dev/null +++ b/src/trussed_auth_impl.rs @@ -0,0 +1,401 @@ +use core::fmt; +use embedded_hal::blocking::delay::DelayUs; +use hex_literal::hex; +use hkdf::Hkdf; +use se05x::{ + se05x::{ + commands::{GetRandom, ReadObject, WriteBinary}, + ObjectId, + }, + t1::I2CForT1, +}; +use serde_byte_array::ByteArray; +use sha2::Sha256; +use trussed::{ + key::{Kind, Secrecy}, + platform::CryptoRng, + serde_extensions::ExtensionImpl, + service::{Filestore, Keystore, RngCore}, + types::{Location, PathBuf}, + Bytes, +}; +use trussed_auth::MAX_HW_KEY_LEN; + +mod data; + +use crate::{ + trussed_auth_impl::data::{ + delete_all_pins, delete_app_salt, expand_app_key, get_app_salt, PinData, + }, + Se050Backend, BACKEND_DIR, +}; + +pub const GLOBAL_SALT_ID: ObjectId = ObjectId(hex!("00000001")); +pub(crate) const SALT_LEN: usize = 16; +pub(crate) const HASH_LEN: usize = 32; +pub(crate) const KEY_LEN: usize = 32; +pub(crate) type Key = ByteArray; +pub(crate) type Salt = ByteArray; + +const AUTH_DIR: &str = "auth"; + +#[derive(Clone)] +pub enum HardwareKey { + None, + Raw(Bytes<{ MAX_HW_KEY_LEN }>), + Extracted(Hkdf), +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum Error { + NotFound, + ReadFailed, + WriteFailed, + DeserializationFailed, + SerializationFailed, + BadPinType, + Se050, +} + +impl From for trussed::error::Error { + fn from(error: Error) -> Self { + match error { + Error::NotFound => Self::NoSuchKey, + Error::ReadFailed => Self::FilesystemReadFailure, + Error::WriteFailed => Self::FilesystemWriteFailure, + Error::DeserializationFailed => Self::ImplementationError, + Error::SerializationFailed => Self::ImplementationError, + Error::BadPinType => Self::MechanismInvalid, + Error::Se050 => Self::FunctionFailed, + } + } +} + +impl From for Error { + fn from(_value: se05x::se05x::Error) -> Self { + Self::Se050 + } +} + +impl fmt::Debug for HardwareKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => f.debug_tuple("None").finish(), + Self::Raw(_) => f.debug_tuple("Raw").field(&"[redacted]").finish(), + Self::Extracted(_) => f.debug_tuple("Raw").field(&"[redacted]").finish(), + } + } +} + +/// Per-client context for [`AuthBackend`][] +#[derive(Default, Debug)] +pub struct AuthContext { + application_key: Option, +} + +impl> Se050Backend { + fn get_global_salt( + &self, + global_fs: &mut impl Filestore, + rng: &mut R, + ) -> Result { + let path = PathBuf::from("salt"); + global_fs + .read(&path, self.metadata_location) + .or_else(|_| { + if global_fs.exists(&path, self.metadata_location) { + return Err(Error::ReadFailed); + } + + let mut salt = Bytes::::default(); + salt.resize_to_capacity(); + rng.fill_bytes(&mut salt); + global_fs + .write(&path, self.metadata_location, &salt) + .or(Err(Error::WriteFailed)) + .and(Ok(salt)) + }) + .and_then(|b| (**b).try_into().or(Err(Error::ReadFailed))) + } + + fn get_se050_salt(&mut self) -> Result { + let buf = &mut [0; 32]; + debug_now!("Attempting to read"); + let tmp = self.se.run_command( + &ReadObject::builder() + .object_id(GLOBAL_SALT_ID) + .length((SALT_LEN as u16).into()) + .build(), + buf, + ); + match tmp { + Ok(res) => return res.data.try_into().map_err(|_| Error::ReadFailed), + Err(se05x::se05x::Error::Status(iso7816::Status::IncorrectDataParameter)) => {} + Err(_err) => { + debug!("Got unexpected error: {_err:?}"); + return Err(Error::Se050); + } + }; + + debug_now!("Generating salt"); + // Salt was not found, need to generate, store it and return it. + let salt: [u8; SALT_LEN] = self + .se + .run_command( + &GetRandom { + length: (SALT_LEN as u16).into(), + }, + buf, + )? + .data + .try_into() + .map_err(|_err| { + debug_now!("Random data failed: {_err:?}"); + Error::ReadFailed + })?; + + debug_now!("Writing salt"); + self.se + .run_command( + &WriteBinary::builder() + .object_id(GLOBAL_SALT_ID) + .offset(0.into()) + .file_length((SALT_LEN as u16).into()) + .data(&salt) + .build(), + buf, + ) + .map_err(|_err| { + debug_now!("Writing data failed: {_err:?}"); + Error::ReadFailed + })?; + + Ok(salt.into()) + } + + fn extract( + &mut self, + global_fs: &mut impl Filestore, + ikm: Option>, + rng: &mut R, + ) -> Result<&Hkdf, Error> { + debug_now!("Extracting key"); + let ikm: &[u8] = ikm.as_deref().map(|i| &**i).unwrap_or(&[]); + let salt = self.get_global_salt(global_fs, rng)?; + debug_now!("Getting se050 salt"); + let se050_salt = self.get_se050_salt()?; + + let mut real_ikm: Bytes<{ SALT_LEN + MAX_HW_KEY_LEN }> = + Bytes::from_slice(&*se050_salt).unwrap(); + real_ikm.extend_from_slice(ikm).unwrap(); + + let kdf = Hkdf::new(Some(&*salt), &real_ikm); + self.hw_key = HardwareKey::Extracted(kdf); + match &self.hw_key { + HardwareKey::Extracted(kdf) => Ok(kdf), + // hw_key was just set to Extracted + _ => unreachable!(), + } + } + + fn expand(kdf: &Hkdf, client_id: &PathBuf) -> Key { + let mut out = Key::default(); + #[allow(clippy::expect_used)] + kdf.expand(client_id.as_ref().as_bytes(), &mut *out) + .expect("Out data is always valid"); + out + } + + fn generate_app_key( + &mut self, + client_id: PathBuf, + global_fs: &mut impl Filestore, + rng: &mut R, + ) -> Result { + debug_now!("Generating app key"); + Ok(match &self.hw_key { + HardwareKey::Extracted(okm) => Self::expand(okm, &client_id), + HardwareKey::Raw(hw_k) => { + let kdf = self.extract(global_fs, Some(hw_k.clone()), rng)?; + Self::expand(kdf, &client_id) + } + HardwareKey::None => { + let kdf = self.extract(global_fs, None, rng)?; + Self::expand(kdf, &client_id) + } + }) + } + + fn get_app_key( + &mut self, + client_id: PathBuf, + global_fs: &mut impl Filestore, + ctx: &mut AuthContext, + rng: &mut R, + ) -> Result { + if let Some(app_key) = ctx.application_key { + return Ok(app_key); + } + + let app_key = self.generate_app_key(client_id, global_fs, rng)?; + ctx.application_key = Some(app_key); + Ok(app_key) + } +} + +impl> ExtensionImpl + for Se050Backend +{ + fn extension_request( + &mut self, + core_ctx: &mut trussed::types::CoreContext, + backend_ctx: &mut Self::Context, + request: &::Request, + resources: &mut trussed::service::ServiceResources

, + ) -> Result< + ::Reply, + trussed::Error, + > { + self.enable()?; + + debug_now!("Trussed Auth request: {request:?}"); + // FIXME: Have a real implementation from trussed + let mut backend_path = core_ctx.path.clone(); + backend_path.push(&PathBuf::from(BACKEND_DIR)); + backend_path.push(&PathBuf::from(AUTH_DIR)); + let fs = &mut resources.filestore(backend_path); + let global_fs = &mut resources.filestore(PathBuf::from(BACKEND_DIR)); + let rng = &mut resources.rng()?; + let client_id = core_ctx.path.clone(); + let keystore = &mut resources.keystore(core_ctx)?; + + use trussed_auth::{reply, request, AuthRequest}; + match request { + AuthRequest::HasPin(request) => { + let has_pin = fs.exists(&request.id.path(), self.metadata_location); + Ok(reply::HasPin { has_pin }.into()) + } + AuthRequest::CheckPin(request) => { + let pin_data = PinData::load(request.id, fs, self.metadata_location)?; + let app_key = self.get_app_key(client_id, global_fs, &mut backend_ctx.auth, rng)?; + let success = pin_data.check(&request.pin, &app_key, &mut self.se, rng)?; + Ok(reply::CheckPin { success }.into()) + } + AuthRequest::GetPinKey(request) => { + let pin_data = + PinData::load(request.id, fs, self.metadata_location).map_err(|_err| { + debug!("Failed to get pin data: {_err:?}"); + _err + })?; + let app_key = self.get_app_key(client_id, global_fs, &mut backend_ctx.auth, rng)?; + let key = pin_data.check_and_get_key(&request.pin, &app_key, &mut self.se, rng)?; + let Some(material) = key else { + return Ok(reply::GetPinKey { result: None }.into()); + }; + let key_id = keystore.store_key( + Location::Volatile, + Secrecy::Secret, + Kind::Symmetric(32), + &*material, + )?; + Ok(reply::GetPinKey { + result: Some(key_id), + } + .into()) + } + AuthRequest::GetApplicationKey(request) => { + let salt = get_app_salt(fs, rng, self.metadata_location)?; + let key = expand_app_key( + &salt, + &self.get_app_key(client_id, global_fs, &mut backend_ctx.auth, rng)?, + &request.info, + ); + let key_id = keystore.store_key( + Location::Volatile, + Secrecy::Secret, + Kind::Symmetric(KEY_LEN), + &*key, + )?; + Ok(reply::GetApplicationKey { key: key_id }.into()) + } + AuthRequest::SetPin(request) => { + if fs.exists(&request.id.path(), self.metadata_location) { + return Err(trussed::Error::FunctionFailed); + } + let pin = PinData::new(request.id, rng, request.derive_key); + let app_key = self.get_app_key(client_id, global_fs, &mut backend_ctx.auth, rng)?; + pin.create( + fs, + self.metadata_location, + &mut self.se, + &app_key, + &request.pin, + request.retries, + )?; + debug_now!("Created pin"); + Ok(reply::SetPin {}.into()) + } + AuthRequest::SetPinWithKey(request) => { + let app_key = self.get_app_key(client_id, global_fs, &mut backend_ctx.auth, rng)?; + let key = + keystore.load_key(Secrecy::Secret, Some(Kind::Symmetric(32)), &request.key)?; + let key: Key = (&*key.material) + .try_into() + .map_err(|_| Error::DeserializationFailed)?; + + PinData::create_with_key( + request.id, + fs, + self.metadata_location, + &mut self.se, + &app_key, + &request.pin, + request.retries, + rng, + &key, + )?; + Ok(reply::SetPinWithKey {}.into()) + } + AuthRequest::ChangePin(request) => { + let mut pin_data = PinData::load(request.id, fs, self.metadata_location)?; + let app_key = self.get_app_key(client_id, global_fs, &mut backend_ctx.auth, rng)?; + let success = pin_data.update( + &mut self.se, + &app_key, + request, + fs, + self.metadata_location, + rng, + )?; + Ok(reply::ChangePin { success }.into()) + } + AuthRequest::DeletePin(request) => { + let pin_data = PinData::load(request.id, fs, self.metadata_location)?; + pin_data.delete(fs, self.metadata_location, &mut self.se)?; + Ok(reply::DeletePin {}.into()) + } + AuthRequest::DeleteAllPins(request::DeleteAllPins) => { + delete_all_pins(fs, self.metadata_location, &mut self.se)?; + Ok(reply::DeleteAllPins.into()) + } + AuthRequest::PinRetries(request) => { + if !fs.exists(&request.id.path(), self.metadata_location) { + Err(Error::NotFound)?; + } + // TODO find a way to make this work + // + // It looks like reading with attestation can give access to this metadata + Ok(reply::PinRetries { retries: Some(3) }.into()) + } + AuthRequest::ResetAppKeys(_req) => { + delete_app_salt(fs, self.metadata_location)?; + Ok(reply::ResetAppKeys.into()) + } + AuthRequest::ResetAuthData(_req) => { + delete_app_salt(fs, self.metadata_location)?; + delete_all_pins(fs, self.metadata_location, &mut self.se)?; + Ok(reply::ResetAuthData.into()) + } + } + } +} diff --git a/src/trussed_auth_impl/data.rs b/src/trussed_auth_impl/data.rs new file mode 100644 index 0000000..e5a2db6 --- /dev/null +++ b/src/trussed_auth_impl/data.rs @@ -0,0 +1,590 @@ +use crate::{generate_object_id, trussed_auth_impl::KEY_LEN}; + +use super::{Error, Key, Salt, HASH_LEN, SALT_LEN}; + +use embedded_hal::blocking::delay::DelayUs; +use hex_literal::hex; +use hmac::{Hmac, Mac}; +use littlefs2::path; +use rand::Rng; +use se05x::{ + se05x::{ + commands::{ + CheckObjectExists, CloseSession, CreateSession, DeleteSecureObject, GetRandom, + ReadObject, VerifySessionUserId, WriteBinary, WriteSymmKey, WriteUserId, + }, + policies::{ObjectAccessRule, ObjectPolicyFlags, Policy, PolicySet}, + ObjectId, ProcessSessionCmd, Se05X, Se05XResult, SymmKeyType, + }, + t1::I2CForT1, +}; +use serde::{Deserialize, Serialize}; +use serde_byte_array::ByteArray; +use sha2::Sha256; +use trussed::{ + platform::CryptoRng, + service::{Filestore, RngCore}, + types::{Bytes, Location, Path, PathBuf}, +}; +use trussed_auth::{request, PinId, MAX_PIN_LENGTH}; + +fn app_salt_path() -> PathBuf { + const SALT_PATH: &str = "application_salt"; + + PathBuf::from(SALT_PATH) +} + +pub(crate) fn get_app_salt( + fs: &mut S, + rng: &mut R, + location: Location, +) -> Result { + if !fs.exists(&app_salt_path(), location) { + create_app_salt(fs, rng, location) + } else { + load_app_salt(fs, location) + } +} + +pub(crate) fn delete_app_salt( + fs: &mut S, + location: Location, +) -> Result<(), trussed::Error> { + if fs.exists(&app_salt_path(), location) { + fs.remove_file(&app_salt_path(), location) + } else { + Ok(()) + } +} + +fn create_app_salt( + fs: &mut S, + rng: &mut R, + location: Location, +) -> Result { + let mut salt = Salt::default(); + rng.fill_bytes(&mut *salt); + fs.write(&app_salt_path(), location, &*salt) + .map_err(|_| Error::WriteFailed)?; + Ok(salt) +} + +fn load_app_salt(fs: &mut S, location: Location) -> Result { + fs.read(&app_salt_path(), location) + .map_err(|_| Error::ReadFailed) + .and_then(|b: Bytes| (**b).try_into().map_err(|_| Error::ReadFailed)) +} + +pub fn expand_app_key(salt: &Salt, application_key: &Key, info: &[u8]) -> Key { + #[allow(clippy::expect_used)] + let mut hmac = Hmac::::new_from_slice(&**application_key) + .expect("Slice will always be of acceptable size"); + hmac.update(&**salt); + hmac.update(&(info.len() as u64).to_be_bytes()); + hmac.update(info); + let tmp: [_; HASH_LEN] = hmac.finalize().into_bytes().into(); + tmp.into() +} + +const PIN_KEY_LEN: usize = 16; +type PinKey = ByteArray; + +fn pin_len(pin: &[u8]) -> u8 { + const _: () = assert!(MAX_PIN_LENGTH <= u8::MAX as usize); + pin.len() as u8 +} + +pub fn expand_pin_key(salt: &Salt, application_key: &Key, id: PinId, pin: &[u8]) -> PinKey { + #[allow(clippy::expect_used)] + let mut hmac = Hmac::::new_from_slice(&**application_key) + .expect("Slice will always be of acceptable size"); + hmac.update(&[u8::from(id)]); + hmac.update(&[pin_len(pin)]); + hmac.update(pin); + hmac.update(&**salt); + let tmp: [_; HASH_LEN] = hmac.finalize().into_bytes().into(); + PinKey::new(tmp[..PIN_KEY_LEN].try_into().unwrap()) +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct PinData { + #[serde(skip)] + id: PinId, + salt: Salt, + /// Id of the AES key authentication object for the PIN + pin_aes_key_id: ObjectId, + /// Id of the binary object protected by the PIN. None if the PIN protects nothing + protected_key_id: Option, +} + +fn simple_pin_policy(pin_aes_key_id: ObjectId) -> [Policy; 2] { + [ + Policy { + object_id: pin_aes_key_id, + access_rule: ObjectAccessRule::from_flags(ObjectPolicyFlags::ALLOW_WRITE), + }, + Policy { + object_id: ObjectId::INVALID, + access_rule: ObjectAccessRule::from_flags(ObjectPolicyFlags::ALLOW_DELETE), + }, + ] +} + +fn pin_policy_with_key(pin_aes_key_id: ObjectId, protected_key_id: ObjectId) -> [Policy; 2] { + [ + Policy { + object_id: pin_aes_key_id, + access_rule: ObjectAccessRule::from_flags(ObjectPolicyFlags::ALLOW_WRITE), + }, + Policy { + object_id: protected_key_id, + access_rule: ObjectAccessRule::from_flags(ObjectPolicyFlags::ALLOW_DELETE), + }, + ] +} + +fn key_policy(pin_aes_key_id: ObjectId) -> [Policy; 2] { + [ + Policy { + object_id: pin_aes_key_id, + access_rule: ObjectAccessRule::from_flags(ObjectPolicyFlags::ALLOW_READ), + }, + Policy { + object_id: ObjectId::INVALID, + access_rule: ObjectAccessRule::from_flags(ObjectPolicyFlags::ALLOW_DELETE), + }, + ] +} + +impl PinData { + pub fn new(id: PinId, rng: &mut R, derived_key: bool) -> Self { + let salt = ByteArray::new(rng.gen()); + let pin_aes_key_id = generate_object_id(rng); + let protected_key_id = derived_key.then(|| generate_object_id(rng)); + Self { + id, + salt, + pin_aes_key_id, + protected_key_id, + } + } + + pub fn save(&self, fs: &mut impl Filestore, location: Location) -> Result<(), Error> { + let data = trussed::cbor_serialize_bytes::<_, 256>(&self) + .map_err(|_| Error::SerializationFailed)?; + fs.write(&self.id.path(), location, &data) + .map_err(|_| Error::WriteFailed)?; + Ok(()) + } + + // Write the necessary objects to the SE050 + pub fn create>( + &self, + fs: &mut impl Filestore, + location: Location, + se050: &mut Se05X, + app_key: &Key, + value: &[u8], + retries: Option, + ) -> Result<(), Error> { + self.save(fs, location)?; + + let buf = &mut [0; 128]; + let pin_aes_key_value = expand_pin_key(&self.salt, app_key, self.id, value); + + let pin_aes_key_policy; + // So that temporary arrays are scoped to the function to please the borrow checker + let (tmp1, tmp2); + if let Some(protected_key_id) = self.protected_key_id { + tmp1 = pin_policy_with_key(self.pin_aes_key_id, protected_key_id); + pin_aes_key_policy = &tmp1; + + let protected_key_policy = &key_policy(self.pin_aes_key_id); + let key = se050.run_command( + &GetRandom { + length: (KEY_LEN as u16).into(), + }, + buf, + )?; + let key: Key = ByteArray::new(key.data.try_into().map_err(|_| Error::Se050)?); + se050.run_command( + &WriteBinary::builder() + .object_id(protected_key_id) + .policy(PolicySet(protected_key_policy)) + .offset(0.into()) + .file_length((KEY_LEN as u16).into()) + .data(&*key) + .build(), + buf, + )?; + } else { + tmp2 = simple_pin_policy(self.pin_aes_key_id); + pin_aes_key_policy = &tmp2; + } + let write = WriteSymmKey::builder() + .is_auth(true) + .key_type(SymmKeyType::Aes) + .policy(PolicySet(pin_aes_key_policy)) + // max_attempts(retries.map(u16::from).map(Be::from)) + .object_id(self.pin_aes_key_id) + .value(&*pin_aes_key_value); + let write = match retries { + None => write.build(), + Some(v) => write.max_attempts((v as u16).into()).build(), + }; + se050.run_command(&write, buf)?; + Ok(()) + } + + // Write the necessary objects to the SE050 + #[allow(clippy::too_many_arguments)] + pub fn create_with_key, R: RngCore + CryptoRng>( + id: PinId, + fs: &mut impl Filestore, + location: Location, + se050: &mut Se05X, + app_key: &Key, + value: &[u8], + retries: Option, + rng: &mut R, + key: &Key, + ) -> Result { + let this = Self::new(id, rng, true); + this.save(fs, location)?; + + let buf = &mut [0; 128]; + let pin_aes_key_value = expand_pin_key(&this.salt, app_key, this.id, value); + + let protected_key_id = this.protected_key_id.unwrap(); + + let pin_aes_key_policy = &pin_policy_with_key(this.pin_aes_key_id, protected_key_id); + + let protected_key_policy = &key_policy(this.pin_aes_key_id); + se050.run_command( + &WriteBinary::builder() + .object_id(protected_key_id) + .policy(PolicySet(protected_key_policy)) + .offset(0.into()) + .file_length((KEY_LEN as u16).into()) + .data(&**key) + .build(), + buf, + )?; + + let write = WriteSymmKey::builder() + .is_auth(true) + .key_type(SymmKeyType::Aes) + .policy(PolicySet(pin_aes_key_policy)) + .object_id(this.pin_aes_key_id) + .value(&*pin_aes_key_value); + let write = match retries { + None => write.build(), + Some(v) => write.max_attempts((v as u16).into()).build(), + }; + se050.run_command(&write, buf)?; + Ok(this) + } + + pub fn check, R: RngCore + CryptoRng>( + &self, + value: &[u8], + app_key: &Key, + se050: &mut Se05X, + rng: &mut R, + ) -> Result { + debug_now!("Checking pin: {:?}", self.id); + let buf = &mut [0; 1024]; + let pin_aes_key_value = expand_pin_key(&self.salt, app_key, self.id, value); + let res = se050.run_command( + &CreateSession { + object_id: self.pin_aes_key_id, + }, + buf, + )?; + let session_id = res.session_id; + let res = match se050.authenticate_aes128_session(session_id, &pin_aes_key_value, rng) { + Ok(()) => Ok(true), + Err(_err) => { + debug_now!("Failed to authenticate pin: {_err:?}"); + Ok(false) + } + }; + se050.run_command( + &ProcessSessionCmd { + session_id, + apdu: CloseSession {}, + }, + buf, + )?; + res + } + + pub fn check_and_get_key, R: RngCore + CryptoRng>( + &self, + value: &[u8], + app_key: &Key, + se050: &mut Se05X, + rng: &mut R, + ) -> Result, Error> { + let Some(protected_key_id) = self.protected_key_id else { + return Err(Error::BadPinType); + }; + + let buf = &mut [0; 1024]; + let pin_aes_key_value = expand_pin_key(&self.salt, app_key, self.id, value); + let res = se050.run_command( + &CreateSession { + object_id: self.pin_aes_key_id, + }, + buf, + )?; + let session_id = res.session_id; + let res = match se050.authenticate_aes128_session(session_id, &pin_aes_key_value, rng) { + Ok(()) => { + let key = se050.run_command( + &ProcessSessionCmd { + session_id, + apdu: ReadObject::builder() + .object_id(protected_key_id) + .length((KEY_LEN as u16).into()) + .build(), + }, + buf, + )?; + Ok(Some( + key.data + .try_into() + .map_err(|_| Error::DeserializationFailed)?, + )) + } + Err(_) => Ok(None), + }; + se050.run_command( + &ProcessSessionCmd { + session_id, + apdu: CloseSession {}, + }, + buf, + )?; + res + } + + pub fn update, R: RngCore + CryptoRng>( + &mut self, + se050: &mut Se05X, + app_key: &Key, + request: &request::ChangePin, + fs: &mut impl Filestore, + location: Location, + rng: &mut R, + ) -> Result { + let buf = &mut [0; 1024]; + let pin_aes_key_value = expand_pin_key(&self.salt, app_key, self.id, &request.old_pin); + let res = se050.run_command( + &CreateSession { + object_id: self.pin_aes_key_id, + }, + buf, + )?; + let session_id = res.session_id; + let res = match se050.authenticate_aes128_session(session_id, &pin_aes_key_value, rng) { + Ok(()) => Ok(true), + Err(_) => Ok(false), + }; + + self.salt = ByteArray::new(rng.gen()); + let new_pin_aes_key_value = expand_pin_key(&self.salt, app_key, self.id, &request.new_pin); + se050.run_command( + &ProcessSessionCmd { + session_id, + apdu: WriteSymmKey::builder() + .is_auth(true) + .key_type(SymmKeyType::Aes) + .object_id(self.pin_aes_key_id) + .value(&*new_pin_aes_key_value) + .build(), + }, + buf, + )?; + self.save(fs, location)?; + se050.run_command( + &ProcessSessionCmd { + session_id, + apdu: CloseSession {}, + }, + buf, + )?; + res + } + + pub fn load(id: PinId, fs: &mut impl Filestore, location: Location) -> Result { + // let data = trussed::cbor_serialize_bytes::<_, 256>(&self) + // .map_err(|_| Error::SerializationFailed)?; + let data = fs + .read::<1024>(&id.path(), location) + .map_err(|_| Error::ReadFailed)?; + let this = trussed::cbor_deserialize(&data).map_err(|_| Error::DeserializationFailed)?; + Ok(Self { id, ..this }) + } + + pub fn delete>( + self, + fs: &mut impl Filestore, + location: Location, + se050: &mut Se05X, + ) -> Result<(), Error> { + let buf = &mut [0; 1024]; + debug!("Deleting {self:02x?}"); + if let Some(protected_key_id) = self.protected_key_id { + debug!("checking existence"); + let exists = se050 + .run_command( + &CheckObjectExists { + object_id: protected_key_id, + }, + buf, + ) + .map_err(|_err| { + debug!("Failed existence check: {_err:?}"); + _err + })? + .result; + if exists == Se05XResult::Success { + debug!("Deleting key"); + se050 + .run_command( + &DeleteSecureObject { + object_id: protected_key_id, + }, + buf, + ) + .map_err(|_err| { + debug!("Failed deletion: {_err:?}"); + _err + })?; + } + + debug!("Writing userid "); + se050.run_command( + &WriteUserId::builder() + .object_id(protected_key_id) + .data(&hex!("01020304")) + .build(), + buf, + )?; + debug!("Creating session"); + let session_id = se050 + .run_command( + &CreateSession { + object_id: protected_key_id, + }, + buf, + )? + .session_id; + debug!("Auth session"); + se050.run_command( + &ProcessSessionCmd { + session_id, + apdu: VerifySessionUserId { + user_id: &hex!("01020304"), + }, + }, + buf, + )?; + debug!("Deleting auth"); + se050 + .run_command( + &ProcessSessionCmd { + session_id, + apdu: DeleteSecureObject { + object_id: self.pin_aes_key_id, + }, + }, + buf, + ) + .map_err(|_err| { + debug!("Failed to delete auth: {_err:?}"); + _err + })?; + debug!("Closing sess"); + se050.run_command( + &ProcessSessionCmd { + session_id, + apdu: CloseSession {}, + }, + buf, + )?; + debug!("Deleting userid"); + se050.run_command( + &DeleteSecureObject { + object_id: protected_key_id, + }, + buf, + )?; + } else { + debug!("Deleting simple"); + se050.run_command( + &DeleteSecureObject { + object_id: self.pin_aes_key_id, + }, + buf, + )?; + } + + debug!("Removing file"); + fs.remove_file(&self.id.path(), location).map_err(|_err| { + debug!("Removing file failed: {_err:?}"); + Error::WriteFailed + })?; + Ok(()) + } +} + +fn delete_from_path>( + path: &Path, + fs: &mut impl Filestore, + location: Location, + se050: &mut Se05X, +) -> Result<(), Error> { + debug!("Deleting {path:?}"); + let path = path + .as_ref() + .strip_prefix(path.parent().as_deref().map(AsRef::as_ref).unwrap_or("")) + .unwrap_or(path.as_ref()); + let path = path.strip_prefix('/').unwrap_or(path.as_ref()); + debug!("Deleting stripped: {path:?}"); + let id = path.parse().map_err(|_err| { + debug!("Parsing name failed: {_err:?}"); + Error::DeserializationFailed + })?; + let pin = PinData::load(id, fs, location).map_err(|_err| { + debug!("Failed loading: {_err:?}"); + _err + })?; + pin.delete(fs, location, se050)?; + Ok(()) +} + +pub(crate) fn delete_all_pins>( + fs: &mut impl Filestore, + location: Location, + se050: &mut Se05X, +) -> Result<(), Error> { + debug!("Deleting all pins"); + let Some((first, mut state)) = fs + .read_dir_first(path!(""), location, None) + .map_err(|_| Error::ReadFailed)? + else { + return Ok(()); + }; + debug!("ReadFirst"); + delete_from_path(first.path(), fs, location, se050)?; + debug!("DeletedFirst"); + + while let Some((entry, new_state)) = fs.read_dir_next(state).map_err(|_| Error::ReadFailed)? { + debug!("DeletingNext"); + state = new_state; + delete_from_path(entry.path(), fs, location, se050)?; + } + Ok(()) +}