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 Foundation
|
||||||
|
import Security
|
||||||
|
|
||||||
/// Contains information about the app group used to share data between main app and extensions
|
/// Contains information about the app group used to share data between main app and extensions
|
||||||
struct AppGroup {
|
struct AppGroup {
|
||||||
@@ -19,3 +20,90 @@ struct AppGroup {
|
|||||||
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
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) {
|
private func generateAndStoreKeyPair() -> (ECPrivateKey, ECPublicKey) {
|
||||||
let domain = Domain.instance(curve: .EC256r1)
|
let domain = Domain.instance(curve: .EC256r1)
|
||||||
let (pubKey, privKey) = domain.makeKeyPair()
|
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)
|
return (privKey, pubKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func loadKeys() -> (ECPrivateKey, ECPublicKey)? {
|
private func loadKeys() -> (ECPrivateKey, ECPublicKey)? {
|
||||||
let keyData = AppGroup.appGroupUD.data(forKey: signingKeyTag)
|
guard let keyData = loadStoredKeyData() else { return nil }
|
||||||
|
|
||||||
guard let keyData = keyData else { return nil }
|
|
||||||
|
|
||||||
let domain = Domain.instance(curve: .EC256r1)
|
let domain = Domain.instance(curve: .EC256r1)
|
||||||
guard let privateKey = try? ECPrivateKey(domain: domain, s: .init(signed: Array(keyData))) else {
|
guard let privateKey = try? ECPrivateKey(domain: domain, s: .init(signed: Array(keyData))) else {
|
||||||
@@ -80,6 +84,32 @@ class IdentityManager {
|
|||||||
|
|
||||||
return (privateKey, publicKey)
|
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
|
// MARK: - Persistence
|
||||||
|
|
||||||
private func loadTrustedCertificates() {
|
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 = [:]
|
trustedCertificates = [:]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var decoded: [String: TrustedCertificate] = [:]
|
trustedCertificates = dict
|
||||||
for (key, data) in dict {
|
|
||||||
if let cert = try? JSONDecoder().decode(TrustedCertificate.self, from: data) {
|
do {
|
||||||
decoded[key] = cert
|
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() {
|
private func saveTrustedCertificates() {
|
||||||
var dict: [String: Data] = [:]
|
do {
|
||||||
for (key, cert) in trustedCertificates {
|
try persistTrustedCertificates()
|
||||||
if let data = try? JSONEncoder().encode(cert) {
|
AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
|
||||||
dict[key] = data
|
} 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 creationDate: Date
|
||||||
let certificateData: Data
|
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>
|
<array>
|
||||||
<string>group.com.leonboettger.neardrop</string>
|
<string>group.com.leonboettger.neardrop</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -8,5 +8,9 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>group.com.leonboettger.neardrop</string>
|
<string>group.com.leonboettger.neardrop</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -6,5 +6,9 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>group.com.leonboettger.neardrop</string>
|
<string>group.com.leonboettger.neardrop</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -8,5 +8,9 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>group.com.leonboettger.neardrop</string>
|
<string>group.com.leonboettger.neardrop</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
Reference in new issue
Block a user