diff --git a/src/backend.rs b/src/backend.rs index 219ce9a..d57310e 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -284,6 +284,24 @@ impl ExtensionImpl for AuthBackend { .save(fs, self.location)?; Ok(reply::SetPin.into()) } + AuthRequest::SetPinWithKey(request) => { + let app_key = self.get_app_key(client_id, trussed_fs, ctx, rng)?; + let key_to_wrap = + keystore.load_key(Secrecy::Secret, Some(Kind::Symmetric(32)), &request.key)?; + let key_to_wrap = (&*key_to_wrap.material) + .try_into() + .map_err(|_| Error::ReadFailed)?; + PinData::reset_with_key( + request.id, + &request.pin, + request.retries, + rng, + &app_key, + key_to_wrap, + ) + .save(fs, self.location)?; + Ok(reply::SetPinWithKey.into()) + } AuthRequest::DeletePin(request) => { let path = request.id.path(); if fs.exists(&path, self.location) { diff --git a/src/backend/data.rs b/src/backend/data.rs index 661c0df..100c82d 100644 --- a/src/backend/data.rs +++ b/src/backend/data.rs @@ -171,6 +171,39 @@ impl PinData { } } + pub fn reset_with_key( + id: PinId, + pin: &Pin, + retries: Option, + rng: &mut R, + application_key: &Key, + mut key_to_wrap: Key, + ) -> Self + where + R: CryptoRng + RngCore, + { + use chacha20poly1305::{AeadInPlace, KeyInit}; + let mut salt = Salt::default(); + rng.fill_bytes(salt.as_mut()); + let pin_key = derive_key(id, pin, &salt, application_key); + let aead = ChaCha8Poly1305::new((&*pin_key).into()); + let nonce = Default::default(); + #[allow(clippy::expect_used)] + let tag: [u8; CHACHA_TAG_LEN] = aead + .encrypt_in_place_detached(&nonce, &[u8::from(id)], &mut *key_to_wrap) + .expect("Wrapping the key should always work, length are acceptable") + .into(); + Self { + id, + retries: retries.map(From::from), + salt, + data: KeyOrHash::Key(WrappedKeyData { + wrapped_key: key_to_wrap, + tag: tag.into(), + }), + } + } + pub fn load(fs: &mut S, location: Location, id: PinId) -> Result { let path = id.path(); if !fs.exists(&path, location) { diff --git a/src/extension.rs b/src/extension.rs index 4f4dbb6..eeaf298 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -7,7 +7,10 @@ pub mod reply; pub mod request; use serde::{Deserialize, Serialize}; -use trussed::serde_extensions::{Extension, ExtensionClient, ExtensionResult}; +use trussed::{ + serde_extensions::{Extension, ExtensionClient, ExtensionResult}, + types::KeyId, +}; use crate::{Pin, PinId}; @@ -32,6 +35,7 @@ pub enum AuthRequest { CheckPin(request::CheckPin), GetPinKey(request::GetPinKey), SetPin(request::SetPin), + SetPinWithKey(request::SetPinWithKey), ChangePin(request::ChangePin), DeletePin(request::DeletePin), DeleteAllPins(request::DeleteAllPins), @@ -45,6 +49,7 @@ pub enum AuthReply { CheckPin(reply::CheckPin), GetPinKey(reply::GetPinKey), SetPin(reply::SetPin), + SetPinWithKey(reply::SetPinWithKey), ChangePin(reply::ChangePin), DeletePin(reply::DeletePin), DeleteAllPins(reply::DeleteAllPins), @@ -114,6 +119,26 @@ pub trait AuthClient: ExtensionClient { }) } + /// Set a pin, resetting its retry counter and setting the key to be wrapped + /// + /// Similar to [`set_pin`](AuthClient::set_pin), but allows the key that the pin will unwrap to be configured. + /// Currently only symmetric 256 bit keys are accepted. This method should be used only with keys that were obtained through [`get_pin_key`](AuthClient::get_pin_key) + /// This allows for example backing up the key for a pin, to be able to restore it from another source. + fn set_pin_with_key>( + &mut self, + id: I, + pin: Pin, + retries: Option, + key: KeyId, + ) -> AuthResult<'_, reply::SetPinWithKey, Self> { + self.extension(request::SetPinWithKey { + id: id.into(), + pin, + retries, + key, + }) + } + /// Change the given PIN and resets its retry counter. /// /// The key obtained by [`get_pin_key`](AuthClient::get_pin_key) will stay the same diff --git a/src/extension/reply.rs b/src/extension/reply.rs index 0777ca1..ed2129f 100644 --- a/src/extension/reply.rs +++ b/src/extension/reply.rs @@ -99,6 +99,26 @@ impl TryFrom for SetPin { } } +#[derive(Debug, Deserialize, Serialize)] +pub struct SetPinWithKey; + +impl From for AuthReply { + fn from(reply: SetPinWithKey) -> Self { + Self::SetPinWithKey(reply) + } +} + +impl TryFrom for SetPinWithKey { + type Error = Error; + + fn try_from(reply: AuthReply) -> Result { + match reply { + AuthReply::SetPinWithKey(reply) => Ok(reply), + _ => Err(Error::InternalError), + } + } +} + #[derive(Debug, Deserialize, Serialize)] pub struct ChangePin { pub success: bool, diff --git a/src/extension/request.rs b/src/extension/request.rs index 04d8117..300c8b2 100644 --- a/src/extension/request.rs +++ b/src/extension/request.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 or MIT use serde::{Deserialize, Serialize}; +use trussed::types::KeyId; use super::AuthRequest; use crate::{Pin, PinId}; @@ -46,7 +47,7 @@ pub struct SetPin { pub id: PinId, pub pin: Pin, pub retries: Option, - /// If true, the PIN can be used to wrap/unwrap an application key + /// If true, the PIN can be used to wrap/unwrap a PIN key pub derive_key: bool, } @@ -56,6 +57,21 @@ impl From for AuthRequest { } } +#[derive(Debug, Deserialize, Serialize)] +pub struct SetPinWithKey { + pub id: PinId, + pub pin: Pin, + pub retries: Option, + /// This key will be wrapped. It can be obtained again via a `GetPinKey` request + pub key: KeyId, +} + +impl From for AuthRequest { + fn from(request: SetPinWithKey) -> Self { + Self::SetPinWithKey(request) + } +} + #[derive(Debug, Deserialize, Serialize)] pub struct ChangePin { pub id: PinId, diff --git a/tests/backend.rs b/tests/backend.rs index 340e5c2..c9c06d0 100644 --- a/tests/backend.rs +++ b/tests/backend.rs @@ -431,6 +431,58 @@ fn pin_key() { ) } +#[test] +fn reset_pin_key() { + run_with_hw_key( + BACKENDS, + Bytes::from_slice(b"Some HW ikm").unwrap(), + |client| { + let pin1 = Bytes::from_slice(b"12345678").unwrap(); + let pin2 = Bytes::from_slice(b"123456").unwrap(); + let pin3 = Bytes::from_slice(b"1234567890").unwrap(); + + syscall!(client.set_pin(Pin::User, pin1.clone(), Some(3), true)); + assert!(syscall!(client.get_pin_key(Pin::User, pin2.clone())) + .result + .is_none()); + assert_eq!(syscall!(client.pin_retries(Pin::User)).retries, Some(2)); + assert!(!syscall!(client.check_pin(Pin::User, pin2.clone())).success); + assert_eq!(syscall!(client.pin_retries(Pin::User)).retries, Some(1)); + assert!(syscall!(client.check_pin(Pin::User, pin1.clone())).success); + let key = syscall!(client.get_pin_key(Pin::User, pin1)) + .result + .unwrap(); + assert_eq!(syscall!(client.pin_retries(Pin::User)).retries, Some(3)); + let mac = syscall!(client.sign_hmacsha256(key, b"Some data")).signature; + + syscall!(client.set_pin_with_key(Pin::User, pin3.clone(), Some(3), key)); + + let key2 = syscall!(client.get_pin_key(Pin::User, pin3.clone())) + .result + .unwrap(); + let mac2 = syscall!(client.sign_hmacsha256(key2, b"Some data")).signature; + assert_eq!(mac, mac2); + + assert!(syscall!(client.change_pin(Pin::User, pin3.clone(), pin2.clone())).success); + + let key3 = syscall!(client.get_pin_key(Pin::User, pin2.clone())) + .result + .unwrap(); + let mac3 = syscall!(client.sign_hmacsha256(key3, b"Some data")).signature; + assert_eq!(mac, mac3); + + assert!(!syscall!(client.check_pin(Pin::User, pin3.clone())).success); + assert!(!syscall!(client.check_pin(Pin::User, pin3.clone())).success); + assert!(!syscall!(client.check_pin(Pin::User, pin3)).success); + assert!(!syscall!(client.check_pin(Pin::User, pin2.clone())).success); + assert!(syscall!(client.get_pin_key(Pin::User, pin2)) + .result + .is_none()); + assert_eq!(syscall!(client.pin_retries(Pin::User)).retries, Some(0)); + }, + ) +} + #[test] fn blocked_pin() { run(BACKENDS, |client| {