Skip to main content

heddle_api/v2/
passkey_label.rs

1//! Passkey display labels are account metadata, outside PasskeyAuthority.
2
3use unicode_general_category::{GeneralCategory, get_general_category};
4use unicode_normalization::UnicodeNormalization;
5
6pub const DEFAULT_PASSKEY_LABEL: &str = "Passkey";
7pub const MAX_PASSKEY_LABEL_BYTES: usize = 256;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
10pub enum PasskeyLabelError {
11    #[error("passkey label contains a forbidden Unicode character")]
12    InvalidCharacter,
13    #[error("passkey label exceeds 256 UTF-8 bytes after NFC")]
14    TooLong,
15}
16
17/// Validate and normalize a registration or rename label before storage. An empty
18/// result clears the stored label so observations use the server default.
19pub fn normalize_passkey_label(label: &str) -> Result<String, PasskeyLabelError> {
20    let normalized: String = label.trim().nfc().collect();
21    if normalized.chars().any(|character| {
22        matches!(
23            get_general_category(character),
24            GeneralCategory::Control
25                | GeneralCategory::Format
26                | GeneralCategory::Surrogate
27                | GeneralCategory::PrivateUse
28                | GeneralCategory::Unassigned
29                | GeneralCategory::LineSeparator
30                | GeneralCategory::ParagraphSeparator
31        )
32    }) {
33        return Err(PasskeyLabelError::InvalidCharacter);
34    }
35    if normalized.len() > MAX_PASSKEY_LABEL_BYTES {
36        return Err(PasskeyLabelError::TooLong);
37    }
38    Ok(normalized)
39}
40
41/// Display the stored label, falling back to the server default for older
42/// passkeys and for labels reset to empty by RenamePasskey.
43pub fn passkey_display_label(stored_label: &str) -> &str {
44    if stored_label.is_empty() {
45        DEFAULT_PASSKEY_LABEL
46    } else {
47        stored_label
48    }
49}