profile.rs (11287B)
1 use harvestcircle_domain::{ 2 EventId, Kind0ProfileCandidate, ProfileMetadata, PublicKey, SafeError, SafeErrorCode, 3 SafeMessage, UnixTimestamp, 4 }; 5 use nostr::{Event, JsonUtil, Kind, Metadata}; 6 7 const MAX_EVENT_JSON_BYTES: usize = 64 * 1_024; 8 const MAX_PROFILE_CONTENT_BYTES: usize = 16 * 1_024; 9 10 /// Verifies and converts one serialized Nostr kind-0 event. 11 /// 12 /// # Errors 13 /// 14 /// Returns a safe profile-refresh error when the event is oversized, 15 /// malformed, invalidly signed, authored by another key, or not kind 0. 16 pub fn parse_verified_kind0( 17 event_json: &str, 18 expected_author: PublicKey, 19 ) -> Result<Kind0ProfileCandidate, SafeError> { 20 if event_json.len() > MAX_EVENT_JSON_BYTES { 21 return Err(invalid_event()); 22 } 23 24 let event = Event::from_json(event_json).map_err(|_| invalid_event())?; 25 event.verify().map_err(|_| invalid_event())?; 26 if event.kind != Kind::Metadata 27 || event.pubkey.to_bytes() != *expected_author.as_bytes() 28 || event.content.len() > MAX_PROFILE_CONTENT_BYTES 29 { 30 return Err(invalid_event()); 31 } 32 33 let metadata = Metadata::from_json(&event.content).map_err(|_| invalid_metadata())?; 34 let profile = ProfileMetadata::new( 35 metadata.name, 36 metadata.display_name, 37 metadata.nip05, 38 metadata.about, 39 metadata.picture, 40 )?; 41 let created_at = i64::try_from(event.created_at.as_secs()) 42 .ok() 43 .and_then(UnixTimestamp::from_seconds) 44 .ok_or_else(invalid_event)?; 45 46 Ok(Kind0ProfileCandidate::new( 47 EventId::from_bytes(event.id.to_bytes()), 48 expected_author, 49 created_at, 50 profile, 51 )) 52 } 53 54 const fn invalid_event() -> SafeError { 55 SafeError::new( 56 SafeErrorCode::ProfileRefreshFailed, 57 SafeMessage::new("The Nostr profile event is invalid."), 58 ) 59 } 60 61 const fn invalid_metadata() -> SafeError { 62 SafeError::new( 63 SafeErrorCode::InvalidProfileMetadata, 64 SafeMessage::new("The Nostr profile metadata is invalid."), 65 ) 66 } 67 68 #[cfg(test)] 69 mod tests { 70 use harvestcircle_domain::{PublicKey, SafeErrorCode}; 71 use nostr::{EventBuilder, JsonUtil, Keys, Metadata, Url}; 72 73 use super::parse_verified_kind0; 74 75 fn signed_profile() -> (Keys, String) { 76 let keys = Keys::generate(); 77 let event = EventBuilder::metadata( 78 &Metadata::new() 79 .name(" farmer ") 80 .display_name(" Farm Identity ") 81 .nip05("farmer@example.test") 82 .about("Local grower") 83 .picture( 84 Url::parse("https://images.example.test/farmer.png") 85 .expect("valid picture URL"), 86 ), 87 ) 88 .sign_with_keys(&keys) 89 .expect("signed metadata event"); 90 (keys, event.as_json()) 91 } 92 93 fn text_at_utf8_limit(unit: &str, maximum: usize) -> String { 94 unit.repeat(maximum / unit.len()) + &"x".repeat(maximum % unit.len()) 95 } 96 97 fn signed_metadata(fields: [Option<String>; 5]) -> (PublicKey, String) { 98 let [name, display_name, nip05, about, picture] = fields; 99 let mut metadata = Metadata::new(); 100 metadata.name = name; 101 metadata.display_name = display_name; 102 metadata.nip05 = nip05; 103 metadata.about = about; 104 metadata.picture = picture; 105 let keys = Keys::generate(); 106 let event = EventBuilder::new(nostr::Kind::Metadata, metadata.as_json()) 107 .sign_with_keys(&keys) 108 .expect("signed metadata"); 109 event 110 .verify() 111 .expect("fixture has a valid signature and event ID"); 112 let author = PublicKey::from_bytes(keys.public_key().to_bytes()).expect("public author"); 113 (author, event.as_json()) 114 } 115 116 fn assert_signed_one_byte_over_limit_is_rejected(index: usize, maximum: usize) { 117 for unit in ["x", "é", "🥕", "e\u{301}"] { 118 let value = text_at_utf8_limit(unit, maximum) + "x"; 119 assert_eq!(value.len(), maximum + 1); 120 let mut fields: [Option<String>; 5] = std::array::from_fn(|_| None); 121 fields[index] = Some(format!(" {value} ")); 122 let (author, json) = signed_metadata(fields); 123 assert_eq!( 124 parse_verified_kind0(&json, author) 125 .expect_err("correctly signed overlimit metadata") 126 .code(), 127 SafeErrorCode::InvalidProfileMetadata 128 ); 129 } 130 } 131 132 #[test] 133 fn signed_kind0_accepts_exact_utf8_profile_boundaries() { 134 for unit in ["x", "é", "🥕", "e\u{301}"] { 135 let fields = [128, 128, 320, 4_096, 2_048] 136 .map(|maximum| Some(text_at_utf8_limit(unit, maximum))); 137 let (author, json) = signed_metadata(fields.clone()); 138 let candidate = parse_verified_kind0(&json, author).expect("signed UTF-8 boundaries"); 139 assert_eq!(candidate.author(), author); 140 assert_eq!(candidate.metadata().name(), fields[0].as_deref()); 141 assert_eq!(candidate.metadata().display_name(), fields[1].as_deref()); 142 assert_eq!(candidate.metadata().nip05(), fields[2].as_deref()); 143 assert_eq!(candidate.metadata().about(), fields[3].as_deref()); 144 assert_eq!(candidate.metadata().picture(), fields[4].as_deref()); 145 } 146 } 147 148 #[test] 149 fn signed_kind0_rejects_name_one_byte_over_utf8_limit() { 150 assert_signed_one_byte_over_limit_is_rejected(0, 128); 151 } 152 153 #[test] 154 fn signed_kind0_rejects_display_name_one_byte_over_utf8_limit() { 155 assert_signed_one_byte_over_limit_is_rejected(1, 128); 156 } 157 158 #[test] 159 fn signed_kind0_rejects_nip05_one_byte_over_utf8_limit() { 160 assert_signed_one_byte_over_limit_is_rejected(2, 320); 161 } 162 163 #[test] 164 fn signed_kind0_rejects_about_one_byte_over_utf8_limit() { 165 assert_signed_one_byte_over_limit_is_rejected(3, 4_096); 166 } 167 168 #[test] 169 fn signed_kind0_rejects_picture_one_byte_over_utf8_limit() { 170 assert_signed_one_byte_over_limit_is_rejected(4, 2_048); 171 } 172 173 #[test] 174 fn signed_kind0_preserves_trim_blank_and_about_layout_policy() { 175 let (author, json) = signed_metadata([ 176 Some(" \u{2003}e\u{301}\u{2003} ".to_owned()), 177 Some(" \t ".to_owned()), 178 None, 179 Some(" \nFirst\nSecond\rThird\tFourth\t ".to_owned()), 180 Some(" ".to_owned()), 181 ]); 182 let candidate = parse_verified_kind0(&json, author).expect("normalized signed metadata"); 183 assert_eq!(candidate.metadata().name(), Some("e\u{301}")); 184 assert_eq!(candidate.metadata().display_name(), None); 185 assert_eq!(candidate.metadata().nip05(), None); 186 assert_eq!( 187 candidate.metadata().about(), 188 Some("First\nSecond\rThird\tFourth") 189 ); 190 assert_eq!(candidate.metadata().picture(), None); 191 192 for index in 0..5 { 193 for control in ['\0', '\u{1b}', '\u{7f}', '\u{85}', '\n', '\r', '\t'] { 194 if index == 3 && matches!(control, '\n' | '\r' | '\t') { 195 continue; 196 } 197 let mut fields: [Option<String>; 5] = std::array::from_fn(|_| None); 198 fields[index] = Some(format!("a{control}b")); 199 let (author, json) = signed_metadata(fields); 200 assert_eq!( 201 parse_verified_kind0(&json, author) 202 .expect_err("signed metadata with forbidden embedded control") 203 .code(), 204 SafeErrorCode::InvalidProfileMetadata 205 ); 206 } 207 } 208 } 209 210 #[test] 211 fn profile_event_verifies_signature_author_kind_and_metadata() { 212 let (keys, json) = signed_profile(); 213 let expected_author = 214 PublicKey::from_bytes(keys.public_key().to_bytes()).expect("valid public key"); 215 216 let candidate = parse_verified_kind0(&json, expected_author).expect("verified profile"); 217 218 assert_eq!(candidate.author(), expected_author); 219 assert_eq!(candidate.metadata().name(), Some("farmer")); 220 assert_eq!(candidate.metadata().display_name(), Some("Farm Identity")); 221 assert_eq!(candidate.metadata().nip05(), Some("farmer@example.test")); 222 assert_eq!(candidate.metadata().about(), Some("Local grower")); 223 assert_eq!( 224 candidate.metadata().picture(), 225 Some("https://images.example.test/farmer.png") 226 ); 227 } 228 229 #[test] 230 fn profile_event_rejects_tampering_wrong_author_kind_and_oversize_content() { 231 let (keys, json) = signed_profile(); 232 let expected_author = 233 PublicKey::from_bytes(keys.public_key().to_bytes()).expect("valid public key"); 234 let wrong_author = PublicKey::from_bytes(Keys::generate().public_key().to_bytes()) 235 .expect("valid public key"); 236 let tampered = json.replace("Local grower", "Remote grower"); 237 let note = EventBuilder::text_note("not metadata") 238 .sign_with_keys(&keys) 239 .expect("signed note") 240 .as_json(); 241 let oversized = EventBuilder::metadata(&Metadata::new().about("x".repeat(16 * 1_024 + 1))) 242 .sign_with_keys(&keys) 243 .expect("signed oversized profile") 244 .as_json(); 245 246 for rejected in [ 247 parse_verified_kind0(&tampered, expected_author), 248 parse_verified_kind0(&json, wrong_author), 249 parse_verified_kind0(¬e, expected_author), 250 parse_verified_kind0(&oversized, expected_author), 251 ] { 252 assert_eq!( 253 rejected.expect_err("invalid event").code(), 254 SafeErrorCode::ProfileRefreshFailed 255 ); 256 } 257 } 258 259 #[test] 260 fn profile_event_rejects_malformed_and_bounded_invalid_metadata() { 261 let keys = Keys::generate(); 262 let malformed = EventBuilder::new(nostr::Kind::Metadata, "not json") 263 .sign_with_keys(&keys) 264 .expect("signed malformed metadata") 265 .as_json(); 266 let invalid = EventBuilder::metadata(&Metadata::new().name("x".repeat(129))) 267 .sign_with_keys(&keys) 268 .expect("signed invalid metadata") 269 .as_json(); 270 let author = PublicKey::from_bytes(keys.public_key().to_bytes()).expect("valid public key"); 271 272 assert_eq!( 273 parse_verified_kind0(&malformed, author) 274 .expect_err("malformed metadata") 275 .code(), 276 SafeErrorCode::InvalidProfileMetadata 277 ); 278 assert_eq!( 279 parse_verified_kind0(&invalid, author) 280 .expect_err("bounded metadata") 281 .code(), 282 SafeErrorCode::InvalidProfileMetadata 283 ); 284 assert_eq!( 285 parse_verified_kind0(&"x".repeat(64 * 1_024 + 1), author) 286 .expect_err("oversized event") 287 .code(), 288 SafeErrorCode::ProfileRefreshFailed 289 ); 290 assert_eq!( 291 super::invalid_event().code(), 292 SafeErrorCode::ProfileRefreshFailed 293 ); 294 assert_eq!( 295 super::invalid_metadata().code(), 296 SafeErrorCode::InvalidProfileMetadata 297 ); 298 } 299 }