damus

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

commit 439f9974c5e99a7b3626b9a3a02b1ffbf38e6792
parent cf243e39c928f37d59d6078da42d1caab3a597c3
Author: Jericho Hasselbush <jericho@sal-et-lucem.com>
Date:   Wed, 11 Oct 2023 08:17:28 -0400

login: add nsec qr-scanning

- Allow scanning of QR codes, and if detects a nsec, will provide it to
  the login prompt.

- If nsec is found, provides option to keep nsec in keychain; default is
  to not store

- User stays logged in until they logout, or app is force-quit if nsec
  is not stored.

damusApp.swift:
  Obtains keypair from the notification generated to allow login.

LoginView.swift:
  New views allowing for adding and logic handling the QR reader in
  QRScanNSECView.swift to enable QR scan for nsec.

QRScanNSECView.swift:
  New view to scan for QR code. The sparkling magnifying glass is enabled
  if the view calling the QR view changes the privKeyFound bound variable.

Tipjar: npub1el277q4kesp8vhs7rq6qkwnhpxfp345u7tnuxykwr67d9wg0wvyslam5n0
Closes: https://github.com/damus-io/damus/issues/1291
Changelog-Added: Add QR scan nsec logins.
Signed-off-by: Jericho Hasselbush <jericho@sal-et-lucem.com>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/Views/LoginView.swift | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Adamus/Views/QRScanNSECView.swift | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/damusApp.swift | 3+++
4 files changed, 194 insertions(+), 35 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -419,6 +419,7 @@ 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; }; 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 */; }; 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 */; }; @@ -1113,6 +1114,7 @@ 9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; @@ -1685,6 +1687,7 @@ 4C3AC79E2833115300E1F516 /* FollowButtonView.swift */, 4C3AC79C2833036D00E1F516 /* FollowingView.swift */, 4C90BD17283A9EE5008EE7EF /* LoginView.swift */, + ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */, 4C363A8D28236FE4006E126D /* NoteContentView.swift */, 4C75EFAC28049CFB0006080F /* PostButton.swift */, 4C75EFA327FA577B0006080F /* PostView.swift */, @@ -2564,6 +2567,7 @@ 4C4793042A993DC000489948 /* midl.c in Sources */, 4C4793012A993CDA00489948 /* mdb.c in Sources */, 4CE9FBBA2A6B3C63007E485C /* nostrdb.c in Sources */, + ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */, 4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */, 5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */, 4C32B9522A9AD44700DC3548 /* Message.swift in Sources */, diff --git a/damus/Views/LoginView.swift b/damus/Views/LoginView.swift @@ -30,6 +30,13 @@ enum ParsedKey { } return false } + + var is_priv: Bool { + if case .priv = self { + return true + } + return false + } } struct LoginView: View { @@ -37,6 +44,7 @@ struct LoginView: View { @State var is_pubkey: Bool = false @State var error: String? = nil @State private var credential_handler = CredentialHandler() + @State private var shouldSaveKey: Bool = true var nav: NavigationCoordinator func get_error(parsed_key: ParsedKey?) -> String? { @@ -57,7 +65,7 @@ struct LoginView: View { SignInHeader() .padding(.top, 100) - SignInEntry(key: $key) + SignInEntry(key: $key, shouldSaveKey: $shouldSaveKey) let parsed = parse_key(key) @@ -83,7 +91,7 @@ struct LoginView: View { Button(action: { Task { do { - try await process_login(p, is_pubkey: is_pubkey) + try await process_login(p, is_pubkey: is_pubkey, shouldSaveKey: shouldSaveKey) } catch { self.error = error.localizedDescription } @@ -168,37 +176,39 @@ enum LoginError: LocalizedError { } } -func process_login(_ key: ParsedKey, is_pubkey: Bool) async throws { - switch key { - case .priv(let priv): - try handle_privkey(priv) - case .pub(let pub): - try clear_saved_privkey() - save_pubkey(pubkey: pub) - - case .nip05(let id): - guard let nip05 = await get_nip05_pubkey(id: id) else { - throw LoginError.nip05_failed - } - - // this is a weird way to login anyways - /* - var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey) - for relay in nip05.relays { - if !(bootstrap_relays.contains { $0 == relay }) { - bootstrap_relays.append(relay) - } - } - */ - save_pubkey(pubkey: nip05.pubkey) - - case .hex(let hexstr): - if is_pubkey, let pubkey = hex_decode_pubkey(hexstr) { +func process_login(_ key: ParsedKey, is_pubkey: Bool, shouldSaveKey: Bool = true) async throws { + if shouldSaveKey { + switch key { + case .priv(let priv): + try handle_privkey(priv) + case .pub(let pub): try clear_saved_privkey() + save_pubkey(pubkey: pub) + + case .nip05(let id): + guard let nip05 = await get_nip05_pubkey(id: id) else { + throw LoginError.nip05_failed + } - save_pubkey(pubkey: pubkey) - } else if let privkey = hex_decode_privkey(hexstr) { - try handle_privkey(privkey) + // this is a weird way to login anyways + /* + var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey) + for relay in nip05.relays { + if !(bootstrap_relays.contains { $0 == relay }) { + bootstrap_relays.append(relay) + } + } + */ + save_pubkey(pubkey: nip05.pubkey) + + case .hex(let hexstr): + if is_pubkey, let pubkey = hex_decode_pubkey(hexstr) { + try clear_saved_privkey() + + save_pubkey(pubkey: pubkey) + } else if let privkey = hex_decode_privkey(hexstr) { + try handle_privkey(privkey) + } } } @@ -213,7 +223,16 @@ func process_login(_ key: ParsedKey, is_pubkey: Bool) async throws { save_pubkey(pubkey: pk) } - guard let keypair = get_saved_keypair() else { + func handle_transient_privkey(_ key: ParsedKey) -> Keypair? { + if case let .priv(priv) = key, let pubkey = privkey_to_pubkey(privkey: priv) { + return Keypair(pubkey: pubkey, privkey: priv) + } + return nil + } + + let keypair = shouldSaveKey ? get_saved_keypair() : handle_transient_privkey(key) + + guard let keypair = keypair else { return } @@ -265,11 +284,15 @@ func get_nip05_pubkey(id: String) async -> NIP05User? { struct KeyInput: View { let title: String let key: Binding<String> + let shouldSaveKey: Binding<Bool> + var privKeyFound: Binding<Bool> @State private var is_secured: Bool = true - init(_ title: String, key: Binding<String>) { + init(_ title: String, key: Binding<String>, shouldSaveKey: Binding<Bool>, privKeyFound: Binding<Bool>) { self.title = title self.key = key + self.shouldSaveKey = shouldSaveKey + self.privKeyFound = privKeyFound } var body: some View { @@ -281,6 +304,8 @@ struct KeyInput: View { self.key.wrappedValue = pastedkey } } + SignInScan(shouldSaveKey: shouldSaveKey, loginKey: key, privKeyFound: privKeyFound) + if is_secured { SecureField("", text: key) .nsecLoginStyle(key: key.wrappedValue, title: title) @@ -323,18 +348,79 @@ struct SignInHeader: View { struct SignInEntry: View { let key: Binding<String> - + let shouldSaveKey: Binding<Bool> + @State private var privKeyFound: Bool = false var body: some View { VStack(alignment: .leading) { Text("Enter your account key", comment: "Prompt for user to enter an account key to login.") .fontWeight(.medium) .padding(.top, 30) - KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."), key: key) + KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."), + key: key, + shouldSaveKey: shouldSaveKey, + privKeyFound: $privKeyFound) + if privKeyFound { + Toggle("Save Key in Secure Keychain", isOn: shouldSaveKey) + } } } } +struct SignInScan: View { + @State var showQR: Bool = false + @State var qrkey: ParsedKey? + @Binding var shouldSaveKey: Bool + @Binding var loginKey: String + @Binding var privKeyFound: Bool + let generator = UINotificationFeedbackGenerator() + + var body: some View { + VStack { + Button(action: { showQR.toggle() }, label: { + Image(systemName: "qrcode.viewfinder")}) + .foregroundColor(.gray) + + } + .sheet(isPresented: $showQR, onDismiss: { + if qrkey == nil { resetView() }} + ) { + QRScanNSECView(showQR: $showQR, + privKeyFound: $privKeyFound, + codeScannerCompletion: { scannerCompletion($0) }) + } + .onChange(of: showQR) { show in + if showQR { resetView() } + } + } + + func handleQRString(_ string: String) { + qrkey = parse_key(string) + if let key = qrkey, key.is_priv { + loginKey = string + privKeyFound = true + shouldSaveKey = false + generator.notificationOccurred(.success) + } + } + + func scannerCompletion(_ result: Result<ScanResult, ScanError>) { + switch result { + case .success(let success): + handleQRString(success.string) + case .failure: + return + } + } + + func resetView() { + loginKey = "" + qrkey = nil + privKeyFound = false + shouldSaveKey = true + } +} + struct CreateAccountPrompt: View { var nav: NavigationCoordinator var body: some View { diff --git a/damus/Views/QRScanNSECView.swift b/damus/Views/QRScanNSECView.swift @@ -0,0 +1,66 @@ +// +// QRScanNSECView.swift +// damus +// +// Created by Jericho Hasselbush on 9/29/23. +// + +import SwiftUI +import VisionKit + +struct QRScanNSECView: View { + @Binding var showQR: Bool + @Binding var privKeyFound: Bool + var codeScannerCompletion: (Result<ScanResult, ScanError>) -> Void + var body: some View { + ZStack { + ZStack { + DamusGradient() + } + VStack { + Text("Scan Your Private Key QR", + comment: "Text to prompt scanning a QR code of a user's privkey to login to their profile.") + .padding(.top, 50) + .font(.system(size: 24, weight: .heavy)) + + Spacer() + CodeScannerView(codeTypes: [.qr], + scanMode: .continuous, + scanInterval: 2.0, + showViewfinder: false, + simulatedData: "", + shouldVibrateOnSuccess: false, + isTorchOn: false, + isGalleryPresented: .constant(false), + videoCaptureDevice: .default(for: .video), + completion: codeScannerCompletion) + .scaledToFit() + .frame(width: 300, height: 300) + .cornerRadius(10) + .overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0)) + .shadow(radius: 10) + + Button(action: { showQR = false }) { + VStack { + Image(systemName: privKeyFound ? "sparkle.magnifyingglass" : "magnifyingglass") + .font(privKeyFound ? .title : .title3) + }} + .padding(.top) + .buttonStyle(GradientButtonStyle()) + + Spacer() + + Spacer() + } + } + } +} + +#Preview { + @State var showQR = true + @State var privKeyFound = false + @State var shouldSaveKey = true + return QRScanNSECView(showQR: $showQR, + privKeyFound: $privKeyFound, + codeScannerCompletion: { _ in }) +} diff --git a/damus/damusApp.swift b/damus/damusApp.swift @@ -32,6 +32,9 @@ struct MainView: View { .onReceive(handle_notify(.login)) { notif in needs_setup = false keypair = get_saved_keypair() + if keypair == nil, let tempkeypair = notif.to_full()?.to_keypair() { + keypair = tempkeypair + } } } }