1
0
mirror of git://jb55.com/damus synced 2024-09-30 00:40:45 +00:00

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>
This commit is contained in:
Jericho Hasselbush 2023-10-11 08:17:28 -04:00 committed by William Casarin
parent cf243e39c9
commit 439f9974c5
4 changed files with 194 additions and 35 deletions

View File

@ -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 */,

View File

@ -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)
save_pubkey(pubkey: pubkey)
} else if let privkey = hex_decode_privkey(hexstr) {
try handle_privkey(privkey)
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) {
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 {

View File

@ -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 })
}

View File

@ -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
}
}
}
}