Skip to content

Commit ab32f69

Browse files
authored
Merge pull request #21 from alexmodrono/fix/revert-dp-keychain
Revert to the file-based keychain (fix auto-logout / can't sign in)
2 parents 3019a51 + 5360de5 commit ab32f69

1 file changed

Lines changed: 37 additions & 82 deletions

File tree

Sources/Networking/Auth/KeychainManager.swift

Lines changed: 37 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)