#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;
}