]> git.saurik.com Git - apple/security.git/blob - keychain/SecureObjectSync/SOSAccountTransaction.m
Security-59754.41.1.tar.gz
[apple/security.git] / keychain / SecureObjectSync / SOSAccountTransaction.m
1 //
2 // SOSAccountTransaction.c
3 // sec
4 //
5 //
6
7 #include "SOSAccountTransaction.h"
8
9 #include <utilities/SecCFWrappers.h>
10 #import <utilities/SecNSAdditions.h>
11 #include <CoreFoundation/CoreFoundation.h>
12
13 #include "keychain/SecureObjectSync/SOSAccount.h"
14 #include "keychain/SecureObjectSync/SOSAccountPriv.h"
15 #include "keychain/SecureObjectSync/SOSPeerInfoV2.h"
16 #import "keychain/SecureObjectSync/SOSTransport.h"
17 #import "keychain/SecureObjectSync/SOSTransportCircle.h"
18 #import "keychain/SecureObjectSync/SOSTransportCircleKVS.h"
19 #import "keychain/SecureObjectSync/SOSAccountTrustClassic+Circle.h"
20 #import "keychain/SecureObjectSync/SOSAccountTrustClassic.h"
21 #import "keychain/SecureObjectSync/SOSTransportMessageKVS.h"
22 #import "Security/SecItemBackup.h"
23
24 #include <keychain/ckks/CKKS.h>
25
26 #define kPublicKeyNotAvailable "com.apple.security.publickeynotavailable"
27
28 // Account dumping state stuff
29
30 #define ACCOUNT_STATE_INTERVAL 200
31
32
33 @interface SOSAccountTransaction ()
34
35 @property BOOL initialInCircle;
36 @property NSSet<NSString*>* initialViews;
37 @property NSSet<NSString*>* initialUnsyncedViews;
38 @property NSString* initialID;
39
40 @property BOOL initialTrusted;
41 @property NSData* initialKeyParameters;
42
43 @property uint initialCirclePeerCount;
44
45 @property bool quiet;
46
47 @property NSMutableSet<NSString*>* peersToRequestSync;
48
49 - (void) start;
50
51 @end
52
53
54
55 @implementation SOSAccountTransaction
56
57 - (uint64_t)currentTrustBitmask {
58 dispatch_assert_queue(_account.queue);
59
60 // A couple of assumptions here, that circle status is 32 bit (it an int), and that we can store
61 // that in th lower half without sign extending it
62 uint64_t circleStatus = (((uint32_t)[_account.trust getCircleStatusOnly:NULL]) & 0xffffffff) | CC_STATISVALID;
63 if(_account.accountKeyIsTrusted) {
64 circleStatus |= CC_UKEY_TRUSTED;
65 if(_account.accountPrivateKey) {
66 circleStatus |= CC_CAN_AUTH;
67 }
68 }
69 // we might be in circle, but invalid - let client see this in bitmask.
70 if([_account.trust isInCircleOnly:NULL]) {
71 circleStatus |= CC_PEER_IS_IN;
72 }
73 return circleStatus;
74 }
75
76 static inline SOSCCStatus reportAsSOSCCStatus(uint64_t x) {
77 return (SOSCCStatus) x & CC_MASK;
78 }
79
80 - (void) updateSOSCircleCachedStatus {
81 // clearly invalid, overwritten by cached value in notify_get_state()
82 // if its the second time we are launchded
83 static uint64_t lastStatus = 0;
84
85 dispatch_assert_queue(_account.queue);
86
87 static dispatch_once_t onceToken;
88 dispatch_once(&onceToken, ^{
89 // pull out what previous instance of securityd though the trust status
90 SOSCachedNotificationOperation(kSOSCCCircleChangedNotification, ^bool(int token, bool gtg) {
91 uint64_t state64;
92 if (notify_get_state(token, &state64) == NOTIFY_STATUS_OK) {
93 if (state64 & CC_STATISVALID) {
94 lastStatus = state64;
95 }
96 }
97 return true;
98 });
99 secnotice("sosnotify", "initial last circle status is: %d", reportAsSOSCCStatus(lastStatus));
100 });
101
102 uint64_t currentStatus = [self currentTrustBitmask];
103 if (lastStatus & CC_STATISVALID) {
104 if(lastStatus != currentStatus) {
105 _account.notifyCircleChangeOnExit = true;
106 }
107 } else {
108 _account.notifyCircleChangeOnExit = true;
109 }
110
111 if(_account.notifyCircleChangeOnExit) {
112 // notify if last cache status was invalid, or it have changed, clients don't get update on changes in
113 // the metadata stored in the upper bits of above CC_MASK (at least not though this path)
114 bool firstLaunch = (lastStatus & CC_STATISVALID) == 0;
115 bool circleChanged = ((lastStatus & CC_MASK) != (currentStatus & CC_MASK));
116
117 bool circleStatusChanged = firstLaunch || circleChanged;
118
119 lastStatus = currentStatus;
120
121 secnotice("sosnotify", "new last circle status is: %d (notify: %s)",
122 reportAsSOSCCStatus(lastStatus),
123 circleStatusChanged ? "yes" : "no");
124
125 SOSCachedNotificationOperation(kSOSCCCircleChangedNotification, ^bool(int token, bool gtg) {
126 if(gtg) {
127 uint32_t status = notify_set_state(token, currentStatus);
128 if(status == NOTIFY_STATUS_OK) {
129 self->_account.notifyCircleChangeOnExit = false;
130
131 if (circleStatusChanged) {
132 secnotice("sosnotify", "posting kSOSCCCircleChangedNotification");
133 notify_post(kSOSCCCircleChangedNotification);
134 }
135 }
136 return true;
137 }
138 return false;
139 });
140
141 // preserve behavior from previous, this should be merged into SOSViewsSetCachedStatus()
142 if (firstLaunch) {
143 _account.notifyViewChangeOnExit = true;
144 }
145 }
146 }
147
148 static void SOSViewsSetCachedStatus(SOSAccount *account) {
149 static uint64_t lastViewBitmask = 0;
150 CFSetRef piViews = SOSAccountCopyEnabledViews(account);
151
152 __block uint64_t viewBitMask = (([account getCircleStatus:NULL] == kSOSCCInCircle) && piViews) ? SOSViewBitmaskFromSet(piViews) :0;
153 CFReleaseNull(piViews);
154
155 if(viewBitMask != lastViewBitmask) {
156 lastViewBitmask = viewBitMask;
157 account.notifyViewChangeOnExit = true; // this is also set within operations and might want the notification for other reasons.
158 }
159
160 if(account.notifyViewChangeOnExit) {
161 SOSCachedNotificationOperation(kSOSCCViewMembershipChangedNotification, ^bool(int token, bool gtg) {
162 if(gtg) {
163 uint32_t status = notify_set_state(token, viewBitMask);
164 if(status == NOTIFY_STATUS_OK) {
165 notify_post(kSOSCCViewMembershipChangedNotification);
166 account.notifyViewChangeOnExit = false;
167 }
168 return true;
169 }
170 return false;
171 });
172 }
173 }
174
175 - (NSString*) description {
176 return [NSString stringWithFormat:@"<SOSAccountTransaction*@%p %ld>",
177 self, (unsigned long)(self.initialViews ? [self.initialViews count] : 0)];
178 }
179
180 - (instancetype) initWithAccount:(SOSAccount *)account quiet:(bool)quiet {
181 if (self = [super init]) {
182 self.account = account;
183 _quiet = quiet;
184 [self start];
185 }
186 return self;
187 }
188
189 - (void) start {
190 [self updateSOSCircleCachedStatus];
191 SOSViewsSetCachedStatus(_account);
192
193 self.initialInCircle = [self.account isInCircle:NULL];
194 self.initialTrusted = self.account.accountKeyIsTrusted;
195 self.initialCirclePeerCount = 0;
196 if(self.initialInCircle) {
197 self.initialCirclePeerCount = SOSCircleCountPeers(self.account.trust.trustedCircle);
198 }
199
200 if (self.initialInCircle) {
201 SOSAccountEnsureSyncChecking(self.account);
202 }
203
204 self.initialUnsyncedViews = (__bridge_transfer NSMutableSet<NSString*>*)SOSAccountCopyOutstandingViews(self.account);
205 self.initialKeyParameters = self.account.accountKeyDerivationParameters ? [NSData dataWithData:self.account.accountKeyDerivationParameters] : nil;
206
207 SOSPeerInfoRef mpi = self.account.peerInfo;
208 if (mpi) {
209 self.initialViews = CFBridgingRelease(SOSPeerInfoCopyEnabledViews(mpi));
210 [self.account ensureOctagonPeerKeys];
211 }
212 self.peersToRequestSync = nil;
213
214 if(self.account.key_interests_need_updating) {
215 SOSUpdateKeyInterest(self.account);
216 }
217
218 if(!self.quiet) {
219 CFStringSetPerformWithDescription((__bridge CFSetRef) self.initialViews, ^(CFStringRef description) {
220 secnotice("acct-txn", "Starting as:%s v:%@", self.initialInCircle ? "member" : "non-member", description);
221 });
222 }
223 }
224
225 - (void) restart {
226 [self finish];
227 [self start];
228 }
229
230
231 - (void) finish {
232 static int do_account_state_at_zero = 0;
233 bool doCircleChanged = self.account.notifyCircleChangeOnExit;
234 bool doViewChanged = false;
235
236
237 CFErrorRef localError = NULL;
238 bool notifyEngines = false;
239
240 SOSPeerInfoRef mpi = self.account.peerInfo;
241
242 bool isInCircle = [self.account isInCircle:NULL];
243
244 if (isInCircle && self.peersToRequestSync) {
245 NSMutableSet<NSString*>* worklist = self.peersToRequestSync;
246 self.peersToRequestSync = nil;
247 SOSCCRequestSyncWithPeers((__bridge CFSetRef)(worklist));
248 }
249
250 if (isInCircle) {
251 SOSAccountEnsureSyncChecking(self.account);
252 } else {
253 SOSAccountCancelSyncChecking(self.account);
254 }
255
256 // If our identity changed our inital set should be everything.
257 if ([self.initialID isEqualToString: (__bridge NSString *)(SOSPeerInfoGetPeerID(mpi))]) {
258 self.initialUnsyncedViews = (__bridge_transfer NSSet<NSString*>*) SOSViewCopyViewSet(kViewSetAll);
259 }
260
261 NSSet<NSString*>* finalUnsyncedViews = (__bridge_transfer NSSet<NSString*>*) SOSAccountCopyOutstandingViews(self.account);
262 if (!NSIsEqualSafe(self.initialUnsyncedViews, finalUnsyncedViews)) {
263 if (SOSAccountHandleOutOfSyncUpdate(self.account,
264 (__bridge CFSetRef)(self.initialUnsyncedViews),
265 (__bridge CFSetRef)(finalUnsyncedViews))) {
266 notifyEngines = true;
267 }
268
269 secnotice("initial-sync", "Unsynced was: %@", [self.initialUnsyncedViews shortDescription]);
270 secnotice("initial-sync", "Unsynced is: %@", [finalUnsyncedViews shortDescription]);
271 }
272
273 if (self.account.engine_peer_state_needs_repair) {
274 // We currently only get here from a failed syncwithallpeers, so
275 // that will retry. If this logic changes, force a syncwithallpeers
276 if (!SOSAccountEnsurePeerRegistration(self.account, &localError)) {
277 secerror("Ensure peer registration while repairing failed: %@", localError);
278 }
279 CFReleaseNull(localError);
280
281 notifyEngines = true;
282 }
283
284 if(self.account.circle_rings_retirements_need_attention){
285 self.account.circle_rings_retirements_need_attention = false;
286 #if OCTAGON
287 [self.account triggerRingUpdate];
288 #endif
289 // Also, there might be some view set change. Ensure that the engine knows...
290 notifyEngines = true;
291 }
292 if(self.account.need_backup_peers_created_after_backup_key_set){
293 self.account.need_backup_peers_created_after_backup_key_set = false;
294 notifyEngines = true;
295 }
296
297 // Always notify the engines if the view set has changed
298 CFSetRef currentViews = self.account.peerInfo ? SOSPeerInfoCopyEnabledViews(self.account.peerInfo) : NULL;
299 BOOL viewSetChanged = !NSIsEqualSafe(self.initialViews, (__bridge NSSet*)currentViews);
300 CFReleaseNull(currentViews);
301 notifyEngines |= viewSetChanged;
302
303 if (notifyEngines) {
304 #if OCTAGON
305 if(!SecCKKSTestDisableSOS()) {
306 #endif
307 SOSAccountNotifyEngines(self.account);
308 #if OCTAGON
309 }
310 #endif
311 }
312
313 if(self.account.key_interests_need_updating) {
314 SOSUpdateKeyInterest(self.account);
315 }
316
317 self.account.engine_peer_state_needs_repair = false;
318
319 [self.account flattenToSaveBlock];
320
321 // Refresh isInCircle since we could have changed our mind
322 isInCircle = [self.account isInCircle:NULL];
323
324 uint finalCirclePeerCount = 0;
325 if(isInCircle) {
326 finalCirclePeerCount = SOSCircleCountPeers(self.account.trust.trustedCircle);
327 }
328
329 if(isInCircle && (finalCirclePeerCount < self.initialCirclePeerCount)) {
330 (void) SOSAccountCleanupAllKVSKeys(_account, NULL);
331 }
332
333 mpi = self.account.peerInfo;
334 CFSetRef views = mpi ? SOSPeerInfoCopyEnabledViews(mpi) : NULL;
335
336 if(!self.quiet) {
337 CFStringSetPerformWithDescription(views, ^(CFStringRef description) {
338 secnotice("acct-txn", "Finished as:%s v:%@", isInCircle ? "member" : "non-member", description);
339 });
340 }
341
342 // This is the logic to detect a new userKey:
343 bool userKeyChanged = !NSIsEqualSafe(self.initialKeyParameters, self.account.accountKeyDerivationParameters);
344
345 // This indicates we initiated a password change.
346 bool weInitiatedKeyChange = (self.initialTrusted &&
347 self.initialInCircle &&
348 userKeyChanged && isInCircle &&
349 self.account.accountKeyIsTrusted);
350
351 if(self.initialInCircle != isInCircle) {
352 doCircleChanged = true;
353 doViewChanged = true;
354 do_account_state_at_zero = 0;
355 secnotice("secdNotify", "Notified clients of kSOSCCCircleChangedNotification && kSOSCCViewMembershipChangedNotification for circle/view change");
356 } else if(isInCircle && !NSIsEqualSafe(self.initialViews, (__bridge NSSet*)views)) {
357 doViewChanged = true;
358 do_account_state_at_zero = 0;
359 secnotice("secdNotify", "Notified clients of kSOSCCViewMembershipChangedNotification for viewchange(only)");
360 } else if(weInitiatedKeyChange) { // We consider this a circleChange so (PCS) can tell the userkey trust changed.
361 doCircleChanged = true;
362 do_account_state_at_zero = 0;
363 secnotice("secdNotify", "Notified clients of kSOSCCCircleChangedNotification for userKey change");
364 }
365
366
367
368 // This is the case of we used to trust the key, were in the circle, the key changed, we don't trust it now.
369 bool fellOutOfTrust = (self.initialTrusted &&
370 self.initialInCircle &&
371 userKeyChanged &&
372 !self.account.accountKeyIsTrusted);
373
374 if(fellOutOfTrust) {
375 secnotice("userKeyTrust", "No longer trust user public key - prompting for password.");
376 notify_post(kPublicKeyNotAvailable);
377 doCircleChanged = true;
378 do_account_state_at_zero = 0;
379 }
380
381 bool userKeyTrustChangedToTrueAndNowInCircle = (!self.initialTrusted && self.account.accountKeyIsTrusted && isInCircle);
382
383 if(userKeyTrustChangedToTrueAndNowInCircle) {
384 secnotice("userKeyTrust", "UserKey is once again trusted and we're valid in circle.");
385 doCircleChanged = true;
386 doViewChanged = true;
387 }
388
389 if(doCircleChanged) {
390 [self updateSOSCircleCachedStatus];
391 }
392 if(doViewChanged) {
393 SOSViewsSetCachedStatus(_account);
394 }
395 if(self.account.notifyBackupOnExit) {
396 notify_post(kSecItemBackupNotification);
397 self.account.notifyBackupOnExit = false;
398 }
399
400
401 if(do_account_state_at_zero <= 0) {
402 SOSAccountLogState(self.account);
403 SOSAccountLogViewState(self.account);
404 do_account_state_at_zero = ACCOUNT_STATE_INTERVAL;
405 }
406 do_account_state_at_zero--;
407
408 CFReleaseNull(views);
409 }
410
411 - (void) requestSyncWith: (NSString*) peerID {
412 if (self.peersToRequestSync == nil) {
413 self.peersToRequestSync = [NSMutableSet<NSString*> set];
414 }
415 [self.peersToRequestSync addObject: peerID];
416 }
417
418 - (void) requestSyncWithPeers: (NSSet<NSString*>*) peerList {
419 if (self.peersToRequestSync == nil) {
420 self.peersToRequestSync = [NSMutableSet<NSString*> set];
421 }
422 [self.peersToRequestSync unionSet: peerList];
423 }
424
425 @end
426
427
428
429
430 //
431 // MARK: Transactional
432 //
433
434 @implementation SOSAccount (Transaction)
435
436 __thread bool __hasAccountQueue = false;
437
438 + (void)performOnQuietAccountQueue:(void (^)(void))action
439 {
440 SOSAccount* account = (__bridge SOSAccount*)GetSharedAccountRef();
441 if(account) {
442 [account performTransaction:true action:^(SOSAccountTransaction * _Nonnull txn) {
443 action();
444 }];
445 } else {
446 secnotice("acct-txn", "No account; running block on local thread");
447 action();
448 }
449 }
450
451 - (void) performTransaction_Locked: (void (^)(SOSAccountTransaction* txn)) action {
452 [self performTransaction_Locked:false action:action];
453 }
454
455 - (void) performTransaction_Locked:(bool)quiet action:(void (^)(SOSAccountTransaction* txn))action {
456 @autoreleasepool {
457 SOSAccountTransaction* transaction = [[SOSAccountTransaction alloc] initWithAccount:self quiet:quiet];
458 action(transaction);
459 [transaction finish];
460 }
461 }
462
463 - (void) performTransaction: (void (^)(SOSAccountTransaction* txn)) action {
464 [self performTransaction:false action:action];
465 }
466
467 - (void)performTransaction:(bool)quiet action:(void (^)(SOSAccountTransaction* txn))action {
468
469 if (__hasAccountQueue) {
470 // Be quiet; we're already in a transaction
471 [self performTransaction_Locked:true action:action];
472 }
473 else {
474 dispatch_sync(self.queue, ^{
475 __hasAccountQueue = true;
476 [self performTransaction_Locked:quiet action:action];
477 __hasAccountQueue = false;
478 });
479 }
480 }
481
482
483 @end