identity.rs (16851B)
1 //! Public identity metadata and lifecycle values. 2 3 use crate::time::UnixTimestamp; 4 use crate::{Npub, PublicKey, SafeError, SafeErrorCode, SafeMessage}; 5 6 const MAX_IDENTITY_LABEL_UTF8_BYTES: usize = 80; 7 8 #[derive(Clone, Debug, Eq, PartialEq)] 9 pub struct NostrIdentityReference { 10 public_key: PublicKey, 11 npub: Npub, 12 } 13 14 impl NostrIdentityReference { 15 /// Constructs one canonical Nostr identity reference and derives its npub. 16 /// 17 /// # Errors 18 /// 19 /// Returns a safe public-key error if canonical NIP-19 encoding fails. 20 pub fn derive(public_key: PublicKey) -> Result<Self, SafeError> { 21 Ok(Self { 22 public_key, 23 npub: Npub::derive(public_key)?, 24 }) 25 } 26 27 /// Reconstitutes persisted identity only when its public forms agree. 28 /// 29 /// # Errors 30 /// 31 /// Returns a safe public-key error for a mismatched or malformed npub. 32 pub fn verify(public_key: PublicKey, npub: String) -> Result<Self, SafeError> { 33 Ok(Self { 34 public_key, 35 npub: Npub::verify(public_key, npub)?, 36 }) 37 } 38 39 #[must_use] 40 pub const fn public_key(&self) -> PublicKey { 41 self.public_key 42 } 43 44 #[must_use] 45 pub const fn npub(&self) -> &Npub { 46 &self.npub 47 } 48 } 49 50 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 51 pub struct LocalKeyringBinding { 52 identity: PublicKey, 53 availability: SignerAvailability, 54 } 55 56 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 57 #[non_exhaustive] 58 pub enum SignerBinding { 59 LocalKeyring(LocalKeyringBinding), 60 } 61 62 impl SignerBinding { 63 #[must_use] 64 pub const fn identity(self) -> PublicKey { 65 match self { 66 Self::LocalKeyring(binding) => binding.identity(), 67 } 68 } 69 70 #[must_use] 71 pub const fn availability(self) -> SignerAvailability { 72 match self { 73 Self::LocalKeyring(binding) => binding.availability(), 74 } 75 } 76 77 #[must_use] 78 pub const fn as_local_keyring(self) -> Option<LocalKeyringBinding> { 79 match self { 80 Self::LocalKeyring(binding) => Some(binding), 81 } 82 } 83 } 84 85 impl From<LocalKeyringBinding> for SignerBinding { 86 fn from(binding: LocalKeyringBinding) -> Self { 87 Self::LocalKeyring(binding) 88 } 89 } 90 91 impl LocalKeyringBinding { 92 #[must_use] 93 pub const fn new(identity: PublicKey, availability: SignerAvailability) -> Self { 94 Self { 95 identity, 96 availability, 97 } 98 } 99 100 #[must_use] 101 pub const fn identity(self) -> PublicKey { 102 self.identity 103 } 104 105 #[must_use] 106 pub const fn availability(self) -> SignerAvailability { 107 self.availability 108 } 109 110 #[must_use] 111 pub const fn repair_action(self) -> Option<SignerRepairAction> { 112 match self.availability { 113 SignerAvailability::Available => None, 114 SignerAvailability::CredentialMissing => Some(SignerRepairAction::ImportCredential), 115 SignerAvailability::StoreUnavailable => Some(SignerRepairAction::RetryCredentialStore), 116 } 117 } 118 119 /// Records a missing credential after a successful store lookup. 120 /// 121 /// # Errors 122 /// 123 /// Returns a safe state error unless the binding was previously available. 124 pub fn mark_credential_missing(&mut self) -> Result<(), SafeError> { 125 self.transition( 126 SignerAvailability::Available, 127 SignerAvailability::CredentialMissing, 128 ) 129 } 130 131 pub fn mark_store_unavailable(&mut self) { 132 self.availability = SignerAvailability::StoreUnavailable; 133 } 134 135 /// Completes an explicit credential repair. 136 /// 137 /// # Errors 138 /// 139 /// Returns a safe state error unless a credential was missing. 140 pub fn repair_credential(&mut self) -> Result<(), SafeError> { 141 self.transition( 142 SignerAvailability::CredentialMissing, 143 SignerAvailability::Available, 144 ) 145 } 146 147 /// Resolves a recovered store lookup to its observed credential state. 148 /// 149 /// # Errors 150 /// 151 /// Returns a safe state error unless the credential store was unavailable. 152 pub fn resolve_store_recovery(&mut self, credential_present: bool) -> Result<(), SafeError> { 153 if self.availability != SignerAvailability::StoreUnavailable { 154 return Err(invalid_identity_metadata()); 155 } 156 self.availability = if credential_present { 157 SignerAvailability::Available 158 } else { 159 SignerAvailability::CredentialMissing 160 }; 161 Ok(()) 162 } 163 164 fn transition( 165 &mut self, 166 expected: SignerAvailability, 167 next: SignerAvailability, 168 ) -> Result<(), SafeError> { 169 if self.availability != expected { 170 return Err(invalid_identity_metadata()); 171 } 172 self.availability = next; 173 Ok(()) 174 } 175 } 176 177 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 178 pub enum SignerAvailability { 179 Available, 180 CredentialMissing, 181 StoreUnavailable, 182 } 183 184 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 185 pub enum SignerRepairAction { 186 ImportCredential, 187 RetryCredentialStore, 188 } 189 190 #[derive(Clone, Debug, Eq, PartialEq)] 191 pub struct IdentityLabel(String); 192 193 impl IdentityLabel { 194 /// Trims and validates an optional human-assigned identity label value. 195 /// 196 /// # Errors 197 /// 198 /// Returns a safe metadata error when the resulting label is empty, too 199 /// long, or contains a control character. 200 pub fn parse(value: &str) -> Result<Self, SafeError> { 201 let normalized = value.trim(); 202 if normalized.is_empty() 203 || normalized.len() > MAX_IDENTITY_LABEL_UTF8_BYTES 204 || normalized.chars().any(char::is_control) 205 { 206 return Err(invalid_identity_metadata()); 207 } 208 Ok(Self(normalized.to_owned())) 209 } 210 211 #[must_use] 212 pub fn as_str(&self) -> &str { 213 &self.0 214 } 215 } 216 217 #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] 218 pub struct IdentityCreatedAt(UnixTimestamp); 219 220 impl IdentityCreatedAt { 221 #[must_use] 222 pub const fn new(timestamp: UnixTimestamp) -> Self { 223 Self(timestamp) 224 } 225 226 #[must_use] 227 pub const fn timestamp(self) -> UnixTimestamp { 228 self.0 229 } 230 } 231 232 #[derive(Clone, Debug, Eq, PartialEq)] 233 pub struct NostrIdentity { 234 identity: NostrIdentityReference, 235 signer_binding: SignerBinding, 236 label: Option<IdentityLabel>, 237 created_at: IdentityCreatedAt, 238 last_used_at: Option<UnixTimestamp>, 239 } 240 241 impl NostrIdentity { 242 /// Creates an identity summary whose identity and signer binding refer to the same identity. 243 /// 244 /// # Errors 245 /// 246 /// Returns an invalid-identity-metadata error when the signer binding belongs to a different 247 /// public key. 248 pub fn new( 249 identity: NostrIdentityReference, 250 signer_binding: impl Into<SignerBinding>, 251 label: Option<IdentityLabel>, 252 created_at: IdentityCreatedAt, 253 last_used_at: Option<UnixTimestamp>, 254 ) -> Result<Self, SafeError> { 255 let signer_binding = signer_binding.into(); 256 if identity.public_key() != signer_binding.identity() { 257 return Err(invalid_identity_metadata()); 258 } 259 Ok(Self { 260 identity, 261 signer_binding, 262 label, 263 created_at, 264 last_used_at, 265 }) 266 } 267 268 #[must_use] 269 pub const fn public_key(&self) -> PublicKey { 270 self.identity.public_key() 271 } 272 273 #[must_use] 274 pub fn npub(&self) -> &Npub { 275 self.identity.npub() 276 } 277 278 #[must_use] 279 pub const fn signer_binding(&self) -> SignerBinding { 280 self.signer_binding 281 } 282 283 #[must_use] 284 pub fn label(&self) -> Option<&IdentityLabel> { 285 self.label.as_ref() 286 } 287 288 #[must_use] 289 pub const fn created_at(&self) -> IdentityCreatedAt { 290 self.created_at 291 } 292 293 #[must_use] 294 pub const fn last_used_at(&self) -> Option<UnixTimestamp> { 295 self.last_used_at 296 } 297 298 #[must_use] 299 pub fn with_local_keyring_availability( 300 &self, 301 availability: SignerAvailability, 302 ) -> Option<Self> { 303 self.signer_binding.as_local_keyring()?; 304 Some(Self { 305 identity: self.identity.clone(), 306 signer_binding: SignerBinding::LocalKeyring(LocalKeyringBinding::new( 307 self.public_key(), 308 availability, 309 )), 310 label: self.label.clone(), 311 created_at: self.created_at, 312 last_used_at: self.last_used_at, 313 }) 314 } 315 316 #[must_use] 317 pub fn with_last_used_at(&self, last_used_at: UnixTimestamp) -> Self { 318 Self { 319 identity: self.identity.clone(), 320 signer_binding: self.signer_binding, 321 label: self.label.clone(), 322 created_at: self.created_at, 323 last_used_at: Some(last_used_at), 324 } 325 } 326 327 #[must_use] 328 pub fn display_label(&self) -> String { 329 self.label 330 .as_ref() 331 .map_or_else(|| self.npub().short(), |label| label.as_str().to_owned()) 332 } 333 } 334 335 const fn invalid_identity_metadata() -> SafeError { 336 SafeError::new( 337 SafeErrorCode::InvalidIdentityMetadata, 338 SafeMessage::new("The identity metadata is invalid."), 339 ) 340 } 341 342 #[cfg(test)] 343 mod tests { 344 use crate::PublicKey; 345 use crate::time::UnixTimestamp; 346 347 use super::{ 348 IdentityCreatedAt, IdentityLabel, LocalKeyringBinding, NostrIdentity, 349 NostrIdentityReference, SignerAvailability, SignerBinding, SignerRepairAction, 350 }; 351 352 const DERIVED_NPUB: &str = "npub1qurswpc8qurswpc8qurswpc8qurswpc8qurswpc8qurswpc8qursnvjvl7"; 353 const MISMATCHED_NPUB: &str = "npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg"; 354 355 fn public_key() -> PublicKey { 356 PublicKey::from_bytes([7_u8; 32]).expect("valid public key") 357 } 358 359 fn identity(label: Option<IdentityLabel>) -> NostrIdentity { 360 let public_key = public_key(); 361 NostrIdentity::new( 362 NostrIdentityReference::derive(public_key).expect("identity"), 363 LocalKeyringBinding::new(public_key, SignerAvailability::Available), 364 label, 365 IdentityCreatedAt::new(UnixTimestamp::from_seconds(10).expect("valid time")), 366 None, 367 ) 368 .expect("identity") 369 } 370 371 fn label_at_utf8_limit(unit: &str) -> String { 372 unit.repeat(80 / unit.len()) + &"x".repeat(80 % unit.len()) 373 } 374 375 #[test] 376 fn identity_label_accepts_exact_utf8_boundaries_after_trimming() { 377 for unit in ["x", "é", "🥕", "e\u{301}"] { 378 let value = label_at_utf8_limit(unit); 379 assert_eq!(value.len(), 80); 380 let padded = format!(" \u{2003}{value}\u{2003} "); 381 let label = IdentityLabel::parse(&padded).expect("80-byte normalized label"); 382 assert_eq!(label.as_str(), value); 383 assert_eq!(identity(Some(label)).display_label(), value); 384 } 385 } 386 387 #[test] 388 fn identity_label_rejects_one_byte_over_utf8_limit() { 389 for unit in ["x", "é", "🥕", "e\u{301}"] { 390 let value = label_at_utf8_limit(unit) + "x"; 391 assert_eq!(value.len(), 81); 392 let error = IdentityLabel::parse(&format!(" {value} ")) 393 .expect_err("81-byte normalized label must fail before storage"); 394 assert_eq!(error.code(), crate::SafeErrorCode::InvalidIdentityMetadata); 395 } 396 } 397 398 #[test] 399 fn identity_label_preserves_blank_and_embedded_control_policy() { 400 for value in [ 401 "", 402 " \u{2003}\t\r\n ", 403 "a\nb", 404 "a\rb", 405 "a\tb", 406 "a\0b", 407 "a\u{1b}b", 408 "a\u{7f}b", 409 "a\u{85}b", 410 ] { 411 assert_eq!( 412 IdentityLabel::parse(value) 413 .expect_err("invalid label") 414 .code(), 415 crate::SafeErrorCode::InvalidIdentityMetadata 416 ); 417 } 418 } 419 420 #[test] 421 fn identity_label_is_trimmed_bounded_and_control_free() { 422 let label = IdentityLabel::parse(" Farm identity ").expect("valid label"); 423 assert_eq!(label.as_str(), "Farm identity"); 424 425 for invalid in ["", " ", "line\nbreak", &"x".repeat(81)] { 426 assert!(IdentityLabel::parse(invalid).is_err()); 427 } 428 } 429 430 #[test] 431 fn identity_display_prefers_label_then_shortened_npub() { 432 let labelled = identity(Some(IdentityLabel::parse("Farm").expect("valid label"))); 433 let unlabelled = identity(None); 434 435 assert_eq!(labelled.display_label(), "Farm"); 436 assert_eq!(unlabelled.display_label(), "npub1qurswpc…rsnvjvl7"); 437 } 438 439 #[test] 440 fn local_identity_summary_contains_public_metadata_only() { 441 let identity = identity(None); 442 let debug = format!("{identity:?}"); 443 444 assert_eq!( 445 identity.signer_binding().availability(), 446 SignerAvailability::Available 447 ); 448 assert!(identity.label().is_none()); 449 assert!(identity.last_used_at().is_none()); 450 assert_eq!(identity.created_at().timestamp().as_seconds(), 10); 451 assert_eq!(identity.public_key(), public_key()); 452 assert_eq!(identity.npub().as_str(), DERIVED_NPUB); 453 assert!(!debug.contains("nsec1")); 454 assert!(!debug.contains(&"11".repeat(32))); 455 } 456 457 #[test] 458 fn nostr_identity_reference_derives_npub_and_rejects_mismatched_persisted_forms() { 459 let public_key = public_key(); 460 let identity = NostrIdentityReference::derive(public_key).expect("identity"); 461 assert_eq!(identity.public_key(), public_key); 462 assert_eq!(identity.npub().as_str(), DERIVED_NPUB); 463 assert_eq!( 464 NostrIdentityReference::verify(public_key, DERIVED_NPUB.to_owned()).expect("verified"), 465 identity 466 ); 467 assert!(NostrIdentityReference::verify(public_key, MISMATCHED_NPUB.to_owned()).is_err()); 468 assert!( 469 NostrIdentityReference::verify( 470 PublicKey::from_hex( 471 "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7", 472 ) 473 .expect("second public key"), 474 MISMATCHED_NPUB.to_owned() 475 ) 476 .is_err() 477 ); 478 } 479 480 #[test] 481 fn local_keyring_binding_carries_only_canonical_identity_reference() { 482 let public_key = public_key(); 483 let identity = NostrIdentityReference::derive(public_key).expect("identity"); 484 let binding = LocalKeyringBinding::new(public_key, SignerAvailability::Available); 485 486 assert_eq!(binding.identity(), identity.public_key()); 487 assert!(!format!("{binding:?}").contains("nsec1")); 488 } 489 490 #[test] 491 fn local_keyring_capability_is_explicit_and_preserves_the_binding() { 492 let binding = LocalKeyringBinding::new(public_key(), SignerAvailability::Available); 493 let signer_binding = SignerBinding::LocalKeyring(binding); 494 495 assert_eq!(signer_binding.as_local_keyring(), Some(binding)); 496 } 497 498 #[test] 499 fn signer_binding_rejects_an_identity_mismatch() { 500 let identity = NostrIdentityReference::derive(public_key()).expect("identity"); 501 let other = PublicKey::from_bytes([8_u8; 32]).expect("other public key"); 502 let binding = SignerBinding::LocalKeyring(LocalKeyringBinding::new( 503 other, 504 SignerAvailability::Available, 505 )); 506 507 let error = NostrIdentity::new( 508 identity, 509 binding, 510 None, 511 IdentityCreatedAt::new(UnixTimestamp::from_seconds(10).expect("valid time")), 512 None, 513 ) 514 .expect_err("mismatched identity and binding"); 515 516 assert_eq!(error.code(), crate::SafeErrorCode::InvalidIdentityMetadata); 517 } 518 519 #[test] 520 fn local_keyring_binding_repair_transitions_are_typed_and_fail_closed() { 521 let public_key = public_key(); 522 let mut binding = LocalKeyringBinding::new(public_key, SignerAvailability::Available); 523 assert_eq!(binding.repair_action(), None); 524 assert!(binding.repair_credential().is_err()); 525 526 binding 527 .mark_credential_missing() 528 .expect("missing credential"); 529 assert_eq!( 530 binding.repair_action(), 531 Some(SignerRepairAction::ImportCredential) 532 ); 533 binding.repair_credential().expect("repair"); 534 535 binding.mark_store_unavailable(); 536 assert_eq!( 537 binding.repair_action(), 538 Some(SignerRepairAction::RetryCredentialStore) 539 ); 540 binding 541 .resolve_store_recovery(false) 542 .expect("store recovery"); 543 assert_eq!( 544 binding.availability(), 545 SignerAvailability::CredentialMissing 546 ); 547 assert!(binding.resolve_store_recovery(true).is_err()); 548 } 549 }