app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

keys.rs (4969B)


      1 use harvestcircle_application::{GeneratedKeyMaterial, ImportedKeyMaterial, KeyMaterialProvider};
      2 use harvestcircle_domain::{
      3     Npub, Nsec, PublicKey, SafeError, SafeErrorCode, SafeMessage, SecretKeyInput,
      4 };
      5 use nostr::{Keys, ToBech32};
      6 
      7 #[derive(Clone, Copy, Debug, Default)]
      8 pub struct NostrKeyMaterialProvider;
      9 
     10 /// Generates one cryptographically random local Nostr keypair.
     11 ///
     12 /// # Errors
     13 ///
     14 /// Returns a safe key error if an upstream encoding cannot be represented by
     15 /// the stricter Radroots domain boundary.
     16 impl KeyMaterialProvider for NostrKeyMaterialProvider {
     17     fn generate(&self) -> Result<GeneratedKeyMaterial, SafeError> {
     18         let keys = Keys::generate();
     19         let (public_key, npub, secret, nsec) = encode_keys(&keys)?;
     20         Ok(GeneratedKeyMaterial::new(public_key, npub, secret, nsec))
     21     }
     22 
     23     fn import(&self, input: SecretKeyInput) -> Result<ImportedKeyMaterial, SafeError> {
     24         let keys = input
     25             .with_exposed_secret(Keys::parse)
     26             .map_err(|_| invalid_secret_key())?;
     27         drop(input);
     28         let public_key = PublicKey::from_bytes(keys.public_key().to_bytes())?;
     29         let npub = keys
     30             .public_key()
     31             .to_bech32()
     32             .map_err(|_| invalid_public_key())
     33             .and_then(Npub::from_encoded)?;
     34         let secret = SecretKeyInput::parse(keys.secret_key().to_secret_hex())?;
     35         Ok(ImportedKeyMaterial::new(public_key, npub, secret))
     36     }
     37 }
     38 
     39 fn encode_keys(keys: &Keys) -> Result<(PublicKey, Npub, SecretKeyInput, Nsec), SafeError> {
     40     let public_key = PublicKey::from_bytes(keys.public_key().to_bytes())?;
     41     let npub = keys
     42         .public_key()
     43         .to_bech32()
     44         .map_err(|_| invalid_public_key())
     45         .and_then(Npub::from_encoded)?;
     46     let secret = SecretKeyInput::parse(keys.secret_key().to_secret_hex())?;
     47     let nsec = keys
     48         .secret_key()
     49         .to_bech32()
     50         .map_err(|_| invalid_secret_key())
     51         .and_then(Nsec::from_encoded)?;
     52     Ok((public_key, npub, secret, nsec))
     53 }
     54 
     55 const fn invalid_secret_key() -> SafeError {
     56     SafeError::new(
     57         SafeErrorCode::InvalidSecretKey,
     58         SafeMessage::new("The Nostr secret key is invalid."),
     59     )
     60 }
     61 
     62 const fn invalid_public_key() -> SafeError {
     63     SafeError::new(
     64         SafeErrorCode::InvalidPublicKey,
     65         SafeMessage::new("The Nostr public key is invalid."),
     66     )
     67 }
     68 
     69 #[cfg(test)]
     70 mod tests {
     71     use harvestcircle_domain::{SafeErrorCode, SecretKeyInput};
     72 
     73     use harvestcircle_application::KeyMaterialProvider;
     74 
     75     use super::{NostrKeyMaterialProvider, invalid_public_key, invalid_secret_key};
     76 
     77     const SECRET_HEX: &str = "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7";
     78     const NSEC: &str = "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5";
     79     const NSEC_PUBLIC_HEX: &str =
     80         "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e";
     81     const HEX_PUBLIC_HEX: &str = "0cfda0afa91cc2fbbd6050c285802fe95c7a1755e0f68323999e13760501dc40";
     82 
     83     #[test]
     84     fn keys_generate_valid_redacted_material() {
     85         let generated = NostrKeyMaterialProvider.generate().expect("generated");
     86         let (public_key, npub, secret, nsec) = generated.into_parts();
     87         assert_eq!(public_key.to_hex().len(), 64);
     88         assert!(npub.as_str().starts_with("npub1"));
     89         assert_eq!(secret.with_exposed_secret(str::len), 64);
     90         assert_eq!(nsec.with_exposed_secret(str::len), 63);
     91         assert_eq!(secret.with_exposed_secret(str::len), 64);
     92         assert_eq!(nsec.with_exposed_secret(str::len), 63);
     93     }
     94 
     95     #[test]
     96     fn keys_import_known_nsec_and_hex_vectors() {
     97         let from_nsec = NostrKeyMaterialProvider
     98             .import(SecretKeyInput::parse(NSEC.to_owned()).expect("nsec"))
     99             .expect("import nsec");
    100         let from_hex = NostrKeyMaterialProvider
    101             .import(SecretKeyInput::parse(SECRET_HEX.to_owned()).expect("hex"))
    102             .expect("import hex");
    103         let (nsec_public, nsec_npub, _) = from_nsec.into_parts();
    104         let (hex_public, hex_npub, _) = from_hex.into_parts();
    105         assert_eq!(nsec_public.to_hex(), NSEC_PUBLIC_HEX);
    106         assert_eq!(hex_public.to_hex(), HEX_PUBLIC_HEX);
    107         assert!(nsec_npub.as_str().starts_with("npub1"));
    108         assert!(hex_npub.as_str().starts_with("npub1"));
    109     }
    110 
    111     #[test]
    112     fn keys_reject_structurally_plausible_nsec_with_invalid_checksum() {
    113         let input = SecretKeyInput::parse(
    114             "nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq".to_owned(),
    115         )
    116         .expect("domain shape");
    117         let error = NostrKeyMaterialProvider
    118             .import(input)
    119             .err()
    120             .expect("invalid checksum");
    121         assert_eq!(error.code(), SafeErrorCode::InvalidSecretKey);
    122         assert_eq!(invalid_secret_key().code(), SafeErrorCode::InvalidSecretKey);
    123         assert_eq!(invalid_public_key().code(), SafeErrorCode::InvalidPublicKey);
    124     }
    125 }