From 2e4f6607fb34263c60651d45eb7c695f4b140629 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 2 Apr 2024 21:50:05 +0200 Subject: [PATCH] Split backend and extension This patch splits the trussed-auth crate into two crates: trussed-auth only defines the AuthExtension and can be used by clients or other backends implementing the extension. trussed-auth-backend contains the AuthBackend that implements the extension using the filesystem. --- Cargo.toml | 29 +-- Makefile | 10 +- README.md | 8 +- backend/CHANGELOG.md | 20 +++ backend/Cargo.toml | 31 ++++ {src/backend => backend/src}/data.rs | 2 +- src/backend.rs => backend/src/lib.rs | 34 +++- {src => backend/src}/migrate.rs | 2 +- {tests => backend/tests}/backend.rs | 15 +- CHANGELOG.md => extension/CHANGELOG.md | 6 +- extension/Cargo.toml | 15 ++ src/extension.rs => extension/src/lib.rs | 169 ++++++++++++++++- {src/extension => extension/src}/reply.rs | 0 {src/extension => extension/src}/request.rs | 0 src/lib.rs | 189 -------------------- 15 files changed, 294 insertions(+), 236 deletions(-) create mode 100644 backend/CHANGELOG.md create mode 100644 backend/Cargo.toml rename {src/backend => backend/src}/data.rs (99%) rename src/backend.rs => backend/src/lib.rs (95%) rename {src => backend/src}/migrate.rs (99%) rename {tests => backend/tests}/backend.rs (98%) rename CHANGELOG.md => extension/CHANGELOG.md (95%) create mode 100644 extension/Cargo.toml rename src/extension.rs => extension/src/lib.rs (62%) rename {src/extension => extension/src}/reply.rs (100%) rename {src/extension => extension/src}/request.rs (100%) delete mode 100644 src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 2c1bc30..b76d223 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,34 +1,23 @@ # Copyright (C) Nitrokey GmbH # SPDX-License-Identifier: CC0-1.0 -[package] -name = "trussed-auth" -version = "0.3.0" +[workspace] +members = ["backend", "extension"] +resolver = "2" + +[workspace.package] authors = ["Nitrokey GmbH "] edition = "2021" -repository = "https://github.com/trussed-dev/trussed-auth" license = "Apache-2.0 OR MIT" -description = "Authentication extension and backend for Trussed" +repository = "https://github.com/trussed-dev/trussed-auth" -[dependencies] -chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["reduced-round"] } -hkdf = "0.12.3" -hmac = "0.12.1" -rand_core = "0.6.4" +[workspace.dependencies] serde = { version = "1", default-features = false } -serde-byte-array = "0.1.2" -sha2 = { version = "0.10.6", default-features = false } -subtle = { version = "2.4.1", default-features = false } trussed = { version = "0.1.0", features = ["serde-extensions"] } -littlefs2 = "0.4.0" - -[dev-dependencies] -quickcheck = { version = "1.0.3", default-features = false } -rand_core = { version = "0.6.4", default-features = false, features = ["getrandom"] } -trussed = { version = "0.1.0", features = ["serde-extensions", "virt"] } -admin-app = { version = "0.1.0", features = ["migration-tests"] } [patch.crates-io] +trussed-auth = { path = "extension" } + littlefs2 = { git = "https://github.com/sosthene-nitrokey/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" } trussed = { git = "https://github.com/Nitrokey/trussed.git", rev = "be04182e2c74e73599a394e814d353bc4bf79484" } trussed-manage = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "manage-v0.1.0" } diff --git a/Makefile b/Makefile index 4291166..357e442 100644 --- a/Makefile +++ b/Makefile @@ -3,18 +3,18 @@ .PHONY: check check: - RUSTLFAGS='-Dwarnings' cargo check --all-features --all-targets + RUSTLFAGS='-Dwarnings' cargo check --all-features --all-targets --workspace .PHONY: lint lint: - cargo clippy --all-features --all-targets -- --deny warnings - cargo fmt -- --check - RUSTDOCFLAGS='-Dwarnings' cargo doc --no-deps + cargo clippy --all-features --all-targets --workspace -- --deny warnings + cargo fmt --all -- --check + RUSTDOCFLAGS='-Dwarnings' cargo doc --no-deps --workspace reuse lint .PHONY: test test: - cargo test --all-features + cargo test --all-features --workspace .PHONY: ci ci: check lint test diff --git a/README.md b/README.md index 42cae2e..dd59c3d 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,14 @@ SPDX-License-Identifier: CC0-1.0 # trussed-auth -`trussed-auth` is an extension and custom backend for [Trussed][] that provides -basic PIN handling. +`trussed-auth` is an extension for [Trussed][] that provides basic PIN +handling. `trussed-auth-backend` is a Trussed backend implementing that +extension using the filesystem. Other implementations are provided by these +backends: +- [`trussed-se050-backend`][] [Trussed]: https://github.com/trussed-dev/trussed +[`trussed-se050-backend`]: https://github.com/Nitrokey/trussed-se050-backend ## License diff --git a/backend/CHANGELOG.md b/backend/CHANGELOG.md new file mode 100644 index 0000000..503001f --- /dev/null +++ b/backend/CHANGELOG.md @@ -0,0 +1,20 @@ + + +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +Extracted from `trussed-auth` v0.3.0. + +### Breaking Changes + +- Remove the `dat` intermediary directory in file storage ([#39][]) + +[#39]: https://github.com/trussed-dev/trussed-auth/pull/39 diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..63b3605 --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,31 @@ +# Copyright (C) Nitrokey GmbH +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "trussed-auth-backend" +version = "0.1.0" +description = "Authentication backend for Trussed" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +serde.workspace = true +trussed.workspace = true + +chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["reduced-round"] } +hkdf = "0.12.3" +hmac = "0.12.1" +rand_core = "0.6.4" +serde-byte-array = "0.1.2" +sha2 = { version = "0.10.6", default-features = false } +subtle = { version = "2.4.1", default-features = false } +trussed-auth = { version = "0.3.0" } +littlefs2 = "0.4.0" + +[dev-dependencies] +quickcheck = { version = "1.0.3", default-features = false } +rand_core = { version = "0.6.4", default-features = false, features = ["getrandom"] } +trussed = { version = "0.1.0", features = ["serde-extensions", "virt"] } +admin-app = { version = "0.1.0", features = ["migration-tests"] } diff --git a/src/backend/data.rs b/backend/src/data.rs similarity index 99% rename from src/backend/data.rs rename to backend/src/data.rs index 5e79feb..fc2800c 100644 --- a/src/backend/data.rs +++ b/backend/src/data.rs @@ -17,7 +17,7 @@ use trussed::{ }; use super::Error; -use crate::{Pin, PinId, MAX_PIN_LENGTH}; +use trussed_auth::{Pin, PinId, MAX_PIN_LENGTH}; pub(crate) const SIZE: usize = 256; pub(crate) const CHACHA_TAG_LEN: usize = 16; diff --git a/src/backend.rs b/backend/src/lib.rs similarity index 95% rename from src/backend.rs rename to backend/src/lib.rs index 0911e55..dc8b015 100644 --- a/src/backend.rs +++ b/backend/src/lib.rs @@ -1,8 +1,28 @@ // Copyright (C) Nitrokey GmbH // SPDX-License-Identifier: Apache-2.0 or MIT +#![no_std] +#![warn( + missing_debug_implementations, + missing_docs, + non_ascii_idents, + trivial_casts, + unused, + unused_qualifications, + clippy::expect_used, + clippy::unwrap_used +)] +#![deny(unsafe_code)] + +//! A Trussed backend implementing the [`AuthExtension`][]. +//! +//! [`AuthBackend`][] is an implementation of the [`AuthExtension`][] that stores PINs in the +//! filesystem. + mod data; +pub mod migrate; + use core::fmt; use hkdf::Hkdf; @@ -19,15 +39,11 @@ use trussed::{ types::{CoreContext, Location, PathBuf}, Bytes, }; +use trussed_auth::{reply, AuthExtension, AuthReply, AuthRequest}; -use crate::{ - backend::data::{expand_app_key, get_app_salt}, - extension::{reply, AuthExtension, AuthReply, AuthRequest}, - BACKEND_DIR, -}; -use data::{Key, PinData, Salt, KEY_LEN, SALT_LEN}; +use data::{delete_app_salt, expand_app_key, get_app_salt, Key, PinData, Salt, KEY_LEN, SALT_LEN}; -use self::data::delete_app_salt; +const BACKEND_DIR: &str = "backend-auth"; /// max accepted length for the hardware initial key material pub const MAX_HW_KEY_LEN: usize = 64; @@ -115,7 +131,7 @@ impl AuthBackend { /// Creates a new `AuthBackend` with a missing hw key /// /// Contrary to [`new`](Self::new) which uses a default `&[]` key, this will make operations depending on the hardware key to fail: - /// - [`set_pin`](crate::AuthClient::set_pin) with `derive_key = true` + /// - [`set_pin`](trussed_auth::AuthClient::set_pin) with `derive_key = true` /// - All operations on a pin that was created with `derive_key = true` pub fn with_missing_hw_key(location: Location, layout: FilesystemLayout) -> Self { Self { @@ -388,7 +404,7 @@ impl ExtensionImpl for AuthBackend { } #[derive(Clone, Copy, Debug)] -pub(crate) enum Error { +enum Error { NotFound, MissingHwKey, ReadFailed, diff --git a/src/migrate.rs b/backend/src/migrate.rs similarity index 99% rename from src/migrate.rs rename to backend/src/migrate.rs index a48c2a7..e078d00 100644 --- a/src/migrate.rs +++ b/backend/src/migrate.rs @@ -36,7 +36,7 @@ fn migrate_single(fs: &dyn DynFilesystem, path: &Path) -> Result<(), Error> { /// ```rust ///# use littlefs2::{fs::Filesystem, const_ram_storage, path}; ///# use trussed::types::{LfsResult, LfsStorage}; -///# use trussed_auth::migrate::migrate_remove_dat; +///# use trussed_auth_backend::migrate::migrate_remove_dat; ///# const_ram_storage!(Storage, 4096); ///# let mut storage = Storage::new(); ///# Filesystem::format(&mut storage); diff --git a/tests/backend.rs b/backend/tests/backend.rs similarity index 98% rename from tests/backend.rs rename to backend/tests/backend.rs index 7e0b108..9eb362b 100644 --- a/tests/backend.rs +++ b/backend/tests/backend.rs @@ -11,7 +11,8 @@ mod dispatch { service::ServiceResources, types::{Bytes, Context, Location}, }; - use trussed_auth::{AuthBackend, AuthContext, AuthExtension, MAX_HW_KEY_LEN}; + use trussed_auth::AuthExtension; + use trussed_auth_backend::{AuthBackend, AuthContext, MAX_HW_KEY_LEN}; pub const BACKENDS: &[BackendId] = &[BackendId::Custom(Backend::Auth), BackendId::Core]; @@ -55,7 +56,10 @@ mod dispatch { impl Dispatch { pub fn new() -> Self { Self { - auth: AuthBackend::new(Location::Internal, trussed_auth::FilesystemLayout::V0), + auth: AuthBackend::new( + Location::Internal, + trussed_auth_backend::FilesystemLayout::V0, + ), } } @@ -64,7 +68,7 @@ mod dispatch { auth: AuthBackend::with_hw_key( Location::Internal, hw_key, - trussed_auth::FilesystemLayout::V0, + trussed_auth_backend::FilesystemLayout::V0, ), } } @@ -72,7 +76,7 @@ mod dispatch { Self { auth: AuthBackend::with_missing_hw_key( Location::Internal, - trussed_auth::FilesystemLayout::V0, + trussed_auth_backend::FilesystemLayout::V0, ), } } @@ -135,7 +139,8 @@ use trussed::{ types::{Bytes, Location, Message, PathBuf}, virt::{self, Ram}, }; -use trussed_auth::{AuthClient as _, PinId, MAX_HW_KEY_LEN}; +use trussed_auth::{AuthClient as _, PinId}; +use trussed_auth_backend::MAX_HW_KEY_LEN; use dispatch::{Backend, Dispatch, BACKENDS}; diff --git a/CHANGELOG.md b/extension/CHANGELOG.md similarity index 95% rename from CHANGELOG.md rename to extension/CHANGELOG.md index c0e0041..853f159 100644 --- a/CHANGELOG.md +++ b/extension/CHANGELOG.md @@ -13,13 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [Unreleased]: https://github.com/trussed-dev/trussed-auth/compare/v0.3.0...HEAD +### Breaking Changes + +- Extract `AuthBackend` into `trussed-auth-backend` crate + ## [0.3.0][] - 2024-03-22 [0.3.0]: https://github.com/trussed-dev/trussed-auth/releases/tag/v0.3.0 ### Breaking Changes -- Remove the `dat` intermediary directory in file storage ([#39][]) - Add `delete_app_keys` and `delete_auth_keys` syscalls. ([#33][]) - `delete_all_pins` now doesn't affect application keys @@ -37,7 +40,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#35]: https://github.com/trussed-dev/trussed-auth/pull/35 [#36]: https://github.com/trussed-dev/trussed-auth/pull/36 [#37]: https://github.com/trussed-dev/trussed-auth/pull/37 -[#39]: https://github.com/trussed-dev/trussed-auth/pull/39 ## [0.2.2][] - 2023-04-26 diff --git a/extension/Cargo.toml b/extension/Cargo.toml new file mode 100644 index 0000000..7e4b0cf --- /dev/null +++ b/extension/Cargo.toml @@ -0,0 +1,15 @@ +# Copyright (C) Nitrokey GmbH +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "trussed-auth" +version = "0.3.0" +description = "Authentication extension for Trussed" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +serde.workspace = true +trussed.workspace = true diff --git a/src/extension.rs b/extension/src/lib.rs similarity index 62% rename from src/extension.rs rename to extension/src/lib.rs index 416a443..7dfa0b6 100644 --- a/src/extension.rs +++ b/extension/src/lib.rs @@ -1,18 +1,166 @@ // Copyright (C) Nitrokey GmbH // SPDX-License-Identifier: Apache-2.0 or MIT +#![cfg_attr(not(test), no_std)] +#![warn( + missing_debug_implementations, + missing_docs, + non_ascii_idents, + trivial_casts, + unused, + unused_qualifications, + clippy::expect_used, + clippy::unwrap_used +)] +#![deny(unsafe_code)] + +//! A Trussed API extension for authentication. +//! +//! This crate contains an API extension for [Trussed][], [`AuthExtension`][]. The extension +//! currently provides basic PIN handling with retry counters. Applications can access it using +//! the [`AuthClient`][] trait. +//! +//! # Examples +//! +//! ``` +//! use trussed::{Bytes, syscall}; +//! use trussed_auth::{AuthClient, PinId}; +//! +//! #[repr(u8)] +//! enum Pin { +//! User = 0, +//! } +//! +//! impl From for PinId { +//! fn from(pin: Pin) -> Self { +//! (pin as u8).into() +//! } +//! } +//! +//! fn authenticate_user(client: &mut C, pin: Option<&[u8]>) -> bool { +//! if !syscall!(client.has_pin(Pin::User)).has_pin { +//! // no PIN set +//! return true; +//! } +//! let Some(pin) = pin else { +//! // PIN is set but not provided +//! return false; +//! }; +//! let Ok(pin) = Bytes::from_slice(pin) else { +//! // provided PIN is too long +//! return false; +//! }; +//! // check PIN +//! syscall!(client.check_pin(Pin::User, pin)).success +//! } +//! ``` +//! +//! [Trussed]: https://docs.rs/trussed + #[allow(missing_docs)] pub mod reply; #[allow(missing_docs)] pub mod request; +use core::str::FromStr; + use serde::{Deserialize, Serialize}; use trussed::{ + config::MAX_SHORT_DATA_LENGTH, serde_extensions::{Extension, ExtensionClient, ExtensionResult}, - types::{KeyId, Message}, + types::{Bytes, KeyId, Message, PathBuf}, }; -use crate::{Pin, PinId}; +/// The maximum length of a PIN. +pub const MAX_PIN_LENGTH: usize = MAX_SHORT_DATA_LENGTH; + +/// A PIN. +pub type Pin = Bytes; + +/// The ID of a PIN within the namespace of a client. +/// +/// It is recommended that applications use an enum that implements `Into`. +/// +/// # Examples +/// +/// ``` +/// use trussed_auth::PinId; +/// +/// #[repr(u8)] +/// enum Pin { +/// User = 0, +/// Admin = 1, +/// ResetCode = 2, +/// } +/// +/// impl From for PinId { +/// fn from(pin: Pin) -> Self { +/// (pin as u8).into() +/// } +/// } +/// ``` +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct PinId(u8); + +/// Error obtained when trying to parse a [`PinId`][] either through [`PinId::from_path`][] or through the [`FromStr`][] implementation. +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct PinIdFromStrError; + +impl PinId { + /// Get the path to the PIN id. + /// + /// Path are of the form `pin.XX` where `xx` is the hexadecimal representation of the PIN number. + pub fn path(&self) -> PathBuf { + let mut path = [0; 6]; + path[0..4].copy_from_slice(b"pin."); + path[4..].copy_from_slice(&self.hex()); + + PathBuf::from(&path) + } + + /// Get the hex representation of the PIN id + pub fn hex(&self) -> [u8; 2] { + const CHARS: &[u8; 16] = b"0123456789abcdef"; + [ + CHARS[usize::from(self.0 >> 4)], + CHARS[usize::from(self.0 & 0xf)], + ] + } + + /// Parse a PinId path + pub fn from_path(path: &str) -> Result { + let path = path.strip_prefix("pin.").ok_or(PinIdFromStrError)?; + if path.len() != 2 { + return Err(PinIdFromStrError); + } + + let id = u8::from_str_radix(path, 16).map_err(|_| PinIdFromStrError)?; + Ok(PinId(id)) + } +} + +impl From for PinId { + fn from(id: u8) -> Self { + Self(id) + } +} + +impl From for u8 { + fn from(id: PinId) -> Self { + id.0 + } +} + +impl FromStr for PinId { + type Err = PinIdFromStrError; + fn from_str(s: &str) -> Result { + Self::from_path(s) + } +} /// A result returned by [`AuthClient`][]. pub type AuthResult<'a, R, C> = ExtensionResult<'a, AuthExtension, R, C>; @@ -197,3 +345,20 @@ pub trait AuthClient: ExtensionClient { } impl> AuthClient for C {} + +#[cfg(test)] +mod tests { + use super::PinId; + use trussed::types::PathBuf; + + #[test] + fn pin_id_path() { + for i in 0..=u8::MAX { + assert_eq!(Ok(PinId(i)), PinId::from_path(PinId(i).path().as_ref())); + let actual = PinId(i).path(); + let expected = PathBuf::from(format!("pin.{i:02x}").as_str()); + println!("id: {i}, actual: {actual}, expected: {expected}"); + assert_eq!(actual, expected); + } + } +} diff --git a/src/extension/reply.rs b/extension/src/reply.rs similarity index 100% rename from src/extension/reply.rs rename to extension/src/reply.rs diff --git a/src/extension/request.rs b/extension/src/request.rs similarity index 100% rename from src/extension/request.rs rename to extension/src/request.rs diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 2e1cf0f..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (C) Nitrokey GmbH -// SPDX-License-Identifier: Apache-2.0 or MIT - -#![cfg_attr(not(test), no_std)] -#![warn( - missing_debug_implementations, - missing_docs, - non_ascii_idents, - trivial_casts, - unused, - unused_qualifications, - clippy::expect_used, - clippy::unwrap_used -)] -#![deny(unsafe_code)] - -//! A Trussed API extension for authentication and a backend that implements it. -//! -//! This crate contains an API extension for [Trussed][], [`AuthExtension`][]. The extension -//! currently provides basic PIN handling with retry counters. Applications can access it using -//! the [`AuthClient`][] trait. -//! -//! This crate also contains [`AuthBackend`][], an implementation of the auth extension that stores -//! PINs in the filesystem. -//! -//! # Examples -//! -//! ``` -//! use trussed::{Bytes, syscall}; -//! use trussed_auth::{AuthClient, PinId}; -//! -//! #[repr(u8)] -//! enum Pin { -//! User = 0, -//! } -//! -//! impl From for PinId { -//! fn from(pin: Pin) -> Self { -//! (pin as u8).into() -//! } -//! } -//! -//! fn authenticate_user(client: &mut C, pin: Option<&[u8]>) -> bool { -//! if !syscall!(client.has_pin(Pin::User)).has_pin { -//! // no PIN set -//! return true; -//! } -//! let Some(pin) = pin else { -//! // PIN is set but not provided -//! return false; -//! }; -//! let Ok(pin) = Bytes::from_slice(pin) else { -//! // provided PIN is too long -//! return false; -//! }; -//! // check PIN -//! syscall!(client.check_pin(Pin::User, pin)).success -//! } -//! ``` -//! -//! [Trussed]: https://docs.rs/trussed - -mod backend; -mod extension; - -pub mod migrate; - -use core::str::FromStr; - -use serde::{Deserialize, Serialize}; -use trussed::{ - config::MAX_SHORT_DATA_LENGTH, - types::{Bytes, PathBuf}, -}; - -pub use backend::{AuthBackend, AuthContext, FilesystemLayout, MAX_HW_KEY_LEN}; -pub use extension::{ - reply, request, AuthClient, AuthExtension, AuthReply, AuthRequest, AuthResult, -}; - -/// The maximum length of a PIN. -pub const MAX_PIN_LENGTH: usize = MAX_SHORT_DATA_LENGTH; - -/// A PIN. -pub type Pin = Bytes; - -const BACKEND_DIR: &str = "backend-auth"; - -/// The ID of a PIN within the namespace of a client. -/// -/// It is recommended that applications use an enum that implements `Into`. -/// -/// # Examples -/// -/// ``` -/// use trussed_auth::PinId; -/// -/// #[repr(u8)] -/// enum Pin { -/// User = 0, -/// Admin = 1, -/// ResetCode = 2, -/// } -/// -/// impl From for PinId { -/// fn from(pin: Pin) -> Self { -/// (pin as u8).into() -/// } -/// } -/// ``` -#[derive( - Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, -)] -pub struct PinId(u8); - -/// Error obtained when trying to parse a [`PinId`][] either through [`PinId::from_path`][] or through the [`FromStr`][] implementation. -#[derive( - Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, -)] -pub struct PinIdFromStrError; - -impl PinId { - /// Get the path to the PIN id. - /// - /// Path are of the form `pin.XX` where `xx` is the hexadecimal representation of the PIN number. - pub fn path(&self) -> PathBuf { - let mut path = [0; 6]; - path[0..4].copy_from_slice(b"pin."); - path[4..].copy_from_slice(&self.hex()); - - PathBuf::from(&path) - } - - /// Get the hex representation of the PIN id - pub fn hex(&self) -> [u8; 2] { - const CHARS: &[u8; 16] = b"0123456789abcdef"; - [ - CHARS[usize::from(self.0 >> 4)], - CHARS[usize::from(self.0 & 0xf)], - ] - } - - /// Parse a PinId path - pub fn from_path(path: &str) -> Result { - let path = path.strip_prefix("pin.").ok_or(PinIdFromStrError)?; - if path.len() != 2 { - return Err(PinIdFromStrError); - } - - let id = u8::from_str_radix(path, 16).map_err(|_| PinIdFromStrError)?; - Ok(PinId(id)) - } -} - -impl From for PinId { - fn from(id: u8) -> Self { - Self(id) - } -} - -impl From for u8 { - fn from(id: PinId) -> Self { - id.0 - } -} - -impl FromStr for PinId { - type Err = PinIdFromStrError; - fn from_str(s: &str) -> Result { - Self::from_path(s) - } -} - -#[cfg(test)] -mod tests { - use super::PinId; - use trussed::types::PathBuf; - - #[test] - fn pin_id_path() { - for i in 0..=u8::MAX { - assert_eq!(Ok(PinId(i)), PinId::from_path(PinId(i).path().as_ref())); - let actual = PinId(i).path(); - let expected = PathBuf::from(format!("pin.{i:02x}").as_str()); - println!("id: {i}, actual: {actual}, expected: {expected}"); - assert_eq!(actual, expected); - } - } -}