Skip to content

Commit f56576f

Browse files
authored
Fix bug where multiple keychain entries would result in user persistence failure (#5930)
* Fix bug where multiple keychain entries would result in user persistence failure * Fix missing header * Fix tests * fix tests on macOS * Always return non-legacy keychain value * Fix thing * Fix missing type * fix missing bridge call
1 parent a372ed2 commit f56576f

File tree

2 files changed

+52
-3
lines changed

2 files changed

+52
-3
lines changed

FirebaseAuth/Sources/Storage/FIRAuthKeychainServices.m

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
#import <Security/Security.h>
2020

21+
#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
22+
2123
#import "FirebaseAuth/Sources/Storage/FIRAuthUserDefaults.h"
2224
#import "FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h"
2325

@@ -129,18 +131,34 @@ - (nullable NSData *)itemWithQuery:(NSDictionary *)query error:(NSError **_Nulla
129131

130132
if (status == noErr && result != NULL) {
131133
NSArray *items = (__bridge_transfer NSArray *)result;
132-
if (items.count != 1) {
134+
if (items.count == 0) {
133135
if (error) {
136+
// The keychain query returned no error, but there were no items found.
134137
*error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
135138
}
136139
return nil;
140+
} else if (items.count > 1) {
141+
// More than one keychain item was found, all but the first will be ignored.
142+
FIRLogWarning(
143+
kFIRLoggerAuth, @"I-AUT000005",
144+
@"Keychain query returned multiple results, all but the first will be ignored: %@",
145+
items);
137146
}
138147

139148
if (error) {
140149
*error = nil;
141150
}
142-
NSDictionary *item = items[0];
143-
return item[(__bridge id)kSecValueData];
151+
// Return the non-legacy item.
152+
for (NSDictionary *item in items) {
153+
if (item[(__bridge NSString *)kSecAttrService] != nil) {
154+
return item[(__bridge id)kSecValueData];
155+
}
156+
}
157+
158+
// If they were all legacy items, just return the first one.
159+
// This should not happen, since only one account should be
160+
// stored.
161+
return items[0][(__bridge id)kSecValueData];
144162
}
145163

146164
if (status == errSecItemNotFound) {

FirebaseAuth/Tests/Unit/FIRAuthKeychainServicesTests.m

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@
8484
return [NSError errorWithDomain:@"ERROR" code:-1 userInfo:nil];
8585
}
8686

87+
@interface FIRAuthKeychainServices ()
88+
89+
// Exposed for testing.
90+
- (nullable NSData *)itemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error;
91+
92+
@end
93+
8794
/** @class FIRAuthKeychainTests
8895
@brief Tests for @c FIRAuthKeychainTests .
8996
*/
@@ -116,6 +123,30 @@ - (void)testReadExisting {
116123
[self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
117124
}
118125

126+
/** @fn testReadMultiple
127+
@brief Tests reading multiple items from keychain returns only the first item.
128+
*/
129+
- (void)testReadMultiple {
130+
[self addPassword:kData account:accountFromKey(kKey) service:kService];
131+
[self addPassword:kOtherData account:accountFromKey(kKey) service:kOtherService];
132+
FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
133+
NSString *queriedAccount = accountFromKey(kKey);
134+
NSDictionary *query = @{
135+
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
136+
(__bridge id)kSecAttrAccount : queriedAccount,
137+
};
138+
NSError *error = fakeError();
139+
// Keychain on macOS returns items in a different order than keychain on iOS,
140+
// so test that the returned object is one of any of the added objects.
141+
NSData *queriedData = [keychain itemWithQuery:query error:&error];
142+
BOOL isValidKeychainItem =
143+
[@[ dataFromString(kData), dataFromString(kOtherData) ] containsObject:queriedData];
144+
XCTAssertTrue(isValidKeychainItem);
145+
XCTAssertNil(error);
146+
[self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
147+
[self deletePasswordWithAccount:accountFromKey(kKey) service:kOtherService];
148+
}
149+
119150
/** @fn testNotReadOtherService
120151
@brief Tests not reading keychain item belonging to other service.
121152
*/

0 commit comments

Comments
 (0)