commit 5642f537c0f1da2ce610ac0139e95075b3f022dd
parent cdd53a3d82c49dbd5901e907fa605b80b543e638
Author: triesap <tyson@radroots.org>
Date: Sat, 13 Jun 2026 02:21:06 -0700
app: harden nostr key custody state
Diffstat:
10 files changed, 262 insertions(+), 45 deletions(-)
diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj
@@ -38,6 +38,7 @@
C8AB3389F7430A5C79AD7DF8 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */; };
D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */; };
D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A4289F43625DD65E6C4B25 /* CopyRow.swift */; };
+ D4CFDE54747B6D6957977025 /* FieldIdentityPublicMetadataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */; };
D5C58A98C950D45AD027962A /* TradeListing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D69D200DB1F5FA7AA561CD7 /* TradeListing.swift */; };
D62E9461833A0AA5E622A1E6 /* ToastModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227028B4EBDC6703999FB9DA /* ToastModifier.swift */; };
DCE468F668A3C346E716B04C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CCF0F7B3C57D8D770F178329 /* Assets.xcassets */; };
@@ -84,6 +85,7 @@
C1D9496F9F05A4E79E73A247 /* RelaysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaysView.swift; sourceTree = "<group>"; };
C71A93F98C7B93188748B99B /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
CA8AAF0C0F1723860A8481E0 /* PostCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCreateView.swift; sourceTree = "<group>"; };
+ CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldIdentityPublicMetadataStore.swift; sourceTree = "<group>"; };
CBE1472FFD63A33F3AEA6C6C /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
CCF0F7B3C57D8D770F178329 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D448C9655B708CA3FA8712B9 /* AppEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEntry.swift; sourceTree = "<group>"; };
@@ -172,6 +174,7 @@
children = (
A71EFADBC7D54AF5B9314773 /* BuildConfig.swift */,
F60FA44F589B6E94223CE685 /* FieldIdentityCustodyStore.swift */,
+ CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */,
E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */,
D4DE3DD8C3BB2F63676F463E /* LoggingSettings.swift */,
63189EB90A86A9929BECD9ED /* Nostr.swift */,
@@ -398,6 +401,7 @@
D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */,
4025E63F6603011431B8A0E1 /* DebugDump.swift in Sources */,
3DA8DBB426B71C53724F09B4 /* FieldIdentityCustodyStore.swift in Sources */,
+ D4CFDE54747B6D6957977025 /* FieldIdentityPublicMetadataStore.swift in Sources */,
D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */,
1E5B41A3E1F9A7D68F63B079 /* HomeView.swift in Sources */,
C512BD267A3E2B8F10FABB3B /* Logger.swift in Sources */,
diff --git a/Radroots/App/AppState.swift b/Radroots/App/AppState.swift
@@ -30,6 +30,8 @@ public final class AppState: ObservableObject {
@Published public private(set) var bootstrapPhase: BootstrapPhase = .idle
@Published public private(set) var infoJSONString: String = ""
@Published public private(set) var hasKey: Bool = false
+ @Published public private(set) var storedIdentityAvailable: Bool = false
+ @Published public private(set) var runtimeIdentityReady: Bool = false
@Published public private(set) var isLocked: Bool = false
@Published public private(set) var npub: String?
@Published public private(set) var identityLabel: String?
@@ -40,11 +42,11 @@ public final class AppState: ObservableObject {
@Published public private(set) var relayLastError: String?
public var canShowAppContent: Bool {
- bootstrapPhase == .ready && hasKey && !isLocked
+ bootstrapPhase == .ready && runtimeIdentityReady && !isLocked
}
public var requiresSetup: Bool {
- bootstrapPhase == .ready && (!hasKey || isLocked)
+ bootstrapPhase == .ready && (!storedIdentityAvailable || isLocked || !runtimeIdentityReady)
}
public var identityDisplayName: String {
@@ -67,6 +69,7 @@ public final class AppState: ObservableObject {
private let lockKey = "field_ios.identity_locked"
private var statusTask: Task<Void, Never>?
private var identityCustodyStore: FieldIdentityCustodyStore?
+ private var identityMetadataStore: FieldIdentityPublicMetadataStore?
public init(radroots: Radroots = Radroots()) {
self.radroots = radroots
@@ -86,16 +89,20 @@ public final class AppState: ObservableObject {
}
let service = try radroots.start()
let custodyStore = try FieldIdentityCustodyStore.configured()
+ let metadataStore = try FieldIdentityPublicMetadataStore.configured()
identityCustodyStore = custodyStore
+ identityMetadataStore = metadataStore
if BuildConfig.bool(.resetLocalState) == true {
try custodyStore.resetLocalState(bundleIdentifier: try bundleIdentifier())
- try await removeAllIdentities(using: service)
+ metadataStore.delete()
+ try await clearRuntimeIdentityState(using: service)
+ applyNoIdentity()
setLocked(false)
} else {
- try await restorePersistedIdentity(using: service, custodyStore: custodyStore)
+ loadStoredIdentityMetadata(metadataStore)
}
try await refreshRuntimeState(using: service)
- if hasKey && !isLocked {
+ if runtimeIdentityReady && !isLocked {
try await connect(using: service)
startPollingStatus()
}
@@ -124,7 +131,7 @@ public final class AppState: ObservableObject {
public func continueWithLocalIdentity() async throws {
let service = try requireRuntimeService()
- try await persistSelectedIdentity(using: service)
+ try await restoreStoredIdentity(using: service)
setLocked(false)
try await connect(using: service)
await refreshRuntimeState(using: service)
@@ -133,8 +140,7 @@ public final class AppState: ObservableObject {
public func createLocalIdentity() async throws {
let service = try requireRuntimeService()
- _ = try await service.nostrIdentityGenerate(label: "Radroots Field", makeSelected: true)
- try await persistSelectedIdentity(using: service)
+ try await createHostCustodyIdentity(using: service)
setLocked(false)
try await connect(using: service)
await refreshRuntimeState(using: service)
@@ -145,12 +151,12 @@ public final class AppState: ObservableObject {
let trimmed = secretKey.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
let service = try requireRuntimeService()
- _ = try await service.nostrIdentityImportSecret(
+ let record = try await service.nostrIdentityImportSecret(
secretKey: trimmed,
label: "Imported Field Identity",
makeSelected: true
)
- try await persistSelectedIdentity(using: service)
+ try persistIdentity(record, secret: trimmed)
setLocked(false)
try await connect(using: service)
await refreshRuntimeState(using: service)
@@ -161,12 +167,20 @@ public final class AppState: ObservableObject {
setLocked(true)
statusTask?.cancel()
statusTask = nil
+ relayConnectedCount = 0
+ relayConnectingCount = 0
+ relayLight = .red
+ Task {
+ await lockRuntimeIdentity()
+ }
}
public func resetLocalIdentity() async throws {
let service = try requireRuntimeService()
try identityCustodyStoreOrConfigured().resetLocalState(bundleIdentifier: try bundleIdentifier())
- try await removeAllIdentities(using: service)
+ try identityMetadataStoreOrConfigured().delete()
+ try await clearRuntimeIdentityState(using: service)
+ applyNoIdentity()
setLocked(false)
relayConnectedCount = 0
relayConnectingCount = 0
@@ -250,39 +264,112 @@ public final class AppState: ObservableObject {
}
private func apply(identity snapshot: NostrIdentitySnapshot) {
- hasKey = snapshot.hasSelectedSigningIdentity
- npub = snapshot.selectedNpub
+ runtimeIdentityReady = snapshot.hasSelectedSigningIdentity
identities = snapshot.identities
- identityLabel = snapshot.identities.first(where: { $0.isSelected })?.label
+ if snapshot.hasSelectedSigningIdentity {
+ storedIdentityAvailable = true
+ hasKey = true
+ npub = snapshot.selectedNpub
+ identityLabel = snapshot.identities.first(where: { $0.isSelected })?.label
+ } else if storedIdentityAvailable {
+ hasKey = true
+ } else {
+ hasKey = false
+ npub = nil
+ identityLabel = nil
+ }
}
- private func removeAllIdentities(using service: FieldRuntimeService) async throws {
- try await service.nostrIdentityResetAll()
- hasKey = false
- npub = nil
- identityLabel = nil
+ private func clearRuntimeIdentityState(using service: FieldRuntimeService) async throws {
+ try await service.nostrIdentityClearRuntimeState()
+ runtimeIdentityReady = false
identities = []
}
- private func restorePersistedIdentity(
- using service: FieldRuntimeService,
- custodyStore: FieldIdentityCustodyStore
- ) async throws {
- let snapshot = try await service.nostrIdentitySnapshot()
- guard !snapshot.hasSelectedSigningIdentity else { return }
- guard let secretHex = try custodyStore.loadSelectedSecretHex() else { return }
- _ = try await service.nostrIdentityImportSecret(
- secretKey: secretHex,
- label: "Radroots Field",
+ private func loadStoredIdentityMetadata(_ metadataStore: FieldIdentityPublicMetadataStore) {
+ guard let metadata = metadataStore.load() else {
+ applyNoIdentity()
+ setLocked(false)
+ return
+ }
+ apply(storedIdentity: metadata)
+ setLocked(true)
+ }
+
+ private func restoreStoredIdentity(using service: FieldRuntimeService) async throws {
+ guard let secret = try identityCustodyStoreOrConfigured().loadSelectedSecretHex() else {
+ throw FieldIdentityCustodyError.missingSelectedSecret
+ }
+ let existingMetadata = try identityMetadataStoreOrConfigured().load()
+ let record = try await service.nostrIdentityRestoreHostSecret(
+ secretKey: secret,
+ label: existingMetadata?.label ?? "Radroots Field",
makeSelected: true
)
+ try persistIdentity(record, secret: secret)
+ }
+
+ private func createHostCustodyIdentity(using service: FieldRuntimeService) async throws {
+ var lastError: Error?
+ for _ in 0..<8 {
+ let secret = try FieldIdentityCustodyStore.generateSecretHex()
+ let record: NostrIdentityRecord
+ do {
+ record = try await service.nostrIdentityRestoreHostSecret(
+ secretKey: secret,
+ label: "Radroots Field",
+ makeSelected: true
+ )
+ } catch {
+ lastError = error
+ continue
+ }
+ try persistIdentity(record, secret: secret)
+ return
+ }
+ throw lastError ?? FieldIdentityCustodyError.missingSelectedSecret
}
- private func persistSelectedIdentity(using service: FieldRuntimeService) async throws {
- guard let secretHex = try await service.nostrIdentityExportSelectedSecretHex() else {
- throw FieldIdentityCustodyError.missingSelectedSecret
+ private func persistIdentity(_ record: NostrIdentityRecord, secret: String) throws {
+ try identityCustodyStoreOrConfigured().saveSelectedSecret(secret)
+ let metadata = FieldIdentityPublicMetadata(record: record)
+ try identityMetadataStoreOrConfigured().save(metadata)
+ apply(storedIdentity: metadata)
+ runtimeIdentityReady = true
+ hasKey = true
+ identities = [record]
+ }
+
+ private func lockRuntimeIdentity() async {
+ guard let service = runtimeService else {
+ runtimeIdentityReady = false
+ identities = []
+ hasKey = storedIdentityAvailable
+ return
+ }
+ do {
+ try await clearRuntimeIdentityState(using: service)
+ } catch {
+ relayLastError = error.localizedDescription
}
- try identityCustodyStoreOrConfigured().saveSelectedSecretHex(secretHex)
+ hasKey = storedIdentityAvailable
+ await refreshRelayStatus(using: service)
+ }
+
+ private func apply(storedIdentity metadata: FieldIdentityPublicMetadata) {
+ storedIdentityAvailable = true
+ hasKey = true
+ npub = metadata.publicKeyNpub
+ identityLabel = metadata.label
+ }
+
+ private func applyNoIdentity() {
+ hasKey = false
+ storedIdentityAvailable = false
+ runtimeIdentityReady = false
+ npub = nil
+ identityLabel = nil
+ identities = []
}
private func identityCustodyStoreOrConfigured() throws -> FieldIdentityCustodyStore {
@@ -294,6 +381,15 @@ public final class AppState: ObservableObject {
return configured
}
+ private func identityMetadataStoreOrConfigured() throws -> FieldIdentityPublicMetadataStore {
+ if let identityMetadataStore {
+ return identityMetadataStore
+ }
+ let configured = try FieldIdentityPublicMetadataStore.configured()
+ identityMetadataStore = configured
+ return configured
+ }
+
private func bundleIdentifier() throws -> String {
guard let bundleIdentifier = Bundle.main.bundleIdentifier,
!bundleIdentifier.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
diff --git a/Radroots/Config/Base.xcconfig b/Radroots/Config/Base.xcconfig
@@ -8,5 +8,6 @@ RADROOTS_FIELD_IOS_LOGGING_FILE_ENABLED = true
RADROOTS_FIELD_IOS_LOGGING_FILE_NAME = field-ios.log
RADROOTS_FIELD_IOS_NOSTR_RELAY_URLS = wss:$(SLASH)$(SLASH)radroots.org
RADROOTS_FIELD_IOS_KEYCHAIN_SERVICE_PREFIX = org.radroots.field_ios
+RADROOTS_FIELD_IOS_KEYCHAIN_ACCESS_POLICY = user_presence_local
RADROOTS_FIELD_IOS_RESET_LOCAL_STATE = false
RADROOTS_FIELD_IOS_TRADE_RHI_PUBKEY =
diff --git a/Radroots/Info.plist b/Radroots/Info.plist
@@ -71,6 +71,8 @@
<string>$(RADROOTS_FIELD_IOS_NOSTR_RELAY_URLS)</string>
<key>RADROOTS_FIELD_IOS_KEYCHAIN_SERVICE_PREFIX</key>
<string>$(RADROOTS_FIELD_IOS_KEYCHAIN_SERVICE_PREFIX)</string>
+ <key>RADROOTS_FIELD_IOS_KEYCHAIN_ACCESS_POLICY</key>
+ <string>$(RADROOTS_FIELD_IOS_KEYCHAIN_ACCESS_POLICY)</string>
<key>RADROOTS_FIELD_IOS_RESET_LOCAL_STATE</key>
<string>$(RADROOTS_FIELD_IOS_RESET_LOCAL_STATE)</string>
<key>RADROOTS_FIELD_IOS_TRADE_RHI_PUBKEY</key>
diff --git a/Radroots/Runtime/BuildConfig.swift b/Radroots/Runtime/BuildConfig.swift
@@ -9,6 +9,7 @@ enum BuildConfigKey: String {
case loggingFileName = "RADROOTS_FIELD_IOS_LOGGING_FILE_NAME"
case nostrRelayUrls = "RADROOTS_FIELD_IOS_NOSTR_RELAY_URLS"
case keychainServicePrefix = "RADROOTS_FIELD_IOS_KEYCHAIN_SERVICE_PREFIX"
+ case keychainAccessPolicy = "RADROOTS_FIELD_IOS_KEYCHAIN_ACCESS_POLICY"
case resetLocalState = "RADROOTS_FIELD_IOS_RESET_LOCAL_STATE"
case tradeRhiPubkey = "RADROOTS_FIELD_IOS_TRADE_RHI_PUBKEY"
}
diff --git a/Radroots/Runtime/FieldIdentityCustodyStore.swift b/Radroots/Runtime/FieldIdentityCustodyStore.swift
@@ -1,11 +1,15 @@
import Foundation
import RadrootsKit
+import Security
enum FieldIdentityCustodyError: LocalizedError {
case missingKeychainServicePrefix
case missingBundleIdentifier
case invalidStoredSecret
case missingSelectedSecret
+ case missingKeychainAccessPolicy
+ case invalidKeychainAccessPolicy(String)
+ case randomSecretGenerationFailed(Int32)
var errorDescription: String? {
switch self {
@@ -17,6 +21,26 @@ enum FieldIdentityCustodyError: LocalizedError {
"Stored Nostr identity secret is invalid."
case .missingSelectedSecret:
"No selected Nostr identity secret is available to secure."
+ case .missingKeychainAccessPolicy:
+ "Missing RADROOTS_FIELD_IOS_KEYCHAIN_ACCESS_POLICY."
+ case .invalidKeychainAccessPolicy(let value):
+ "Invalid RADROOTS_FIELD_IOS_KEYCHAIN_ACCESS_POLICY: \(value)."
+ case .randomSecretGenerationFailed(let status):
+ "Secure Nostr identity generation failed with status \(status)."
+ }
+ }
+}
+
+enum FieldIdentityKeychainAccessPolicy: String {
+ case userPresenceLocal = "user_presence_local"
+ case secureLocal = "secure_local"
+
+ var storePolicy: RadrootsSecretAccessPolicy {
+ switch self {
+ case .userPresenceLocal:
+ .userPresenceLocalSecret
+ case .secureLocal:
+ .secureLocalSecret
}
}
}
@@ -56,12 +80,16 @@ struct FieldIdentityCustodyStore {
return trimmed.isEmpty ? nil : trimmed
}
- func saveSelectedSecretHex(_ secretHex: String) throws {
- let trimmed = secretHex.trimmingCharacters(in: .whitespacesAndNewlines)
+ func saveSelectedSecret(_ secret: String) throws {
+ let trimmed = secret.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
throw FieldIdentityCustodyError.missingSelectedSecret
}
- try store.put(Data(trimmed.utf8), for: Self.selectedSecretKey, policy: .secureLocalSecret)
+ try store.put(
+ Data(trimmed.utf8),
+ for: Self.selectedSecretKey,
+ policy: try Self.configuredAccessPolicy().storePolicy
+ )
}
func deleteSelectedSecret() throws {
@@ -85,6 +113,25 @@ struct FieldIdentityCustodyStore {
try selectedSecretKey.serviceName(servicePrefix: servicePrefix)
}
+ static func configuredAccessPolicy() throws -> FieldIdentityKeychainAccessPolicy {
+ guard let rawValue = BuildConfig.string(.keychainAccessPolicy) else {
+ throw FieldIdentityCustodyError.missingKeychainAccessPolicy
+ }
+ guard let policy = FieldIdentityKeychainAccessPolicy(rawValue: rawValue) else {
+ throw FieldIdentityCustodyError.invalidKeychainAccessPolicy(rawValue)
+ }
+ return policy
+ }
+
+ static func generateSecretHex() throws -> String {
+ var bytes = [UInt8](repeating: 0, count: 32)
+ let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
+ guard status == errSecSuccess else {
+ throw FieldIdentityCustodyError.randomSecretGenerationFailed(status)
+ }
+ return bytes.map { String(format: "%02x", $0) }.joined()
+ }
+
private static var selectedSecretKey: RadrootsSecureStoreKey {
RadrootsSecureStoreKey(namespace: namespace, name: selectedSecretName)
}
diff --git a/Radroots/Runtime/FieldIdentityPublicMetadataStore.swift b/Radroots/Runtime/FieldIdentityPublicMetadataStore.swift
@@ -0,0 +1,50 @@
+import Foundation
+
+struct FieldIdentityPublicMetadata: Codable, Equatable, Sendable {
+ let selectedIdentityId: String
+ let publicKeyHex: String
+ let publicKeyNpub: String
+ let label: String?
+ let updatedAtUnix: UInt64
+
+ init(record: NostrIdentityRecord, updatedAtUnix: UInt64 = UInt64(Date().timeIntervalSince1970)) {
+ self.selectedIdentityId = record.id
+ self.publicKeyHex = record.publicKeyHex
+ self.publicKeyNpub = record.publicKeyNpub
+ self.label = record.label
+ self.updatedAtUnix = updatedAtUnix
+ }
+}
+
+struct FieldIdentityPublicMetadataStore {
+ private let userDefaults: UserDefaults
+ private let key: String
+
+ init(servicePrefix: String, userDefaults: UserDefaults = .standard) {
+ self.userDefaults = userDefaults
+ self.key = "field_ios.identity.public_metadata.\(servicePrefix)"
+ }
+
+ static func configured() throws -> FieldIdentityPublicMetadataStore {
+ guard let servicePrefix = BuildConfig.string(.keychainServicePrefix) else {
+ throw FieldIdentityCustodyError.missingKeychainServicePrefix
+ }
+ return FieldIdentityPublicMetadataStore(servicePrefix: servicePrefix)
+ }
+
+ func load() -> FieldIdentityPublicMetadata? {
+ guard let data = userDefaults.data(forKey: key) else {
+ return nil
+ }
+ return try? JSONDecoder().decode(FieldIdentityPublicMetadata.self, from: data)
+ }
+
+ func save(_ metadata: FieldIdentityPublicMetadata) throws {
+ let data = try JSONEncoder().encode(metadata)
+ userDefaults.set(data, forKey: key)
+ }
+
+ func delete() {
+ userDefaults.removeObject(forKey: key)
+ }
+}
diff --git a/Radroots/Runtime/FieldRuntimeService.swift b/Radroots/Runtime/FieldRuntimeService.swift
@@ -52,7 +52,7 @@ public final class FieldRuntimeService: @unchecked Sendable {
try await run { try $0.nostrIdentityList() }
}
- public func nostrIdentityGenerate(label: String?, makeSelected: Bool) async throws -> String {
+ public func nostrIdentityGenerate(label: String?, makeSelected: Bool) async throws -> NostrIdentityRecord {
try await run { try $0.nostrIdentityGenerate(label: label, makeSelected: makeSelected) }
}
@@ -60,7 +60,7 @@ public final class FieldRuntimeService: @unchecked Sendable {
secretKey: String,
label: String?,
makeSelected: Bool
- ) async throws -> String {
+ ) async throws -> NostrIdentityRecord {
try await run {
try $0.nostrIdentityImportSecret(
secretKey: secretKey,
@@ -70,16 +70,30 @@ public final class FieldRuntimeService: @unchecked Sendable {
}
}
+ public func nostrIdentityRestoreHostSecret(
+ secretKey: String,
+ label: String?,
+ makeSelected: Bool
+ ) async throws -> NostrIdentityRecord {
+ try await run {
+ try $0.nostrIdentityRestoreHostSecret(
+ secretKey: secretKey,
+ label: label,
+ makeSelected: makeSelected
+ )
+ }
+ }
+
public func nostrIdentityRemove(identityId: String) async throws {
try await run { try $0.nostrIdentityRemove(identityId: identityId) }
}
- public func nostrIdentityResetAll() async throws {
- try await run { try $0.nostrIdentityResetAll() }
+ public func nostrIdentityClearRuntimeState() async throws {
+ try await run { try $0.nostrIdentityClearRuntimeState() }
}
- public func nostrIdentityExportSelectedSecretHex() async throws -> String? {
- try await run { try $0.nostrIdentityExportSelectedSecretHex() }
+ public func nostrIdentityResetAll() async throws {
+ try await run { try $0.nostrIdentityResetAll() }
}
public func nostrProfileForSelf() async -> NostrProfileEventMetadata? {
diff --git a/Radroots/Views/SettingsView.swift b/Radroots/Views/SettingsView.swift
@@ -16,7 +16,8 @@ struct SettingsView: View {
Text("No local Nostr identity is selected.")
.foregroundStyle(.secondary)
}
- LabeledContent("Stored identities", value: "\(app.identities.count)")
+ LabeledContent("Stored identities", value: app.storedIdentityAvailable ? "1" : "0")
+ LabeledContent("Runtime identity", value: app.runtimeIdentityReady ? "Ready" : "Locked")
NavigationLink {
ProfileView()
diff --git a/Radroots/Views/SetupView.swift b/Radroots/Views/SetupView.swift
@@ -155,10 +155,11 @@ struct SetupView: View {
private func importIdentity() {
errorMessage = nil
isWorking = true
+ let submittedSecret = secretKey
+ secretKey = ""
Task {
do {
- try await app.importNostrSecret(secretKey)
- secretKey = ""
+ try await app.importNostrSecret(submittedSecret)
onSuccess?()
} catch {
errorMessage = error.localizedDescription