GIDMDMPasscodeCache.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import "GoogleSignIn/Sources/GIDMDMPasscodeCache.h"
  15. #import <Foundation/Foundation.h>
  16. #import <LocalAuthentication/LocalAuthentication.h>
  17. #import <Security/Security.h>
  18. #import <UIKit/UIKit.h>
  19. #import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
  20. #import "GoogleSignIn/Sources/GIDMDMPasscodeState_Private.h"
  21. NS_ASSUME_NONNULL_BEGIN
  22. /** The JSON key for passcode info obtained by LocalAuthentication API. */
  23. static NSString *const kLocalAuthenticationKey = @"LocalAuthentication";
  24. /** The JSON key for passcode info obtained by Keychain API. */
  25. static NSString *const kKeychainKey = @"Keychain";
  26. /** The JSON key for API result. */
  27. static NSString *const kResultKey = @"result";
  28. /** The JSON key for error domain. */
  29. static NSString *const kErrorDomainKey = @"error_domain";
  30. /** The JSON key for error code. */
  31. static NSString *const kErrorCodeKey = @"error_code";
  32. /** Service name for the keychain item used to probe passcode state. */
  33. static NSString * const kPasscodeStatusService = @"com.google.MDM.PasscodeKeychainService";
  34. /** Account name for the keychain item used to probe passcode state. */
  35. static NSString * const kPasscodeStatusAccount = @"com.google.MDM.PasscodeKeychainAccount";
  36. /** The time for passcode state retrieved by Keychain API to be cached. */
  37. static const NSTimeInterval kKeychainInfoCacheTime = 5;
  38. /** The time to wait (in nanaoseconds) on obtaining keychain info. */
  39. static const int64_t kObtainKeychainInfoWaitTime = 3 * NSEC_PER_SEC;
  40. @implementation GIDMDMPasscodeCache {
  41. /** Whether or not LocalAuthentication API is available. */
  42. BOOL _hasLocalAuthentication;
  43. /** The passcode information obtained by LocalAuthentication API. */
  44. NSDictionary<NSString *, NSObject *> *_localAuthenticationInfo;
  45. /** Whether the app has entered background since _localAuthenticationInfo was obtained. */
  46. BOOL _hasEnteredBackground;
  47. /** Whether or not Keychain API is available. */
  48. BOOL _hasKeychain;
  49. /** The passcode information obtained by LocalAuthentication API. */
  50. NSDictionary<NSString *, NSObject *> *_keychainInfo;
  51. /** The timestamp for _keychainInfo to expire. */
  52. NSDate *_keychainExpireTime;
  53. /** The cached passcode state. */
  54. GIDMDMPasscodeState *_cachedState;
  55. }
  56. - (instancetype)init {
  57. self = [super init];
  58. if (self) {
  59. _hasLocalAuthentication = [self hasLocalAuthentication];
  60. _hasKeychain = [self hasKeychain];
  61. [[NSNotificationCenter defaultCenter] addObserver:self
  62. selector:@selector(applicationDidEnterBackground:)
  63. name:UIApplicationDidEnterBackgroundNotification
  64. object:nil];
  65. }
  66. return self;
  67. }
  68. - (void)dealloc {
  69. [[NSNotificationCenter defaultCenter] removeObserver:self];
  70. }
  71. + (instancetype)sharedInstance {
  72. static GIDMDMPasscodeCache *sharedInstance;
  73. static dispatch_once_t onceToken;
  74. dispatch_once(&onceToken, ^{
  75. sharedInstance = [[GIDMDMPasscodeCache alloc] init];
  76. });
  77. return sharedInstance;
  78. }
  79. - (GIDMDMPasscodeState *)passcodeState {
  80. // If the method is called by multiple threads at the same time, they need to execute sequentially
  81. // to maintain internal data integrity.
  82. @synchronized(self) {
  83. BOOL refreshLocalAuthentication = _hasLocalAuthentication &&
  84. (_localAuthenticationInfo == nil || _hasEnteredBackground);
  85. BOOL refreshKeychain = _hasKeychain &&
  86. (_keychainInfo == nil || [_keychainExpireTime timeIntervalSinceNow] < 0);
  87. if (!refreshLocalAuthentication && !refreshKeychain && _cachedState) {
  88. return _cachedState;
  89. }
  90. static dispatch_queue_t workQueue;
  91. static dispatch_semaphore_t semaphore;
  92. if (!workQueue) {
  93. workQueue = dispatch_queue_create("com.google.MDM.PasscodeWorkQueue", DISPATCH_QUEUE_SERIAL);
  94. semaphore = dispatch_semaphore_create(0);
  95. }
  96. if (refreshKeychain) {
  97. _keychainInfo = nil;
  98. dispatch_async(workQueue, ^() {
  99. [self obtainKeychainInfo];
  100. dispatch_semaphore_signal(semaphore);
  101. });
  102. }
  103. if (refreshLocalAuthentication) {
  104. [self obtainLocalAuthenticationInfo];
  105. }
  106. if (refreshKeychain) {
  107. dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, kObtainKeychainInfoWaitTime);
  108. dispatch_semaphore_wait(semaphore, timeout);
  109. }
  110. _cachedState = [[GIDMDMPasscodeState alloc] initWithStatus:[self status] info:[self info]];
  111. return _cachedState;
  112. }
  113. }
  114. #pragma mark - Private Methods
  115. /**
  116. * Detects whether LocalAuthentication API is available for passscode detection purpose.
  117. */
  118. - (BOOL)hasLocalAuthentication {
  119. // While the LocalAuthentication framework itself is available at iOS 8+, the particular constant
  120. // we need, kLAPolicyDeviceOwnerAuthentication, is only available at iOS 9+. Since the constant
  121. // is defined as a macro, there is no good way to detect its availability at runtime, so we can
  122. // only check OS version here.
  123. NSProcessInfo *processInfo = [NSProcessInfo processInfo];
  124. return [processInfo respondsToSelector:@selector(isOperatingSystemAtLeastVersion:)] &&
  125. [processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 9}];
  126. }
  127. /**
  128. * Detects whether Keychain API is available for passscode detection purpose.
  129. */
  130. - (BOOL)hasKeychain {
  131. // While the Keychain Source is available at iOS 4+, the particular constant we need,
  132. // kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, is only available at iOS 8+.
  133. #pragma clang diagnostic push
  134. #pragma clang diagnostic ignored "-Wtautological-pointer-compare"
  135. return &kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly != NULL;
  136. #pragma clang diagnostic pop
  137. }
  138. /**
  139. * Handles the notification for the application entering background.
  140. */
  141. - (void)applicationDidEnterBackground:(NSNotification *)notification {
  142. _hasEnteredBackground = YES;
  143. }
  144. /**
  145. * Obtains device passcode presence info with LocalAuthentication APIs.
  146. */
  147. - (void)obtainLocalAuthenticationInfo {
  148. #if DEBUG
  149. NSLog(@"Calling LocalAuthentication API for device passcode state...");
  150. #endif
  151. _hasEnteredBackground = NO;
  152. static LAContext *context;
  153. @try {
  154. if (!context) {
  155. context = [[LAContext alloc] init];
  156. }
  157. } @catch (NSException *) {
  158. // In theory there should be no exceptions but in practice there may be: b/23200390, b/23218643.
  159. return;
  160. }
  161. int result;
  162. NSError *error;
  163. result = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&error] ? 1 : 0;
  164. if (error) {
  165. _localAuthenticationInfo = @{
  166. kResultKey : @(result),
  167. kErrorDomainKey : error.domain,
  168. kErrorCodeKey : @(error.code),
  169. };
  170. } else {
  171. _localAuthenticationInfo = @{
  172. kResultKey : @(result),
  173. };
  174. }
  175. }
  176. /**
  177. * Obtains device passcode presence info with Keychain APIs.
  178. */
  179. - (void)obtainKeychainInfo {
  180. #if DEBUG
  181. NSLog(@"Calling Keychain API for device passcode state...");
  182. #endif
  183. _keychainExpireTime = [NSDate dateWithTimeIntervalSinceNow:kKeychainInfoCacheTime];
  184. static NSDictionary *attributes;
  185. static NSDictionary *query;
  186. if (!attributes) {
  187. NSData *secret = [@"Has passcode set?" dataUsingEncoding:NSUTF8StringEncoding];
  188. attributes = @{
  189. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  190. (__bridge id)kSecAttrService : kPasscodeStatusService,
  191. (__bridge id)kSecAttrAccount : kPasscodeStatusAccount,
  192. (__bridge id)kSecValueData : secret,
  193. (__bridge id)kSecAttrAccessible :
  194. (__bridge id)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
  195. };
  196. query = @{
  197. (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
  198. (__bridge id)kSecAttrService: kPasscodeStatusService,
  199. (__bridge id)kSecAttrAccount: kPasscodeStatusAccount
  200. };
  201. }
  202. OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
  203. if (status == errSecDuplicateItem) {
  204. // If for some reason the item already exists, delete the item and try again.
  205. SecItemDelete((__bridge CFDictionaryRef)query);
  206. status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
  207. };
  208. if (status == errSecSuccess) {
  209. SecItemDelete((__bridge CFDictionaryRef)query);
  210. }
  211. _keychainInfo = @{
  212. kResultKey : @(status)
  213. };
  214. }
  215. /**
  216. * Computes the status string from the current data.
  217. */
  218. - (NSString *)status {
  219. // Prefer LocalAuthentication info if available.
  220. if (_localAuthenticationInfo != nil) {
  221. return ((NSNumber *)_localAuthenticationInfo[kResultKey]).boolValue ? @"YES" : @"NO";
  222. }
  223. if (_keychainInfo != nil){
  224. switch ([(NSNumber *)_keychainInfo[kResultKey] intValue]) {
  225. case errSecSuccess:
  226. return @"YES";
  227. case errSecDecode: // iOS 8.0+
  228. case errSecAuthFailed: // iOS 9.1+
  229. case errSecNotAvailable: // iOS 11.0+
  230. return @"NO";
  231. default:
  232. break;
  233. }
  234. }
  235. return @"UNCHECKED";
  236. }
  237. /**
  238. * Computes the encoded detailed information string from the current data.
  239. */
  240. - (NSString *)info {
  241. NSMutableDictionary<NSString *, NSDictionary<NSString *, NSObject *> *> *infoDict =
  242. [NSMutableDictionary dictionaryWithCapacity:2];
  243. if (_localAuthenticationInfo) {
  244. infoDict[kLocalAuthenticationKey] = _localAuthenticationInfo;
  245. }
  246. if (_keychainInfo) {
  247. infoDict[kKeychainKey] = _keychainInfo;
  248. }
  249. NSData *data = [NSJSONSerialization dataWithJSONObject:infoDict
  250. options:0
  251. error:NULL];
  252. NSString *string = [data base64EncodedStringWithOptions:0];
  253. string = [string stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
  254. string = [string stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
  255. return string ?: @"e30="; // Use encoded "{}" in case of error.
  256. }
  257. @end
  258. NS_ASSUME_NONNULL_END