]> git.saurik.com Git - apple/security.git/blob - keychain/SecureObjectSync/SOSAccountGhost.m
Security-59754.41.1.tar.gz
[apple/security.git] / keychain / SecureObjectSync / SOSAccountGhost.m
1 //
2 // SOSAccountGhost.c
3 // sec
4 //
5 // Created by Richard Murphy on 4/12/16.
6 //
7 //
8
9 #include "SOSAccountPriv.h"
10 #include "SOSAccountGhost.h"
11 #include "keychain/SecureObjectSync/SOSPeerInfoCollections.h"
12 #include "keychain/SecureObjectSync/SOSInternal.h"
13 #include "keychain/SecureObjectSync/SOSAccount.h"
14 #include "keychain/SecureObjectSync/SOSCircle.h"
15 #include <Security/SecureObjectSync/SOSPeerInfo.h>
16 #include "keychain/SecureObjectSync/SOSPeerInfoV2.h"
17 #include "keychain/SecureObjectSync/SOSAccountTrustClassic+Circle.h"
18 #include "keychain/SecureObjectSync/SOSPeerInfoPriv.h"
19 #include "keychain/SecureObjectSync/SOSAuthKitHelpers.h"
20 #import "Analytics/Clients/SOSAnalytics.h"
21 #import <Security/SecBase64.h>
22 #include "utilities/SecTrace.h"
23
24
25 #define DETECT_IOS_ONLY 1
26
27 static bool sosGhostCheckValid(SOSPeerInfoRef pi) {
28 #if DETECT_IOS_ONLY
29 bool retval = false;
30 require_quiet(pi, retOut);
31 SOSPeerInfoDeviceClass peerClass = SOSPeerInfoGetClass(pi);
32 switch(peerClass) {
33 case SOSPeerInfo_iOS:
34 case SOSPeerInfo_tvOS:
35 case SOSPeerInfo_watchOS:
36 retval = true;
37 break;
38 case SOSPeerInfo_macOS: // go back and quit killing macos ghosts so people can multi-boot
39 case SOSPeerInfo_iCloud:
40 case SOSPeerInfo_unknown:
41 default:
42 retval = false;
43 break;
44 }
45 retOut:
46 return retval;
47 #else
48 return true;
49 #endif
50 }
51
52 static CFSetRef SOSCircleCreateGhostsOfPeerSet(SOSCircleRef circle, SOSPeerInfoRef me) {
53 CFMutableSetRef ghosts = NULL;
54 require_quiet(me, errOut);
55 require_quiet(sosGhostCheckValid(me), errOut);
56 require_quiet(circle, errOut);
57 require_quiet(SOSPeerInfoSerialNumberIsSet(me), errOut);
58 CFStringRef mySerial = SOSPeerInfoCopySerialNumber(me);
59 require_quiet(mySerial, errOut);
60 CFStringRef myPeerID = SOSPeerInfoGetPeerID(me);
61 ghosts = CFSetCreateMutableForCFTypes(kCFAllocatorDefault);
62 require_quiet(ghosts, errOut1);
63 SOSCircleForEachPeer(circle, ^(SOSPeerInfoRef pi) {
64 CFStringRef theirPeerID = SOSPeerInfoGetPeerID(pi);
65 if(!CFEqual(myPeerID, theirPeerID)) {
66 CFStringRef piSerial = SOSPeerInfoCopySerialNumber(pi);
67 if(CFEqualSafe(mySerial, piSerial)) {
68 CFSetAddValue(ghosts, theirPeerID);
69 }
70 CFReleaseNull(piSerial);
71 }
72 });
73 errOut1:
74 CFReleaseNull(mySerial);
75 errOut:
76 return ghosts;
77 }
78
79 static void SOSCircleClearMyGhosts(SOSCircleRef circle, SOSPeerInfoRef me) {
80 CFSetRef ghosts = SOSCircleCreateGhostsOfPeerSet(circle, me);
81 if(!ghosts || CFSetGetCount(ghosts) == 0) {
82 CFReleaseNull(ghosts);
83 return;
84 }
85 SOSCircleRemovePeersByIDUnsigned(circle, ghosts);
86 CFReleaseNull(ghosts);
87 }
88
89 // This only works if you're in the circle and have the private key
90 CF_RETURNS_RETAINED SOSCircleRef SOSAccountCloneCircleWithoutMyGhosts(SOSAccount* account, SOSCircleRef startCircle) {
91 SOSCircleRef newCircle = NULL;
92 CFSetRef ghosts = NULL;
93 require_quiet(account, retOut);
94 SecKeyRef userPrivKey = SOSAccountGetPrivateCredential(account, NULL);
95 require_quiet(userPrivKey, retOut);
96 SOSPeerInfoRef me = account.peerInfo;
97 require_quiet(me, retOut);
98 bool iAmApplicant = SOSCircleHasApplicant(startCircle, me, NULL);
99
100 ghosts = SOSCircleCreateGhostsOfPeerSet(startCircle, me);
101 require_quiet(ghosts, retOut);
102 require_quiet(CFSetGetCount(ghosts), retOut);
103
104 CFStringSetPerformWithDescription(ghosts, ^(CFStringRef description) {
105 secnotice("ghostbust", "Removing peers: %@", description);
106 });
107
108 newCircle = SOSCircleCopyCircle(kCFAllocatorDefault, startCircle, NULL);
109 require_quiet(newCircle, retOut);
110 if(iAmApplicant) {
111 if(SOSCircleRemovePeersByIDUnsigned(newCircle, ghosts) && (SOSCircleCountPeers(newCircle) == 0)) {
112 secnotice("resetToOffering", "Reset to offering with last ghost and me as applicant");
113 if(!SOSCircleResetToOffering(newCircle, userPrivKey, account.fullPeerInfo, NULL) ||
114 ![account.trust addiCloudIdentity:newCircle key:userPrivKey err:NULL]){
115 CFReleaseNull(newCircle);
116 }
117 account.notifyBackupOnExit = true;
118 } else {
119 CFReleaseNull(newCircle);
120 }
121 } else {
122 SOSCircleRemovePeersByID(newCircle, userPrivKey, account.fullPeerInfo, ghosts, NULL);
123 }
124 retOut:
125 CFReleaseNull(ghosts);
126 return newCircle;
127 }
128
129
130 static NSUInteger SOSGhostBustThinSerialClones(SOSCircleRef circle, NSString *myPeerID) {
131 NSUInteger gbcount = 0;
132 CFMutableArrayRef sortPeers = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault);
133 SOSCircleForEachPeer(circle, ^(SOSPeerInfoRef peer) {
134 CFArrayAppendValue(sortPeers, peer);
135 });
136 CFRange range = CFRangeMake(0, CFArrayGetCount(sortPeers));
137 CFArraySortValues(sortPeers, range, SOSPeerInfoCompareByApplicationDate, NULL);
138
139 NSMutableDictionary *latestPeers = [[NSMutableDictionary alloc] init];
140 NSMutableSet *removals = [[NSMutableSet alloc] init];
141
142 for(CFIndex i = CFArrayGetCount(sortPeers); i > 0; i--) {
143 SOSPeerInfoRef pi = (SOSPeerInfoRef) CFArrayGetValueAtIndex(sortPeers, i-1);
144 NSString *peerID = (__bridge NSString *)SOSPeerInfoGetPeerID(pi);
145
146 if(![peerID isEqualToString: myPeerID] && sosGhostCheckValid(pi)) {
147 NSString *serial = CFBridgingRelease(SOSPeerInfoV2DictionaryCopyString(pi, sSerialNumberKey));
148 if(serial != nil) {
149 if([latestPeers objectForKey:serial] != nil) {
150 secnotice("ghostBust", "There is a more recent peer than %@ for this serial number", SOSPeerInfoGetSPID(pi));
151 [removals addObject:peerID];
152 } else {
153 [latestPeers setObject:peerID forKey:serial];
154 }
155 } else {
156 secnotice("ghostBust", "Removing peerID (%@) with no serial number", SOSPeerInfoGetSPID(pi));
157 [removals addObject:peerID];
158 }
159 }
160 }
161 gbcount = [removals count];
162 if(gbcount > 0) {
163 SOSCircleRemovePeersByIDUnsigned(circle, (__bridge CFSetRef)(removals));
164 }
165 CFReleaseNull(sortPeers);
166 return gbcount;
167 }
168
169 static void SOSCircleRemoveiCloudIdentities(SOSCircleRef circle) {
170 NSMutableSet *removals = [[NSMutableSet alloc] init];
171 SOSCircleForEachiCloudIdentityPeer(circle, ^(SOSPeerInfoRef peer) {
172 NSString *peerID = (__bridge NSString *)SOSPeerInfoGetPeerID(peer);
173 [removals addObject:peerID];
174 });
175 SOSCircleRemovePeersByIDUnsigned(circle, (__bridge CFSetRef)(removals));
176 }
177
178 // this is only used to determine if we have a circle that can't accept new peers because of ghosts.
179 static void SOSCircleRemoveWindowsPeers(SOSCircleRef circle) {
180 NSMutableSet *removals = [[NSMutableSet alloc] init];
181 SOSCircleForEachActivePeer(circle, ^(SOSPeerInfoRef peer) {
182 NSString *devType = (__bridge NSString *)SOSPeerInfoGetPeerDeviceType(peer);
183 if([devType hasPrefix: @"Windows"]) {
184 NSString *peerID = (__bridge NSString *)SOSPeerInfoGetPeerID(peer);
185 [removals addObject:peerID];
186 }
187 });
188 SOSCircleRemovePeersByIDUnsigned(circle, (__bridge CFSetRef)(removals));
189 }
190
191 static void SOSCircleRemovePeersNotInMidlist(SOSCircleRef circle) {
192 __block SOSAuthKitHelpers *akh = nil;
193
194 if([SOSAuthKitHelpers accountIsHSA2]) {
195 [SOSAuthKitHelpers activeMIDs:^(NSSet <SOSTrustedDeviceAttributes *> * _Nullable activeMIDs, NSError * _Nullable error) {
196 akh = [[SOSAuthKitHelpers alloc] initWithActiveMIDS:activeMIDs];
197 }];
198 }
199
200 NSMutableSet *removals = [[NSMutableSet alloc] init];
201 if(akh) {
202 SOSCircleForEachActivePeer(circle, ^(SOSPeerInfoRef peer) {
203 NSString *mid = CFBridgingRelease(SOSPeerInfoV2DictionaryCopyString(peer, sMachineIDKey));
204 if(![akh midIsValidInList:mid]) {
205 NSString *peerID = (__bridge NSString *)SOSPeerInfoGetPeerID(peer);
206 [removals addObject:peerID];
207 }
208 });
209 SOSCircleRemovePeersByIDUnsigned(circle, (__bridge CFSetRef)(removals));
210 }
211 }
212
213
214 bool SOSAccountGhostResultsInReset(SOSAccount* account) {
215 if(!account.peerID || !account.trust.trustedCircle) return false;
216 SOSCircleRef newCircle = SOSCircleCopyCircle(kCFAllocatorDefault, account.trust.trustedCircle, NULL);
217 if(!newCircle) return false;
218 SOSCircleClearMyGhosts(newCircle, account.peerInfo);
219 SOSCircleRemoveRetired(newCircle, NULL);
220 SOSCircleRemoveiCloudIdentities(newCircle);
221 SOSCircleRemoveWindowsPeers(newCircle);
222 SOSCircleRemovePeersNotInMidlist(newCircle);
223 int npeers = SOSCircleCountPeers(newCircle);
224 CFReleaseNull(newCircle);
225 return npeers == 0;
226 }
227
228 static NSUInteger SOSGhostBustThinByMIDList(SOSCircleRef circle, NSString *myPeerID, SOSAuthKitHelpers *akh, SOSAccountGhostBustingOptions options, NSMutableDictionary *attributes) {
229 __block unsigned int gbmid = 0;
230 __block unsigned int gbserial = 0;
231
232 NSMutableSet *removals = [[NSMutableSet alloc] init];
233 SOSCircleForEachPeer(circle, ^(SOSPeerInfoRef peer) {
234 NSString *peerID = (__bridge NSString *)SOSPeerInfoGetPeerID(peer);
235 if([peerID isEqualToString:myPeerID]) {
236 return;
237 }
238 if(options & SOSGhostBustByMID) {
239 NSString *mid = CFBridgingRelease(SOSPeerInfoV2DictionaryCopyString(peer, sMachineIDKey));
240 if(![akh midIsValidInList:mid]) {
241 secnotice("ghostBust", "Removing peerInfo %@ - mid is not in list", SOSPeerInfoGetSPID(peer));
242 [removals addObject:peerID];
243 gbmid++;
244 return;
245 }
246 }
247 if(options & SOSGhostBustBySerialNumber) {
248 NSString *serial = CFBridgingRelease(SOSPeerInfoV2DictionaryCopyString(peer, sSerialNumberKey));
249 if(![akh serialIsValidInList: serial]) {
250 secnotice("ghostBust", "Removing peerInfo %@ - serial# is not in list", SOSPeerInfoGetSPID(peer));
251 [removals addObject:peerID];
252 gbserial++;
253 return;
254 }
255 }
256 });
257
258 // Now use the removal set to ghostbust the circle
259 SOSCircleRemovePeersByIDUnsigned(circle, (__bridge CFSetRef) removals);
260 attributes[@"byMID"] = @(gbmid);
261 attributes[@"bySerial"] = @(gbserial);
262 return [removals count];
263 }
264
265 static NSUInteger SOSGhostBustiCloudIdentityPrivateKeys(SOSAccount *account) {
266 __block NSUInteger cleaned = 0;
267
268 [ account iCloudIdentityStatus_internal:^(NSDictionary *tableSpid, NSError *error) {
269 if(!tableSpid) {
270 secnotice("ghostBust", "Couldn't work on iCloud Identities (%@)", error);
271 }
272
273 size_t keysToRemove = [tableSpid[kSOSIdentityStatusKeyOnly] count];
274
275 if(keysToRemove == 0) {
276 return;
277 }
278
279 if([tableSpid[kSOSIdentityStatusCompleteIdentity] count] == 0) {
280 secnotice("ghostBust", "No iCloud Identity FPI, can't remove iCloudIdentity extra keys");
281 return;
282 }
283
284 for (NSString *pid in tableSpid[kSOSIdentityStatusKeyOnly]) {
285 CFStringRef fullKeyLabel = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("Cloud Identity - '%@'"), pid);
286
287 if(fullKeyLabel) {
288 CFDictionaryRef query = CFDictionaryCreateForCFTypes(kCFAllocatorDefault,
289 kSecClass, kSecClassKey,
290 kSecAttrKeyClass, kSecAttrKeyClassPrivate,
291 kSecAttrSynchronizable, kSecAttrSynchronizableAny,
292 kSecUseTombstones, kCFBooleanTrue,
293 kSecAttrLabel, fullKeyLabel,
294 NULL);
295 OSStatus status = SecItemDelete(query);
296 if(errSecSuccess == status) {
297 secnotice("ghostBust", "removed %@", pid);
298 cleaned++;
299 } else {
300 secnotice("ghostbust", "Delete for %@ returned %d", pid, (int) status);
301 }
302 CFReleaseNull(query);
303 CFReleaseNull(fullKeyLabel);
304 }
305 }
306 secnotice("ghostBust", "Removed %zu of %zu deserted icloud private keys", cleaned, keysToRemove);
307 }];
308 return cleaned;
309 }
310
311 bool SOSAccountGhostBustCircle(SOSAccount *account, SOSAuthKitHelpers *akh, SOSAccountGhostBustingOptions options, int mincount) {
312 __block bool result = false;
313 __block bool actionTaken = false;
314 CFErrorRef localError = NULL;
315 __block NSUInteger nbusted = 9999;
316 NSMutableDictionary *attributes =[NSMutableDictionary new];
317
318 int circleSize = SOSCircleCountPeers(account.trust.trustedCircle);
319
320 if(options & SOSGhostBustiCloudIdentities && [account isInCircle:nil]) {
321 secnotice("ghostBust", "Callout to cleanup icloud identities");
322 nbusted = SOSGhostBustiCloudIdentityPrivateKeys(account);
323 if(nbusted) {
324 attributes[@"iCloudPrivKeysBusted"] = @(nbusted);
325 [[SOSAnalytics logger] logSoftFailureForEventNamed:@"GhostBust" withAttributes:attributes];
326 result = true;
327 }
328 actionTaken = true;
329 }
330
331 if(options & (~SOSGhostBustiCloudIdentities)) {
332 if ([akh isUseful] && [account isInCircle:nil] && circleSize > mincount) {
333 [account.trust modifyCircle:account.circle_transport err:&localError action:^(SOSCircleRef circle) {
334 if((options & SOSGhostBustByMID) || (options & SOSGhostBustBySerialNumber)) {
335 nbusted = SOSGhostBustThinByMIDList(circle, account.peerID, akh, options, attributes);
336 secnotice("ghostbust", "Removed %lu ghosts from circle by midlist && serialNumber", (unsigned long)nbusted);
337 actionTaken = true;
338 }
339 if(options & SOSGhostBustSerialByAge) {
340 NSUInteger thinBusted = 9999;
341 thinBusted = SOSGhostBustThinSerialClones(circle, account.peerID);
342 nbusted += thinBusted;
343 attributes[@"byAge"] = @(thinBusted);
344 actionTaken = true;
345 }
346 attributes[@"total"] = @(SecBucket1Significant(nbusted));
347 attributes[@"startCircleSize"] = @(SecBucket1Significant(circleSize));
348 result = nbusted > 0;
349 if(result) {
350 SOSAccountRestartPrivateCredentialTimer(account);
351 if((SOSAccountGetPrivateCredential(account, NULL) != NULL) || SOSAccountAssertStashedAccountCredential(account, NULL)) {
352 result = SOSCircleGenerationSign(circle, SOSAccountGetPrivateCredential(account, NULL), account.fullPeerInfo, NULL);
353 } else {
354 result = false;
355 }
356 }
357 return result;
358 }];
359 secnotice("circleOps", "Ghostbusting %@ (%@)", result ? CFSTR("Performed") : CFSTR("Not Performed"), localError);
360 }
361 }
362 CFReleaseNull(localError);
363
364 if(actionTaken) {
365 if(result) {
366 [[SOSAnalytics logger] logSoftFailureForEventNamed:@"GhostBust" withAttributes:attributes];
367 } else if(nbusted == 0){
368 [[SOSAnalytics logger] logSuccessForEventNamed:@"GhostBust"];
369 } else {
370 [[SOSAnalytics logger] logHardFailureForEventNamed:@"GhostBust" withAttributes:nil];
371 }
372 }
373 return result;
374 }