Fix: stabilize per-geohash identity seed by storing in Keychain with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly and caching in memory to avoid transient regenerations (#473)

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-08-22 01:05:44 +02:00
committed by GitHub
co-authored by jack
parent bc27e16899
commit 83ee5abb60
+14 -3
View File
@@ -5,13 +5,16 @@ import Security
// Keychain helper for secure storage // Keychain helper for secure storage
struct KeychainHelper { struct KeychainHelper {
static func save(key: String, data: Data, service: String) { static func save(key: String, data: Data, service: String, accessible: CFString? = nil) {
let query: [String: Any] = [ var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword, kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service, kSecAttrService as String: service,
kSecAttrAccount as String: key, kSecAttrAccount as String: key,
kSecValueData as String: data kSecValueData as String: data
] ]
if let accessible = accessible {
query[kSecAttrAccessible as String] = accessible
}
SecItemDelete(query as CFDictionary) SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil) SecItemAdd(query as CFDictionary, nil)
@@ -106,6 +109,8 @@ struct NostrIdentityBridge {
private static let keychainService = "chat.bitchat.nostr" private static let keychainService = "chat.bitchat.nostr"
private static let currentIdentityKey = "nostr-current-identity" private static let currentIdentityKey = "nostr-current-identity"
private static let deviceSeedKey = "nostr-device-seed" private static let deviceSeedKey = "nostr-device-seed"
// In-memory cache to avoid transient keychain access issues
private static var deviceSeedCache: Data?
/// Get or create the current Nostr identity /// Get or create the current Nostr identity
static func getCurrentNostrIdentity() throws -> NostrIdentity? { static func getCurrentNostrIdentity() throws -> NostrIdentity? {
@@ -159,14 +164,20 @@ struct NostrIdentityBridge {
/// Returns a stable device seed used to derive unlinkable per-geohash identities. /// Returns a stable device seed used to derive unlinkable per-geohash identities.
/// Stored only on device keychain. /// Stored only on device keychain.
private static func getOrCreateDeviceSeed() -> Data { private static func getOrCreateDeviceSeed() -> Data {
if let cached = deviceSeedCache { return cached }
if let existing = KeychainHelper.load(key: deviceSeedKey, service: keychainService) { if let existing = KeychainHelper.load(key: deviceSeedKey, service: keychainService) {
// Migrate to AfterFirstUnlockThisDeviceOnly for stability during lock
KeychainHelper.save(key: deviceSeedKey, data: existing, service: keychainService, accessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)
deviceSeedCache = existing
return existing return existing
} }
var seed = Data(count: 32) var seed = Data(count: 32)
_ = seed.withUnsafeMutableBytes { ptr in _ = seed.withUnsafeMutableBytes { ptr in
SecRandomCopyBytes(kSecRandomDefault, 32, ptr.baseAddress!) SecRandomCopyBytes(kSecRandomDefault, 32, ptr.baseAddress!)
} }
KeychainHelper.save(key: deviceSeedKey, data: seed, service: keychainService) // Ensure availability after first unlock to prevent unintended rotation when locked
KeychainHelper.save(key: deviceSeedKey, data: seed, service: keychainService, accessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)
deviceSeedCache = seed
return seed return seed
} }