commit 9122de4849b2743665778898c615462791d53af4
parent af6d14ec68a4387bc09a25d138350b8323d4abb5
Author: Zack Newman <zack@philomathiclife.com>
Date: Tue, 11 Feb 2025 20:32:21 -0700
add array-based UserHandle
Diffstat:
5 files changed, 147 insertions(+), 11 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -9,18 +9,18 @@ license = "MIT OR Apache-2.0"
name = "webauthn_rp"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/webauthn_rp/"
-rust-version = "1.83.0"
-version = "0.2.3"
+rust-version = "1.84.0"
+version = "0.2.4"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
-data-encoding = { version = "2.7.0", default-features = false }
+data-encoding = { version = "2.8.0", default-features = false }
ed25519-dalek = { version = "2.1.1", default-features = false, features = ["fast"] }
p256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] }
-p384 = { version = "0.13.0", default-features = false, features = ["ecdsa"] }
+p384 = { version = "0.13.1", default-features = false, features = ["ecdsa"] }
precis-profiles = { version = "0.1.11", default-features = false }
rand = { version = "0.9.0", default-features = false, features = ["thread_rng"] }
rsa = { version = "0.9.7", default-features = false, features = ["sha2"] }
@@ -29,10 +29,10 @@ serde_json = { version = "1.0.138", default-features = false, features = ["alloc
url = { version = "2.5.4", default-features = false }
[dev-dependencies]
-data-encoding = { version = "2.6.0", default-features = false, features = ["alloc"] }
+data-encoding = { version = "2.8.0", default-features = false, features = ["alloc"] }
ed25519-dalek = { version = "2.1.1", default-features = false, features = ["alloc", "pkcs8"] }
p256 = { version = "0.13.2", default-features = false, features = ["pem"] }
-p384 = { version = "0.13.0", default-features = false, features = ["pkcs8"] }
+p384 = { version = "0.13.1", default-features = false, features = ["pkcs8"] }
serde_json = { version = "1.0.138", default-features = false, features = ["preserve_order"] }
diff --git a/src/lib.rs b/src/lib.rs
@@ -1163,12 +1163,13 @@ const fn base64url_nopad_len(n: usize) -> Option<usize> {
// This won't overflow since this maxes at `isize::MAX` since
// `n` <= ⌊3*isize::MAX/4⌋; thus `quot` <= ⌊isize::MAX/4⌋.
// `n` can be partitioned into 4 possibilities:
- // (1) n ≡ 0 (mod 4) = 4quot + 0 <= ⌊3*isize::MAX/4⌋
- // (2) n ≡ 1 (mod 4) = 4quot + 1 <= ⌊3*isize::MAX/4⌋
- // (3) n ≡ 2 (mod 4) = 4quot + 2 <= ⌊3*isize::MAX/4⌋
- // (4) n ≡ 3 (mod 4) = 4quot + 3 <= ⌊3*isize::MAX/4⌋
+ // (1) n ≡ 0 (mod 4) = 4quot + 0
+ // (2) n ≡ 1 (mod 4) = 4quot + 1
+ // (3) n ≡ 2 (mod 4) = 4quot + 2
+ // (4) n ≡ 3 (mod 4) = 4quot + 3
// For (1), rem is 0; thus 4quot + 0 = `n` which is fine.
- // (2) is not possible per the proof in `webauthn_rp::base64url_nopad_decode_len`.
+ // For (2), rem is 1; thus 4quot + 2 = n - 1 + 2 = n + 1 <= ⌊3*isize::MAX/4⌋ + 1 <= isize::MAX for
+ // isize::MAX > 0. Clearly `isize::MAX > 0`; otherwise we couldn't allocate anything.
// For (3), rem is 2; thus 4quot + 3 = n - 2 + 3 = n + 1 <= ⌊3*isize::MAX/4⌋ + 1 <= isize::MAX for
// isize::MAX > 0. Clearly `isize::MAX > 0`; otherwise we couldn't allocate anything.
// For (4), rem is 3; thus 4quot + 4 = n - 3 + 4 = n + 1 <= ⌊3*isize::MAX/4⌋ + 1 <= isize::MAX for
diff --git a/src/request/register.rs b/src/request/register.rs
@@ -748,6 +748,58 @@ impl UserHandle<Vec<u8>> {
Self::rand(64).unwrap_or_else(|_e| unreachable!("there is a bug in UserHandle::rand"))
}
}
+/// Implements [`Default`] for [`UserHandle`] of array of length of the passed `usize` literal.
+///
+/// Only [`USER_HANDLE_MIN_LEN`]–[`USER_HANDLE_MAX_LEN`] inclusively are allowed to be passed.
+macro_rules! user {
+ ( $( $x:literal),* ) => {
+ $(
+impl Default for UserHandle<[u8; $x]> {
+ #[inline]
+ fn default() -> Self {
+ let mut data = [0; $x];
+ rand::fill(data.as_mut_slice());
+ Self(data)
+ }
+}
+ )*
+ };
+}
+// MUST only pass [`USER_HANDLE_MIN_LEN`]–[`USER_HANDLE_MAX_LEN`] inclusively.
+user!(
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
+ 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+ 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64
+);
+impl<const LEN: usize> UserHandle<[u8; LEN]>
+where
+ Self: Default,
+{
+ /// Returns a new `UserHandle` based on `LEN` randomly-generated [`u8`]s.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{UserHandle, USER_HANDLE_MIN_LEN, USER_HANDLE_MAX_LEN};
+ /// assert_eq!(
+ /// UserHandle::<[u8; USER_HANDLE_MIN_LEN]>::new_rand()
+ /// .as_ref()
+ /// .len(),
+ /// 1
+ /// );
+ /// // The probability of an all-zero `UserHandle` being generated (assuming a good entropy
+ /// // source) is 2^-512 ≈ 7.5 x 10^-155.
+ /// assert_ne!(
+ /// UserHandle::<[u8; USER_HANDLE_MAX_LEN]>::new_rand().as_ref(),
+ /// [0; USER_HANDLE_MAX_LEN]
+ /// );
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn new_rand() -> Self {
+ Self::default()
+ }
+}
impl Default for UserHandle<Vec<u8>> {
#[inline]
fn default() -> Self {
@@ -766,6 +818,24 @@ impl<T: Borrow<[u8]>> Borrow<[u8]> for UserHandle<T> {
self.0.borrow()
}
}
+impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<[u8; LEN]>> for UserHandle<&'b [u8; LEN]> {
+ #[inline]
+ fn from(value: &'a UserHandle<[u8; LEN]>) -> Self {
+ Self(&value.0)
+ }
+}
+impl<'a: 'b, 'b, const LEN: usize> From<UserHandle<&'a [u8; LEN]>> for UserHandle<&'b [u8]> {
+ #[inline]
+ fn from(value: UserHandle<&'a [u8; LEN]>) -> Self {
+ Self(value.0.as_slice())
+ }
+}
+impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<[u8; LEN]>> for UserHandle<&'b [u8]> {
+ #[inline]
+ fn from(value: &'a UserHandle<[u8; LEN]>) -> Self {
+ Self(value.0.as_slice())
+ }
+}
impl<'a: 'b, 'b> From<&'a UserHandle<Vec<u8>>> for UserHandle<&'b Vec<u8>> {
#[inline]
fn from(value: &'a UserHandle<Vec<u8>>) -> Self {
diff --git a/src/request/register/custom.rs b/src/request/register/custom.rs
@@ -1,4 +1,10 @@
use super::{UserHandle, UserHandleErr};
+impl<const LEN: usize> From<[u8; LEN]> for UserHandle<[u8; LEN]> {
+ #[inline]
+ fn from(value: [u8; LEN]) -> Self {
+ Self(value)
+ }
+}
impl<'a: 'b, 'b> TryFrom<&'a [u8]> for UserHandle<&'b [u8]> {
type Error = UserHandleErr;
#[inline]
diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs
@@ -853,6 +853,65 @@ impl<'de> Deserialize<'de> for UserHandle<Vec<u8>> {
deserializer.deserialize_str(UserHandleVisitor)
}
}
+impl<'de, const LEN: usize> Deserialize<'de> for UserHandle<[u8; LEN]>
+where
+ Self: Default,
+{
+ /// Deserializes [`prim@str`] based on
+ /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-userhandle).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{UserHandle, USER_HANDLE_MIN_LEN};
+ /// # #[cfg(feature = "custom")]
+ /// assert_eq!(
+ /// serde_json::from_str::<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>(r#""AA""#)?,
+ /// UserHandle::from([0; USER_HANDLE_MIN_LEN])
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ ///```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `UserHandle`.
+ struct UserHandleVisitor<const L: usize>;
+ impl<const L: usize> Visitor<'_> for UserHandleVisitor::<L>
+ where
+ UserHandle<[u8; L]>: Default,
+ {
+ type Value = UserHandle<[u8; L]>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("UserHandle")
+ }
+ #[expect(clippy::panic_in_result_fn, clippy::unreachable, reason = "we want to crash when there is a bug")]
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ // Any value between `USER_HANDLE_MIN_LEN` and `USER_HANDLE_MAX_LEN` can be base64url encoded
+ // without fear since that range is just 1 to 64, and 4/3 of 64 is less than `usize::MAX`.
+ if crate::base64url_nopad_len(L).unwrap_or_else(|| unreachable!("there is a bug in webauthn_rp::base64url_nopad_len")) == v.len() {
+ let mut data = [0; L];
+ BASE64URL_NOPAD
+ .decode_mut(v.as_bytes(), data.as_mut_slice())
+ .map_err(|e| E::custom(e.error))
+ .map(|len| {
+ assert_eq!(len, L, "there is a bug in data_encoding::BASE64URL_NOPAD::decode_mut");
+ UserHandle(data)
+ })
+ } else {
+ Err(E::invalid_value(
+ Unexpected::Str(v), &format!("{L} bytes encoded in base64url without padding").as_str()
+ ))
+ }
+ }
+ }
+ deserializer.deserialize_str(UserHandleVisitor)
+ }
+}
impl<'de: 'name + 'display_name, 'name, 'display_name> Deserialize<'de>
for PublicKeyCredentialUserEntity<'name, 'display_name, Vec<u8>>
{