From 02fc065005738abbc84fe8102a48a0e0903f8624 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sat, 15 Apr 2023 16:01:00 -0700 Subject: [PATCH] Always check signatures on profile events These contain sensitive data (lightning addresses) and it would be really bad if these were forged. Changelog-Changed: Always check signatures of profile events --- damus/Models/FollowersModel.swift | 2 +- damus/Models/FollowingModel.swift | 2 +- damus/Models/HomeModel.swift | 150 +++++++++++++++++------------ damus/Models/ProfileModel.swift | 2 +- damus/Models/SearchHomeModel.swift | 2 +- damus/Models/ThreadModel.swift | 2 +- damus/Nostr/NostrEvent.swift | 14 +-- damus/Util/EventCache.swift | 9 ++ damus/Views/EventView.swift | 13 --- damus/Views/Events/TextEvent.swift | 1 - 10 files changed, 106 insertions(+), 91 deletions(-) diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift index c245d60d..c064a6f9 100644 --- a/damus/Models/FollowersModel.swift +++ b/damus/Models/FollowersModel.swift @@ -82,7 +82,7 @@ class FollowersModel: ObservableObject { if ev.known_kind == .contacts { handle_contact_event(ev) } else if ev.known_kind == .metadata { - process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) + process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) } case .notice(let msg): diff --git a/damus/Models/FollowingModel.swift b/damus/Models/FollowingModel.swift index a9e635dc..05109d27 100644 --- a/damus/Models/FollowingModel.swift +++ b/damus/Models/FollowingModel.swift @@ -62,7 +62,7 @@ class FollowingModel { break case .event(_, let ev): if ev.kind == 0 { - process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) + process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) } case .notice(let msg): print("followingmodel notice: \(msg)") diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index 19fc86d4..cc8757b0 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -186,10 +186,6 @@ class HomeModel: ObservableObject { } func handle_channel_create(_ ev: NostrEvent) { - guard ev.is_valid else { - return - } - self.channels[ev.id] = ev } @@ -212,10 +208,6 @@ class HomeModel: ObservableObject { } func handle_delete_event(_ ev: NostrEvent) { - guard ev.is_valid else { - return - } - self.deleted_events.insert(ev.id) } @@ -237,7 +229,7 @@ class HomeModel: ObservableObject { if let inner_ev = ev.inner_event { boost_ev_id = inner_ev.id - guard inner_ev.is_valid else { + guard validate_event(ev: inner_ev) == .ok else { return } @@ -453,7 +445,7 @@ class HomeModel: ObservableObject { } func handle_metadata_event(_ ev: NostrEvent) { - process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) + process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) } func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? { @@ -664,66 +656,98 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) { print("-----") } -func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) { - DispatchQueue.global(qos: .background).async { - guard let profile: Profile = decode_data(Data(ev.content.utf8)) else { +func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: Profile, ev: NostrEvent) { + if our_pubkey == ev.pubkey && (profile.deleted ?? false) { + DispatchQueue.main.async { + notify(.deleted_account, ()) + } + return + } + + var old_nip05: String? = nil + if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) { + old_nip05 = mprof.profile.nip05 + if mprof.timestamp > ev.created_at { + // skip if we already have an newer profile return } - + } + + let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at, event: ev) + profiles.add(id: ev.pubkey, profile: tprof) + + if let nip05 = profile.nip05, old_nip05 != profile.nip05 { + Task.detached(priority: .background) { + let validated = await validate_nip05(pubkey: ev.pubkey, nip05_str: nip05) + if validated != nil { + print("validated nip05 for '\(nip05)'") + } + + DispatchQueue.main.async { + profiles.validated[ev.pubkey] = validated + profiles.nip05_pubkey[nip05] = ev.pubkey + notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) + } + } + } + + // load pfps asap + let picture = tprof.profile.picture ?? robohash(ev.pubkey) + if URL(string: picture) != nil { DispatchQueue.main.async { - if our_pubkey == ev.pubkey && (profile.deleted ?? false) { - DispatchQueue.main.async { - notify(.deleted_account, ()) - } - return - } - - var old_nip05: String? = nil - if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) { - old_nip05 = mprof.profile.nip05 - if mprof.timestamp > ev.created_at { - // skip if we already have an newer profile - return - } - } - - let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at, event: ev) - profiles.add(id: ev.pubkey, profile: tprof) - - if let nip05 = profile.nip05, old_nip05 != profile.nip05 { - Task.detached(priority: .background) { - let validated = await validate_nip05(pubkey: ev.pubkey, nip05_str: nip05) - if validated != nil { - print("validated nip05 for '\(nip05)'") - } - - DispatchQueue.main.async { - profiles.validated[ev.pubkey] = validated - profiles.nip05_pubkey[nip05] = ev.pubkey - notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) - } - } - } - - // load pfps asap - let picture = tprof.profile.picture ?? robohash(ev.pubkey) - if URL(string: picture) != nil { - DispatchQueue.main.async { - notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) - } - } - - let banner = tprof.profile.banner ?? "" - if URL(string: banner) != nil { - DispatchQueue.main.async { - notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) - } - } - notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) } } + let banner = tprof.profile.banner ?? "" + if URL(string: banner) != nil { + DispatchQueue.main.async { + notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) + } + } + + notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) + +} + +func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) { + let validated = events.is_event_valid(ev.id) + + switch validated { + case .unknown: + Task { + let result = validate_event(ev: ev) + + DispatchQueue.main.async { + events.validation[ev.id] = result + guard result == .ok else { + return + } + callback() + } + } + + case .ok: + callback() + + case .bad_id: fallthrough + case .bad_sig: + break + } +} + +func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent) { + guard_valid_event(events: events, ev: ev) { + DispatchQueue.global(qos: .background).async { + guard let profile: Profile = decode_data(Data(ev.content.utf8)) else { + return + } + + DispatchQueue.main.async { + process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev) + } + } + } } func robohash(_ pk: String) -> String { diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift index c264175b..b0777c18 100644 --- a/damus/Models/ProfileModel.swift +++ b/damus/Models/ProfileModel.swift @@ -119,7 +119,7 @@ class ProfileModel: ObservableObject, Equatable { } else if ev.known_kind == .contacts { handle_profile_contact_event(ev) } else if ev.known_kind == .metadata { - process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev) + process_metadata_event(events: damus.events, our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev) } seen_event.insert(ev.id) } diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift index c73b81e5..13a56b55 100644 --- a/damus/Models/SearchHomeModel.swift +++ b/damus/Models/SearchHomeModel.swift @@ -161,7 +161,7 @@ func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad } if ev.known_kind == .metadata { - process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) + process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) } } diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift index e22befcc..f5a2d989 100644 --- a/damus/Models/ThreadModel.swift +++ b/damus/Models/ThreadModel.swift @@ -129,7 +129,7 @@ class ThreadModel: ObservableObject { } if ev.known_kind == .metadata { - process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) + process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) } else if ev.is_textlike { self.add_event(ev, privkey: self.damus_state.keypair.privkey) } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift index 0820b4cd..3a106c06 100644 --- a/damus/Nostr/NostrEvent.swift +++ b/damus/Nostr/NostrEvent.swift @@ -13,11 +13,15 @@ import CryptoKit import NaturalLanguage - enum ValidationResult: Decodable { + case unknown case ok case bad_id case bad_sig + + var is_bad: Bool { + return self == .bad_id || self == .bad_sig + } } struct OtherEvent { @@ -93,14 +97,6 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has return calculate_event_id(ev: self) == self.id } - var is_valid: Bool { - return validity == .ok - } - - lazy var validity: ValidationResult = { - return .ok //validate_event(ev: self) - }() - private var _blocks: [Block]? = nil func blocks(_ privkey: String?) -> [Block] { if let bs = _blocks { diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift index 929f9dff..c2f81be2 100644 --- a/damus/Util/EventCache.swift +++ b/damus/Util/EventCache.swift @@ -15,6 +15,7 @@ class EventCache { private var cancellable: AnyCancellable? private var translations: [String: TranslateStatus] = [:] private var artifacts: [String: NoteArtifacts] = [:] + var validation: [String: ValidationResult] = [:] //private var thread_latest: [String: Int64] @@ -26,6 +27,14 @@ class EventCache { } } + func is_event_valid(_ evid: String) -> ValidationResult { + guard let result = validation[evid] else { + return .unknown + } + + return result + } + func store_translation_artifacts(evid: String, translated: TranslateStatus) { self.translations[evid] = translated } diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift index 1582564a..23d24cfa 100644 --- a/damus/Views/EventView.swift +++ b/damus/Views/EventView.swift @@ -69,19 +69,6 @@ func should_show_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos return false } -func event_validity_color(_ validation: ValidationResult) -> some View { - Group { - switch validation { - case .ok: - EmptyView() - case .bad_id: - Color.orange.opacity(0.4) - case .bad_sig: - Color.red.opacity(0.4) - } - } -} - extension View { func pubkey_context_menu(bech32_pubkey: String) -> some View { return self.contextMenu { diff --git a/damus/Views/Events/TextEvent.swift b/damus/Views/Events/TextEvent.swift index f18dbaff..f2c0d3d4 100644 --- a/damus/Views/Events/TextEvent.swift +++ b/damus/Views/Events/TextEvent.swift @@ -37,7 +37,6 @@ struct TextEvent: View { } } .contentShape(Rectangle()) - .background(event_validity_color(event.validity)) .id(event.id) .frame(maxWidth: .infinity, minHeight: PFP_SIZE) .padding([.bottom], 2)