Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-15 11:23:48 -06:00
parent d60e7d0916
commit 92c451fa33
4 changed files with 20 additions and 24 deletions

View File

@ -20,15 +20,6 @@ public enum KeychainAccessibility: Sendable, CaseIterable {
/// Data is not migrated to a new device. /// Data is not migrated to a new device.
case afterFirstUnlockThisDeviceOnly case afterFirstUnlockThisDeviceOnly
/// Item is always accessible, regardless of device lock state.
/// Least secure - use only when absolutely necessary.
@available(iOS, deprecated: 12.0, message: "Use an accessibility level that provides some user protection, such as afterFirstUnlock")
case always
/// Item is always accessible but not migrated to new devices.
@available(iOS, deprecated: 12.0, message: "Use an accessibility level that provides some user protection, such as afterFirstUnlockThisDeviceOnly")
case alwaysThisDeviceOnly
/// Item is only accessible when the device has a passcode set. /// Item is only accessible when the device has a passcode set.
/// If passcode is removed, item becomes inaccessible. /// If passcode is removed, item becomes inaccessible.
case whenPasscodeSetThisDeviceOnly case whenPasscodeSetThisDeviceOnly
@ -44,10 +35,6 @@ public enum KeychainAccessibility: Sendable, CaseIterable {
return kSecAttrAccessibleWhenUnlockedThisDeviceOnly return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
case .afterFirstUnlockThisDeviceOnly: case .afterFirstUnlockThisDeviceOnly:
return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
case .always:
return kSecAttrAccessibleAlways
case .alwaysThisDeviceOnly:
return kSecAttrAccessibleAlwaysThisDeviceOnly
case .whenPasscodeSetThisDeviceOnly: case .whenPasscodeSetThisDeviceOnly:
return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
} }
@ -64,12 +51,18 @@ public enum KeychainAccessibility: Sendable, CaseIterable {
return "When Unlocked (This Device)" return "When Unlocked (This Device)"
case .afterFirstUnlockThisDeviceOnly: case .afterFirstUnlockThisDeviceOnly:
return "After First Unlock (This Device)" return "After First Unlock (This Device)"
case .always:
return "Always"
case .alwaysThisDeviceOnly:
return "Always (This Device)"
case .whenPasscodeSetThisDeviceOnly: case .whenPasscodeSetThisDeviceOnly:
return "When Passcode Set (This Device)" return "When Passcode Set (This Device)"
} }
} }
public static var allCases: [KeychainAccessibility] {
[
.whenUnlocked,
.afterFirstUnlock,
.whenUnlockedThisDeviceOnly,
.afterFirstUnlockThisDeviceOnly,
.whenPasscodeSetThisDeviceOnly
]
}
} }

View File

@ -316,7 +316,10 @@ public actor StorageRouter: StorageProviding {
// MARK: - Storage Operations // MARK: - Storage Operations
private func store(_ data: Data, for key: any StorageKey) async throws { private func store(_ data: Data, for key: any StorageKey) async throws {
let descriptor = StorageKeyDescriptor.from(key) try await store(data, for: .from(key))
}
private func store(_ data: Data, for descriptor: StorageKeyDescriptor) async throws {
switch descriptor.domain { switch descriptor.domain {
case .userDefaults(let suite): case .userDefaults(let suite):
try await UserDefaultsHelper.shared.set(data, forKey: descriptor.name, suite: suite) try await UserDefaultsHelper.shared.set(data, forKey: descriptor.name, suite: suite)
@ -438,7 +441,7 @@ public actor StorageRouter: StorageProviding {
// The data received is already 'secured' (encrypted if necessary) by the sender. // The data received is already 'secured' (encrypted if necessary) by the sender.
// We can store it directly in our local domain. // We can store it directly in our local domain.
try await store(data, for: entry) try await store(data, for: entry.descriptor)
Logger.info("Successfully updated local storage from sync for key: \(keyName)") Logger.info("Successfully updated local storage from sync for key: \(keyName)")
} }

View File

@ -12,7 +12,7 @@ struct KeychainHelperTests {
let data = Data("secret-password".utf8) let data = Data("secret-password".utf8)
defer { defer {
try? KeychainHelper.shared.delete(service: testService, key: key) Task { try? await KeychainHelper.shared.delete(service: testService, key: key) }
} }
try await KeychainHelper.shared.set( try await KeychainHelper.shared.set(
@ -47,7 +47,7 @@ struct KeychainHelperTests {
let data = Data("test".utf8) let data = Data("test".utf8)
defer { defer {
try? KeychainHelper.shared.delete(service: testService, key: key) Task { try? await KeychainHelper.shared.delete(service: testService, key: key) }
} }
let beforeExists = try await KeychainHelper.shared.exists(service: testService, key: key) let beforeExists = try await KeychainHelper.shared.exists(service: testService, key: key)
@ -70,7 +70,7 @@ struct KeychainHelperTests {
let updatedData = Data("updated".utf8) let updatedData = Data("updated".utf8)
defer { defer {
try? KeychainHelper.shared.delete(service: testService, key: key) Task { try? await KeychainHelper.shared.delete(service: testService, key: key) }
} }
try await KeychainHelper.shared.set( try await KeychainHelper.shared.set(
@ -127,7 +127,7 @@ struct KeychainHelperTests {
let data = Data("data-for-\(accessibility)".utf8) let data = Data("data-for-\(accessibility)".utf8)
defer { defer {
try? KeychainHelper.shared.delete(service: testService, key: key) Task { try? await KeychainHelper.shared.delete(service: testService, key: key) }
} }
try await KeychainHelper.shared.set( try await KeychainHelper.shared.set(

View File

@ -56,7 +56,7 @@ struct LocalDataTests {
#expect(fetched == storedValue) #expect(fetched == storedValue)
try await StorageRouter.shared.remove(key) try await StorageRouter.shared.remove(key)
#expect(throws: StorageError.notFound) { await #expect(throws: StorageError.notFound) {
_ = try await StorageRouter.shared.get(key) _ = try await StorageRouter.shared.get(key)
} }
} }