diff --git a/NearbyShare/Util/AppGroup.swift b/NearbyShare/Util/AppGroup.swift
index cd2561f..9be117d 100644
--- a/NearbyShare/Util/AppGroup.swift
+++ b/NearbyShare/Util/AppGroup.swift
@@ -6,6 +6,7 @@
//
import Foundation
+import Security
/// Contains information about the app group used to share data between main app and extensions
struct AppGroup {
@@ -19,3 +20,90 @@ struct AppGroup {
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
}
}
+
+
+struct KeychainStore {
+ private static let service = "com.leonboettger.quickdrop.securestorage"
+ // Must match the shared keychain access group declared in all Apple entitlements.
+ private static let sharedAccessGroup = "92SDAWLN76.com.leonboettger.neardrop"
+
+ static func loadData(account: String) throws -> Data? {
+ var query = baseQuery(account: account)
+ query[kSecReturnData as String] = true
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
+
+ var result: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ switch status {
+ case errSecSuccess:
+ return result as? Data
+ case errSecItemNotFound:
+ return nil
+ default:
+ throw KeychainStoreError.unexpectedStatus(status)
+ }
+ }
+
+
+ static func saveData(_ data: Data, account: String) throws {
+ var addQuery = baseQuery(account: account)
+ addQuery[kSecValueData as String] = data
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+
+ let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
+ if addStatus == errSecSuccess {
+ return
+ }
+ if addStatus != errSecDuplicateItem {
+ throw KeychainStoreError.unexpectedStatus(addStatus)
+ }
+
+ let attributesToUpdate: [String: Any] = [
+ kSecValueData as String: data,
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
+ ]
+
+ let updateStatus = SecItemUpdate(baseQuery(account: account) as CFDictionary, attributesToUpdate as CFDictionary)
+ guard updateStatus == errSecSuccess else {
+ throw KeychainStoreError.unexpectedStatus(updateStatus)
+ }
+ }
+
+
+ static func deleteItem(account: String) throws {
+ let status = SecItemDelete(baseQuery(account: account) as CFDictionary)
+ guard status == errSecSuccess || status == errSecItemNotFound else {
+ throw KeychainStoreError.unexpectedStatus(status)
+ }
+ }
+
+
+ private static func baseQuery(account: String) -> [String: Any] {
+ var query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecAttrAccount as String: account,
+ kSecUseDataProtectionKeychain as String: true,
+ ]
+
+ query[kSecAttrAccessGroup as String] = sharedAccessGroup
+
+ return query
+ }
+}
+
+
+enum KeychainStoreError: LocalizedError {
+ case unexpectedStatus(OSStatus)
+
+ var errorDescription: String? {
+ switch self {
+ case let .unexpectedStatus(status):
+ if let message = SecCopyErrorMessageString(status, nil) as String? {
+ return "Keychain error \(status): \(message)"
+ }
+ return "Keychain error \(status)"
+ }
+ }
+}
diff --git a/NearbyShare/Util/IdentityManager.swift b/NearbyShare/Util/IdentityManager.swift
index a912378..d0c63ff 100644
--- a/NearbyShare/Util/IdentityManager.swift
+++ b/NearbyShare/Util/IdentityManager.swift
@@ -51,17 +51,21 @@ class IdentityManager {
private func generateAndStoreKeyPair() -> (ECPrivateKey, ECPublicKey) {
let domain = Domain.instance(curve: .EC256r1)
let (pubKey, privKey) = domain.makeKeyPair()
-
- AppGroup.appGroupUD.set(Data(privKey.s.asSignedBytes()), forKey: signingKeyTag)
+
+ let privateKeyData = Data(privKey.s.asSignedBytes())
+ do {
+ try KeychainStore.saveData(privateKeyData, account: signingKeyTag)
+ AppGroup.appGroupUD.removeObject(forKey: signingKeyTag)
+ } catch {
+ log("[IdentityManager] Failed to store private key in keychain: \(error.localizedDescription)")
+ }
return (privKey, pubKey)
}
private func loadKeys() -> (ECPrivateKey, ECPublicKey)? {
- let keyData = AppGroup.appGroupUD.data(forKey: signingKeyTag)
-
- guard let keyData = keyData else { return nil }
+ guard let keyData = loadStoredKeyData() else { return nil }
let domain = Domain.instance(curve: .EC256r1)
guard let privateKey = try? ECPrivateKey(domain: domain, s: .init(signed: Array(keyData))) else {
@@ -80,6 +84,32 @@ class IdentityManager {
return (privateKey, publicKey)
}
+
+
+ private func loadStoredKeyData() -> Data? {
+ do {
+ if let keyData = try KeychainStore.loadData(account: signingKeyTag) {
+ AppGroup.appGroupUD.removeObject(forKey: signingKeyTag)
+ return keyData
+ }
+ } catch {
+ log("[IdentityManager] Failed to load private key from keychain: \(error.localizedDescription)")
+ }
+
+ guard let legacyKeyData = AppGroup.appGroupUD.data(forKey: signingKeyTag) else {
+ return nil
+ }
+
+ do {
+ try KeychainStore.saveData(legacyKeyData, account: signingKeyTag)
+ AppGroup.appGroupUD.removeObject(forKey: signingKeyTag)
+ log("[IdentityManager] Migrated private key from shared defaults to keychain.")
+ } catch {
+ log("[IdentityManager] Failed to migrate private key to keychain: \(error.localizedDescription)")
+ }
+
+ return legacyKeyData
+ }
}
diff --git a/NearbyShare/Util/TrustStore.swift b/NearbyShare/Util/TrustStore.swift
index 87e5f99..151d96c 100644
--- a/NearbyShare/Util/TrustStore.swift
+++ b/NearbyShare/Util/TrustStore.swift
@@ -25,29 +25,40 @@ class TrustStore: ObservableObject {
// MARK: - Persistence
private func loadTrustedCertificates() {
- guard let dict = AppGroup.appGroupUD.dictionary(forKey: trustedKeysKey) as? [String: Data] else {
+ do {
+ if let data = try KeychainStore.loadData(account: trustedKeysKey) {
+ trustedCertificates = try JSONDecoder().decode([String: TrustedCertificate].self, from: data)
+ AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
+ return
+ }
+ } catch {
+ print("[TrustStore] Failed to load trusted certificates from keychain: \(error.localizedDescription)")
+ }
+
+ guard let dict = legacyTrustedCertificates() else {
trustedCertificates = [:]
return
}
- var decoded: [String: TrustedCertificate] = [:]
- for (key, data) in dict {
- if let cert = try? JSONDecoder().decode(TrustedCertificate.self, from: data) {
- decoded[key] = cert
- }
+ trustedCertificates = dict
+
+ do {
+ try persistTrustedCertificates()
+ AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
+ print("[TrustStore] Migrated trusted certificates from shared defaults to keychain.")
+ } catch {
+ print("[TrustStore] Failed to migrate trusted certificates to keychain: \(error.localizedDescription)")
}
- trustedCertificates = decoded
}
private func saveTrustedCertificates() {
- var dict: [String: Data] = [:]
- for (key, cert) in trustedCertificates {
- if let data = try? JSONEncoder().encode(cert) {
- dict[key] = data
- }
+ do {
+ try persistTrustedCertificates()
+ AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
+ } catch {
+ print("[TrustStore] Failed to save trusted certificates to keychain: \(error.localizedDescription)")
}
- AppGroup.appGroupUD.set(dict, forKey: trustedKeysKey)
}
@@ -84,4 +95,25 @@ class TrustStore: ObservableObject {
let creationDate: Date
let certificateData: Data
}
+
+
+ private func persistTrustedCertificates() throws {
+ let data = try JSONEncoder().encode(trustedCertificates)
+ try KeychainStore.saveData(data, account: trustedKeysKey)
+ }
+
+
+ private func legacyTrustedCertificates() -> [String: TrustedCertificate]? {
+ guard let dict = AppGroup.appGroupUD.dictionary(forKey: trustedKeysKey) as? [String: Data] else {
+ return nil
+ }
+
+ var decoded: [String: TrustedCertificate] = [:]
+ for (key, data) in dict {
+ if let cert = try? JSONDecoder().decode(TrustedCertificate.self, from: data) {
+ decoded[key] = cert
+ }
+ }
+ return decoded
+ }
}
diff --git a/QuickDrop/QuickDrop.entitlements b/QuickDrop/QuickDrop.entitlements
index a8302e4..afe12fd 100644
--- a/QuickDrop/QuickDrop.entitlements
+++ b/QuickDrop/QuickDrop.entitlements
@@ -6,5 +6,9 @@
group.com.leonboettger.neardrop
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)com.leonboettger.neardrop
+
diff --git a/QuickDropiOS/QuickDropiOS.entitlements b/QuickDropiOS/QuickDropiOS.entitlements
index dfc9800..c2bc796 100644
--- a/QuickDropiOS/QuickDropiOS.entitlements
+++ b/QuickDropiOS/QuickDropiOS.entitlements
@@ -8,5 +8,9 @@
group.com.leonboettger.neardrop
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)com.leonboettger.neardrop
+
diff --git a/ShareExtension/ShareExtension.entitlements b/ShareExtension/ShareExtension.entitlements
index a8302e4..afe12fd 100644
--- a/ShareExtension/ShareExtension.entitlements
+++ b/ShareExtension/ShareExtension.entitlements
@@ -6,5 +6,9 @@
group.com.leonboettger.neardrop
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)com.leonboettger.neardrop
+
diff --git a/iOSShareExtension/iOSShareExtension.entitlements b/iOSShareExtension/iOSShareExtension.entitlements
index dfc9800..c2bc796 100644
--- a/iOSShareExtension/iOSShareExtension.entitlements
+++ b/iOSShareExtension/iOSShareExtension.entitlements
@@ -8,5 +8,9 @@
group.com.leonboettger.neardrop
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)com.leonboettger.neardrop
+