pns.rs (8089B)
1 //! NIP-PNS: Private Note Storage 2 //! 3 //! Deterministic key derivation and encryption for storing private nostr 4 //! events on relays. Only the owner of the device key can publish and 5 //! decrypt PNS events (kind 1080). 6 //! 7 //! Key derivation: 8 //! pns_key = hkdf_extract(ikm=device_key, salt="nip-pns") 9 //! pns_keypair = derive_secp256k1_keypair(pns_key) 10 //! pns_nip44_key = hkdf_extract(ikm=pns_key, salt="nip44-v2") 11 12 use base64::engine::general_purpose::STANDARD as BASE64; 13 use base64::Engine; 14 use hkdf::Hkdf; 15 use nostr::nips::nip44::v2::{self, ConversationKey}; 16 use sha2::Sha256; 17 18 use crate::{FullKeypair, Pubkey}; 19 20 /// Kind number for PNS events. 21 pub const PNS_KIND: u32 = 1080; 22 23 /// Salt used for deriving pns_key from the device key. 24 const PNS_SALT: &[u8] = b"nip-pns"; 25 26 /// Salt used for deriving the NIP-44 symmetric key from pns_key. 27 const NIP44_SALT: &[u8] = b"nip44-v2"; 28 29 /// Derived PNS keys — everything needed to create and decrypt PNS events. 30 pub struct PnsKeys { 31 /// Keypair for signing kind-1080 events (derived from pns_key). 32 pub keypair: FullKeypair, 33 /// NIP-44 conversation key for encrypting/decrypting content. 34 pub conversation_key: ConversationKey, 35 } 36 37 /// Derive all PNS keys from a device secret key. 38 /// 39 /// This is deterministic: the same device key always produces the same 40 /// PNS keypair and encryption key. 41 pub fn derive_pns_keys(device_key: &[u8; 32]) -> PnsKeys { 42 let pns_key = hkdf_extract(device_key, PNS_SALT); 43 let keypair = keypair_from_bytes(&pns_key); 44 let nip44_key = hkdf_extract(&pns_key, NIP44_SALT); 45 let conversation_key = ConversationKey::new(nip44_key); 46 47 PnsKeys { 48 keypair, 49 conversation_key, 50 } 51 } 52 53 /// Encrypt an inner event JSON string for PNS storage. 54 /// 55 /// Returns base64-encoded NIP-44 v2 ciphertext suitable for the 56 /// `content` field of a kind-1080 event. 57 pub fn encrypt(conversation_key: &ConversationKey, inner_json: &str) -> Result<String, PnsError> { 58 let payload = v2::encrypt_to_bytes(conversation_key, inner_json).map_err(PnsError::Encrypt)?; 59 Ok(BASE64.encode(payload)) 60 } 61 62 /// Decrypt a PNS event's content field back to the inner event JSON. 63 /// 64 /// Takes base64-encoded NIP-44 v2 ciphertext from a kind-1080 event. 65 pub fn decrypt(conversation_key: &ConversationKey, content: &str) -> Result<String, PnsError> { 66 let payload = BASE64.decode(content).map_err(PnsError::Base64)?; 67 let plaintext = v2::decrypt_to_bytes(conversation_key, &payload).map_err(PnsError::Decrypt)?; 68 String::from_utf8(plaintext).map_err(PnsError::Utf8) 69 } 70 71 /// HMAC-SHA256(key=salt, msg=ikm) → 32-byte key. 72 /// 73 /// This matches the nostrdb C implementation which uses raw HMAC-SHA256 74 /// (i.e. HKDF-Extract only, without HKDF-Expand). 75 fn hkdf_extract(ikm: &[u8; 32], salt: &[u8]) -> [u8; 32] { 76 let (prk, _) = Hkdf::<Sha256>::extract(Some(salt), ikm); 77 let mut out = [0u8; 32]; 78 out.copy_from_slice(&prk); 79 out 80 } 81 82 /// Derive a secp256k1 keypair from 32 bytes of key material. 83 fn keypair_from_bytes(key: &[u8; 32]) -> FullKeypair { 84 let secret_key = 85 nostr::SecretKey::from_slice(key).expect("32 bytes of HKDF output is a valid secret key"); 86 let (xopk, _) = secret_key.x_only_public_key(&nostr::SECP256K1); 87 FullKeypair { 88 pubkey: Pubkey::new(xopk.serialize()), 89 secret_key, 90 } 91 } 92 93 #[derive(Debug)] 94 pub enum PnsError { 95 Encrypt(nostr::nips::nip44::Error), 96 Decrypt(nostr::nips::nip44::Error), 97 Base64(base64::DecodeError), 98 Utf8(std::string::FromUtf8Error), 99 } 100 101 impl std::fmt::Display for PnsError { 102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 103 match self { 104 PnsError::Encrypt(e) => write!(f, "PNS encrypt failed: {e}"), 105 PnsError::Decrypt(e) => write!(f, "PNS decrypt failed: {e}"), 106 PnsError::Base64(e) => write!(f, "PNS base64 decode failed: {e}"), 107 PnsError::Utf8(e) => write!(f, "PNS decrypted content is not UTF-8: {e}"), 108 } 109 } 110 } 111 112 impl std::error::Error for PnsError {} 113 114 #[cfg(test)] 115 mod tests { 116 use super::*; 117 118 fn test_device_key() -> [u8; 32] { 119 // Deterministic test key 120 let mut key = [0u8; 32]; 121 key[0] = 0x01; 122 key[31] = 0xff; 123 key 124 } 125 126 #[test] 127 fn test_derive_pns_keys_deterministic() { 128 let dk = test_device_key(); 129 let keys1 = derive_pns_keys(&dk); 130 let keys2 = derive_pns_keys(&dk); 131 132 assert_eq!(keys1.keypair.pubkey, keys2.keypair.pubkey); 133 assert_eq!( 134 keys1.conversation_key.as_bytes(), 135 keys2.conversation_key.as_bytes() 136 ); 137 } 138 139 #[test] 140 fn test_pns_pubkey_differs_from_device_pubkey() { 141 let dk = test_device_key(); 142 let pns = derive_pns_keys(&dk); 143 144 // Device pubkey 145 let device_sk = nostr::SecretKey::from_slice(&dk).unwrap(); 146 let (device_xopk, _) = device_sk.x_only_public_key(&nostr::SECP256K1); 147 let device_pubkey = Pubkey::new(device_xopk.serialize()); 148 149 // PNS pubkey should be different (derived via HKDF) 150 assert_ne!(pns.keypair.pubkey, device_pubkey); 151 } 152 153 #[test] 154 fn test_encrypt_decrypt_roundtrip() { 155 let dk = test_device_key(); 156 let keys = derive_pns_keys(&dk); 157 158 let inner = r#"{"kind":1,"pubkey":"abc","content":"hello","tags":[],"created_at":0}"#; 159 let encrypted = encrypt(&keys.conversation_key, inner).unwrap(); 160 161 // Should be base64 162 assert!(BASE64.decode(&encrypted).is_ok()); 163 164 let decrypted = decrypt(&keys.conversation_key, &encrypted).unwrap(); 165 assert_eq!(decrypted, inner); 166 } 167 168 #[test] 169 fn test_different_keys_cannot_decrypt() { 170 let dk1 = test_device_key(); 171 let mut dk2 = test_device_key(); 172 dk2[0] = 0x02; 173 174 let keys1 = derive_pns_keys(&dk1); 175 let keys2 = derive_pns_keys(&dk2); 176 177 let inner = r#"{"content":"secret"}"#; 178 let encrypted = encrypt(&keys1.conversation_key, inner).unwrap(); 179 180 // Different key should fail to decrypt 181 assert!(decrypt(&keys2.conversation_key, &encrypted).is_err()); 182 } 183 184 #[test] 185 fn test_matches_nostrdb_c_test_vector() { 186 // Same device key as nostrdb's test_pns_unwrap in test.c: 187 // unsigned char device_sec[32] = {0,...,0,2}; 188 let mut device_key = [0u8; 32]; 189 device_key[31] = 0x02; 190 191 let keys = derive_pns_keys(&device_key); 192 193 // The C test expects PNS pubkey: 194 // "fa22d53e9d38ca7af1e66dcf88f5fb2444368df6bd16580b5827c8cfbc622d4e" 195 let expected_pns_pubkey = 196 "fa22d53e9d38ca7af1e66dcf88f5fb2444368df6bd16580b5827c8cfbc622d4e"; 197 let actual_pns_pubkey = hex::encode(keys.keypair.pubkey.bytes()); 198 assert_eq!( 199 actual_pns_pubkey, expected_pns_pubkey, 200 "PNS pubkey must match nostrdb C implementation" 201 ); 202 203 // Also verify device pubkey matches (sanity check): 204 // c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 205 let device_sk = nostr::SecretKey::from_slice(&device_key).unwrap(); 206 let (device_xopk, _) = device_sk.x_only_public_key(&nostr::SECP256K1); 207 let expected_device_pubkey = 208 "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"; 209 assert_eq!(hex::encode(device_xopk.serialize()), expected_device_pubkey); 210 } 211 212 #[test] 213 fn test_encrypt_produces_different_ciphertext() { 214 // NIP-44 uses random nonce, so encrypting same plaintext twice 215 // should produce different ciphertext 216 let dk = test_device_key(); 217 let keys = derive_pns_keys(&dk); 218 219 let inner = r#"{"content":"hello"}"#; 220 let enc1 = encrypt(&keys.conversation_key, inner).unwrap(); 221 let enc2 = encrypt(&keys.conversation_key, inner).unwrap(); 222 223 assert_ne!(enc1, enc2); 224 225 // But both should decrypt to the same thing 226 assert_eq!(decrypt(&keys.conversation_key, &enc1).unwrap(), inner); 227 assert_eq!(decrypt(&keys.conversation_key, &enc2).unwrap(), inner); 228 } 229 }