damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 02fc065005738abbc84fe8102a48a0e0903f8624
parent 668b0a94df0b17c406b4476d2bd2e2dbae9af01a
Author: William Casarin <jb55@jb55.com>
Date:   Sat, 15 Apr 2023 16:01:00 -0700

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

Diffstat:
Mdamus/Models/FollowersModel.swift | 2+-
Mdamus/Models/FollowingModel.swift | 2+-
Mdamus/Models/HomeModel.swift | 138++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mdamus/Models/ProfileModel.swift | 2+-
Mdamus/Models/SearchHomeModel.swift | 2+-
Mdamus/Models/ThreadModel.swift | 2+-
Mdamus/Nostr/NostrEvent.swift | 14+++++---------
Mdamus/Util/EventCache.swift | 9+++++++++
Mdamus/Views/EventView.swift | 13-------------
Mdamus/Views/Events/TextEvent.swift | 1-
10 files changed, 100 insertions(+), 85 deletions(-)

diff --git 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 @@ -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 @@ -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 } - - 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)'") } - - 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)) - } - } + 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)) + +} + +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) - // 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)) + DispatchQueue.main.async { + events.validation[ev.id] = result + guard result == .ok else { + return } + callback() } - - let banner = tprof.profile.banner ?? "" - if URL(string: banner) != nil { - DispatchQueue.main.async { - notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) - } + } + + 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 } - notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) + 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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)