From: Charlie Fish <con...@charlie.fish>
Lightning Invoice: lnbc1pjcpaakpp5gjs4f626hf8w6slx84xz3wwhlf309z503rjutckdxv6wwg5ldavsdqqcqzpgxqrrs0fppqjaxxw43p7em4g59vedv7pzl76kt0qyjfsp5qcp9de7a7t8h6zs5mcssfaqp4exrnkehqtg2hf0ary3z5cjnasvs9qyyssq55523e4h3cazhkv7f8jqf5qp0n8spykls49crsu5t3m636u3yj4qdqjkdl2nxf6jet5t2r2pfrxmm8rjpqjd3ylrzqq89m4gqt5l6ycqf92c7h
Closes:
https://github.com/damus-io/damus/issues/940
Signed-off-by: Charlie Fish <con...@charlie.fish>
---
README.md | 2 +
damus.xcodeproj/project.pbxproj | 20 ++
damus/ContentView.swift | 7 +-
damus/Models/EventsModel.swift | 2 +
damus/Models/FollowersModel.swift | 2 +
damus/Models/HomeModel.swift | 2 +
damus/Models/ProfileModel.swift | 2 +
damus/Models/SearchHomeModel.swift | 4 +
damus/Models/SearchModel.swift | 3 +
damus/Models/ZapsModel.swift | 2 +
damus/Nostr/NostrAuth.swift | 14 ++
damus/Nostr/NostrRequest.swift | 5 +-
damus/Nostr/NostrResponse.swift | 15 +-
damus/Nostr/Relay.swift | 23 ++-
damus/Nostr/RelayConnection.swift | 15 +-
damus/Nostr/RelayPool.swift | 40 +++-
damus/Notify/ReconnectRelaysNotify.swift | 26 +++
.../Onboarding/SuggestedUsersViewModel.swift | 3 +
.../Detail/RelayAuthenticationDetail.swift | 35 ++++
damus/Views/Relays/RelayDetailView.swift | 17 +-
damus/Views/SaveKeysView.swift | 2 +
damus/damusApp.swift | 3 +
damusTests/AuthIntegrationTests.swift | 180 ++++++++++++++++++
damusTests/Mocking/MockDamusState.swift | 13 +-
damusTests/RequestTests.swift | 29 ++-
damusTests/Util/NdbExtensions.swift | 26 +++
nostrdb/nostrdb.c | 11 ++
nostrdb/nostrdb.h | 1 +
nostrscript/NostrScript.swift | 5 +-
29 files changed, 484 insertions(+), 25 deletions(-)
create mode 100644 damus/Nostr/NostrAuth.swift
create mode 100644 damus/Notify/ReconnectRelaysNotify.swift
create mode 100644 damus/Views/Relays/Detail/RelayAuthenticationDetail.swift
create mode 100644 damusTests/AuthIntegrationTests.swift
create mode 100644 damusTests/Util/NdbExtensions.swift
diff --git a/README.md b/README.md
index 8e237a1b..4ba44849 100644
--- a/README.md
+++ b/README.md
@@ -16,12 +16,14 @@ damus implements the following [Nostr Implementation Possibilities][nips]
- [NIP-08: Mentions][nip08]
- [NIP-10: Reply conventions][nip10]
- [NIP-12: Generic tag queries (hashtags)][nip12]
+- [NIP-42: Authentication of clients to relays][nip42]
[nips]:
https://github.com/nostr-protocol/nips
[nip01]:
https://github.com/nostr-protocol/nips/blob/master/01.md
[nip08]:
https://github.com/nostr-protocol/nips/blob/master/08.md
[nip10]:
https://github.com/nostr-protocol/nips/blob/master/10.md
[nip12]:
https://github.com/nostr-protocol/nips/blob/master/12.md
+[nip42]:
https://github.com/nostr-protocol/nips/blob/master/42.md
## Getting Started on Damus
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
index 4e2dbb0c..1697e38e 100644
--- a/damus.xcodeproj/project.pbxproj
+++ b/damus.xcodeproj/project.pbxproj
@@ -418,6 +418,11 @@
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */; };
+ B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B501062C2B363036003874F5 /* AuthIntegrationTests.swift */; };
+ B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */; };
+ B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */; };
+ B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C652B312C3700A232C0 /* NostrAuth.swift */; };
+ B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B4D1422B37D47600844320 /* NdbExtensions.swift */; };
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
@@ -1228,6 +1233,11 @@
9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = "<group>"; };
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanNSECView.swift; sourceTree = "<group>"; };
+ B501062C2B363036003874F5 /* AuthIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthIntegrationTests.swift; sourceTree = "<group>"; usesTabs = 0; };
+ B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReconnectRelaysNotify.swift; sourceTree = "<group>"; };
+ B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayAuthenticationDetail.swift; sourceTree = "<group>"; };
+ B57B4C652B312C3700A232C0 /* NostrAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NostrAuth.swift; sourceTree = "<group>"; };
+ B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = "<group>"; usesTabs = 0; };
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
@@ -1883,6 +1893,7 @@
4C2B7BF12A71B6540049DEE7 /* Id.swift */,
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */,
D798D22B2B086C7400234419 /* NostrEvent+.swift */,
+ B57B4C652B312C3700A232C0 /* NostrAuth.swift */,
);
path = Nostr;
sourceTree = "<group>";
@@ -2024,6 +2035,7 @@
isa = PBXGroup;
children = (
4C9B0DED2A65A75F00CBDA21 /* AttrStringTestExtensions.swift */,
+ B5B4D1422B37D47600844320 /* NdbExtensions.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -2059,6 +2071,7 @@
4C12536B2A76D4B00004F4B8 /* RepostedNotify.swift */,
4C4E137A2A76D5FB00BDD832 /* MuteThreadNotify.swift */,
4C4E137C2A76D63600BDD832 /* UnmuteThreadNotify.swift */,
+ B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */,
);
path = Notify;
sourceTree = "<group>";
@@ -2335,6 +2348,7 @@
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */,
D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */,
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */,
+ B501062C2B363036003874F5 /* AuthIntegrationTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -2363,6 +2377,7 @@
isa = PBXGroup;
children = (
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */,
+ B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */,
);
path = Detail;
sourceTree = "<group>";
@@ -2795,6 +2810,7 @@
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */,
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
+ B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */,
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
@@ -2848,6 +2864,7 @@
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */,
3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */,
+ B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */,
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
@@ -3145,6 +3162,7 @@
4C75EFB528049D790006080F /* Relay.swift in Sources */,
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
+ B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */,
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
@@ -3178,6 +3196,7 @@
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */,
+ B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */,
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */,
@@ -3193,6 +3212,7 @@
75AD872B2AA23A460085EF2C /* Block+Tests.swift in Sources */,
F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */,
3A5E47C72A4A76C800C0D090 /* TrieTests.swift in Sources */,
+ B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */,
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
index 1bbf76aa..a545f848 100644
--- a/damus/ContentView.swift
+++ b/damus/ContentView.swift
@@ -448,6 +448,9 @@ struct ContentView: View {
break
}
}
+ .onReceive(handle_notify(.disconnect_relays)) { () in
+ damus_state.pool.disconnect()
+ }
.onChange(of: scenePhase) { (phase: ScenePhase) in
switch phase {
case .background:
@@ -617,7 +620,7 @@ struct ContentView: View {
guard let ndb = mndb else { return }
- let pool = RelayPool(ndb: ndb)
+ let pool = RelayPool(ndb: ndb, keypair: keypair)
let model_cache = RelayModelCache()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
@@ -895,6 +898,8 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
}
case .notice:
break
+ case .auth:
+ break
}
}
diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift
index 25a61bad..4923fd17 100644
--- a/damus/Models/EventsModel.swift
+++ b/damus/Models/EventsModel.swift
@@ -63,6 +63,8 @@ class EventsModel: ObservableObject {
break
case .ok:
break
+ case .auth:
+ break
case .eose:
let txn = NdbTxn(ndb: self.state.ndb)
load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn)
diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift
index 1f5ca6ca..ab78c4d2 100644
--- a/damus/Models/FollowersModel.swift
+++ b/damus/Models/FollowersModel.swift
@@ -91,6 +91,8 @@ class FollowersModel: ObservableObject {
case .ok:
break
+ case .auth:
+ break
}
}
}
diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift
index 266e853b..e31b5089 100644
--- a/damus/Models/HomeModel.swift
+++ b/damus/Models/HomeModel.swift
@@ -446,6 +446,8 @@ class HomeModel {
case .ok:
break
+ case .auth:
+ break
}
}
diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift
index 15941596..6f0b2080 100644
--- a/damus/Models/ProfileModel.swift
+++ b/damus/Models/ProfileModel.swift
@@ -129,6 +129,8 @@ class ProfileModel: ObservableObject, Equatable {
}
progress += 1
break
+ case .auth:
+ break
}
}
}
diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift
index ff78035a..07344f66 100644
--- a/damus/Models/SearchHomeModel.swift
+++ b/damus/Models/SearchHomeModel.swift
@@ -87,6 +87,8 @@ class SearchHomeModel: ObservableObject {
load_profiles(context: "universe", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state, txn: txn)
}
+ break
+ case .auth:
break
}
}
@@ -159,6 +161,8 @@ func load_profiles<Y>(context: String, profiles_subid: String, relay_id: String,
break
case .notice:
break
+ case .auth:
+ break
}
}
diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift
index 520ea7cf..9dc49723 100644
--- a/damus/Models/SearchModel.swift
+++ b/damus/Models/SearchModel.swift
@@ -130,6 +130,9 @@ func handle_subid_event(pool: RelayPool, relay_id: String, ev: NostrConnectionEv
case .eose(let subid):
return (subid, true)
+
+ case .auth:
+ return (nil, false)
}
}
}
diff --git a/damus/Models/ZapsModel.swift b/damus/Models/ZapsModel.swift
index 2b1cb9fd..b20f3957 100644
--- a/damus/Models/ZapsModel.swift
+++ b/damus/Models/ZapsModel.swift
@@ -66,6 +66,8 @@ class ZapsModel: ObservableObject {
}
self.state.add_zap(zap: .zap(zap))
+ case .auth:
+ break
}
diff --git a/damus/Nostr/NostrAuth.swift b/damus/Nostr/NostrAuth.swift
new file mode 100644
index 00000000..d08d23d0
--- /dev/null
+++ b/damus/Nostr/NostrAuth.swift
@@ -0,0 +1,14 @@
+//
+// NostrAuth.swift
+// damus
+//
+// Created by Charlie Fish on 12/18/23.
+//
+
+import Foundation
+
+func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: Relay) -> NostrEvent? {
+ let tags: [[String]] = [["relay",
relay.descriptor.url.id],["challenge", challenge_string]]
+ let event = NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 22242, tags: tags)
+ return event
+}
diff --git a/damus/Nostr/NostrRequest.swift b/damus/Nostr/NostrRequest.swift
index 2b272997..ec06f75c 100644
--- a/damus/Nostr/NostrRequest.swift
+++ b/damus/Nostr/NostrRequest.swift
@@ -38,7 +38,8 @@ enum NostrRequest {
case subscribe(NostrSubscribe)
case unsubscribe(String)
case event(NostrEvent)
-
+ case auth(NostrEvent)
+
var is_write: Bool {
switch self {
case .subscribe:
@@ -47,6 +48,8 @@ enum NostrRequest {
return false
case .event:
return true
+ case .auth:
+ return false
}
}
diff --git a/damus/Nostr/NostrResponse.swift b/damus/Nostr/NostrResponse.swift
index cf1695ad..48c2bc37 100644
--- a/damus/Nostr/NostrResponse.swift
+++ b/damus/Nostr/NostrResponse.swift
@@ -23,7 +23,11 @@ enum NostrResponse {
case notice(String)
case eose(String)
case ok(CommandResult)
-
+ /// An [NIP-42](
https://github.com/nostr-protocol/nips/blob/master/42.md) `auth` challenge.
+ ///
+ /// The associated type of this case is the challenge string sent by the server.
+ case auth(String)
+
var subid: String? {
switch self {
case .ok:
@@ -34,6 +38,8 @@ enum NostrResponse {
return sub_id
case .notice:
return nil
+ case .auth(let challenge_string):
+ return challenge_string
}
}
@@ -94,6 +100,13 @@ enum NostrResponse {
case NDB_TCE_NOTICE:
free(data)
return .notice("")
+ case NDB_TCE_AUTH:
+ defer { free(data) }
+
+ guard let challenge_string = sized_cstr(cstr: tce.subid, len: tce.subid_len) else {
+ return nil
+ }
+ return .auth(challenge_string)
default:
free(data)
return nil
diff --git a/damus/Nostr/Relay.swift b/damus/Nostr/Relay.swift
index 7fcf14c6..18ddf4c1 100644
--- a/damus/Nostr/Relay.swift
+++ b/damus/Nostr/Relay.swift
@@ -57,6 +57,25 @@ enum RelayFlags: Int {
case broken = 1
}
+enum RelayAuthenticationError {
+ /// Only a public key was provided in keypair to sign challenge.
+ ///
+ /// A private key is required to sign `auth` challenge.
+ case no_private_key
+ /// No keypair was provided to sign challenge.
+ case no_key
+}
+enum RelayAuthenticationState: Equatable {
+ /// No `auth` request has been made from this relay
+ case none
+ /// We have received an `auth` challenge, but have not yet replied to the challenge
+ case pending
+ /// We have received an `auth` challenge and replied with an `auth` event
+ case verified
+ /// We received an `auth` challenge but failed to reply to the challenge
+ case error(RelayAuthenticationError)
+}
+
struct Limitations: Codable {
let payment_required: Bool?
@@ -85,13 +104,15 @@ struct RelayMetadata: Codable {
class Relay: Identifiable {
let descriptor: RelayDescriptor
let connection: RelayConnection
-
+ var authentication_state: RelayAuthenticationState
+
var flags: Int
init(descriptor: RelayDescriptor, connection: RelayConnection) {
self.flags = 0
self.descriptor = descriptor
self.connection = connection
+ self.authentication_state = RelayAuthenticationState.none
}
var is_broken: Bool {
diff --git a/damus/Nostr/RelayConnection.swift b/damus/Nostr/RelayConnection.swift
index 5822cfc8..24a46a12 100644
--- a/damus/Nostr/RelayConnection.swift
+++ b/damus/Nostr/RelayConnection.swift
@@ -97,7 +97,7 @@ final class RelayConnection: ObservableObject {
socket.send(.string(req))
}
- func send(_ req: NostrRequestType) {
+ func send(_ req: NostrRequestType, callback: ((String) -> Void)? = nil) {
switch req {
case .typical(let req):
guard let req = make_nostr_req(req) else {
@@ -105,9 +105,11 @@ final class RelayConnection: ObservableObject {
return
}
send_raw(req)
+ callback?(req)
case .custom(let req):
send_raw(req)
+ callback?(req)
}
}
@@ -201,9 +203,20 @@ func make_nostr_req(_ req: NostrRequest) -> String? {
return make_nostr_unsubscribe_req(sub_id)
case .event(let ev):
return make_nostr_push_event(ev: ev)
+ case .auth(let ev):
+ return make_nostr_auth_event(ev: ev)
}
}
+func make_nostr_auth_event(ev: NostrEvent) -> String? {
+ guard let event = encode_json(ev) else {
+ return nil
+ }
+ let encoded = "[\"AUTH\",\(event)]"
+ print(encoded)
+ return encoded
+}
+
func make_nostr_push_event(ev: NostrEvent) -> String? {
guard let event = encode_json(ev) else {
return nil
diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift
index bac13588..23a1e8c1 100644
--- a/damus/Nostr/RelayPool.swift
+++ b/damus/Nostr/RelayPool.swift
@@ -31,13 +31,17 @@ class RelayPool {
var seen: Set<SeenEvent> = Set()
var counts: [String: UInt64] = [:]
var ndb: Ndb
+ var keypair: Keypair?
+ var message_received_function: (((String, RelayDescriptor)) -> Void)?
+ var message_sent_function: (((String, Relay)) -> Void)?
private let network_monitor = NWPathMonitor()
private let network_monitor_queue = DispatchQueue(label: "io.damus.network_monitor")
private var last_network_status: NWPath.Status = .unsatisfied
- init(ndb: Ndb) {
+ init(ndb: Ndb, keypair: Keypair? = nil) {
self.ndb = ndb
+ self.keypair = keypair
network_monitor.pathUpdateHandler = { [weak self] path in
if (path.status == .satisfied || path.status == .requiresConnection) && self?.last_network_status != path.status {
@@ -121,6 +125,7 @@ class RelayPool {
else { return }
let _ = self.ndb.process_event(str)
+ self.message_received_function?((str, desc))
})
let relay = Relay(descriptor: desc, connection: conn)
self.relays.append(relay)
@@ -244,7 +249,9 @@ class RelayPool {
continue
}
- relay.connection.send(req)
+ relay.connection.send(req, callback: { str in
+ self.message_sent_function?((str, relay))
+ })
}
}
@@ -298,7 +305,34 @@ class RelayPool {
run_queue(relay_id)
}
}
-
+
+ // Handle auth
+ if case let .nostr_event(nostrResponse) = event,
+ case let .auth(challenge_string) = nostrResponse {
+ if let relay = get_relay(relay_id) {
+ print("received auth request from \(
relay.descriptor.url.id)")
+ relay.authentication_state = .pending
+ if let keypair {
+ if let fullKeypair = keypair.to_full() {
+ if let authRequest = make_auth_request(keypair: fullKeypair, challenge_string: challenge_string, relay: relay) {
+ send(.auth(authRequest), to: [relay_id], skip_ephemeral: false)
+ relay.authentication_state = .verified
+ } else {
+ print("failed to make auth request")
+ }
+ } else {
+ print("keypair provided did not contain private key, can not sign auth request")
+ relay.authentication_state = .error(.no_private_key)
+ }
+ } else {
+ print("no keypair to reply to auth request")
+ relay.authentication_state = .error(.no_key)
+ }
+ } else {
+ print("no relay found for \(relay_id)")
+ }
+ }
+
for handler in handlers {
handler.callback(relay_id, event)
}
diff --git a/damus/Notify/ReconnectRelaysNotify.swift b/damus/Notify/ReconnectRelaysNotify.swift
new file mode 100644
index 00000000..b35952d4
--- /dev/null
+++ b/damus/Notify/ReconnectRelaysNotify.swift
@@ -0,0 +1,26 @@
+//
+// ReconnectRelaysNotify.swift
+// damus
+//
+// Created by Charlie Fish on 12/18/23.
+//
+
+import Foundation
+
+struct ReconnectRelaysNotify: Notify {
+ typealias Payload = ()
+ var payload: ()
+}
+
+extension NotifyHandler {
+ static var disconnect_relays: NotifyHandler<ReconnectRelaysNotify> {
+ .init()
+ }
+}
+
+extension Notifications {
+ /// Reconnects all relays.
+ static var disconnect_relays: Notifications<ReconnectRelaysNotify> {
+ .init(.init(payload: ()))
+ }
+}
diff --git a/damus/Views/Onboarding/SuggestedUsersViewModel.swift b/damus/Views/Onboarding/SuggestedUsersViewModel.swift
index 8e91e589..f78ae7b6 100644
--- a/damus/Views/Onboarding/SuggestedUsersViewModel.swift
+++ b/damus/Views/Onboarding/SuggestedUsersViewModel.swift
@@ -97,6 +97,9 @@ class SuggestedUsersViewModel: ObservableObject {
case .ok:
break
+
+ case .auth:
+ break
}
}
}
diff --git a/damus/Views/Relays/Detail/RelayAuthenticationDetail.swift b/damus/Views/Relays/Detail/RelayAuthenticationDetail.swift
new file mode 100644
index 00000000..3e270963
--- /dev/null
+++ b/damus/Views/Relays/Detail/RelayAuthenticationDetail.swift
@@ -0,0 +1,35 @@
+//
+// RelayAuthenticationDetail.swift
+// damus
+//
+// Created by Charlie Fish on 12/18/23.
+//
+
+import SwiftUI
+
+struct RelayAuthenticationDetail: View {
+ let state: RelayAuthenticationState
+
+ var body: some View {
+ switch state {
+ case .none:
+ EmptyView()
+ case .pending:
+ Text(NSLocalizedString("Pending", comment: "Label to display that authentication to a server is pending."))
+ case .verified:
+ Text(NSLocalizedString("Authenticated", comment: "Label to display that authentication to a server has succeeded."))
+ .foregroundStyle(DamusColors.success)
+ case .error:
+ Text(NSLocalizedString("Error", comment: "Label to display that authentication to a server has failed."))
+ .foregroundStyle(DamusColors.danger)
+ }
+ }
+}
+
+struct RelayAuthenticationDetail_Previews: PreviewProvider {
+ static var previews: some View {
+ RelayAuthenticationDetail(state: .none)
+ RelayAuthenticationDetail(state: .pending)
+ RelayAuthenticationDetail(state: .verified)
+ }
+}
diff --git a/damus/Views/Relays/RelayDetailView.swift b/damus/Views/Relays/RelayDetailView.swift
index 522d76f4..c88c68b4 100644
--- a/damus/Views/Relays/RelayDetailView.swift
+++ b/damus/Views/Relays/RelayDetailView.swift
@@ -92,7 +92,14 @@ struct RelayDetailView: View {
}
}
}
-
+
+ if let authentication_state: RelayAuthenticationState = relay_object?.authentication_state,
+ authentication_state != .none {
+ Section(NSLocalizedString("Authentication", comment: "Header label to display authentication details for a given relay.")) {
+ RelayAuthenticationDetail(state: authentication_state)
+ }
+ }
+
if let pubkey = nip11?.pubkey {
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
UserViewRow(damus_state: state, pubkey: pubkey)
@@ -175,9 +182,13 @@ struct RelayDetailView: View {
}
return attrString
}
-
+
+ private var relay_object: Relay? {
+ state.pool.get_relay(relay)
+ }
+
private var relay_connection: RelayConnection? {
- state.pool.get_relay(relay)?.connection
+ relay_object?.connection
}
}
diff --git a/damus/Views/SaveKeysView.swift b/damus/Views/SaveKeysView.swift
index 27d9850b..6a657c75 100644
--- a/damus/Views/SaveKeysView.swift
+++ b/damus/Views/SaveKeysView.swift
@@ -165,6 +165,8 @@ struct SaveKeysView: View {
break
case .ok:
break
+ case .auth:
+ break
}
}
}
diff --git a/damus/damusApp.swift b/damus/damusApp.swift
index d32d57fc..f7966d79 100644
--- a/damus/damusApp.swift
+++ b/damus/damusApp.swift
@@ -43,6 +43,9 @@ struct MainView: View {
.onReceive(handle_notify(.logout)) { () in
try? clear_keypair()
keypair = nil
+ // We need to disconnect and reconnect to all relays when the user signs out
+ // This is to conform to NIP-42 and ensure we aren't persisting old connections
+ notify(.disconnect_relays)
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
orientationTracker.setDeviceMajorAxis()
diff --git a/damusTests/AuthIntegrationTests.swift b/damusTests/AuthIntegrationTests.swift
new file mode 100644
index 00000000..91005c12
--- /dev/null
+++ b/damusTests/AuthIntegrationTests.swift
@@ -0,0 +1,180 @@
+//
+// AuthIntegrationTests.swift
+// damusTests
+//
+// Created by Charlie Fish on 12/22/23.
+//
+
+import XCTest
+@testable import damus
+
+final class AuthIntegrationTests: XCTestCase {
+ func testAuthIntegrationFilterNostrWine() {
+ // Create relay pool and connect to `wss://filter.nostr.wine`
+ let relay_url = RelayURL("wss://filter.nostr.wine")!
+ var received_messages: [String] = []
+ var sent_messages: [String] = []
+ let keypair: Keypair = generate_new_keypair().to_keypair()
+ let pool = RelayPool(ndb: Ndb.test, keypair: keypair)
+ pool.message_received_function = { obj in
+ let str = obj.0
+ let descriptor = obj.1
+
+ if
descriptor.url.id !=
relay_url.id {
+ XCTFail("The descriptor we recieved the message from should equal the relayURL")
+ }
+
+ received_messages.append(str)
+ }
+ pool.message_sent_function = { obj in
+ let str = obj.0
+ let relay = obj.1
+
+ if
relay.descriptor.url.id !=
relay_url.id {
+ XCTFail("The descriptor we sent the message to should equal the relayURL")
+ }
+
+ sent_messages.append(str)
+ }
+ XCTAssertEqual(pool.relays.count, 0)
+ let relay_descriptor = RelayDescriptor.init(url: relay_url, info: .rw)
+ try! pool.add_relay(relay_descriptor)
+ XCTAssertEqual(pool.relays.count, 1)
+ let connection_expectation = XCTestExpectation(description: "Waiting for connection")
+ Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
+ if pool.num_connected == 1 {
+ connection_expectation.fulfill()
+ timer.invalidate()
+ }
+ }
+ wait(for: [connection_expectation], timeout: 30.0)
+ XCTAssertEqual(pool.num_connected, 1)
+ // Assert that AUTH message has been received
+ XCTAssertTrue(received_messages.count >= 1, "expected recieved_messages to be >= 1")
+ let json_received = try! JSONSerialization.jsonObject(with: received_messages[0].data(using: .utf8)!, options: []) as! [Any]
+ XCTAssertEqual(json_received[0] as! String, "AUTH")
+ // Assert that we've replied with the AUTH response
+ XCTAssertEqual(sent_messages.count, 1)
+ let json_sent = try! JSONSerialization.jsonObject(with: sent_messages[0].data(using: .utf8)!, options: []) as! [Any]
+ XCTAssertEqual(json_sent[0] as! String, "AUTH")
+ let sent_msg = json_sent[1] as! [String: Any]
+ XCTAssertEqual(sent_msg["kind"] as! Int, 22242)
+ XCTAssertEqual((sent_msg["tags"] as! [[String]]).first { $0[0] == "challenge" }![1], json_received[1] as! String)
+ }
+
+ func testAuthIntegrationRelayDamusIo() {
+ // Create relay pool and connect to `wss://
relay.damus.io`
+ let relay_url = RelayURL("wss://
relay.damus.io")!
+ var received_messages: [String] = []
+ var sent_messages: [String] = []
+ let keypair: Keypair = generate_new_keypair().to_keypair()
+ let pool = RelayPool(ndb: Ndb.test, keypair: keypair)
+ pool.message_received_function = { obj in
+ let str = obj.0
+ let descriptor = obj.1
+
+ if
descriptor.url.id !=
relay_url.id {
+ XCTFail("The descriptor we recieved the message from should equal the relayURL")
+ }
+
+ received_messages.append(str)
+ }
+ pool.message_sent_function = { obj in
+ let str = obj.0
+ let relay = obj.1
+
+ if
relay.descriptor.url.id !=
relay_url.id {
+ XCTFail("The descriptor we sent the message to should equal the relayURL")
+ }
+
+ sent_messages.append(str)
+ }
+ XCTAssertEqual(pool.relays.count, 0)
+ let relay_descriptor = RelayDescriptor.init(url: relay_url, info: .rw)
+ try! pool.add_relay(relay_descriptor)
+ XCTAssertEqual(pool.relays.count, 1)
+ let connection_expectation = XCTestExpectation(description: "Waiting for connection")
+ Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
+ if pool.num_connected == 1 {
+ connection_expectation.fulfill()
+ timer.invalidate()
+ }
+ }
+ wait(for: [connection_expectation], timeout: 30.0)
+ XCTAssertEqual(pool.num_connected, 1)
+ // Assert that no AUTH messages have been received
+ XCTAssertEqual(received_messages.count, 0)
+ }
+
+ func testAuthIntegrationNostrWine() {
+ // Create relay pool and connect to `wss://nostr.wine`
+ let relay_url = RelayURL("wss://nostr.wine")!
+ var received_messages: [String] = []
+ var sent_messages: [String] = []
+ let keypair: Keypair = generate_new_keypair().to_keypair()
+ let pool = RelayPool(ndb: Ndb.test, keypair: keypair)
+ pool.message_received_function = { obj in
+ let str = obj.0
+ let descriptor = obj.1
+
+ if
descriptor.url.id !=
relay_url.id {
+ XCTFail("The descriptor we recieved the message from should equal the relayURL")
+ }
+
+ received_messages.append(str)
+ }
+ pool.message_sent_function = { obj in
+ let str = obj.0
+ let relay = obj.1
+
+ if
relay.descriptor.url.id !=
relay_url.id {
+ XCTFail("The descriptor we sent the message to should equal the relayURL")
+ }
+
+ sent_messages.append(str)
+ }
+ XCTAssertEqual(pool.relays.count, 0)
+ let relay_descriptor = RelayDescriptor.init(url: relay_url, info: .rw)
+ try! pool.add_relay(relay_descriptor)
+ XCTAssertEqual(pool.relays.count, 1)
+ let connection_expectation = XCTestExpectation(description: "Waiting for connection")
+ Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
+ if pool.num_connected == 1 {
+ connection_expectation.fulfill()
+ timer.invalidate()
+ }
+ }
+ wait(for: [connection_expectation], timeout: 30.0)
+ XCTAssertEqual(pool.num_connected, 1)
+ // Assert that no AUTH messages have been received
+ XCTAssertEqual(received_messages.count, 0)
+ // Generate UUID for subscription_id
+ let uuid = UUID().uuidString
+ // Send `["REQ", subscription_id, {"kinds": [4]}]`
+ let subscribe = NostrSubscribe(filters: [
+ NostrFilter(kinds: [.dm])
+ ], sub_id: uuid)
+ pool.send(NostrRequest.subscribe(subscribe))
+ // Wait for AUTH message to have been received & sent
+ let msg_expectation = XCTestExpectation(description: "Waiting for messages")
+ Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
+ if received_messages.count >= 2 && sent_messages.count >= 2 {
+ msg_expectation.fulfill()
+ timer.invalidate()
+ }
+ }
+ wait(for: [msg_expectation], timeout: 30.0)
+ // Assert that AUTH message has been received
+ XCTAssertTrue(received_messages.count >= 1, "expected recieved_messages to be >= 1")
+ let json_received = try! JSONSerialization.jsonObject(with: received_messages[0].data(using: .utf8)!, options: []) as! [Any]
+ XCTAssertEqual(json_received[0] as! String, "AUTH")
+ // Assert that we've replied with the AUTH response
+ XCTAssertEqual(sent_messages.count, 2)
+ let json_sent = try! JSONSerialization.jsonObject(with: sent_messages[1].data(using: .utf8)!, options: []) as! [Any]
+ XCTAssertEqual(json_sent[0] as! String, "AUTH")
+ let sent_msg = json_sent[1] as! [String: Any]
+ XCTAssertEqual(sent_msg["kind"] as! Int, 22242)
+ XCTAssertEqual((sent_msg["tags"] as! [[String]]).first { $0[0] == "challenge" }![1], json_received[1] as! String)
+ }
+
+}
diff --git a/damusTests/Mocking/MockDamusState.swift b/damusTests/Mocking/MockDamusState.swift
index db52ba35..ebdd587e 100644
--- a/damusTests/Mocking/MockDamusState.swift
+++ b/damusTests/Mocking/MockDamusState.swift
@@ -13,18 +13,7 @@ func generate_test_damus_state(
mock_profile_info: [Pubkey: Profile]?
) -> DamusState {
// Create a unique temporary directory
- var tempDir: String!
- do {
- let fileManager = FileManager.default
- let temp = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString)
- try fileManager.createDirectory(at: temp, withIntermediateDirectories: true, attributes: nil)
- tempDir = temp.absoluteString
- } catch {
- tempDir = "."
- }
-
- print("opening \(tempDir!)")
- let ndb = try! Ndb(path: tempDir)!
+ let ndb = Ndb.test
let our_pubkey = test_pubkey
let pool = RelayPool(ndb: ndb)
let settings = UserSettingsStore()
diff --git a/damusTests/RequestTests.swift b/damusTests/RequestTests.swift
index 35cc0999..63977e14 100644
--- a/damusTests/RequestTests.swift
+++ b/damusTests/RequestTests.swift
@@ -16,7 +16,34 @@ final class RequestTests: XCTestCase {
let expectedResult = "[\"CLOSE\",\"64FD064D-EB9E-4771-8255-8D16981B920B\"]"
XCTAssertEqual(result, expectedResult)
}
-
+
+ func testMakeAuthRequest() {
+ let challenge_string = "8bc847dd-f2f6-4b3a-9c8a-71776ad9b071"
+ let url = RelayURL("wss://
example.com")!
+ let relayInfo = RelayInfo(read: true, write: true)
+ let relayDescriptor = RelayDescriptor(url: url, info: relayInfo)
+ let relayConnection = RelayConnection(url: url) { _ in
+ } processEvent: { _ in
+ }
+
+ let relay = Relay(descriptor: relayDescriptor, connection: relayConnection)
+ let event = make_auth_request(keypair: FullKeypair.init(pubkey: Pubkey.empty, privkey: Privkey.empty), challenge_string: challenge_string, relay: relay)!
+
+ let result = make_nostr_auth_event(ev: event)
+ let json = try! JSONSerialization.jsonObject(with: result!.data(using: .utf8)!, options: []) as! [Any]
+
+ XCTAssertEqual(json[0] as! String, "AUTH")
+ let dictionary = json[1] as! [String: Any]
+ XCTAssertEqual(dictionary["content"] as! String, "")
+ XCTAssertEqual(dictionary["kind"] as! Int, 22242)
+ XCTAssertEqual(dictionary["sig"] as! String, String(repeating: "0", count: 128))
+ XCTAssertEqual(dictionary["pubkey"] as! String, String(repeating: "0", count: 64))
+ let tags = dictionary["tags"] as! [[String]]
+ XCTAssertEqual(tags.first { $0[0] == "relay" }![1], "wss://
example.com")
+ XCTAssertEqual(tags.first { $0[0] == "challenge" }![1], challenge_string)
+ XCTAssertEqual(dictionary["id"] as! String, String(repeating: "0", count: 64))
+ }
+
/* FIXME: these tests depend on order of json fields which is undefined
func testMakePushEvent() {
let now = Int64(Date().timeIntervalSince1970)
diff --git a/damusTests/Util/NdbExtensions.swift b/damusTests/Util/NdbExtensions.swift
new file mode 100644
index 00000000..b488656c
--- /dev/null
+++ b/damusTests/Util/NdbExtensions.swift
@@ -0,0 +1,26 @@
+//
+// NdbExtensions.swift
+// damusTests
+//
+// Created by Charlie Fish on 12/23/23.
+//
+
+import Foundation
+@testable import damus
+
+extension Ndb {
+ static var test: Ndb {
+ var tempDir: String!
+ do {
+ let fileManager = FileManager.default
+ let temp = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ try fileManager.createDirectory(at: temp, withIntermediateDirectories: true, attributes: nil)
+ tempDir = temp.absoluteString
+ } catch {
+ tempDir = "."
+ }
+
+ print("opening \(tempDir!)")
+ return Ndb(path: tempDir)!
+ }
+}
diff --git a/nostrdb/nostrdb.c b/nostrdb/nostrdb.c
index 6012adc4..efa889ba 100644
--- a/nostrdb/nostrdb.c
+++ b/nostrdb/nostrdb.c
@@ -4016,6 +4016,17 @@ int ndb_ws_event_from_json(const char *json, int len, struct ndb_tce *tce,
tce->command_result.msg = json + tok->start;
tce->command_result.msglen = toksize(tok);
+ return 1;
+ } else if (tok_len == 4 && !memcmp("AUTH", json + tok->start, 4)) {
+ tce->evtype = NDB_TCE_AUTH;
+
+ tok = &parser.toks[parser.i++];
+ if (tok->type != JSMN_STRING)
+ return 0;
+
+ tce->subid = json + tok->start;
+ tce->subid_len = toksize(tok);
+
return 1;
}
diff --git a/nostrdb/nostrdb.h b/nostrdb/nostrdb.h
index c21b4b17..483d3edc 100644
--- a/nostrdb/nostrdb.h
+++ b/nostrdb/nostrdb.h
@@ -103,6 +103,7 @@ enum tce_type {
NDB_TCE_OK = 0x2,
NDB_TCE_NOTICE = 0x3,
NDB_TCE_EOSE = 0x4,
+ NDB_TCE_AUTH = 0x5,
};
enum ndb_ingest_filter_action {
diff --git a/nostrscript/NostrScript.swift b/nostrscript/NostrScript.swift
index 39c5c43a..8dfc4fa6 100644
--- a/nostrscript/NostrScript.swift
+++ b/nostrscript/NostrScript.swift
@@ -193,7 +193,8 @@ enum NScriptEventType: Int {
case note = 2
case notice = 3
case eose = 4
-
+ case auth = 5
+
init(resp: NostrResponse) {
switch resp {
case .event:
@@ -204,6 +205,8 @@ enum NScriptEventType: Int {
self = .eose
case .ok:
self = .ok
+ case .auth:
+ self = .auth
}
}
}
--
2.39.3 (Apple Git-145)