lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

profile.rs (6818B)


      1 //! Public, transport-neutral identity profiles.
      2 //!
      3 //! [`Profile`] contains optional public metadata only. [`PublicIdentity`]
      4 //! couples a validated [`PublicKey`] to its derived [`IdentityId`] and rejects
      5 //! mismatched separately decoded parts. Empty profiles are omitted so one
      6 //! public identity has one canonical native representation.
      7 //!
      8 //! Profiles do not perform discovery, authorization, event verification,
      9 //! persistence, or secret handling.
     10 
     11 use crate::{Error, IdentityId, PublicKey, Username};
     12 
     13 /// Public identity metadata that is independent of any transport event.
     14 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     15 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
     16 #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
     17 pub struct Profile {
     18     #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
     19     username: Option<Username>,
     20 }
     21 
     22 impl Profile {
     23     /// Creates an empty public profile.
     24     #[must_use]
     25     pub const fn new() -> Self {
     26         Self { username: None }
     27     }
     28 
     29     /// Returns a profile with its canonical username set.
     30     #[must_use]
     31     pub fn with_username(mut self, username: Username) -> Self {
     32         self.username = Some(username);
     33         self
     34     }
     35 
     36     /// Borrows the canonical username, when present.
     37     #[must_use]
     38     pub fn username(&self) -> Option<&Username> {
     39         self.username.as_ref()
     40     }
     41 
     42     /// Reports whether the profile contains no public metadata.
     43     #[must_use]
     44     pub const fn is_empty(&self) -> bool {
     45         self.username.is_none()
     46     }
     47 }
     48 
     49 /// A public identity with an invariant-matched identifier and public key.
     50 ///
     51 /// Secret-bearing identity containers are intentionally absent:
     52 ///
     53 /// ```compile_fail
     54 /// use radroots_identity::RadrootsIdentity;
     55 /// ```
     56 ///
     57 /// Public identities expose no secret-key access:
     58 ///
     59 /// ```compile_fail
     60 /// use radroots_identity::{PublicIdentity, PublicKey};
     61 ///
     62 /// let key = PublicKey::from_hex(
     63 ///     "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
     64 /// ).unwrap();
     65 /// let identity = PublicIdentity::new(key);
     66 /// let _ = identity.secret_key_bytes();
     67 /// ```
     68 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
     69 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
     70 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
     71 pub struct PublicIdentity {
     72     id: IdentityId,
     73     public_key: PublicKey,
     74     #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
     75     profile: Option<Profile>,
     76 }
     77 
     78 impl PublicIdentity {
     79     /// Creates a public identity whose identifier is derived from its key.
     80     #[must_use]
     81     pub const fn new(public_key: PublicKey) -> Self {
     82         Self {
     83             id: IdentityId::from_public_key(public_key),
     84             public_key,
     85             profile: None,
     86         }
     87     }
     88 
     89     /// Validates an identity assembled from separately decoded parts.
     90     pub fn try_from_parts(
     91         id: IdentityId,
     92         public_key: PublicKey,
     93         profile: Option<Profile>,
     94     ) -> Result<Self, Error> {
     95         if id != IdentityId::from_public_key(public_key) {
     96             return Err(Error::IdentityIdMismatch);
     97         }
     98         Ok(Self {
     99             id,
    100             public_key,
    101             profile: profile.filter(|value| !value.is_empty()),
    102         })
    103     }
    104 
    105     /// Returns a public identity with non-empty profile metadata attached.
    106     #[must_use]
    107     pub fn with_profile(mut self, profile: Profile) -> Self {
    108         self.profile = (!profile.is_empty()).then_some(profile);
    109         self
    110     }
    111 
    112     /// Returns the canonical identity identifier.
    113     #[must_use]
    114     pub const fn id(&self) -> IdentityId {
    115         self.id
    116     }
    117 
    118     /// Returns the canonical public key.
    119     #[must_use]
    120     pub const fn public_key(&self) -> PublicKey {
    121         self.public_key
    122     }
    123 
    124     /// Borrows public profile metadata, when present.
    125     #[must_use]
    126     pub fn profile(&self) -> Option<&Profile> {
    127         self.profile.as_ref()
    128     }
    129 }
    130 
    131 #[cfg(feature = "serde")]
    132 impl<'de> serde::Deserialize<'de> for PublicIdentity {
    133     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    134     where
    135         D: serde::Deserializer<'de>,
    136     {
    137         #[derive(serde::Deserialize)]
    138         #[serde(deny_unknown_fields)]
    139         struct PublicIdentityRepr {
    140             id: IdentityId,
    141             public_key: PublicKey,
    142             #[serde(default)]
    143             profile: Option<Profile>,
    144         }
    145 
    146         let value = PublicIdentityRepr::deserialize(deserializer)?;
    147         Self::try_from_parts(value.id, value.public_key, value.profile)
    148             .map_err(serde::de::Error::custom)
    149     }
    150 }
    151 
    152 #[cfg(test)]
    153 mod tests {
    154     use super::*;
    155 
    156     const ALICE: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
    157     const BOB: &str = "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
    158 
    159     #[test]
    160     fn public_identity_derives_and_protects_its_identifier() {
    161         let public_key = PublicKey::from_hex(ALICE).unwrap();
    162         let identity = PublicIdentity::new(public_key);
    163 
    164         assert_eq!(identity.id(), IdentityId::from(public_key));
    165         assert_eq!(identity.public_key(), public_key);
    166         assert!(identity.profile().is_none());
    167 
    168         let mismatched_id = IdentityId::from_hex(BOB).unwrap();
    169         assert!(matches!(
    170             PublicIdentity::try_from_parts(mismatched_id, public_key, None),
    171             Err(Error::IdentityIdMismatch)
    172         ));
    173     }
    174 
    175     #[test]
    176     fn public_identity_discards_empty_profiles_and_retains_usernames() {
    177         let public_key = PublicKey::from_hex(ALICE).unwrap();
    178         let empty = PublicIdentity::new(public_key).with_profile(Profile::new());
    179         assert!(empty.profile().is_none());
    180 
    181         let username = Username::parse("Alice.Farm").unwrap();
    182         let identity = PublicIdentity::new(public_key)
    183             .with_profile(Profile::new().with_username(username.clone()));
    184         assert_eq!(
    185             identity.profile().and_then(Profile::username),
    186             Some(&username)
    187         );
    188     }
    189 
    190     #[cfg(feature = "serde")]
    191     #[test]
    192     fn public_identity_serde_revalidates_key_identity_and_profile() {
    193         let public_key = PublicKey::from_hex(ALICE).unwrap();
    194         let identity = PublicIdentity::new(public_key)
    195             .with_profile(Profile::new().with_username(Username::parse("Alice.Farm").unwrap()));
    196         let encoded = serde_json::to_string(&identity).unwrap();
    197         assert_eq!(
    198             serde_json::from_str::<PublicIdentity>(&encoded).unwrap(),
    199             identity
    200         );
    201 
    202         let mismatched = encoded.replace(ALICE, BOB).replacen(BOB, ALICE, 1);
    203         assert!(serde_json::from_str::<PublicIdentity>(&mismatched).is_err());
    204         assert!(!encoded.contains("metadata"));
    205         assert!(!encoded.contains("application_handler"));
    206     }
    207 }