]> git.saurik.com Git - apple/security.git/blobdiff - keychain/SecureObjectSync/SOSAccountTransaction.m
Security-59306.11.20.tar.gz
[apple/security.git] / keychain / SecureObjectSync / SOSAccountTransaction.m
diff --git a/keychain/SecureObjectSync/SOSAccountTransaction.m b/keychain/SecureObjectSync/SOSAccountTransaction.m
new file mode 100644 (file)
index 0000000..7fa7331
--- /dev/null
@@ -0,0 +1,483 @@
+//
+//  SOSAccountTransaction.c
+//  sec
+//
+//
+
+#include "SOSAccountTransaction.h"
+
+#include <utilities/SecCFWrappers.h>
+#import <utilities/SecNSAdditions.h>
+#include <CoreFoundation/CoreFoundation.h>
+
+#include "keychain/SecureObjectSync/SOSAccount.h"
+#include "keychain/SecureObjectSync/SOSAccountPriv.h"
+#include "keychain/SecureObjectSync/SOSPeerInfoV2.h"
+#import "keychain/SecureObjectSync/SOSTransport.h"
+#import "keychain/SecureObjectSync/SOSTransportCircle.h"
+#import "keychain/SecureObjectSync/SOSTransportCircleKVS.h"
+#import "keychain/SecureObjectSync/SOSAccountTrustClassic+Circle.h"
+#import "keychain/SecureObjectSync/SOSAccountTrustClassic.h"
+#import "keychain/SecureObjectSync/SOSTransportMessageKVS.h"
+#import "Security/SecItemBackup.h"
+
+#include <keychain/ckks/CKKS.h>
+
+#define kPublicKeyNotAvailable "com.apple.security.publickeynotavailable"
+
+// Account dumping state stuff
+
+#define ACCOUNT_STATE_INTERVAL 200
+
+
+@interface SOSAccountTransaction ()
+
+@property BOOL              initialInCircle;
+@property NSSet<NSString*>* initialViews;
+@property NSSet<NSString*>* initialUnsyncedViews;
+@property NSString*         initialID;
+
+@property BOOL              initialTrusted;
+@property NSData*           initialKeyParameters;
+
+@property uint              initialCirclePeerCount;
+
+@property bool              quiet;
+
+@property NSMutableSet<NSString*>* peersToRequestSync;
+
+- (void) start;
+
+@end
+
+
+
+@implementation SOSAccountTransaction
+
+- (uint64_t)currentTrustBitmask {
+    dispatch_assert_queue(_account.queue);
+
+    // A couple of assumptions here, that circle status is 32 bit (it an int), and that we can store
+    // that in th lower half without sign extending it
+    uint64_t circleStatus = (((uint32_t)[_account.trust getCircleStatusOnly:NULL]) & 0xffffffff) | CC_STATISVALID;
+    if(_account.accountKeyIsTrusted) {
+        circleStatus |= CC_UKEY_TRUSTED;
+        if(_account.accountPrivateKey) {
+            circleStatus |= CC_CAN_AUTH;
+        }
+    }
+    // we might be in circle, but invalid - let client see this in bitmask.
+    if([_account.trust isInCircleOnly:NULL]) {
+       circleStatus |= CC_PEER_IS_IN;
+    }
+    return circleStatus;
+}
+
+- (void) updateSOSCircleCachedStatus {
+    // clearly invalid, overwritten by cached value in notify_get_state()
+    // if its the second time we are launchded
+    static uint64_t lastStatus = 0;
+
+    dispatch_assert_queue(_account.queue);
+
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        // pull out what previous instance of securityd though the trust status
+        SOSCachedNotificationOperation(kSOSCCCircleChangedNotification, ^bool(int token, bool gtg) {
+            uint64_t state64;
+            if (notify_get_state(token, &state64) == NOTIFY_STATUS_OK) {
+                if (state64 & CC_STATISVALID) {
+                    lastStatus = state64;
+                }
+            }
+            return true;
+        });
+        secnotice("sosnotify", "initial last circle status is: %llu", (unsigned long long)lastStatus);
+    });
+
+    uint64_t currentStatus = [self currentTrustBitmask];
+    if (lastStatus & CC_STATISVALID) {
+        if(lastStatus != currentStatus) {
+            _account.notifyCircleChangeOnExit = true;
+        }
+    } else {
+        _account.notifyCircleChangeOnExit = true;
+    }
+
+    if(_account.notifyCircleChangeOnExit) {
+        // notify if last cache status was invalid, or it have changed, clients don't get update on changes in
+        // the metadata stored in the upper bits of above CC_MASK (at least not though this path)
+        bool firstLaunch = (lastStatus & CC_STATISVALID) == 0;
+        bool circleChanged = ((lastStatus & CC_MASK) != (currentStatus & CC_MASK));
+
+        bool circleStatusChanged = firstLaunch || circleChanged;
+
+        lastStatus = currentStatus;
+
+        secnotice("sosnotify", "new last circle status is: %llu (notify: %s)",
+                  (unsigned long long)lastStatus,
+                  circleStatusChanged ? "yes" : "no");
+
+        SOSCachedNotificationOperation(kSOSCCCircleChangedNotification, ^bool(int token, bool gtg) {
+            if(gtg) {
+                uint32_t status = notify_set_state(token, currentStatus);
+                if(status == NOTIFY_STATUS_OK) {
+                    self->_account.notifyCircleChangeOnExit = false;
+
+                    if (circleStatusChanged) {
+                        secnotice("sosnotify", "posting kSOSCCCircleChangedNotification");
+                        notify_post(kSOSCCCircleChangedNotification);
+                    }
+                }
+                return true;
+            }
+            return false;
+        });
+
+        // preserve behavior from previous, this should be merged into SOSViewsSetCachedStatus()
+        if (firstLaunch) {
+            _account.notifyViewChangeOnExit = true;
+        }
+    }
+}
+
+static void SOSViewsSetCachedStatus(SOSAccount *account) {
+    static uint64_t lastViewBitmask = 0;
+    CFSetRef piViews = SOSAccountCopyEnabledViews(account);
+    
+    __block uint64_t viewBitMask = (([account getCircleStatus:NULL] == kSOSCCInCircle) && piViews) ? SOSViewBitmaskFromSet(piViews) :0;
+    CFReleaseNull(piViews);
+
+    if(viewBitMask != lastViewBitmask) {
+        lastViewBitmask = viewBitMask;
+        account.notifyViewChangeOnExit = true; // this is also set within operations and might want the notification for other reasons.
+    }
+
+    if(account.notifyViewChangeOnExit) {
+        SOSCachedNotificationOperation(kSOSCCViewMembershipChangedNotification, ^bool(int token, bool gtg) {
+            if(gtg) {
+                uint32_t status = notify_set_state(token, viewBitMask);
+                if(status == NOTIFY_STATUS_OK) {
+                    notify_post(kSOSCCViewMembershipChangedNotification);
+                    account.notifyViewChangeOnExit = false;
+                }
+                return true;
+            }
+            return false;
+        });
+    }
+}
+
+- (NSString*) description {
+    return [NSString stringWithFormat:@"<SOSAccountTransaction*@%p %ld>",
+                                      self, (unsigned long)(self.initialViews ? [self.initialViews count] : 0)];
+}
+
+- (instancetype) initWithAccount:(SOSAccount *)account quiet:(bool)quiet {
+    if (self = [super init]) {
+        self.account = account;
+        _quiet = quiet;
+        [self start];
+    }
+    return self;
+}
+
+- (void) start {
+    [self updateSOSCircleCachedStatus];
+    SOSViewsSetCachedStatus(_account);
+
+    self.initialInCircle = [self.account isInCircle:NULL];
+    self.initialTrusted = self.account.accountKeyIsTrusted;
+    self.initialCirclePeerCount = 0;
+    if(self.initialInCircle) {
+        self.initialCirclePeerCount = SOSCircleCountPeers(self.account.trust.trustedCircle);
+    }
+
+    if (self.initialInCircle) {
+        SOSAccountEnsureSyncChecking(self.account);
+    }
+
+    self.initialUnsyncedViews = (__bridge_transfer NSMutableSet<NSString*>*)SOSAccountCopyOutstandingViews(self.account);
+    self.initialKeyParameters = self.account.accountKeyDerivationParamters ? [NSData dataWithData:self.account.accountKeyDerivationParamters] : nil;
+
+    SOSPeerInfoRef mpi = self.account.peerInfo;
+    if (mpi) {
+        self.initialViews = CFBridgingRelease(SOSPeerInfoCopyEnabledViews(mpi));
+        [self.account ensureOctagonPeerKeys];
+    }
+    self.peersToRequestSync = nil;
+
+    if(self.account.key_interests_need_updating) {
+        SOSUpdateKeyInterest(self.account);
+    }
+
+    if(!self.quiet) {
+        CFStringSetPerformWithDescription((__bridge CFSetRef) self.initialViews, ^(CFStringRef description) {
+            secnotice("acct-txn", "Starting as:%s v:%@", self.initialInCircle ? "member" : "non-member", description);
+        });
+    }
+}
+
+- (void) restart {
+    [self finish];
+    [self start];
+}
+
+
+- (void) finish {
+    static int do_account_state_at_zero = 0;
+    bool doCircleChanged = self.account.notifyCircleChangeOnExit;
+    bool doViewChanged = false;
+
+
+    CFErrorRef localError = NULL;
+    bool notifyEngines = false;
+
+    SOSPeerInfoRef mpi = self.account.peerInfo;
+
+    bool isInCircle = [self.account isInCircle:NULL];
+
+    if (isInCircle && self.peersToRequestSync) {
+        SOSCCRequestSyncWithPeers((__bridge CFSetRef)(self.peersToRequestSync));
+    }
+    self.peersToRequestSync = nil;
+
+    if (isInCircle) {
+        SOSAccountEnsureSyncChecking(self.account);
+    } else {
+        SOSAccountCancelSyncChecking(self.account);
+    }
+
+    // If our identity changed our inital set should be everything.
+    if ([self.initialID isEqualToString: (__bridge NSString *)(SOSPeerInfoGetPeerID(mpi))]) {
+        self.initialUnsyncedViews = (__bridge_transfer NSSet<NSString*>*) SOSViewCopyViewSet(kViewSetAll);
+    }
+
+    NSSet<NSString*>* finalUnsyncedViews = (__bridge_transfer NSSet<NSString*>*) SOSAccountCopyOutstandingViews(self.account);
+    if (!NSIsEqualSafe(self.initialUnsyncedViews, finalUnsyncedViews)) {
+        if (SOSAccountHandleOutOfSyncUpdate(self.account,
+                                            (__bridge CFSetRef)(self.initialUnsyncedViews),
+                                            (__bridge CFSetRef)(finalUnsyncedViews))) {
+            notifyEngines = true;
+        }
+
+        secnotice("initial-sync", "Unsynced was: %@", [self.initialUnsyncedViews shortDescription]);
+        secnotice("initial-sync", "Unsynced is: %@", [finalUnsyncedViews shortDescription]);
+    }
+
+    if (self.account.engine_peer_state_needs_repair) {
+        // We currently only get here from a failed syncwithallpeers, so
+        // that will retry. If this logic changes, force a syncwithallpeers
+        if (!SOSAccountEnsurePeerRegistration(self.account, &localError)) {
+            secerror("Ensure peer registration while repairing failed: %@", localError);
+        }
+        CFReleaseNull(localError);
+
+        notifyEngines = true;
+    }
+   
+    if(self.account.circle_rings_retirements_need_attention){
+        SOSAccountRecordRetiredPeersInCircle(self.account);
+
+        SOSAccountEnsureRecoveryRing(self.account);
+        SOSAccountEnsureInBackupRings(self.account);
+
+        CFErrorRef localError = NULL;
+        if(![self.account.circle_transport flushChanges:&localError]){
+            secerror("flush circle failed %@", localError);
+        }
+        CFReleaseSafe(localError);
+
+        notifyEngines = true;
+    }
+
+    if (notifyEngines) {
+#if OCTAGON
+        if(!SecCKKSTestDisableSOS()) {
+#endif
+            SOSAccountNotifyEngines(self.account);
+#if OCTAGON
+        }
+#endif
+    }
+
+    if(self.account.key_interests_need_updating) {
+        SOSUpdateKeyInterest(self.account);
+    }
+
+    self.account.circle_rings_retirements_need_attention = false;
+    self.account.engine_peer_state_needs_repair = false;
+
+    [self.account flattenToSaveBlock];
+
+    // Refresh isInCircle since we could have changed our mind
+    isInCircle = [self.account isInCircle:NULL];
+
+    uint finalCirclePeerCount = 0;
+    if(isInCircle) {
+        finalCirclePeerCount = SOSCircleCountPeers(self.account.trust.trustedCircle);
+    }
+
+    if(isInCircle && (finalCirclePeerCount < self.initialCirclePeerCount)) {
+        (void) SOSAccountCleanupAllKVSKeys(_account, NULL);
+    }
+
+    mpi = self.account.peerInfo;
+    CFSetRef views = mpi ? SOSPeerInfoCopyEnabledViews(mpi) : NULL;
+
+    if(!self.quiet) {
+        CFStringSetPerformWithDescription(views, ^(CFStringRef description) {
+            secnotice("acct-txn", "Finished as:%s v:%@", isInCircle ? "member" : "non-member", description);
+        });
+    }
+
+    // This is the logic to detect a new userKey:
+    bool userKeyChanged = !NSIsEqualSafe(self.initialKeyParameters, self.account.accountKeyDerivationParamters);
+    
+    // This indicates we initiated a password change.
+    bool weInitiatedKeyChange = (self.initialTrusted &&
+                                 self.initialInCircle &&
+                                 userKeyChanged && isInCircle &&
+                                 self.account.accountKeyIsTrusted);
+    
+    if(self.initialInCircle != isInCircle) {
+        doCircleChanged = true;
+        doViewChanged = true;
+        do_account_state_at_zero = 0;
+        secnotice("secdNotify", "Notified clients of kSOSCCCircleChangedNotification && kSOSCCViewMembershipChangedNotification for circle/view change");
+    } else if(isInCircle && !NSIsEqualSafe(self.initialViews, (__bridge NSSet*)views)) {
+        doViewChanged = true;
+        do_account_state_at_zero = 0;
+        secnotice("secdNotify", "Notified clients of kSOSCCViewMembershipChangedNotification for viewchange(only)");
+    } else if(weInitiatedKeyChange) { // We consider this a circleChange so (PCS) can tell the userkey trust changed.
+        doCircleChanged = true;
+        do_account_state_at_zero = 0;
+        secnotice("secdNotify", "Notified clients of kSOSCCCircleChangedNotification for userKey change");
+    }
+    
+
+
+    // This is the case of we used to trust the key, were in the circle, the key changed, we don't trust it now.
+    bool fellOutOfTrust = (self.initialTrusted &&
+                           self.initialInCircle &&
+                           userKeyChanged &&
+                           !self.account.accountKeyIsTrusted);
+    
+    if(fellOutOfTrust) {
+        secnotice("userKeyTrust", "No longer trust user public key - prompting for password.");
+        notify_post(kPublicKeyNotAvailable);
+        doCircleChanged = true;
+        do_account_state_at_zero = 0;
+    }
+    
+    bool userKeyTrustChangedToTrueAndNowInCircle = (!self.initialTrusted && self.account.accountKeyIsTrusted && isInCircle);
+    
+    if(userKeyTrustChangedToTrueAndNowInCircle) {
+        secnotice("userKeyTrust", "UserKey is once again trusted and we're valid in circle.");
+        doCircleChanged = true;
+        doViewChanged = true;
+    }
+    
+    if(doCircleChanged) {
+        [self updateSOSCircleCachedStatus];
+    }
+    if(doViewChanged) {
+        SOSViewsSetCachedStatus(_account);
+    }
+    if(self.account.notifyBackupOnExit) {
+        notify_post(kSecItemBackupNotification);
+        self.account.notifyBackupOnExit = false;
+    }
+
+
+    if(do_account_state_at_zero <= 0) {
+        SOSAccountLogState(self.account);
+        SOSAccountLogViewState(self.account);
+        do_account_state_at_zero = ACCOUNT_STATE_INTERVAL;
+    }
+    do_account_state_at_zero--;
+    
+    CFReleaseNull(views);
+}
+
+- (void) requestSyncWith: (NSString*) peerID {
+    if (self.peersToRequestSync == nil) {
+        self.peersToRequestSync = [NSMutableSet<NSString*> set];
+    }
+    [self.peersToRequestSync addObject: peerID];
+}
+
+- (void) requestSyncWithPeers: (NSSet<NSString*>*) peerList {
+    if (self.peersToRequestSync == nil) {
+        self.peersToRequestSync = [NSMutableSet<NSString*> set];
+    }
+    [self.peersToRequestSync unionSet: peerList];
+}
+
+@end
+
+
+
+
+//
+// MARK: Transactional
+//
+
+@implementation SOSAccount (Transaction)
+
+__thread bool __hasAccountQueue = false;
+
++ (void)performWhileHoldingAccountQueue:(void (^)(void))action
+{
+    bool hadAccountQueue = __hasAccountQueue;
+    __hasAccountQueue = true;
+    action();
+    __hasAccountQueue = hadAccountQueue;
+}
+
++ (void)performOnQuietAccountQueue:(void (^)(void))action
+{
+    SOSAccount* account = (__bridge SOSAccount*)GetSharedAccountRef();
+    if(account) {
+        [account performTransaction:true action:^(SOSAccountTransaction * _Nonnull txn) {
+            action();
+        }];
+    } else {
+        secnotice("acct-txn", "No account; running block on local thread");
+        action();
+    }
+}
+
+- (void) performTransaction_Locked: (void (^)(SOSAccountTransaction* txn)) action {
+    [self performTransaction_Locked:false action:action];
+}
+
+- (void) performTransaction_Locked:(bool)quiet action:(void (^)(SOSAccountTransaction* txn))action {
+    @autoreleasepool {
+        SOSAccountTransaction* transaction = [[SOSAccountTransaction alloc] initWithAccount:self quiet:quiet];
+        action(transaction);
+        [transaction finish];
+    }
+}
+
+- (void) performTransaction: (void (^)(SOSAccountTransaction* txn)) action {
+    [self performTransaction:false action:action];
+}
+
+- (void)performTransaction:(bool)quiet action:(void (^)(SOSAccountTransaction* txn))action {
+
+    if (__hasAccountQueue) {
+        // Be quiet; we're already in a transaction
+        [self performTransaction_Locked:true action:action];
+    }
+    else {
+        dispatch_sync(self.queue, ^{
+            __hasAccountQueue = true;
+            [self performTransaction_Locked:quiet action:action];
+            __hasAccountQueue = false;
+        });
+    }
+}
+
+
+@end