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 }