mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Use Keychain for trusted devices
This commit is contained in:
1 parent
2ef3bc3b44
commit
843ed9fe4f
7 files changed
+184
-18
No files matched your search
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,9 @@
|
||||
<array>
|
||||
<string>group.com.leonboettger.neardrop</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -8,5 +8,9 @@
|
||||
<array>
|
||||
<string>group.com.leonboettger.neardrop</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -6,5 +6,9 @@
|
||||
<array>
|
||||
<string>group.com.leonboettger.neardrop</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -8,5 +8,9 @@
|
||||
<array>
|
||||
<string>group.com.leonboettger.neardrop</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in new issue
Block a user