Add private key validation and storage
This commit is contained in:
@@ -14,11 +14,11 @@
|
|||||||
"comment" : "Button to create a key."
|
"comment" : "Button to create a key."
|
||||||
},
|
},
|
||||||
"Done" : {
|
"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."
|
"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!" : {
|
"Hooo-raaaayyy!" : {
|
||||||
"comment" : "Title of view that confirms the user’s selection of the signing policy."
|
"comment" : "Title of view that confirms the user’s selection of the signing policy."
|
||||||
},
|
},
|
||||||
@@ -39,6 +39,9 @@
|
|||||||
},
|
},
|
||||||
"Select a signing policy" : {
|
"Select a signing policy" : {
|
||||||
"comment" : "Title of view to 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" : {
|
"Yeti: Nostr Helper" : {
|
||||||
"comment" : "Application title."
|
"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." : {
|
"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."
|
"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."
|
"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." : {
|
"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.
|
// Created by Terry Yiu on 1/19/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import NostrSDK
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AddKeyView: View {
|
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 {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@@ -26,8 +31,17 @@ struct AddKeyView: View {
|
|||||||
localized: "nsec / private key",
|
localized: "nsec / private key",
|
||||||
comment: "Prompt asking user to enter in a Nostr 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: {
|
footer: {
|
||||||
Text(
|
Text(
|
||||||
@@ -40,12 +54,26 @@ Footer text explaining that the private key is stored locally and only the user
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
NavigationLink(destination: SigningPolicySelectionView()) {
|
Button("Next", action: {
|
||||||
Text("Next", comment: "Button to go to the next view that adds the user’s entered private key.")
|
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 {
|
#Preview {
|
||||||
|
|||||||
@@ -37,10 +37,12 @@ struct ContentView: View {
|
|||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navigationTitle(String(localized: "Home", comment: "Navigation title of home view."))
|
||||||
|
.toolbar(removing: .title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ContentView()
|
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."
|
comment: "Description of what the user should expect after selecting the basic signing policy."
|
||||||
)
|
)
|
||||||
|
.font(.caption)
|
||||||
case .manual:
|
case .manual:
|
||||||
Text(
|
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."
|
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.
|
// Created by Terry Yiu on 1/20/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import NostrSDK
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SigningPolicySelectionView: View {
|
struct SigningPolicySelectionView: View {
|
||||||
|
let keypair: Keypair
|
||||||
|
|
||||||
@State var signingPolicy: SigningPolicy = .basic
|
@State var signingPolicy: SigningPolicy = .basic
|
||||||
|
|
||||||
|
@State private var navigationDestinationPresented: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
@@ -19,6 +24,12 @@ struct SigningPolicySelectionView: View {
|
|||||||
.listRowInsets(EdgeInsets())
|
.listRowInsets(EdgeInsets())
|
||||||
.background(Color(UIColor.systemGroupedBackground))
|
.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(
|
Section(
|
||||||
content: {
|
content: {
|
||||||
Picker(
|
Picker(
|
||||||
@@ -40,56 +51,22 @@ struct SigningPolicySelectionView: View {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
NavigationLink(destination: SigningPolicyConfirmationView(signingPolicy: signingPolicy)) {
|
Button("Done", action: {
|
||||||
Text("Done", comment: "Button to go to the next view that adds the user’s entered private key.")
|
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 {
|
#Preview {
|
||||||
SigningPolicySelectionView()
|
SigningPolicySelectionView(keypair: Keypair()!)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct YetiApp: App {
|
|||||||
|
|
||||||
var sharedModelContainer: ModelContainer = {
|
var sharedModelContainer: ModelContainer = {
|
||||||
let schema = Schema([
|
let schema = Schema([
|
||||||
Item.self
|
SigningPolicyModel.self
|
||||||
])
|
])
|
||||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user