identity.rs (18486B)
1 use crate::error::IdentityError; 2 use core::convert::Infallible; 3 use core::fmt; 4 use nostr::{Keys, SecretKey}; 5 #[cfg(feature = "nip49")] 6 use nostr::{ 7 nips::nip19::{FromBech32, ToBech32}, 8 nips::nip49::{EncryptedSecretKey, KeySecurity}, 9 }; 10 #[cfg(feature = "profile")] 11 use radroots_events::profile::RadrootsProfile; 12 use serde::{Deserialize, Serialize}; 13 14 #[cfg(not(feature = "std"))] 15 use alloc::string::String; 16 #[cfg(all(feature = "std", feature = "json-file"))] 17 use radroots_runtime::JsonFile; 18 #[cfg(feature = "std")] 19 use radroots_runtime_paths::{ 20 RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, default_shared_identity_path, 21 }; 22 #[cfg(feature = "std")] 23 use std::{ 24 fs, 25 path::{Path, PathBuf}, 26 }; 27 28 pub const DEFAULT_IDENTITY_PATH: &str = "default.json"; 29 30 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 31 pub struct RadrootsIdentityId(String); 32 33 #[derive(Debug, Clone)] 34 pub struct RadrootsIdentity { 35 keys: Keys, 36 profile: Option<RadrootsIdentityProfile>, 37 } 38 39 #[derive(Debug, Clone, Serialize, Deserialize)] 40 pub struct RadrootsIdentityPublic { 41 pub id: RadrootsIdentityId, 42 pub public_key_hex: String, 43 pub public_key_npub: String, 44 #[serde(skip_serializing_if = "Option::is_none")] 45 pub profile: Option<RadrootsIdentityProfile>, 46 } 47 48 #[derive(Debug, Clone, Default, Serialize, Deserialize)] 49 pub struct RadrootsIdentityProfile { 50 #[serde(skip_serializing_if = "Option::is_none")] 51 pub identifier: Option<String>, 52 #[serde(skip_serializing_if = "Option::is_none")] 53 pub metadata: Option<nostr::Event>, 54 #[serde(skip_serializing_if = "Option::is_none")] 55 pub application_handler: Option<nostr::Event>, 56 #[cfg(feature = "profile")] 57 #[serde(skip_serializing_if = "Option::is_none")] 58 pub profile: Option<RadrootsProfile>, 59 } 60 61 #[derive(Debug, Clone, Serialize, Deserialize)] 62 pub struct RadrootsIdentityFile { 63 pub secret_key: String, 64 #[serde(skip_serializing_if = "Option::is_none")] 65 pub public_key: Option<String>, 66 #[serde(skip_serializing_if = "Option::is_none")] 67 pub identifier: Option<String>, 68 #[serde(skip_serializing_if = "Option::is_none")] 69 pub metadata: Option<nostr::Event>, 70 #[serde(skip_serializing_if = "Option::is_none")] 71 pub application_handler: Option<nostr::Event>, 72 #[cfg(feature = "profile")] 73 #[serde(skip_serializing_if = "Option::is_none")] 74 pub profile: Option<RadrootsProfile>, 75 } 76 77 #[derive(Debug, Clone, Copy)] 78 pub enum RadrootsIdentitySecretKeyFormat { 79 Hex, 80 Nsec, 81 } 82 83 #[cfg(feature = "nip49")] 84 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] 85 pub enum RadrootsIdentityEncryptedSecretKeySecurity { 86 Weak, 87 Medium, 88 #[default] 89 Unknown, 90 } 91 92 #[cfg(feature = "nip49")] 93 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 94 pub struct RadrootsIdentityEncryptedSecretKeyOptions { 95 pub log_n: u8, 96 pub key_security: RadrootsIdentityEncryptedSecretKeySecurity, 97 } 98 99 #[cfg(feature = "nip49")] 100 impl Default for RadrootsIdentityEncryptedSecretKeyOptions { 101 fn default() -> Self { 102 Self { 103 log_n: 16, 104 key_security: RadrootsIdentityEncryptedSecretKeySecurity::Unknown, 105 } 106 } 107 } 108 109 #[cfg(feature = "nip49")] 110 impl From<RadrootsIdentityEncryptedSecretKeySecurity> for KeySecurity { 111 fn from(value: RadrootsIdentityEncryptedSecretKeySecurity) -> Self { 112 match value { 113 RadrootsIdentityEncryptedSecretKeySecurity::Weak => Self::Weak, 114 RadrootsIdentityEncryptedSecretKeySecurity::Medium => Self::Medium, 115 RadrootsIdentityEncryptedSecretKeySecurity::Unknown => Self::Unknown, 116 } 117 } 118 } 119 120 impl RadrootsIdentityId { 121 pub fn from_public_key(public_key: nostr::PublicKey) -> Self { 122 Self(public_key.to_hex()) 123 } 124 125 pub fn parse(value: &str) -> Result<Self, IdentityError> { 126 let public_key = parse_public_key(value)?; 127 Ok(Self::from_public_key(public_key)) 128 } 129 130 pub fn as_str(&self) -> &str { 131 self.0.as_str() 132 } 133 134 pub fn into_string(self) -> String { 135 self.0 136 } 137 } 138 139 impl From<nostr::PublicKey> for RadrootsIdentityId { 140 fn from(value: nostr::PublicKey) -> Self { 141 Self::from_public_key(value) 142 } 143 } 144 145 impl TryFrom<&str> for RadrootsIdentityId { 146 type Error = IdentityError; 147 148 fn try_from(value: &str) -> Result<Self, Self::Error> { 149 Self::parse(value) 150 } 151 } 152 153 impl AsRef<str> for RadrootsIdentityId { 154 fn as_ref(&self) -> &str { 155 self.as_str() 156 } 157 } 158 159 impl fmt::Display for RadrootsIdentityId { 160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 161 f.write_str(self.0.as_str()) 162 } 163 } 164 165 impl RadrootsIdentityPublic { 166 pub fn new(public_key: nostr::PublicKey) -> Self { 167 let id = RadrootsIdentityId::from_public_key(public_key); 168 use nostr::nips::nip19::ToBech32; 169 let public_key_npub = infallible_to_string(public_key.to_bech32()); 170 Self { 171 id, 172 public_key_hex: public_key.to_hex(), 173 public_key_npub, 174 profile: None, 175 } 176 } 177 178 pub fn with_profile(mut self, profile: RadrootsIdentityProfile) -> Self { 179 self.profile = if profile.is_empty() { 180 None 181 } else { 182 Some(profile) 183 }; 184 self 185 } 186 } 187 188 impl RadrootsIdentityProfile { 189 pub fn is_empty(&self) -> bool { 190 #[cfg(feature = "profile")] 191 let profile_empty = self.profile.is_none(); 192 #[cfg(not(feature = "profile"))] 193 let profile_empty = true; 194 195 self.identifier.is_none() 196 && self.metadata.is_none() 197 && self.application_handler.is_none() 198 && profile_empty 199 } 200 } 201 202 impl RadrootsIdentity { 203 pub fn new(keys: Keys) -> Self { 204 Self { 205 keys, 206 profile: None, 207 } 208 } 209 210 pub fn with_profile(keys: Keys, profile: RadrootsIdentityProfile) -> Self { 211 let profile = if profile.is_empty() { 212 None 213 } else { 214 Some(profile) 215 }; 216 Self { keys, profile } 217 } 218 219 #[cfg(feature = "std")] 220 pub fn generate() -> Self { 221 Self::new(Keys::generate()) 222 } 223 224 #[cfg(feature = "std")] 225 pub fn generate_with_profile(profile: RadrootsIdentityProfile) -> Self { 226 Self::with_profile(Keys::generate(), profile) 227 } 228 229 pub fn keys(&self) -> &Keys { 230 &self.keys 231 } 232 233 pub fn into_keys(self) -> Keys { 234 self.keys 235 } 236 237 pub fn public_key(&self) -> nostr::PublicKey { 238 self.keys.public_key() 239 } 240 241 pub fn id(&self) -> RadrootsIdentityId { 242 RadrootsIdentityId::from_public_key(self.keys.public_key()) 243 } 244 245 pub fn public_key_hex(&self) -> String { 246 self.keys.public_key().to_hex() 247 } 248 249 pub fn public_key_npub(&self) -> String { 250 use nostr::nips::nip19::ToBech32; 251 infallible_to_string(self.keys.public_key().to_bech32()) 252 } 253 254 pub fn npub(&self) -> String { 255 self.public_key_npub() 256 } 257 258 pub fn secret_key_hex(&self) -> String { 259 self.keys.secret_key().to_secret_hex() 260 } 261 262 pub fn secret_key_nsec(&self) -> String { 263 use nostr::nips::nip19::ToBech32; 264 infallible_to_string(self.keys.secret_key().to_bech32()) 265 } 266 267 pub fn nsec(&self) -> String { 268 self.secret_key_nsec() 269 } 270 271 #[cfg(feature = "nip49")] 272 /// Export the current secret key as a NIP-49 `ncryptsec` payload. 273 /// 274 /// This is an explicit operator-facing import or export format, not the 275 /// canonical local file-storage contract for Radroots runtimes. 276 pub fn encrypt_secret_key_ncryptsec(&self, password: &str) -> Result<String, IdentityError> { 277 self.encrypt_secret_key_ncryptsec_with_options( 278 password, 279 RadrootsIdentityEncryptedSecretKeyOptions::default(), 280 ) 281 } 282 283 #[cfg(feature = "nip49")] 284 /// Export the current secret key as a NIP-49 `ncryptsec` payload with 285 /// explicit encryption options. 286 /// 287 /// This remains scoped to import or export behavior and must not become the 288 /// generic local secret-storage format. 289 pub fn encrypt_secret_key_ncryptsec_with_options( 290 &self, 291 password: &str, 292 options: RadrootsIdentityEncryptedSecretKeyOptions, 293 ) -> Result<String, IdentityError> { 294 let encrypted = EncryptedSecretKey::new( 295 self.keys.secret_key(), 296 password, 297 options.log_n, 298 options.key_security.into(), 299 ) 300 .map_err(|source| IdentityError::EncryptSecretKey(source.to_string()))?; 301 // The ncryptsec HRP and payload shape are fixed here, so encoding should not fail. 302 Ok(encrypted 303 .to_bech32() 304 .expect("ncryptsec bech32 encoding should succeed")) 305 } 306 307 pub fn secret_key_bytes(&self) -> [u8; SecretKey::LEN] { 308 self.keys.secret_key().to_secret_bytes() 309 } 310 311 #[cfg(feature = "secrecy")] 312 pub fn secret_key_hex_secret(&self) -> secrecy::SecretString { 313 use secrecy::SecretString; 314 SecretString::new(self.secret_key_hex().into()) 315 } 316 317 #[cfg(feature = "zeroize")] 318 pub fn secret_key_bytes_zeroizing(&self) -> zeroize::Zeroizing<[u8; SecretKey::LEN]> { 319 zeroize::Zeroizing::new(self.secret_key_bytes()) 320 } 321 322 pub fn profile(&self) -> Option<&RadrootsIdentityProfile> { 323 self.profile.as_ref() 324 } 325 326 pub fn profile_mut(&mut self) -> Option<&mut RadrootsIdentityProfile> { 327 self.profile.as_mut() 328 } 329 330 pub fn set_profile(&mut self, profile: RadrootsIdentityProfile) { 331 self.profile = if profile.is_empty() { 332 None 333 } else { 334 Some(profile) 335 }; 336 } 337 338 pub fn clear_profile(&mut self) { 339 self.profile = None; 340 } 341 342 pub fn to_public(&self) -> RadrootsIdentityPublic { 343 let mut public = RadrootsIdentityPublic::new(self.keys.public_key()); 344 if let Some(profile) = &self.profile { 345 public.profile = Some(profile.clone()); 346 } 347 public 348 } 349 350 pub fn to_file(&self) -> RadrootsIdentityFile { 351 self.to_file_with_secret_format(RadrootsIdentitySecretKeyFormat::Hex) 352 } 353 354 pub fn to_file_with_secret_format( 355 &self, 356 format: RadrootsIdentitySecretKeyFormat, 357 ) -> RadrootsIdentityFile { 358 let secret_key = match format { 359 RadrootsIdentitySecretKeyFormat::Hex => self.secret_key_hex(), 360 RadrootsIdentitySecretKeyFormat::Nsec => self.secret_key_nsec(), 361 }; 362 #[cfg(feature = "profile")] 363 let (identifier, metadata, application_handler, profile) = match &self.profile { 364 Some(profile) => ( 365 profile.identifier.clone(), 366 profile.metadata.clone(), 367 profile.application_handler.clone(), 368 profile.profile.clone(), 369 ), 370 None => (None, None, None, None), 371 }; 372 #[cfg(not(feature = "profile"))] 373 let (identifier, metadata, application_handler) = match &self.profile { 374 Some(profile) => ( 375 profile.identifier.clone(), 376 profile.metadata.clone(), 377 profile.application_handler.clone(), 378 ), 379 None => (None, None, None), 380 }; 381 #[cfg(feature = "profile")] 382 { 383 RadrootsIdentityFile { 384 secret_key, 385 public_key: Some(self.public_key_hex()), 386 identifier, 387 metadata, 388 application_handler, 389 profile, 390 } 391 } 392 #[cfg(not(feature = "profile"))] 393 { 394 RadrootsIdentityFile { 395 secret_key, 396 public_key: Some(self.public_key_hex()), 397 identifier, 398 metadata, 399 application_handler, 400 } 401 } 402 } 403 404 #[cfg(feature = "std")] 405 pub fn from_file(file: RadrootsIdentityFile) -> Result<Self, IdentityError> { 406 Self::try_from(file) 407 } 408 409 #[cfg(feature = "std")] 410 pub fn from_secret_key_str(secret_key: &str) -> Result<Self, IdentityError> { 411 Ok(Self::new(Keys::parse(secret_key)?)) 412 } 413 414 #[cfg(feature = "nip49")] 415 /// Import a secret key from a NIP-49 `ncryptsec` payload. 416 /// 417 /// This path is explicit by design so encrypted exports do not become an 418 /// ambient local file-storage format. 419 pub fn from_encrypted_secret_key_str( 420 secret_key: &str, 421 password: &str, 422 ) -> Result<Self, IdentityError> { 423 let encrypted = EncryptedSecretKey::from_bech32(secret_key) 424 .map_err(|source| IdentityError::InvalidEncryptedSecretKey(source.to_string()))?; 425 let secret_key = encrypted 426 .decrypt(password) 427 .map_err(|source| IdentityError::DecryptEncryptedSecretKey(source.to_string()))?; 428 Ok(Self::new(Keys::new(secret_key))) 429 } 430 431 #[cfg(feature = "std")] 432 pub fn from_secret_key_bytes(secret_key: &[u8]) -> Result<Self, IdentityError> { 433 if secret_key.len() != SecretKey::LEN { 434 return Err(IdentityError::InvalidIdentityFormat); 435 } 436 let secret_key = SecretKey::from_slice(secret_key)?; 437 Ok(Self::new(Keys::new(secret_key))) 438 } 439 440 #[cfg(feature = "std")] 441 pub fn load_from_path_auto(path: impl AsRef<Path>) -> Result<Self, IdentityError> { 442 let path = path.as_ref(); 443 let bytes = read_identity_bytes(path)?; 444 parse_identity_bytes(&bytes) 445 } 446 447 #[cfg(feature = "std")] 448 pub fn default_path() -> Result<PathBuf, IdentityError> { 449 Self::default_path_for( 450 &RadrootsPathResolver::current(), 451 RadrootsPathProfile::InteractiveUser, 452 &RadrootsPathOverrides::default(), 453 ) 454 } 455 456 #[cfg(feature = "std")] 457 pub fn default_path_for( 458 resolver: &RadrootsPathResolver, 459 profile: RadrootsPathProfile, 460 overrides: &RadrootsPathOverrides, 461 ) -> Result<PathBuf, IdentityError> { 462 Ok(default_shared_identity_path(resolver, profile, overrides)?) 463 } 464 465 #[cfg(all(feature = "std", feature = "json-file"))] 466 fn resolve_load_or_generate_path<P: AsRef<Path>>( 467 path: Option<P>, 468 ) -> Result<PathBuf, IdentityError> { 469 path.map(|p| p.as_ref().to_path_buf()) 470 .map(Ok) 471 .unwrap_or_else(Self::default_path) 472 } 473 474 #[cfg(all(feature = "std", feature = "json-file"))] 475 fn load_or_generate_at( 476 path: Result<PathBuf, IdentityError>, 477 allow_generate: bool, 478 ) -> Result<Self, IdentityError> { 479 let path = path?; 480 if path.exists() { 481 return Self::load_from_path_auto(&path); 482 } 483 if !allow_generate { 484 return Err(IdentityError::GenerationNotAllowed(path)); 485 } 486 let identity = Self::generate(); 487 identity.save_json(&path)?; 488 Ok(identity) 489 } 490 491 #[cfg(all(feature = "std", feature = "json-file"))] 492 pub fn load_or_generate<P: AsRef<Path>>( 493 path: Option<P>, 494 allow_generate: bool, 495 ) -> Result<Self, IdentityError> { 496 Self::load_or_generate_at(Self::resolve_load_or_generate_path(path), allow_generate) 497 } 498 499 #[cfg(all(feature = "std", feature = "json-file"))] 500 pub fn save_json(&self, path: impl AsRef<Path>) -> Result<(), IdentityError> { 501 let payload = self.to_file(); 502 let mut store = JsonFile::load_or_create_with(path.as_ref(), || payload.clone())?; 503 store.value = payload; 504 store.save()?; 505 Ok(()) 506 } 507 } 508 509 #[cfg(feature = "std")] 510 impl TryFrom<RadrootsIdentityFile> for RadrootsIdentity { 511 type Error = IdentityError; 512 513 fn try_from(file: RadrootsIdentityFile) -> Result<Self, Self::Error> { 514 let keys = Keys::parse(&file.secret_key)?; 515 validate_public_key(&keys, file.public_key.as_deref())?; 516 #[cfg(feature = "profile")] 517 let profile = RadrootsIdentityProfile { 518 identifier: file.identifier, 519 metadata: file.metadata, 520 application_handler: file.application_handler, 521 profile: file.profile, 522 }; 523 #[cfg(not(feature = "profile"))] 524 let profile = RadrootsIdentityProfile { 525 identifier: file.identifier, 526 metadata: file.metadata, 527 application_handler: file.application_handler, 528 }; 529 if profile.is_empty() { 530 Ok(Self::new(keys)) 531 } else { 532 Ok(Self::with_profile(keys, profile)) 533 } 534 } 535 } 536 537 impl From<Keys> for RadrootsIdentity { 538 fn from(keys: Keys) -> Self { 539 Self::new(keys) 540 } 541 } 542 543 #[cfg(feature = "std")] 544 fn read_identity_bytes(path: &Path) -> Result<Vec<u8>, IdentityError> { 545 match fs::read(path) { 546 Ok(bytes) => Ok(bytes), 547 Err(err) if err.kind() == std::io::ErrorKind::NotFound => { 548 Err(IdentityError::NotFound(path.to_path_buf())) 549 } 550 Err(err) => Err(IdentityError::Read(path.to_path_buf(), err)), 551 } 552 } 553 554 #[cfg(feature = "std")] 555 fn parse_identity_bytes(bytes: &[u8]) -> Result<RadrootsIdentity, IdentityError> { 556 if bytes.len() == SecretKey::LEN { 557 return RadrootsIdentity::from_secret_key_bytes(bytes); 558 } 559 560 let text = std::str::from_utf8(bytes).map_err(|_| IdentityError::InvalidIdentityFormat)?; 561 let trimmed = text.trim(); 562 if trimmed.is_empty() { 563 return Err(IdentityError::InvalidIdentityFormat); 564 } 565 if trimmed.starts_with('{') { 566 let file: RadrootsIdentityFile = serde_json::from_str(trimmed)?; 567 return RadrootsIdentity::from_file(file); 568 } 569 RadrootsIdentity::from_secret_key_str(trimmed) 570 } 571 572 fn validate_public_key(keys: &Keys, public_key: Option<&str>) -> Result<(), IdentityError> { 573 let Some(public_key) = public_key else { 574 return Ok(()); 575 }; 576 let parsed = parse_public_key(public_key)?; 577 if parsed != keys.public_key() { 578 return Err(IdentityError::PublicKeyMismatch); 579 } 580 Ok(()) 581 } 582 583 fn parse_public_key(value: &str) -> Result<nostr::PublicKey, IdentityError> { 584 let trimmed = value.trim(); 585 if trimmed.is_empty() { 586 return Err(IdentityError::InvalidPublicKey(value.to_string())); 587 } 588 nostr::PublicKey::parse(trimmed) 589 .or_else(|_| nostr::PublicKey::from_hex(trimmed)) 590 .map_err(|_| IdentityError::InvalidPublicKey(value.to_string())) 591 } 592 593 fn infallible_to_string(value: Result<String, Infallible>) -> String { 594 match value { 595 Ok(value) => value, 596 Err(err) => match err {}, 597 } 598 }