CKKSLockStateTracker.m   [plain text]


/*
 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_LICENSE_HEADER_END@
 */

#if OCTAGON

#include <notify.h>
#include <dispatch/dispatch.h>
#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 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)initWithProvider:(id<CKKSLockStateProviderProtocol>)provider
{
    if((self = [super init])) {
        _lockStateProvider = provider;

        _queue = dispatch_queue_create("lock-state-tracker", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
        _operationQueue = [[NSOperationQueue alloc] init];

        _notify_token = NOTIFY_TOKEN_INVALID;
        _queueIsLocked = true;
        _observers = [NSHashTable weakObjectsHashTable];
        [self resetUnlockDependency];

        WEAKIFY(self);

        // If this is a live server, register with notify
        if(!SecCKKSTestsEnabled()) {
            notify_register_dispatch(kUserKeybagStateChangeNotification, &_notify_token, _queue, ^(int t) {
                STRONGIFY(self);
                [self _onqueueRecheck];
            });
        }

        dispatch_async(_queue, ^{
            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 {
    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]) {
        CKKSResultOperation* op = [CKKSResultOperation named:@"keybag-unlocked-dependency" withBlock: ^{
            ckksinfo_global("ckks", "Keybag unlocked");
        }];
        op.descriptionErrorCode = CKKSResultDescriptionPendingUnlock;
        self.unlockDependency = op;
    }
}

-(void)_onqueueRecheck {
    dispatch_assert_queue(self.queue);

    static bool first = true;
    bool wasLocked = self.queueIsLocked;
    self.queueIsLocked = [self.lockStateProvider queryAKSLocked];

    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];
            });
        }
    }
}

-(void)recheck {
    dispatch_sync(self.queue, ^{
        [self _onqueueRecheck];
    });
}

- (bool)lockedError:(NSError *)error
{
    bool isLockedError = error.code == errSecInteractionNotAllowed &&
        ([error.domain isEqualToString:@"securityd"] || [error.domain isEqualToString:(__bridge NSString*)kSecErrorDomain]);
    return isLockedError;
}

- (bool)checkErrorChainForLockState:(NSError*)error
{
    while(error != nil) {
        if([self lockedError:error]) {
            return true;
        }

        error = error.userInfo[NSUnderlyingErrorKey];
    }

    return false;
}

-(bool)isLockedError:(NSError *)error {
    bool isLockedError = [self checkErrorChainForLockState:error];

    /*
     * 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] initWithProvider:[[CKKSActualLockStateProvider alloc] init]];
    });
    return tracker;
}


@end

@implementation CKKSActualLockStateProvider
- (instancetype)init {
    if((self = [super init])) {
    }
    return self;
}

- (BOOL)queryAKSLocked {
    CFErrorRef aksError = NULL;
    bool locked = true;

    if(!SecAKSGetIsLocked(&locked, &aksError)) {
        ckkserror_global("ckks", "error querying lock state: %@", aksError);
        CFReleaseNull(aksError);
    }

    return locked ? YES : NO;
}
@end

#endif // OCTAGON