GIDMDMPasscodeCache.m 10 KB

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