notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

key_parsing.rs (7499B)


      1 use std::collections::HashMap;
      2 use std::str::FromStr;
      3 
      4 use crate::Error;
      5 use enostr::{Keypair, Pubkey, SecretKey};
      6 use poll_promise::Promise;
      7 use reqwest::{Request, Response};
      8 use serde::{Deserialize, Serialize};
      9 
     10 #[derive(Debug, PartialEq)]
     11 pub enum LoginError {
     12     InvalidKey,
     13     Nip05Failed(String),
     14 }
     15 
     16 impl std::fmt::Display for LoginError {
     17     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
     18         match self {
     19             LoginError::InvalidKey => write!(f, "The inputted key is invalid."),
     20             LoginError::Nip05Failed(e) => write!(f, "Failed to get pubkey from Nip05 address: {e}"),
     21         }
     22     }
     23 }
     24 
     25 impl std::error::Error for LoginError {}
     26 
     27 #[derive(Deserialize, Serialize)]
     28 pub struct Nip05Result {
     29     pub names: HashMap<String, String>,
     30     pub relays: Option<HashMap<String, Vec<String>>>,
     31 }
     32 
     33 async fn parse_nip05_response(response: Response) -> Result<Nip05Result, Error> {
     34     match response.bytes().await {
     35         Ok(bytes) => {
     36             serde_json::from_slice::<Nip05Result>(&bytes).map_err(|e| Error::Generic(e.to_string()))
     37         }
     38         Err(e) => Err(Error::Generic(e.to_string())),
     39     }
     40 }
     41 
     42 fn get_pubkey_from_result(result: Nip05Result, user: String) -> Result<Pubkey, Error> {
     43     match result.names.get(&user).to_owned() {
     44         Some(pubkey_str) => Pubkey::from_hex(pubkey_str).map_err(|e| {
     45             Error::Generic("Could not parse pubkey: ".to_string() + e.to_string().as_str())
     46         }),
     47         None => Err(Error::Generic("Could not find user in json.".to_string())),
     48     }
     49 }
     50 
     51 async fn get_nip05_pubkey(id: &str) -> Result<Pubkey, Error> {
     52     let mut parts = id.split('@');
     53 
     54     let user = match parts.next() {
     55         Some(user) => user,
     56         None => {
     57             return Err(Error::Generic(
     58                 "Address does not contain username.".to_string(),
     59             ));
     60         }
     61     };
     62     let host = match parts.next() {
     63         Some(host) => host,
     64         None => {
     65             return Err(Error::Generic(
     66                 "Nip05 address does not contain host.".to_string(),
     67             ));
     68         }
     69     };
     70 
     71     if parts.next().is_some() {
     72         return Err(Error::Generic(
     73             "Nip05 address contains extraneous parts.".to_string(),
     74         ));
     75     }
     76 
     77     let url = format!("https://{host}/.well-known/nostr.json?name={user}");
     78     let request = Request::new(reqwest::Method::GET, url.parse().unwrap());
     79     let cloned_user = user.to_string();
     80 
     81     let client = reqwest::Client::new();
     82     match client.execute(request).await {
     83         Ok(resp) => match parse_nip05_response(resp).await {
     84             Ok(result) => match get_pubkey_from_result(result, cloned_user) {
     85                 Ok(pubkey) => Ok(pubkey),
     86                 Err(e) => Err(Error::Generic(e.to_string())),
     87             },
     88             Err(e) => Err(Error::Generic(e.to_string())),
     89         },
     90         Err(e) => Err(Error::Generic(e.to_string())),
     91     }
     92 }
     93 
     94 fn retrieving_nip05_pubkey(key: &str) -> bool {
     95     key.contains('@')
     96 }
     97 
     98 pub fn perform_key_retrieval(key: &str) -> Promise<Result<Keypair, LoginError>> {
     99     let key_string = String::from(key);
    100     Promise::spawn_async(async move { get_login_key(&key_string).await })
    101 }
    102 
    103 /// Attempts to turn a string slice key from the user into a Nostr-Sdk Keypair object.
    104 /// The `key` can be in any of the following formats:
    105 /// - Public Bech32 key (prefix "npub"): "npub1xyz..."
    106 /// - Private Bech32 key (prefix "nsec"): "nsec1xyz..."
    107 /// - Public hex key: "02a1..."
    108 /// - Private hex key: "5dab..."
    109 /// - NIP-05 address: "example@nostr.com"
    110 ///
    111 pub async fn get_login_key(key: &str) -> Result<Keypair, LoginError> {
    112     let tmp_key: &str = if let Some(stripped) = key.strip_prefix('@') {
    113         stripped
    114     } else {
    115         key
    116     };
    117 
    118     if retrieving_nip05_pubkey(tmp_key) {
    119         match get_nip05_pubkey(tmp_key).await {
    120             Ok(pubkey) => Ok(Keypair::only_pubkey(pubkey)),
    121             Err(e) => Err(LoginError::Nip05Failed(e.to_string())),
    122         }
    123     } else if let Ok(pubkey) = Pubkey::try_from_bech32_string(tmp_key, true) {
    124         Ok(Keypair::only_pubkey(pubkey))
    125     } else if let Ok(pubkey) = Pubkey::try_from_hex_str_with_verify(tmp_key) {
    126         Ok(Keypair::only_pubkey(pubkey))
    127     } else if let Ok(secret_key) = SecretKey::from_str(tmp_key) {
    128         Ok(Keypair::from_secret(secret_key))
    129     } else {
    130         Err(LoginError::InvalidKey)
    131     }
    132 }
    133 
    134 #[cfg(test)]
    135 mod tests {
    136     use super::*;
    137     use crate::promise_assert;
    138 
    139     #[tokio::test]
    140     async fn test_pubkey_async() {
    141         let pubkey_str = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s";
    142         let expected_pubkey =
    143             Pubkey::try_from_bech32_string(pubkey_str, false).expect("Should not have errored.");
    144         let login_key_result = get_login_key(pubkey_str).await;
    145 
    146         assert_eq!(Ok(Keypair::only_pubkey(expected_pubkey)), login_key_result);
    147     }
    148 
    149     #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    150     async fn test_pubkey() {
    151         let pubkey_str = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s";
    152         let expected_pubkey =
    153             Pubkey::try_from_bech32_string(pubkey_str, false).expect("Should not have errored.");
    154         let login_key_result = perform_key_retrieval(pubkey_str);
    155 
    156         promise_assert!(
    157             assert_eq,
    158             Ok(Keypair::only_pubkey(expected_pubkey)),
    159             &login_key_result
    160         );
    161     }
    162 
    163     #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    164     async fn test_hex_pubkey() {
    165         let pubkey_str = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245";
    166         let expected_pubkey = Pubkey::from_hex(pubkey_str).expect("Should not have errored.");
    167         let login_key_result = perform_key_retrieval(pubkey_str);
    168 
    169         promise_assert!(
    170             assert_eq,
    171             Ok(Keypair::only_pubkey(expected_pubkey)),
    172             &login_key_result
    173         );
    174     }
    175 
    176     #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    177     async fn test_privkey() {
    178         let privkey_str = "nsec1g8wt3hlwjpa4827xylr3r0lccufxltyekhraexes8lqmpp2hensq5aujhs";
    179         let expected_privkey = SecretKey::from_str(privkey_str).expect("Should not have errored.");
    180         let login_key_result = perform_key_retrieval(privkey_str);
    181 
    182         promise_assert!(
    183             assert_eq,
    184             Ok(Keypair::from_secret(expected_privkey)),
    185             &login_key_result
    186         );
    187     }
    188 
    189     #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    190     async fn test_hex_privkey() {
    191         let privkey_str = "41dcb8dfee907b53abc627c711bff8c7126fac99b5c7dc9b303fc1b08557cce0";
    192         let expected_privkey = SecretKey::from_str(privkey_str).expect("Should not have errored.");
    193         let login_key_result = perform_key_retrieval(privkey_str);
    194 
    195         promise_assert!(
    196             assert_eq,
    197             Ok(Keypair::from_secret(expected_privkey)),
    198             &login_key_result
    199         );
    200     }
    201 
    202     #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    203     async fn test_nip05() {
    204         let nip05_str = "damus@damus.io";
    205         let expected_pubkey = Pubkey::try_from_bech32_string(
    206             "npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955",
    207             false,
    208         )
    209         .expect("Should not have errored.");
    210         let login_key_result = perform_key_retrieval(nip05_str);
    211 
    212         promise_assert!(
    213             assert_eq,
    214             Ok(Keypair::only_pubkey(expected_pubkey)),
    215             &login_key_result
    216         );
    217     }
    218 }