diff --git a/Cargo.toml b/Cargo.toml index e88dab2..0dcb81a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,38 +1,22 @@ # 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", default-features = false, features = ["serde-extensions"] } trussed-core = { version = "0.1.0-rc.1", features = ["serde-extensions"] } -littlefs2-core = "0.1.0" - -[dev-dependencies] -quickcheck = { version = "1.0.3", default-features = false } -rand_core = { version = "0.6.4", default-features = false, features = ["getrandom"] } -serde_test = "1.0.176" -trussed = { version = "0.1.0", default-features = false, features = ["clients-1", "crypto-client", "filesystem-client", "hmac-sha256", "serde-extensions", "virt"] } -admin-app = { version = "0.1.0", features = ["migration-tests"] } -serde_cbor = { version = "0.11.2", features = ["std"] } -hex-literal = "0.4.1" [patch.crates-io] +trussed-auth = { path = "extension" } + trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "6bba8fde36d05c0227769eb63345744e87d84b2b" } -trussed-manage = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } -admin-app = { git = "https://github.com/Nitrokey/admin-app.git", branch = "ctaphid-app" } +admin-app = { git = "https://github.com/Nitrokey/admin-app.git", tag = "v0.1.0-nitrokey.19" } 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..ba33e5f --- /dev/null +++ b/backend/CHANGELOG.md @@ -0,0 +1,23 @@ + + +# 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 + +- Use serde(rename) to save space on on the size of stored credentials ([#38][]) +- Remove the `dat` intermediary directory in file storage ([#39][]) +- Use `trussed-core` and remove default features for `trussed` + +[#38]: https://github.com/trussed-dev/trussed-auth/pull/38 +[#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..2f92344 --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,35 @@ +# 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-core.workspace = true + +chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["reduced-round"] } +hkdf = "0.12.3" +hmac = "0.12.1" +littlefs2-core = "0.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 = { version = "0.1.0", default-features = false, features = ["serde-extensions"] } +trussed-auth = { version = "0.3.0" } + +[dev-dependencies] +admin-app = { version = "0.1.0", features = ["migration-tests"] } +hex-literal = "0.4.1" +quickcheck = { version = "1.0.3", default-features = false } +rand_core = { version = "0.6.4", default-features = false, features = ["getrandom"] } +serde_cbor = { version = "0.11.2", features = ["std"] } +serde_test = "1.0.176" +trussed = { version = "0.1.0", default-features = false, features = ["clients-1", "crypto-client", "filesystem-client", "hmac-sha256", "serde-extensions", "virt"] } 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 39b430f..b3886c8 100644 --- a/src/backend/data.rs +++ b/backend/src/data.rs @@ -18,7 +18,7 @@ use trussed::{ }; use super::Error; -use crate::{Pin, PinId, MAX_PIN_LENGTH}; +use trussed_auth::{Pin, PinId, MAX_PIN_LENGTH}; const APP_SALT_PATH: &Path = path!("application_salt"); 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 5887f2e..7b59adc 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; @@ -20,15 +40,11 @@ use trussed::{ types::{CoreContext, Location}, 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: &Path = path!("backend-auth"); /// max accepted length for the hardware initial key material pub const MAX_HW_KEY_LEN: usize = 64; @@ -116,7 +132,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 { @@ -412,7 +428,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 6839b53..0db3761 100644 --- a/src/migrate.rs +++ b/backend/src/migrate.rs @@ -35,7 +35,7 @@ fn migrate_single(fs: &dyn DynFilesystem, path: &Path) -> Result<(), Error> { /// /// ```rust ///# use littlefs2_core::{DynFilesystem, Error, path}; -///# use trussed_auth::migrate::migrate_remove_dat; +///# use trussed_auth_backend::migrate::migrate_remove_dat; ///# fn test(fs: &dyn DynFilesystem) -> Result<(), Error> { /// migrate_remove_dat(fs, &[path!("secrets"), path!("opcard")])?; ///# Ok(()) 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 b3a1d53..b788f59 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, ), } } @@ -136,7 +140,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 90% rename from CHANGELOG.md rename to extension/CHANGELOG.md index 5b43a04..7fd67d5 100644 --- a/CHANGELOG.md +++ b/extension/CHANGELOG.md @@ -11,14 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased][] -- Use serde(rename) to save space on on the size of stored credentials ([#38][]) -- Remove the `dat` intermediary directory in file storage ([#39][]) -- Use `trussed-core` and remove default features for `trussed` +[Unreleased]: https://github.com/trussed-dev/trussed-auth/compare/v0.3.0...HEAD -[#38]: https://github.com/trussed-dev/trussed-auth/pull/38 -[#39]: https://github.com/trussed-dev/trussed-auth/pull/39 +### Breaking Changes -[Unreleased]: https://github.com/trussed-dev/trussed-auth/compare/v0.3.0...HEAD +- Extract `AuthBackend` into `trussed-auth-backend` crate +- Use `trussed-core` and remove default features for `trussed` ## [0.3.0][] - 2024-03-22 diff --git a/extension/Cargo.toml b/extension/Cargo.toml new file mode 100644 index 0000000..7b0c30c --- /dev/null +++ b/extension/Cargo.toml @@ -0,0 +1,18 @@ +# 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-core.workspace = true + +[dev-dependencies] +heapless-bytes = "0.3" diff --git a/src/extension.rs b/extension/src/lib.rs similarity index 61% rename from src/extension.rs rename to extension/src/lib.rs index b9793cb..cda19f3 100644 --- a/src/extension.rs +++ b/extension/src/lib.rs @@ -1,18 +1,169 @@ // 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 heapless_bytes::Bytes; +//! use trussed_auth::{AuthClient, PinId}; +//! use trussed_core::syscall; +//! +//! #[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_core::{ + 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()); + + // path has only ASCII characters and is not too long + #[allow(clippy::unwrap_used)] + PathBuf::try_from(&path).ok().unwrap() + } + + /// 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 +348,21 @@ pub trait AuthClient: ExtensionClient { } impl> AuthClient for C {} + +#[cfg(test)] +mod tests { + use super::PinId; + use trussed_core::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(); + #[allow(clippy::unwrap_used)] + let expected = PathBuf::try_from(format!("pin.{i:02x}").as_str()).unwrap(); + 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 121d707..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,190 +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_core::{types::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 littlefs2_core::{path, Path, PathBuf}; -use serde::{Deserialize, Serialize}; -use trussed_core::{config::MAX_SHORT_DATA_LENGTH, types::Bytes}; - -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: &Path = path!("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()); - - // path has only ASCII characters and is not too long - #[allow(clippy::unwrap_used)] - PathBuf::try_from(&path).ok().unwrap() - } - - /// 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_core::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(); - #[allow(clippy::unwrap_used)] - let expected = PathBuf::try_from(format!("pin.{i:02x}").as_str()).unwrap(); - println!("id: {i}, actual: {actual}, expected: {expected}"); - assert_eq!(actual, expected); - } - } -}