apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README

RadrootsSecureStoreTests.swift (7147B)


      1 import Foundation
      2 import Security
      3 import Testing
      4 @testable import RadrootsKit
      5 
      6 @Test func secureStoreKeyBuildsServiceName() throws {
      7     let key = RadrootsSecureStoreKey(namespace: "session", name: "token")
      8     #expect(try key.serviceName(servicePrefix: "org.radroots.test") == "org.radroots.test.session")
      9 }
     10 
     11 @Test func secureStoreKeyNormalizesServicePrefixNamespaceAndName() throws {
     12     let key = RadrootsSecureStoreKey(namespace: " session ", name: " token ")
     13     let normalizedKey = try key.normalized()
     14 
     15     #expect(normalizedKey.namespace == "session")
     16     #expect(normalizedKey.name == "token")
     17     #expect(try key.serviceName(servicePrefix: " org.radroots.test ") == "org.radroots.test.session")
     18 }
     19 
     20 @Test func secureStoreKeyRejectsBlankNamespace() throws {
     21     let key = RadrootsSecureStoreKey(namespace: " ", name: "token")
     22     #expect(throws: RadrootsAppleSecurityError.self) {
     23         _ = try key.serviceName(servicePrefix: "org.radroots.test")
     24     }
     25 }
     26 
     27 @Test func secureStoreKeyRejectsBlankName() throws {
     28     let key = RadrootsSecureStoreKey(namespace: "session", name: " ")
     29     #expect(throws: RadrootsAppleSecurityError.self) {
     30         _ = try key.normalized()
     31     }
     32 }
     33 
     34 @Test func secureStoreKeyRejectsBlankServicePrefix() throws {
     35     let key = RadrootsSecureStoreKey(namespace: "session", name: "token")
     36     #expect(throws: RadrootsAppleSecurityError.self) {
     37         _ = try key.serviceName(servicePrefix: " ")
     38     }
     39 }
     40 
     41 @Test func keychainBaseQueryUsesNormalizedAccountName() throws {
     42     let store = RadrootsAppleKeychainSecureStore(servicePrefix: " org.radroots.tests ")
     43     let query = try store.baseQuery(
     44         for: RadrootsSecureStoreKey(namespace: " identity ", name: " secret ")
     45     )
     46 
     47     #expect(query[kSecAttrService as String] as? String == "org.radroots.tests.identity")
     48     #expect(query[kSecAttrAccount as String] as? String == "secret")
     49 }
     50 
     51 @Test func keychainNamespaceQueryUsesSharedValidation() throws {
     52     let store = RadrootsAppleKeychainSecureStore(servicePrefix: " org.radroots.tests ")
     53     let query = try store.namespaceQuery(" identity ")
     54 
     55     #expect(query[kSecAttrService as String] as? String == "org.radroots.tests.identity")
     56 }
     57 
     58 @Test func keychainStoreRoundTripsLocalSecret() throws {
     59     let store = RadrootsAppleKeychainSecureStore(
     60         servicePrefix: "org.radroots.tests.\(UUID().uuidString)"
     61     )
     62     let key = RadrootsSecureStoreKey(namespace: "roundtrip", name: "token")
     63     let data = Data("secret-token".utf8)
     64 
     65     try store.put(data, for: key)
     66     #expect(try store.get(key) == data)
     67 
     68     try store.delete(key)
     69     #expect(try store.get(key) == nil)
     70 }
     71 
     72 @Test func keychainStoreChecksPresenceWithoutReturningSecret() throws {
     73     let store = RadrootsAppleKeychainSecureStore(
     74         servicePrefix: "org.radroots.tests.\(UUID().uuidString)"
     75     )
     76     let key = RadrootsSecureStoreKey(namespace: "presence", name: "token")
     77 
     78     #expect(try store.contains(key) == false)
     79 
     80     try store.put(Data("secret-token".utf8), for: key)
     81     #expect(try store.contains(key) == true)
     82 
     83     try store.delete(key)
     84     #expect(try store.contains(key) == false)
     85 }
     86 
     87 @Test func keychainStoreReplacesExistingSecret() throws {
     88     let store = RadrootsAppleKeychainSecureStore(
     89         servicePrefix: "org.radroots.tests.\(UUID().uuidString)"
     90     )
     91     let key = RadrootsSecureStoreKey(namespace: "replacement", name: "token")
     92 
     93     try store.put(Data("old-secret".utf8), for: key)
     94     try store.put(Data("new-secret".utf8), for: key)
     95 
     96     #expect(try store.get(key) == Data("new-secret".utf8))
     97     try store.delete(key)
     98 }
     99 
    100 @Test func keychainStorePreservesExistingSecretWhenReplacementPreparationFails() throws {
    101     let servicePrefix = "org.radroots.tests.\(UUID().uuidString)"
    102     let key = RadrootsSecureStoreKey(namespace: "replacement-failure", name: "token")
    103     let store = RadrootsAppleKeychainSecureStore(servicePrefix: servicePrefix)
    104     let failingStore = RadrootsAppleKeychainSecureStore(
    105         servicePrefix: servicePrefix,
    106         accessControlFactory: { _ in
    107             throw RadrootsAppleSecurityError.invalidRequest("forced access control failure")
    108         }
    109     )
    110 
    111     try store.put(Data("old-secret".utf8), for: key)
    112     #expect(throws: RadrootsAppleSecurityError.self) {
    113         try failingStore.put(Data("new-secret".utf8), for: key, policy: .userPresenceLocalSecret)
    114     }
    115 
    116     #expect(try store.get(key) == Data("old-secret".utf8))
    117     try store.delete(key)
    118 }
    119 
    120 @Test func secureLocalSecretMapsToDeviceLocalWhenUnlockedKeychainPolicy() {
    121     let store = RadrootsAppleKeychainSecureStore()
    122     let mapping = store.keychainPolicyMapping(for: .secureLocalSecret)
    123 
    124     #expect(String(mapping.accessibilityConstant) == String(kSecAttrAccessibleWhenUnlockedThisDeviceOnly))
    125     #expect(mapping.usesAccessControl == false)
    126     #expect(mapping.accessControlFlags.isEmpty)
    127 }
    128 
    129 @Test func userPresenceLocalSecretMapsToDeviceLocalWhenUnlockedAccessControl() throws {
    130     let store = RadrootsAppleKeychainSecureStore()
    131     let mapping = store.keychainPolicyMapping(for: .userPresenceLocalSecret)
    132 
    133     #expect(String(mapping.accessibilityConstant) == String(kSecAttrAccessibleWhenUnlockedThisDeviceOnly))
    134     #expect(mapping.usesAccessControl == true)
    135     #expect(mapping.accessControlFlags == .userPresence)
    136     _ = try store.accessControl(for: mapping)
    137 }
    138 
    139 @Test func afterFirstUnlockPolicyCanRemainDeviceLocal() {
    140     let store = RadrootsAppleKeychainSecureStore()
    141     let mapping = store.keychainPolicyMapping(
    142         for: RadrootsSecretAccessPolicy(
    143             accessibility: .afterFirstUnlock,
    144             deviceLocalOnly: true,
    145             userPresenceRequired: false
    146         )
    147     )
    148 
    149     #expect(String(mapping.accessibilityConstant) == String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly))
    150     #expect(mapping.usesAccessControl == false)
    151     #expect(mapping.accessControlFlags.isEmpty)
    152 }
    153 
    154 @Test func resetAllowsMissingState() throws {
    155     let request = RadrootsAppLocalStateResetRequest(
    156         appIdentifier: "org.radroots.tests.\(UUID().uuidString)",
    157         keychainServiceNames: ["org.radroots.tests.\(UUID().uuidString)"]
    158     )
    159 
    160     try RadrootsAppLocalStateReset.reset(request)
    161 }
    162 
    163 @Test func resetClearsNamedKeychainService() throws {
    164     let servicePrefix = "org.radroots.tests.\(UUID().uuidString)"
    165     let store = RadrootsAppleKeychainSecureStore(servicePrefix: servicePrefix)
    166     let key = RadrootsSecureStoreKey(namespace: "reset", name: "secret")
    167     let serviceName = try key.serviceName(servicePrefix: servicePrefix)
    168 
    169     try store.put(Data("secret".utf8), for: key)
    170     #expect(try store.get(key) == Data("secret".utf8))
    171 
    172     try RadrootsAppLocalStateReset.reset(
    173         RadrootsAppLocalStateResetRequest(
    174             appIdentifier: "org.radroots.tests.\(UUID().uuidString)",
    175             keychainServiceNames: [serviceName]
    176         )
    177     )
    178 
    179     #expect(try store.get(key) == nil)
    180 }
    181 
    182 @Test func userPresenceStatusIsInspectable() async throws {
    183     let userPresence = RadrootsAppleUserPresence()
    184     let status = try await userPresence.currentStatus()
    185     switch status.support {
    186     case .none, .deviceCredential, .biometricsOrDeviceCredential:
    187         break
    188     }
    189 }