]> git.saurik.com Git - apple/security.git/blobdiff - keychain/ckks/CKKSLockStateTracker.m
Security-59306.41.2.tar.gz
[apple/security.git] / keychain / ckks / CKKSLockStateTracker.m
index 49680e8da0bc6b4afbb7e3004648d82173c3079e..46e6cb2aa845625f361afa60a4a960e70a832a75 100644 (file)
 #include <utilities/SecAKSWrappers.h>
 
 #import "keychain/ckks/CKKS.h"
+#import "keychain/ckks/CKKSResultOperation.h"
 #import "keychain/ckks/CKKSGroupOperation.h"
 #import "keychain/ckks/CKKSLockStateTracker.h"
+#import "keychain/ot/ObjCImprovements.h"
 
 @interface CKKSLockStateTracker ()
-@property bool isLocked;
+@property bool queueIsLocked;
 @property dispatch_queue_t queue;
 @property NSOperationQueue* operationQueue;
+@property NSHashTable<id<CKKSLockStateNotification>> *observers;
+@property (assign) int notify_token;
+
+@property (nullable) NSDate* lastUnlockedTime;
+
 @end
 
 @implementation CKKSLockStateTracker
 
 - (instancetype)init {
     if((self = [super init])) {
-        _queue = dispatch_queue_create("lock-state-tracker", DISPATCH_QUEUE_SERIAL);
+        _queue = dispatch_queue_create("lock-state-tracker", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
         _operationQueue = [[NSOperationQueue alloc] init];
 
-        _isLocked = true;
+        _notify_token = NOTIFY_TOKEN_INVALID;
+        _queueIsLocked = true;
+        _observers = [NSHashTable weakObjectsHashTable];
         [self resetUnlockDependency];
 
-        __weak __typeof(self) weakSelf = self;
+        WEAKIFY(self);
 
         // If this is a live server, register with notify
         if(!SecCKKSTestsEnabled()) {
-            int token = 0;
-            notify_register_dispatch(kUserKeybagStateChangeNotification, &token, _queue, ^(int t) {
-                [weakSelf _onqueueRecheck];
+            notify_register_dispatch(kUserKeybagStateChangeNotification, &_notify_token, _queue, ^(int t) {
+                STRONGIFY(self);
+                [self _onqueueRecheck];
             });
         }
 
         dispatch_async(_queue, ^{
-            [weakSelf _onqueueRecheck];
+            STRONGIFY(self);
+            [self _onqueueRecheck];
         });
     }
     return self;
 }
 
+- (void)dealloc {
+    if (_notify_token != NOTIFY_TOKEN_INVALID) {
+        notify_cancel(_notify_token);
+    }
+}
+
+- (bool)isLocked {
+    // if checking close after launch ``isLocked'' needs to be blocked on the initial fetch operation
+    __block bool locked;
+    dispatch_sync(_queue, ^{
+        locked = self->_queueIsLocked;
+    });
+    return locked;
+}
+
+- (NSDate*)lastUnlockTime {
+    // If unlocked, the last unlock time is now. Otherwise, used the cached value.
+    __block NSDate* date = nil;
+    dispatch_sync(self.queue, ^{
+        if(self.queueIsLocked) {
+            date = self.lastUnlockedTime;
+        } else {
+            date = [NSDate date];
+            self.lastUnlockedTime = date;
+        }
+    });
+    return date;
+}
+
 -(NSString*)description {
-    return [NSString stringWithFormat: @"<CKKSLockStateTracker: %@>", self.isLocked ? @"locked" : @"unlocked"];
+    bool isLocked = self.isLocked;
+    return [NSString stringWithFormat: @"<CKKSLockStateTracker: %@ last:%@>",
+            isLocked ? @"locked" : @"unlocked",
+            isLocked ? self.lastUnlockedTime : @"now"];
 }
 
 -(void)resetUnlockDependency {
     if(self.unlockDependency == nil || ![self.unlockDependency isPending]) {
-        self.unlockDependency = [NSBlockOperation blockOperationWithBlock: ^{
+        CKKSResultOperation* op = [CKKSResultOperation named:@"keybag-unlocked-dependency" withBlock: ^{
             secinfo("ckks", "Keybag unlocked");
         }];
-        self.unlockDependency.name = @"keybag-unlocked-dependency";
+        op.descriptionErrorCode = CKKSResultDescriptionPendingUnlock;
+        self.unlockDependency = op;
     }
 }
 
 -(void)_onqueueRecheck {
     dispatch_assert_queue(self.queue);
 
-    bool wasLocked = self.isLocked;
-    self.isLocked = [CKKSLockStateTracker queryAKSLocked];
+    static bool first = true;
+    bool wasLocked = self.queueIsLocked;
+    self.queueIsLocked = [CKKSLockStateTracker queryAKSLocked];
 
-    if(wasLocked != self.isLocked) {
-        if(self.isLocked) {
+    if(wasLocked != self.queueIsLocked || first) {
+        first = false;
+        if(self.queueIsLocked) {
             // We're locked now.
             [self resetUnlockDependency];
+
+            if(wasLocked) {
+                self.lastUnlockedTime = [NSDate date];
+            }
         } else {
             [self.operationQueue addOperation: self.unlockDependency];
             self.unlockDependency = nil;
+            self.lastUnlockedTime = [NSDate date];
+        }
+
+        bool isUnlocked = (self.queueIsLocked == false);
+        for (id<CKKSLockStateNotification> observer in _observers) {
+            __strong typeof(observer) strongObserver = observer;
+            if (strongObserver == nil) {
+                return;
+            }
+            dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
+                [strongObserver lockStateChangeNotification:isUnlocked];
+            });
         }
     }
 }
 }
 
 -(bool)isLockedError:(NSError *)error {
-    return ([error.domain isEqualToString:@"securityd"] || [error.domain isEqualToString:(__bridge NSString*)kSecErrorDomain])
-            && error.code == errSecInteractionNotAllowed;
+    bool isLockedError = error.code == errSecInteractionNotAllowed &&
+        ([error.domain isEqualToString:@"securityd"] || [error.domain isEqualToString:(__bridge NSString*)kSecErrorDomain]);
+
+    /*
+     * If we are locked, and the the current lock state track disagree, lets double check
+     * if we are actually locked since we might have missed a lock state notification,
+     * and cause spinning to happen.
+     *
+     * We don't update the local variable, since the error code was a locked error.
+     */
+    if (isLockedError) {
+        dispatch_sync(self.queue, ^{
+            if (self.queueIsLocked == false) {
+                [self _onqueueRecheck];
+            }
+        });
+    }
+    return isLockedError;
+}
+
+-(void)addLockStateObserver:(id<CKKSLockStateNotification>)object
+{
+    dispatch_async(self.queue, ^{
+        [self->_observers addObject:object];
+        bool isUnlocked = (self.queueIsLocked == false);
+        dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
+            [object lockStateChangeNotification:isUnlocked];
+        });
+    });
+}
+
++ (CKKSLockStateTracker*)globalTracker
+{
+    static CKKSLockStateTracker* tracker;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        tracker = [[CKKSLockStateTracker alloc] init];
+    });
+    return tracker;
 }