@@ -18,13 +18,6 @@ public final class KeychainManager: Sendable {
1818 private init ( ) { }
1919
2020 /// Store a token for a given account.
21- ///
22- /// Writes to the data-protection keychain so the token is shared with the
23- /// File Provider extension through their common `keychain-access-group`
24- /// entitlement. Access there is granted by entitlement rather than a
25- /// per-binary ACL, so it survives the app being re-signed (e.g. by a Sparkle
26- /// update) — unlike the file-based keychain, where a re-signed extension
27- /// silently loses read access and the domain shows as "signed out".
2821 public func storeToken( _ token: String , forAccount account: String ) throws {
2922 let data = Data ( token. utf8)
3023
@@ -33,13 +26,17 @@ public final class KeychainManager: Sendable {
3326 // the user would silently be logged out. SecItemUpdate is atomic
3427 // and also normalises any accessibility-class mismatch from older
3528 // builds (where the item may have been stored with WhenUnlocked).
29+ let lookupQuery : [ String : Any ] = [
30+ kSecClass as String : kSecClassGenericPassword,
31+ kSecAttrService as String : service,
32+ kSecAttrAccount as String : account
33+ ]
3634 let updateAttributes : [ String : Any ] = [
3735 kSecValueData as String : data,
3836 kSecAttrAccessible as String : kSecAttrAccessibleAfterFirstUnlock
3937 ]
40- let updateStatus = SecItemUpdate ( query ( account : account ) as CFDictionary , updateAttributes as CFDictionary )
38+ let updateStatus = SecItemUpdate ( lookupQuery as CFDictionary , updateAttributes as CFDictionary )
4139 if updateStatus == errSecSuccess {
42- try ? deleteLegacyToken ( account: account)
4340 return
4441 }
4542 guard updateStatus == errSecItemNotFound else {
@@ -48,111 +45,69 @@ public final class KeychainManager: Sendable {
4845 }
4946
5047 // Item doesn't exist yet — add it.
51- var addQuery = query ( account: account)
52- addQuery [ kSecValueData as String ] = data
53- addQuery [ kSecAttrAccessible as String ] = kSecAttrAccessibleAfterFirstUnlock
48+ let addQuery : [ String : Any ] = [
49+ kSecClass as String : kSecClassGenericPassword,
50+ kSecAttrService as String : service,
51+ kSecAttrAccount as String : account,
52+ kSecValueData as String : data,
53+ kSecAttrAccessible as String : kSecAttrAccessibleAfterFirstUnlock
54+ ]
5455
5556 let addStatus = SecItemAdd ( addQuery as CFDictionary , nil )
5657 guard addStatus == errSecSuccess else {
5758 logger. error ( " Keychain add failed: \( addStatus) " )
5859 throw KeychainError . storeFailed ( status: addStatus)
5960 }
60- try ? deleteLegacyToken ( account: account)
6161 }
6262
6363 /// Retrieve a token for a given account.
6464 public func retrieveToken( forAccount account: String ) throws -> String ? {
65- // Preferred: the shared data-protection keychain.
66- if let token = try copyToken ( account: account, dataProtection: true ) {
67- return token
68- }
69-
70- // Legacy: older builds stored the token in the file-based login keychain,
71- // which the File Provider extension cannot read after the app is
72- // re-signed. Migrate any such token into the shared keychain so the
73- // extension regains access without the user signing in again.
74- if let legacy = ( try ? copyToken ( account: account, dataProtection: false ) ) ?? nil {
75- try ? storeToken ( legacy, forAccount: account)
76- return legacy
77- }
78-
79- return nil
80- }
81-
82- /// Delete a token for a given account.
83- public func deleteToken( forAccount account: String ) throws {
84- let status = SecItemDelete ( query ( account: account) as CFDictionary )
85- guard status == errSecSuccess || status == errSecItemNotFound else {
86- logger. error ( " Failed to delete token: \( status) " )
87- throw KeychainError . deleteFailed ( status: status)
88- }
89- try ? deleteLegacyToken ( account: account)
90- }
91-
92- /// Delete all tokens for this app, in both the shared and legacy keychains.
93- public func deleteAllTokens( ) throws {
94- for dataProtection in [ true , false ] {
95- var deleteQuery : [ String : Any ] = [
96- kSecClass as String : kSecClassGenericPassword,
97- kSecAttrService as String : service
98- ]
99- deleteQuery [ kSecUseDataProtectionKeychain as String ] = dataProtection
100- let status = SecItemDelete ( deleteQuery as CFDictionary )
101- guard status == errSecSuccess || status == errSecItemNotFound else {
102- throw KeychainError . deleteFailed ( status: status)
103- }
104- }
105- }
106-
107- // MARK: - Keychain query helpers
108-
109- /// Base query against the data-protection keychain. Items land in the app's
110- /// default access group — the sole `keychain-access-groups` entitlement entry
111- /// shared by the app and the extension — so no explicit access group is set.
112- private func query( account: String ? ) -> [ String : Any ] {
113- var q : [ String : Any ] = [
114- kSecClass as String : kSecClassGenericPassword,
115- kSecAttrService as String : service,
116- kSecUseDataProtectionKeychain as String : true
117- ]
118- if let account {
119- q [ kSecAttrAccount as String ] = account
120- }
121- return q
122- }
123-
124- private func copyToken( account: String , dataProtection: Bool ) throws -> String ? {
125- let copyQuery : [ String : Any ] = [
65+ let query : [ String : Any ] = [
12666 kSecClass as String : kSecClassGenericPassword,
12767 kSecAttrService as String : service,
12868 kSecAttrAccount as String : account,
129- kSecUseDataProtectionKeychain as String : dataProtection,
13069 kSecReturnData as String : true ,
13170 kSecMatchLimit as String : kSecMatchLimitOne
13271 ]
13372
13473 var result : AnyObject ?
135- let status = SecItemCopyMatching ( copyQuery as CFDictionary , & result)
74+ let status = SecItemCopyMatching ( query as CFDictionary , & result)
13675
13776 if status == errSecItemNotFound {
13877 return nil
13978 }
79+
14080 guard status == errSecSuccess, let data = result as? Data else {
14181 logger. error ( " Failed to retrieve token: \( status) " )
14282 throw KeychainError . retrieveFailed ( status: status)
14383 }
84+
14485 return String ( data: data, encoding: . utf8)
14586 }
14687
147- /// Remove the token from the legacy file-based keychain used by older builds .
148- private func deleteLegacyToken ( account: String ) throws {
149- let legacyQuery : [ String : Any ] = [
88+ /// Delete a token for a given account .
89+ public func deleteToken ( forAccount account: String ) throws {
90+ let query : [ String : Any ] = [
15091 kSecClass as String : kSecClassGenericPassword,
15192 kSecAttrService as String : service,
152- kSecAttrAccount as String : account,
153- kSecUseDataProtectionKeychain as String : false
93+ kSecAttrAccount as String : account
94+ ]
95+
96+ let status = SecItemDelete ( query as CFDictionary )
97+ guard status == errSecSuccess || status == errSecItemNotFound else {
98+ logger. error ( " Failed to delete token: \( status) " )
99+ throw KeychainError . deleteFailed ( status: status)
100+ }
101+ }
102+
103+ /// Delete all tokens for this app.
104+ public func deleteAllTokens( ) throws {
105+ let query : [ String : Any ] = [
106+ kSecClass as String : kSecClassGenericPassword,
107+ kSecAttrService as String : service
154108 ]
155- let status = SecItemDelete ( legacyQuery as CFDictionary )
109+
110+ let status = SecItemDelete ( query as CFDictionary )
156111 guard status == errSecSuccess || status == errSecItemNotFound else {
157112 throw KeychainError . deleteFailed ( status: status)
158113 }
0 commit comments