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 }