Skip to content

Commit ae9450a

Browse files
KM-17456: Fix Mac Catalyst reconnect keychain race (#370)
* Fix Mac Catalyst reconnect race by updating keychain password in place instead of delete-then-add * Handle errSecDuplicateItem in keychain password add fallback to close first-write race
1 parent c7635b2 commit ae9450a

1 file changed

Lines changed: 32 additions & 5 deletions

File tree

LocalPackages/PIALibrary/Sources/PIALibrary/Util/Keychain.swift

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,44 @@ public class Keychain {
8282

8383
/// :nodoc:
8484
public func set(password: String, for username: String) throws {
85-
removePassword(for: username)
85+
guard let data = password.data(using: .utf8) else {
86+
throw KeychainError.add
87+
}
8688

8789
var query = [String: Any]()
8890
setScope(query: &query)
8991
query[kSecClass as String] = kSecClassGenericPassword
9092
query[kSecAttrAccount as String] = username
91-
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
92-
query[kSecValueData as String] = password.data(using: .utf8)
9393

94-
let status = SecItemAdd(query as CFDictionary, nil)
95-
guard (status == errSecSuccess) else {
94+
let attributesToUpdate: [String: Any] = [
95+
kSecValueData as String: data,
96+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
97+
]
98+
let updateStatus = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
99+
100+
switch updateStatus {
101+
case errSecSuccess:
102+
return
103+
case errSecItemNotFound:
104+
// No existing item — add a fresh one.
105+
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
106+
query[kSecValueData as String] = data
107+
let addStatus = SecItemAdd(query as CFDictionary, nil)
108+
switch addStatus {
109+
case errSecSuccess:
110+
return
111+
case errSecDuplicateItem:
112+
// Lost a first-write race — the item now exists, so update it in place.
113+
query.removeValue(forKey: kSecAttrAccessible as String)
114+
query.removeValue(forKey: kSecValueData as String)
115+
guard SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) == errSecSuccess
116+
else {
117+
throw KeychainError.add
118+
}
119+
default:
120+
throw KeychainError.add
121+
}
122+
default:
96123
throw KeychainError.add
97124
}
98125
}

0 commit comments

Comments
 (0)