lib

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

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 }