Skip to content

Commit 4e1ff95

Browse files
authored
[Auth] Conform 'AuthKeychainServices' to 'Sendable' (#14862)
1 parent 491d037 commit 4e1ff95

11 files changed

+51
-49
lines changed

FirebaseAuth/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Unreleased
2+
- [fixed] Synchronize internal `AuthKeychainServices` class to prevent
3+
crashes from concurrent access. (#14835)
4+
15
# 11.12.0
26
- [fixed] Fix a `fatalError` unenrolling from MFA. An invalid user token now throws an
37
`invalidUserToken` error instead of crashing. (#14663)

FirebaseAuth/Sources/Swift/Auth/Auth.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1625,7 +1625,7 @@ extension Auth: AuthInterop {
16251625
// MARK: Internal methods
16261626

16271627
init(app: FirebaseApp,
1628-
keychainStorageProvider: AuthKeychainStorage = AuthKeychainStorageReal(),
1628+
keychainStorageProvider: AuthKeychainStorage = AuthKeychainStorageReal.shared,
16291629
backend: AuthBackend = .init(rpcIssuer: AuthBackendRPCIssuer()),
16301630
authDispatcher: AuthDispatcher = .init()) {
16311631
self.app = app

FirebaseAuth/Sources/Swift/Auth/AuthComponent.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ class AuthComponent: NSObject, Library, ComponentLifecycleMaintainer {
7878
// This doesn't stop any request already issued, see b/27704535
7979

8080
if let keychainServiceName = Auth.deleteKeychainServiceNameForAppName(app.name) {
81-
let keychain = AuthKeychainServices(service: keychainServiceName)
81+
let keychain = AuthKeychainServices(
82+
service: keychainServiceName,
83+
storage: AuthKeychainStorageReal.shared
84+
)
8285
let userKey = "\(app.name)_firebase_user"
8386
try? keychain.removeData(forKey: userKey)
8487
}

FirebaseAuth/Sources/Swift/Storage/AuthKeychainServices.swift

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
import FirebaseCoreExtension
16+
import FirebaseCoreInternal
1617
import Foundation
1718

1819
/// The prefix string for keychain item account attribute before the key.
@@ -22,16 +23,15 @@ private let kAccountPrefix = "firebase_auth_1_"
2223

2324
/// The utility class to manipulate data in iOS Keychain.
2425
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
25-
final class AuthKeychainServices {
26+
final class AuthKeychainServices: Sendable {
2627
/// The name of the keychain service.
27-
let service: String
28+
private let service: String
2829

29-
let keychainStorage: AuthKeychainStorage
30+
private let keychainStorage: AuthKeychainStorage
3031

3132
// MARK: - Internal methods for shared keychain operations
3233

33-
required init(service: String = "Unset service",
34-
storage: AuthKeychainStorage = AuthKeychainStorageReal()) {
34+
required init(service: String = "Unset service", storage: AuthKeychainStorage) {
3535
self.service = service
3636
keychainStorage = storage
3737
}
@@ -102,19 +102,15 @@ final class AuthKeychainServices {
102102
/// been deleted.
103103
///
104104
/// This dictionary is to avoid unnecessary keychain operations against legacy items.
105-
private var legacyEntryDeletedForKey: Set<String> = []
106-
107-
static func storage(identifier: String) -> Self {
108-
return Self(service: identifier)
109-
}
105+
private let legacyEntryDeletedForKey = FIRAllocatedUnfairLock<Set<String>>(initialState: [])
110106

111107
func data(forKey key: String) throws -> Data? {
112108
if let data = try getItemLegacy(query: genericPasswordQuery(key: key)) {
113109
return data
114110
}
115111

116112
// Check for legacy form.
117-
if legacyEntryDeletedForKey.contains(key) {
113+
if legacyEntryDeletedForKey.value().contains(key) {
118114
return nil
119115
}
120116
if let data = try getItemLegacy(query: legacyGenericPasswordQuery(key: key)) {
@@ -124,7 +120,7 @@ final class AuthKeychainServices {
124120
return data
125121
} else {
126122
// Mark legacy data as non-existing so we don't have to query it again.
127-
legacyEntryDeletedForKey.insert(key)
123+
legacyEntryDeletedForKey.withLock { $0.insert(key) }
128124
return nil
129125
}
130126
}
@@ -214,12 +210,12 @@ final class AuthKeychainServices {
214210
/// Deletes legacy item from the keychain if it is not already known to be deleted.
215211
/// - Parameter key: The key for the item.
216212
private func deleteLegacyItem(key: String) {
217-
if legacyEntryDeletedForKey.contains(key) {
213+
if legacyEntryDeletedForKey.value().contains(key) {
218214
return
219215
}
220216
let query = legacyGenericPasswordQuery(key: key)
221217
keychainStorage.delete(query: query)
222-
legacyEntryDeletedForKey.insert(key)
218+
legacyEntryDeletedForKey.withLock { $0.insert(key) }
223219
}
224220

225221
/// Returns a keychain query of generic password to be used to manipulate key'ed value.

FirebaseAuth/Sources/Swift/Storage/AuthKeychainStorage.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Foundation
1717
/// Protocol to manage keychain updates. Tests can do a fake implementation.
1818

1919
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
20-
protocol AuthKeychainStorage {
20+
protocol AuthKeychainStorage: Sendable {
2121
func get(query: [String: Any], result: inout AnyObject?) -> OSStatus
2222
func add(query: [String: Any]) -> OSStatus
2323
func update(query: [String: Any], attributes: [String: Any]) -> OSStatus

FirebaseAuth/Sources/Swift/Storage/AuthKeychainStorageReal.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
import Foundation
1616

1717
/// The utility class to update the real keychain
18-
1918
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
20-
class AuthKeychainStorageReal: AuthKeychainStorage {
19+
final class AuthKeychainStorageReal: AuthKeychainStorage {
20+
static let shared: AuthKeychainStorageReal = .init()
21+
22+
private init() {}
23+
2124
func get(query: [String: Any], result: inout AnyObject?) -> OSStatus {
2225
return SecItemCopyMatching(query as CFDictionary, &result)
2326
}

FirebaseAuth/Sources/Swift/Storage/AuthUserDefaults.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,11 @@ private let kPersistentDomainNamePrefix = "com.google.Firebase.Auth."
1919
/// The utility class to manage data storage in NSUserDefaults.
2020
class AuthUserDefaults {
2121
/// The name of the persistent domain in user defaults.
22-
2322
private let persistentDomainName: String
2423

2524
/// The backing NSUserDefaults storage for this instance.
26-
2725
private let storage: UserDefaults
2826

29-
static func storage(identifier: String) -> Self {
30-
return Self(service: identifier)
31-
}
32-
3327
required init(service: String) {
3428
persistentDomainName = kPersistentDomainNamePrefix + service
3529
storage = UserDefaults()

FirebaseAuth/Tests/Unit/AuthKeychainServicesTests.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,15 @@ class AuthKeychainServicesTests: XCTestCase {
3333
}
3434

3535
var keychain: AuthKeychainServices!
36+
#if (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE
37+
let storage: AuthKeychainStorage = FakeAuthKeychainStorage()
38+
#else
39+
let storage: AuthKeychainStorage = AuthKeychainStorageReal.shared
40+
#endif // (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE
3641

3742
override func setUp() {
3843
super.setUp()
39-
#if (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE
40-
keychain = AuthKeychainServices(service: Self.service, storage: FakeAuthKeychainStorage())
41-
#else
42-
keychain = AuthKeychainServices(service: Self.service)
43-
#endif // (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE
44+
keychain = AuthKeychainServices(service: Self.service, storage: storage)
4445
}
4546

4647
func testReadNonexisting() throws {
@@ -142,7 +143,7 @@ class AuthKeychainServicesTests: XCTestCase {
142143
}
143144

144145
var result: CFTypeRef?
145-
let status = keychain.keychainStorage.get(query: query as [String: Any], result: &result)
146+
let status = storage.get(query: query as [String: Any], result: &result)
146147

147148
guard let result = result as? Data, status != errSecItemNotFound else {
148149
if let resultArray = result as? [[String: Any]],
@@ -168,7 +169,7 @@ class AuthKeychainServicesTests: XCTestCase {
168169
if let service {
169170
query[kSecAttrService] = service
170171
}
171-
XCTAssertEqual(keychain.keychainStorage.add(query: query as [String: Any]), errSecSuccess)
172+
XCTAssertEqual(storage.add(query: query as [String: Any]), errSecSuccess)
172173
}
173174

174175
private func setPassword(_ password: String?,
@@ -192,6 +193,6 @@ class AuthKeychainServicesTests: XCTestCase {
192193
if let service {
193194
query[kSecAttrService] = service
194195
}
195-
XCTAssertEqual(keychain.keychainStorage.delete(query: query as [String: Any]), errSecSuccess)
196+
XCTAssertEqual(storage.delete(query: query as [String: Any]), errSecSuccess)
196197
}
197198
}

FirebaseAuth/Tests/Unit/AuthTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class AuthTests: RPCBaseTests {
4343
#if (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE
4444
let keychainStorageProvider = FakeAuthKeychainStorage()
4545
#else
46-
let keychainStorageProvider = AuthKeychainStorageReal()
46+
let keychainStorageProvider = AuthKeychainStorageReal.shared
4747
#endif // (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE
4848

4949
// Stub the implementation to save the token refresh task for later execution.

FirebaseAuth/Tests/Unit/Fakes/FakeAuthKeychainStorage.swift

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,28 @@
1313
// limitations under the License.
1414

1515
@testable import FirebaseAuth
16+
import FirebaseCoreInternal
1617
import Foundation
1718
import XCTest
1819

19-
/** @class AuthKeychainStorage
20-
@brief The utility class to update the real keychain
21-
*/
20+
/// The utility class to update the real keychain
2221
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
23-
class FakeAuthKeychainStorage: AuthKeychainStorage {
22+
final class FakeAuthKeychainStorage: AuthKeychainStorage {
2423
// Fake Keychain. It's a dictionary, keyed by service name, for each key-value store dictionary
25-
private var fakeKeychain: [String: [String: Any]] = [:]
24+
private let fakeKeychain = FIRAllocatedUnfairLock<[String: [String: Any]]>(initialState: [:])
2625

27-
private var fakeLegacyKeychain: [String: Any] = [:]
26+
private let fakeLegacyKeychain = FIRAllocatedUnfairLock<[String: Any]>(initialState: [:])
2827

2928
func get(query: [String: Any], result: inout AnyObject?) -> OSStatus {
3029
if let service = queryService(query) {
31-
guard let value = fakeKeychain[service]?[queryKey(query)] else {
30+
guard let value = fakeKeychain.value()[service]?[queryKey(query)] else {
3231
return errSecItemNotFound
3332
}
3433
let returnArrayofDictionary = [[kSecValueData as String: value]]
3534
result = returnArrayofDictionary as AnyObject
3635
return noErr
3736
} else {
38-
guard let value = fakeLegacyKeychain[queryKey(query)] else {
37+
guard let value = fakeLegacyKeychain.value()[queryKey(query)] else {
3938
return errSecItemNotFound
4039
}
4140
let returnArrayofDictionary = [[kSecValueData as String: value]]
@@ -46,9 +45,9 @@ class FakeAuthKeychainStorage: AuthKeychainStorage {
4645

4746
func add(query: [String: Any]) -> OSStatus {
4847
if let service = queryService(query) {
49-
fakeKeychain[service]?[queryKey(query)] = query[kSecValueData as String]
48+
fakeKeychain.withLock { $0[service]?[queryKey(query)] = query[kSecValueData as String] }
5049
} else {
51-
fakeLegacyKeychain[queryKey(query)] = query[kSecValueData as String]
50+
fakeLegacyKeychain.withLock { $0[queryKey(query)] = query[kSecValueData as String] }
5251
}
5352
return noErr
5453
}
@@ -59,9 +58,9 @@ class FakeAuthKeychainStorage: AuthKeychainStorage {
5958

6059
@discardableResult func delete(query: [String: Any]) -> OSStatus {
6160
if let service = queryService(query) {
62-
fakeKeychain[service]?[queryKey(query)] = nil
61+
fakeKeychain.withLock { $0[service]?[queryKey(query)] = nil }
6362
} else {
64-
fakeLegacyKeychain[queryKey(query)] = nil
63+
fakeLegacyKeychain.withLock { $0[queryKey(query)] = nil }
6564
}
6665
return noErr
6766
}
@@ -79,8 +78,10 @@ class FakeAuthKeychainStorage: AuthKeychainStorage {
7978
guard let service = query[kSecAttrService as String] as? String else {
8079
return nil
8180
}
82-
if fakeKeychain[service] == nil {
83-
fakeKeychain[service] = [:]
81+
fakeKeychain.withLock { fakeKeychain in
82+
if fakeKeychain[service] == nil {
83+
fakeKeychain[service] = [:]
84+
}
8485
}
8586
return service
8687
}

FirebaseAuth/Tests/Unit/UserTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class UserTests: RPCBaseTests {
4545
#if (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE
4646
let keychainStorageProvider = FakeAuthKeychainStorage()
4747
#else
48-
let keychainStorageProvider = AuthKeychainStorageReal()
48+
let keychainStorageProvider = AuthKeychainStorageReal.shared
4949
#endif // (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE
5050
auth = Auth(
5151
app: FirebaseApp.app(name: "test-UserTests")!,

0 commit comments

Comments
 (0)