Add private key validation and storage
This commit is contained in:
@@ -14,11 +14,11 @@
|
||||
"comment" : "Button to create a key."
|
||||
},
|
||||
"Done" : {
|
||||
"comment" : "Button to go to the next view that adds the user's entered private key."
|
||||
},
|
||||
"Got it!" : {
|
||||
"comment" : "Button to go to the next view that adds the user’s entered private key."
|
||||
},
|
||||
"Home" : {
|
||||
"comment" : "Navigation title of home view."
|
||||
},
|
||||
"Hooo-raaaayyy!" : {
|
||||
"comment" : "Title of view that confirms the user’s selection of the signing policy."
|
||||
},
|
||||
@@ -39,6 +39,9 @@
|
||||
},
|
||||
"Select a signing policy" : {
|
||||
"comment" : "Title of view to select a signing policy."
|
||||
},
|
||||
"Should I approve Nostr events automatically or would you like to review them for each app?" : {
|
||||
|
||||
},
|
||||
"Yeti: Nostr Helper" : {
|
||||
"comment" : "Application title."
|
||||
@@ -46,7 +49,7 @@
|
||||
"You’re all set! I’ll take care of most of the approval for you.\nIf I see an event I don’t recognize, I’ll ask you to review it." : {
|
||||
"comment" : "Description of what the user should expect after selecting the basic signing policy."
|
||||
},
|
||||
"You’re set for now. You’ll need to come back here with every new app and approve some nostr events." : {
|
||||
"You’re set for now. You’ll need to come back here with every new app and approve some Nostr events." : {
|
||||
"comment" : "Description of what the user should expect after selecting the manual signing policy."
|
||||
},
|
||||
"Your private key is stored locally. Only you can see it." : {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// Item.swift
|
||||
// Yeti
|
||||
//
|
||||
// Created by Terry Yiu on 1/19/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Item {
|
||||
var timestamp: Date
|
||||
|
||||
init(timestamp: Date) {
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
}
|
||||
60
Yeti/Model/SigningPolicyModel.swift
Normal file
60
Yeti/Model/SigningPolicyModel.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// SigningPolicyModel.swift
|
||||
// Yeti
|
||||
//
|
||||
// Created by Terry Yiu on 1/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class SigningPolicyModel {
|
||||
var signingPolicy: SigningPolicy
|
||||
|
||||
init(signingPolicy: SigningPolicy) {
|
||||
self.signingPolicy = signingPolicy
|
||||
}
|
||||
}
|
||||
|
||||
enum SigningPolicy: Int, Codable, CaseIterable {
|
||||
case basic = 0
|
||||
case manual = 1
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .basic:
|
||||
return String(
|
||||
localized: "Approve basic actions",
|
||||
comment: "Name of event signing policy that approves basic actions."
|
||||
)
|
||||
case .manual:
|
||||
return String(
|
||||
localized: "Manually approve each app",
|
||||
comment: "Name of event signing policy that requires manual approval to sign each event."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .basic:
|
||||
return String(
|
||||
localized:
|
||||
"""
|
||||
Recommended for most people. This policy will minimize the number of interruptions during your app usage.
|
||||
""",
|
||||
comment: "Description of event signing policy that approves basic actions."
|
||||
)
|
||||
case .manual:
|
||||
return String(
|
||||
localized:
|
||||
"""
|
||||
Recommended for privacy-minded people who would like control over each app.
|
||||
Choosing this policy will prompt you to set a preference every time you try a new app.
|
||||
""",
|
||||
comment: "Description of event signing policy that requires manual approval to sign each event."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
72
Yeti/Utilities/PrivateKeySecureStorage.swift
Normal file
72
Yeti/Utilities/PrivateKeySecureStorage.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// PrivateKeySecureStorage.swift
|
||||
// Yeti
|
||||
//
|
||||
// Created by Terry Yiu on 1/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import NostrSDK
|
||||
|
||||
class PrivateKeySecureStorage {
|
||||
|
||||
static let shared = PrivateKeySecureStorage()
|
||||
|
||||
private let service = "yeti-private-keys"
|
||||
|
||||
func keypair(for publicKey: PublicKey) -> Keypair? {
|
||||
let query = [
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: publicKey.hex,
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecReturnData: true,
|
||||
kSecMatchLimit: kSecMatchLimitOne
|
||||
] as [CFString: Any] as CFDictionary
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query, &result)
|
||||
|
||||
if status == errSecSuccess,
|
||||
let data = result as? Data,
|
||||
let privateKeyHex = String(data: data, encoding: .utf8) {
|
||||
return Keypair(hex: privateKeyHex)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func store(for keypair: Keypair) {
|
||||
let query = [
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: keypair.publicKey.hex,
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecValueData: keypair.privateKey.hex.data(using: .utf8) as Any
|
||||
] as [CFString: Any] as CFDictionary
|
||||
|
||||
var status = SecItemAdd(query, nil)
|
||||
|
||||
if status == errSecDuplicateItem {
|
||||
let query = [
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: keypair.publicKey.hex,
|
||||
kSecClass: kSecClassGenericPassword
|
||||
] as [CFString: Any] as CFDictionary
|
||||
|
||||
let updates = [
|
||||
kSecValueData: keypair.privateKey.hex.data(using: .utf8) as Any
|
||||
] as CFDictionary
|
||||
|
||||
status = SecItemUpdate(query, updates)
|
||||
}
|
||||
}
|
||||
|
||||
func delete(for publicKey: PublicKey) {
|
||||
let query = [
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: publicKey.hex,
|
||||
kSecClass: kSecClassGenericPassword
|
||||
] as [CFString: Any] as CFDictionary
|
||||
|
||||
_ = SecItemDelete(query)
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,15 @@
|
||||
// Created by Terry Yiu on 1/19/25.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import NostrSDK
|
||||
import SwiftUI
|
||||
|
||||
struct AddKeyView: View {
|
||||
@State private var key: String = ""
|
||||
@State private var keypair: Keypair?
|
||||
@State private var nostrIdentifier: String = ""
|
||||
|
||||
@State private var navigationDestinationPresented: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -26,8 +31,17 @@ struct AddKeyView: View {
|
||||
localized: "nsec / private key",
|
||||
comment: "Prompt asking user to enter in a Nostr private key."
|
||||
),
|
||||
text: $key
|
||||
text: $nostrIdentifier
|
||||
)
|
||||
.autocorrectionDisabled(false)
|
||||
.textContentType(.password)
|
||||
.textInputAutocapitalization(.never)
|
||||
.onReceive(Just(nostrIdentifier)) { newValue in
|
||||
let filtered = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
nostrIdentifier = filtered
|
||||
|
||||
self.keypair = Keypair(nsec: filtered)
|
||||
}
|
||||
},
|
||||
footer: {
|
||||
Text(
|
||||
@@ -40,12 +54,26 @@ Footer text explaining that the private key is stored locally and only the user
|
||||
}
|
||||
)
|
||||
|
||||
NavigationLink(destination: SigningPolicySelectionView()) {
|
||||
Text("Next", comment: "Button to go to the next view that adds the user’s entered private key.")
|
||||
Button("Next", action: {
|
||||
navigationDestinationPresented = true
|
||||
})
|
||||
.disabled(!validPrivateKey)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
.listRowInsets(EdgeInsets())
|
||||
.background(Color(UIColor.systemGroupedBackground))
|
||||
}
|
||||
.navigationDestination(isPresented: $navigationDestinationPresented) {
|
||||
if let keypair {
|
||||
SigningPolicySelectionView(keypair: keypair)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var validPrivateKey: Bool {
|
||||
keypair != nil
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
||||
@@ -37,10 +37,12 @@ struct ContentView: View {
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.navigationTitle(String(localized: "Home", comment: "Navigation title of home view."))
|
||||
.toolbar(removing: .title)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
.modelContainer(for: Item.self, inMemory: true)
|
||||
.modelContainer(for: SigningPolicyModel.self, inMemory: true)
|
||||
}
|
||||
|
||||
@@ -24,15 +24,18 @@ If I see an event I don’t recognize, I’ll ask you to review it.
|
||||
""",
|
||||
comment: "Description of what the user should expect after selecting the basic signing policy."
|
||||
)
|
||||
.font(.caption)
|
||||
case .manual:
|
||||
Text(
|
||||
"""
|
||||
You’re set for now. You’ll need to come back here with every new app and approve some nostr events.
|
||||
You’re set for now. You’ll need to come back here with every new app and approve some Nostr events.
|
||||
""",
|
||||
comment: "Description of what the user should expect after selecting the manual signing policy."
|
||||
)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.toolbar(.hidden)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
// Created by Terry Yiu on 1/20/25.
|
||||
//
|
||||
|
||||
import NostrSDK
|
||||
import SwiftUI
|
||||
|
||||
struct SigningPolicySelectionView: View {
|
||||
let keypair: Keypair
|
||||
|
||||
@State var signingPolicy: SigningPolicy = .basic
|
||||
|
||||
@State private var navigationDestinationPresented: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
@@ -19,6 +24,12 @@ struct SigningPolicySelectionView: View {
|
||||
.listRowInsets(EdgeInsets())
|
||||
.background(Color(UIColor.systemGroupedBackground))
|
||||
|
||||
Text("Should I approve Nostr events automatically or would you like to review them for each app?")
|
||||
.font(.caption)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
.listRowInsets(EdgeInsets())
|
||||
.background(Color(UIColor.systemGroupedBackground))
|
||||
|
||||
Section(
|
||||
content: {
|
||||
Picker(
|
||||
@@ -40,56 +51,22 @@ struct SigningPolicySelectionView: View {
|
||||
}
|
||||
)
|
||||
|
||||
NavigationLink(destination: SigningPolicyConfirmationView(signingPolicy: signingPolicy)) {
|
||||
Text("Done", comment: "Button to go to the next view that adds the user’s entered private key.")
|
||||
}
|
||||
Button("Done", action: {
|
||||
PrivateKeySecureStorage.shared.store(for: keypair)
|
||||
navigationDestinationPresented = true
|
||||
})
|
||||
.buttonStyle(.borderedProminent)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
.listRowInsets(EdgeInsets())
|
||||
.background(Color(UIColor.systemGroupedBackground))
|
||||
}
|
||||
.navigationDestination(isPresented: $navigationDestinationPresented) {
|
||||
SigningPolicyConfirmationView(signingPolicy: signingPolicy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SigningPolicy: CaseIterable {
|
||||
case basic
|
||||
case manual
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .basic:
|
||||
return String(
|
||||
localized: "Approve basic actions",
|
||||
comment: "Name of event signing policy that approves basic actions."
|
||||
)
|
||||
case .manual:
|
||||
return String(
|
||||
localized: "Manually approve each app",
|
||||
comment: "Name of event signing policy that requires manual approval to sign each event."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .basic:
|
||||
return String(
|
||||
localized:
|
||||
"""
|
||||
Recommended for most people. This policy will minimize the number of interruptions during your app usage.
|
||||
""",
|
||||
comment: "Description of event signing policy that approves basic actions."
|
||||
)
|
||||
case .manual:
|
||||
return String(
|
||||
localized:
|
||||
"""
|
||||
Recommended for privacy-minded people who would like control over each app.
|
||||
Choosing this policy will prompt you to set a preference every time you try a new app.
|
||||
""",
|
||||
comment: "Description of event signing policy that requires manual approval to sign each event."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SigningPolicySelectionView()
|
||||
SigningPolicySelectionView(keypair: Keypair()!)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ struct YetiApp: App {
|
||||
|
||||
var sharedModelContainer: ModelContainer = {
|
||||
let schema = Schema([
|
||||
Item.self
|
||||
SigningPolicyModel.self
|
||||
])
|
||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user