1 |
efrain |
1 |
/*
|
|
|
2 |
* Copyright 2019 Google
|
|
|
3 |
*
|
|
|
4 |
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
5 |
* you may not use this file except in compliance with the License.
|
|
|
6 |
* You may obtain a copy of the License at
|
|
|
7 |
*
|
|
|
8 |
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
9 |
*
|
|
|
10 |
* Unless required by applicable law or agreed to in writing, software
|
|
|
11 |
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
12 |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
13 |
* See the License for the specific language governing permissions and
|
|
|
14 |
* limitations under the License.
|
|
|
15 |
*/
|
|
|
16 |
|
|
|
17 |
#import "FirebaseMessaging/Sources/Token/FIRMessagingAuthKeychain.h"
|
|
|
18 |
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
|
|
|
19 |
#import "FirebaseMessaging/Sources/Token/FIRMessagingKeychain.h"
|
|
|
20 |
|
|
|
21 |
/**
|
|
|
22 |
* The error type representing why we couldn't read data from the keychain.
|
|
|
23 |
*/
|
|
|
24 |
typedef NS_ENUM(int, FIRMessagingKeychainErrorType) {
|
|
|
25 |
kFIRMessagingKeychainErrorBadArguments = -1301,
|
|
|
26 |
};
|
|
|
27 |
|
|
|
28 |
NSString *const kFIRMessagingKeychainWildcardIdentifier = @"*";
|
|
|
29 |
|
|
|
30 |
@interface FIRMessagingAuthKeychain ()
|
|
|
31 |
|
|
|
32 |
@property(nonatomic, copy) NSString *generic;
|
|
|
33 |
// cachedKeychainData is keyed by service and account, the value is an array of NSData.
|
|
|
34 |
// It is used to cache the tokens per service, per account, as well as checkin data per service,
|
|
|
35 |
// per account inside the keychain.
|
|
|
36 |
@property(nonatomic, strong)
|
|
|
37 |
NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, NSArray<NSData *> *> *>
|
|
|
38 |
*cachedKeychainData;
|
|
|
39 |
|
|
|
40 |
@end
|
|
|
41 |
|
|
|
42 |
@implementation FIRMessagingAuthKeychain
|
|
|
43 |
|
|
|
44 |
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
|
|
45 |
self = [super init];
|
|
|
46 |
if (self) {
|
|
|
47 |
_generic = [identifier copy];
|
|
|
48 |
_cachedKeychainData = [[NSMutableDictionary alloc] init];
|
|
|
49 |
}
|
|
|
50 |
return self;
|
|
|
51 |
}
|
|
|
52 |
|
|
|
53 |
+ (NSMutableDictionary *)keychainQueryForService:(NSString *)service
|
|
|
54 |
account:(NSString *)account
|
|
|
55 |
generic:(NSString *)generic {
|
|
|
56 |
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword};
|
|
|
57 |
|
|
|
58 |
NSMutableDictionary *finalQuery = [NSMutableDictionary dictionaryWithDictionary:query];
|
|
|
59 |
if ([generic length] && ![kFIRMessagingKeychainWildcardIdentifier isEqualToString:generic]) {
|
|
|
60 |
finalQuery[(__bridge NSString *)kSecAttrGeneric] = generic;
|
|
|
61 |
}
|
|
|
62 |
if ([account length] && ![kFIRMessagingKeychainWildcardIdentifier isEqualToString:account]) {
|
|
|
63 |
finalQuery[(__bridge NSString *)kSecAttrAccount] = account;
|
|
|
64 |
}
|
|
|
65 |
if ([service length] && ![kFIRMessagingKeychainWildcardIdentifier isEqualToString:service]) {
|
|
|
66 |
finalQuery[(__bridge NSString *)kSecAttrService] = service;
|
|
|
67 |
}
|
|
|
68 |
return finalQuery;
|
|
|
69 |
}
|
|
|
70 |
|
|
|
71 |
- (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account {
|
|
|
72 |
return [[self class] keychainQueryForService:service account:account generic:self.generic];
|
|
|
73 |
}
|
|
|
74 |
|
|
|
75 |
- (NSArray<NSData *> *)itemsMatchingService:(NSString *)service account:(NSString *)account {
|
|
|
76 |
// If query wildcard service, it asks for all the results, which always query from keychain.
|
|
|
77 |
if (![service isEqualToString:kFIRMessagingKeychainWildcardIdentifier] &&
|
|
|
78 |
![account isEqualToString:kFIRMessagingKeychainWildcardIdentifier] &&
|
|
|
79 |
_cachedKeychainData[service][account]) {
|
|
|
80 |
// As long as service, account array exist, even it's empty, it means we've queried it before,
|
|
|
81 |
// returns the cache value.
|
|
|
82 |
return _cachedKeychainData[service][account];
|
|
|
83 |
}
|
|
|
84 |
|
|
|
85 |
NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
|
|
|
86 |
NSMutableArray<NSData *> *results;
|
|
|
87 |
keychainQuery[(__bridge id)kSecReturnData] = (__bridge id)kCFBooleanTrue;
|
|
|
88 |
#if TARGET_OS_IOS || TARGET_OS_TV
|
|
|
89 |
keychainQuery[(__bridge id)kSecReturnAttributes] = (__bridge id)kCFBooleanTrue;
|
|
|
90 |
keychainQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
|
|
|
91 |
// FIRMessagingKeychain should only take a query and return a result, will handle the query here.
|
|
|
92 |
NSArray *passwordInfos =
|
|
|
93 |
CFBridgingRelease([[FIRMessagingKeychain sharedInstance] itemWithQuery:keychainQuery]);
|
|
|
94 |
#elif TARGET_OS_OSX || TARGET_OS_WATCH
|
|
|
95 |
keychainQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
|
|
96 |
NSData *passwordInfos =
|
|
|
97 |
CFBridgingRelease([[FIRMessagingKeychain sharedInstance] itemWithQuery:keychainQuery]);
|
|
|
98 |
#endif
|
|
|
99 |
|
|
|
100 |
if (!passwordInfos) {
|
|
|
101 |
// Nothing was found, simply return from this sync block.
|
|
|
102 |
// Make sure to label the cache entry empty, signaling that we've queried this entry.
|
|
|
103 |
if ([service isEqualToString:kFIRMessagingKeychainWildcardIdentifier] ||
|
|
|
104 |
[account isEqualToString:kFIRMessagingKeychainWildcardIdentifier]) {
|
|
|
105 |
// Do not update cache if it's wildcard query.
|
|
|
106 |
return @[];
|
|
|
107 |
} else if (_cachedKeychainData[service]) {
|
|
|
108 |
[_cachedKeychainData[service] setObject:@[] forKey:account];
|
|
|
109 |
} else {
|
|
|
110 |
[_cachedKeychainData setObject:[@{account : @[]} mutableCopy] forKey:service];
|
|
|
111 |
}
|
|
|
112 |
return @[];
|
|
|
113 |
}
|
|
|
114 |
results = [[NSMutableArray alloc] init];
|
|
|
115 |
#if TARGET_OS_IOS || TARGET_OS_TV
|
|
|
116 |
NSInteger numPasswords = passwordInfos.count;
|
|
|
117 |
for (NSUInteger i = 0; i < numPasswords; i++) {
|
|
|
118 |
NSDictionary *passwordInfo = [passwordInfos objectAtIndex:i];
|
|
|
119 |
if (passwordInfo[(__bridge id)kSecValueData]) {
|
|
|
120 |
[results addObject:passwordInfo[(__bridge id)kSecValueData]];
|
|
|
121 |
}
|
|
|
122 |
}
|
|
|
123 |
#elif TARGET_OS_OSX || TARGET_OS_WATCH
|
|
|
124 |
[results addObject:passwordInfos];
|
|
|
125 |
#endif
|
|
|
126 |
// We query the keychain because it didn't exist in cache, now query is done, update the result in
|
|
|
127 |
// the cache.
|
|
|
128 |
if ([service isEqualToString:kFIRMessagingKeychainWildcardIdentifier] ||
|
|
|
129 |
[account isEqualToString:kFIRMessagingKeychainWildcardIdentifier]) {
|
|
|
130 |
// Do not update cache if it's wildcard query.
|
|
|
131 |
return [results copy];
|
|
|
132 |
} else if (_cachedKeychainData[service]) {
|
|
|
133 |
[_cachedKeychainData[service] setObject:[results copy] forKey:account];
|
|
|
134 |
} else {
|
|
|
135 |
NSMutableDictionary *entry = [@{account : [results copy]} mutableCopy];
|
|
|
136 |
[_cachedKeychainData setObject:entry forKey:service];
|
|
|
137 |
}
|
|
|
138 |
return [results copy];
|
|
|
139 |
}
|
|
|
140 |
|
|
|
141 |
- (NSData *)dataForService:(NSString *)service account:(NSString *)account {
|
|
|
142 |
NSArray<NSData *> *items = [self itemsMatchingService:service account:account];
|
|
|
143 |
// If items is nil or empty, nil will be returned.
|
|
|
144 |
return items.firstObject;
|
|
|
145 |
}
|
|
|
146 |
|
|
|
147 |
- (void)removeItemsMatchingService:(NSString *)service
|
|
|
148 |
account:(NSString *)account
|
|
|
149 |
handler:(void (^)(NSError *error))handler {
|
|
|
150 |
if ([service isEqualToString:kFIRMessagingKeychainWildcardIdentifier]) {
|
|
|
151 |
// Delete all keychain items.
|
|
|
152 |
_cachedKeychainData = [[NSMutableDictionary alloc] init];
|
|
|
153 |
} else if ([account isEqualToString:kFIRMessagingKeychainWildcardIdentifier]) {
|
|
|
154 |
// Delete all entries under service,
|
|
|
155 |
if (_cachedKeychainData[service]) {
|
|
|
156 |
_cachedKeychainData[service] = [[NSMutableDictionary alloc] init];
|
|
|
157 |
}
|
|
|
158 |
} else if (_cachedKeychainData[service]) {
|
|
|
159 |
// We should keep the service/account entry instead of nil so we know
|
|
|
160 |
// it's "empty entry" instead of "not query from keychain yet".
|
|
|
161 |
[_cachedKeychainData[service] setObject:@[] forKey:account];
|
|
|
162 |
} else {
|
|
|
163 |
[_cachedKeychainData setObject:[@{account : @[]} mutableCopy] forKey:service];
|
|
|
164 |
}
|
|
|
165 |
NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
|
|
|
166 |
[[FIRMessagingKeychain sharedInstance] removeItemWithQuery:keychainQuery handler:handler];
|
|
|
167 |
}
|
|
|
168 |
|
|
|
169 |
- (void)setData:(NSData *)data
|
|
|
170 |
forService:(NSString *)service
|
|
|
171 |
account:(NSString *)account
|
|
|
172 |
handler:(void (^)(NSError *))handler {
|
|
|
173 |
if ([service isEqualToString:kFIRMessagingKeychainWildcardIdentifier] ||
|
|
|
174 |
[account isEqualToString:kFIRMessagingKeychainWildcardIdentifier]) {
|
|
|
175 |
if (handler) {
|
|
|
176 |
handler([NSError errorWithDomain:kFIRMessagingKeychainErrorDomain
|
|
|
177 |
code:kFIRMessagingKeychainErrorBadArguments
|
|
|
178 |
userInfo:nil]);
|
|
|
179 |
}
|
|
|
180 |
return;
|
|
|
181 |
}
|
|
|
182 |
[self removeItemsMatchingService:service
|
|
|
183 |
account:account
|
|
|
184 |
handler:^(NSError *error) {
|
|
|
185 |
if (error) {
|
|
|
186 |
if (handler) {
|
|
|
187 |
handler(error);
|
|
|
188 |
}
|
|
|
189 |
return;
|
|
|
190 |
}
|
|
|
191 |
if (data.length > 0) {
|
|
|
192 |
NSMutableDictionary *keychainQuery =
|
|
|
193 |
[self keychainQueryForService:service account:account];
|
|
|
194 |
keychainQuery[(__bridge id)kSecValueData] = data;
|
|
|
195 |
|
|
|
196 |
keychainQuery[(__bridge id)kSecAttrAccessible] =
|
|
|
197 |
(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
|
|
198 |
[[FIRMessagingKeychain sharedInstance] addItemWithQuery:keychainQuery
|
|
|
199 |
handler:handler];
|
|
|
200 |
}
|
|
|
201 |
}];
|
|
|
202 |
// Set the cache value. This must happen after removeItemsMatchingService:account:handler was
|
|
|
203 |
// called, so the cache value was reset before setting a new value.
|
|
|
204 |
if (_cachedKeychainData[service]) {
|
|
|
205 |
if (_cachedKeychainData[service][account]) {
|
|
|
206 |
_cachedKeychainData[service][account] = @[ data ];
|
|
|
207 |
} else {
|
|
|
208 |
[_cachedKeychainData[service] setObject:@[ data ] forKey:account];
|
|
|
209 |
}
|
|
|
210 |
} else {
|
|
|
211 |
[_cachedKeychainData setObject:[@{account : @[ data ]} mutableCopy] forKey:service];
|
|
|
212 |
}
|
|
|
213 |
}
|
|
|
214 |
|
|
|
215 |
- (void)setCacheData:(NSData *)data forService:(NSString *)service account:(NSString *)account {
|
|
|
216 |
if (_cachedKeychainData[service]) {
|
|
|
217 |
if (_cachedKeychainData[service][account]) {
|
|
|
218 |
_cachedKeychainData[service][account] = @[ data ];
|
|
|
219 |
} else {
|
|
|
220 |
[_cachedKeychainData[service] setObject:@[ data ] forKey:account];
|
|
|
221 |
}
|
|
|
222 |
} else {
|
|
|
223 |
[_cachedKeychainData setObject:[@{account : @[ data ]} mutableCopy] forKey:service];
|
|
|
224 |
}
|
|
|
225 |
}
|
|
|
226 |
|
|
|
227 |
@end
|