]> git.saurik.com Git - apple/security.git/blob - keychain/SecureObjectSync/SOSAccountGhost.m
Security-59306.41.2.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 #include "utilities/SecTrace.h"
22
23
24 #define DETECT_IOS_ONLY 1
25
26 static bool sosGhostCheckValid(SOSPeerInfoRef pi) {
27 #if DETECT_IOS_ONLY
28 bool retval = false;
29 require_quiet(pi, retOut);
30 SOSPeerInfoDeviceClass peerClass = SOSPeerInfoGetClass(pi);
31 switch(peerClass) {
32 case SOSPeerInfo_iOS:
33 case SOSPeerInfo_tvOS:
34 case SOSPeerInfo_watchOS:
35 case SOSPeerInfo_macOS:
36 retval = true;
37 break;
38 case SOSPeerInfo_iCloud:
39 case SOSPeerInfo_unknown:
40 default:
41 retval = false;
42 break;
43 }
44 retOut:
45 return retval;
46 #else
47 return true;
48 #endif
49 }
50
51 static CFSetRef SOSCircleCreateGhostsOfPeerSet(SOSCircleRef circle, SOSPeerInfoRef me) {
52 CFMutableSetRef ghosts = NULL;
53 require_quiet(me, errOut);
54 require_quiet(sosGhostCheckValid(me), errOut);
55 require_quiet(circle, errOut);
56 require_quiet(SOSPeerInfoSerialNumberIsSet(me), errOut);
57 CFStringRef mySerial = SOSPeerInfoCopySerialNumber(me);
58 require_quiet(mySerial, errOut);
59 CFStringRef myPeerID = SOSPeerInfoGetPeerID(me);
60 ghosts = CFSetCreateMutableForCFTypes(kCFAllocatorDefault);
61 require_quiet(ghosts, errOut1);
62 SOSCircleForEachPeer(circle, ^(SOSPeerInfoRef pi) {
63 CFStringRef theirPeerID = SOSPeerInfoGetPeerID(pi);
64 if(!CFEqual(myPeerID, theirPeerID)) {
65 CFStringRef piSerial = SOSPeerInfoCopySerialNumber(pi);
66 if(CFEqualSafe(mySerial, piSerial)) {
67 CFSetAddValue(ghosts, theirPeerID);
68 }
69 CFReleaseNull(piSerial);
70 }
71 });
72 errOut1:
73 CFReleaseNull(mySerial);
74 errOut:
75 return ghosts;
76 }
77
78 static void SOSCircleClearMyGhosts(SOSCircleRef circle, SOSPeerInfoRef me) {
79 CFSetRef ghosts = SOSCircleCreateGhostsOfPeerSet(circle, me);
80 if(!ghosts || CFSetGetCount(ghosts) == 0) {
81 CFReleaseNull(ghosts);
82 return;
83 }
84 SOSCircleRemovePeersByIDUnsigned(circle, ghosts);
85 CFReleaseNull(ghosts);
86 }
87
88 // This only works if you're in the circle and have the private key
89 CF_RETURNS_RETAINED SOSCircleRef SOSAccountCloneCircleWithoutMyGhosts(SOSAccount* account, SOSCircleRef startCircle) {
90 SOSCircleRef newCircle = NULL;
91 CFSetRef ghosts = NULL;
92 require_quiet(account, retOut);
93 SecKeyRef userPrivKey = SOSAccountGetPrivateCredential(account, NULL);
94 require_quiet(userPrivKey, retOut);
95 SOSPeerInfoRef me = account.peerInfo;
96 require_quiet(me, retOut);
97 bool iAmApplicant = SOSCircleHasApplicant(startCircle, me, NULL);
98
99 ghosts = SOSCircleCreateGhostsOfPeerSet(startCircle, me);
100 require_quiet(ghosts, retOut);
101 require_quiet(CFSetGetCount(ghosts), retOut);
102
103 CFStringSetPerformWithDescription(ghosts, ^(CFStringRef description) {
104 secnotice("ghostbust", "Removing peers: %@", description);
105 });
106
107 newCircle = SOSCircleCopyCircle(kCFAllocatorDefault, startCircle, NULL);
108 require_quiet(newCircle, retOut);
109 if(iAmApplicant) {
110 if(SOSCircleRemovePeersByIDUnsigned(newCircle, ghosts) && (SOSCircleCountPeers(newCircle) == 0)) {
111 secnotice("resetToOffering", "Reset to offering with last ghost and me as applicant");
112 if(!SOSCircleResetToOffering(newCircle, userPrivKey, account.fullPeerInfo, NULL) ||
113 ![account.trust addiCloudIdentity:newCircle key:userPrivKey err:NULL]){
114 CFReleaseNull(newCircle);
115 }
116 account.notifyBackupOnExit = true;
117 } else {
118 CFReleaseNull(newCircle);
119 }
120 } else {
121 SOSCircleRemovePeersByID(newCircle, userPrivKey, account.fullPeerInfo, ghosts, NULL);
122 }
123 retOut:
124 CFReleaseNull(ghosts);
125 return newCircle;
126 }
127
128
129 static NSUInteger SOSGhostBustThinSerialClones(SOSCircleRef circle, NSString *myPeerID) {
130 NSUInteger gbcount = 0;
131 CFMutableArrayRef sortPeers = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault);
132 SOSCircleForEachPeer(circle, ^(SOSPeerInfoRef peer) {
133 CFArrayAppendValue(sortPeers, peer);
134 });
135 CFRange range = CFRangeMake(0, CFArrayGetCount(sortPeers));
136 CFArraySortValues(sortPeers, range, SOSPeerInfoCompareByApplicationDate, NULL);
137
138 NSMutableDictionary *latestPeers = [[NSMutableDictionary alloc] init];
139 NSMutableSet *removals = [[NSMutableSet alloc] init];
140
141 for(CFIndex i = CFArrayGetCount(sortPeers); i > 0; i--) {
142 SOSPeerInfoRef pi = (SOSPeerInfoRef) CFArrayGetValueAtIndex(sortPeers, i-1);
143
144 if(sosGhostCheckValid(pi)) {
145 NSString *serial = CFBridgingRelease(SOSPeerInfoV2DictionaryCopyString(pi, sSerialNumberKey));
146 NSString *peerID = (__bridge NSString *)SOSPeerInfoGetPeerID(pi);
147 if(serial != nil) {
148 if([latestPeers objectForKey:serial] != nil) {
149 if(peerID != myPeerID) {
150 [removals addObject:peerID];
151 } else {
152 secnotice("ghostBust", "There is a more recent peer for this serial number");
153 }
154 } else {
155 [latestPeers setObject:peerID forKey:serial];
156 }
157 } else {
158 secnotice("ghostBust", "Removing peerID (%@) with no serial number", peerID);
159 [removals addObject:peerID];
160 }
161 }
162 }
163 SOSCircleRemovePeersByIDUnsigned(circle, (__bridge CFSetRef)(removals));
164 gbcount = [removals count];
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 bool SOSAccountGhostResultsInReset(SOSAccount* account) {
179 if(!account.peerID || !account.trust.trustedCircle) return false;
180 SOSCircleRef newCircle = SOSCircleCopyCircle(kCFAllocatorDefault, account.trust.trustedCircle, NULL);
181 if(!newCircle) return false;
182 SOSCircleClearMyGhosts(newCircle, account.peerInfo);
183 SOSCircleRemoveRetired(newCircle, NULL);
184 SOSCircleRemoveiCloudIdentities(newCircle);
185 int npeers = SOSCircleCountPeers(newCircle);
186 CFReleaseNull(newCircle);
187 return npeers == 0;
188 }
189
190 static NSUInteger SOSGhostBustThinByMIDList(SOSCircleRef circle, NSString *myPeerID, SOSAuthKitHelpers *akh, SOSAccountGhostBustingOptions options, NSMutableDictionary *attributes) {
191 __block unsigned int gbmid = 0;
192 __block unsigned int gbserial = 0;
193
194 NSMutableSet *removals = [[NSMutableSet alloc] init];
195 SOSCircleForEachPeer(circle, ^(SOSPeerInfoRef peer) {
196 NSString *peerID = (__bridge NSString *)SOSPeerInfoGetPeerID(peer);
197 if([peerID isEqualToString:myPeerID]) {
198 return;
199 }
200 if(options & SOSGhostBustByMID) {
201 NSString *mid = CFBridgingRelease(SOSPeerInfoV2DictionaryCopyString(peer, sMachineIDKey));
202 if(![akh midIsValidInList:mid]) {
203 [removals addObject:peerID];
204 gbmid++;
205 return;
206 }
207 }
208 if(options & SOSGhostBustBySerialNumber) {
209 NSString *serial = CFBridgingRelease(SOSPeerInfoV2DictionaryCopyString(peer, sSerialNumberKey));
210 if(![akh serialIsValidInList: serial]) {
211 [removals addObject:peerID];
212 gbserial++;
213 return;
214 }
215 }
216 });
217
218 // Now use the removal set to ghostbust the circle
219 SOSCircleRemovePeersByIDUnsigned(circle, (__bridge CFSetRef) removals);
220 attributes[@"byMID"] = @(gbmid);
221 attributes[@"bySerial"] = @(gbserial);
222 return [removals count];
223 }
224
225 static bool SOSGhostBustiCloudIdentityPrivateKeys(SOSAccount *account) {
226 __block bool cleaned = false;
227 SecKeyRef pubKey = NULL;
228 CFTypeRef queryResult = NULL;
229 CFDataRef pubKeyHash = NULL;
230 CFDictionaryRef query = NULL;
231 __block int removed = 0;
232
233 SOSFullPeerInfoRef iCloudIdentity = SOSCircleCopyiCloudFullPeerInfoVerifier(account.trust.trustedCircle, NULL);
234 require_action_quiet(iCloudIdentity, retOut, secnotice("ghostBust", "No iCloud Identity FPI"));
235 pubKey = SOSFullPeerInfoCopyPubKey(iCloudIdentity, NULL);
236 require_action_quiet(pubKey, retOut, secnotice("ghostBust", "Can't get iCloud Identity pubkey"));
237 pubKeyHash = SecKeyCopyPublicKeyHash(pubKey);
238 require_action_quiet(pubKeyHash, retOut, secnotice("ghostBust", "Can't get iCloud Identity pubkey hash"));
239 query = CFDictionaryCreateForCFTypes(kCFAllocatorDefault,
240 kSecMatchLimit, kSecMatchLimitAll,
241 kSecClass, kSecClassKey,
242 kSecAttrKeyClass, kSecAttrKeyClassPrivate,
243 kSecAttrSynchronizable, kSecAttrSynchronizableAny,
244 kSecReturnAttributes, kCFBooleanTrue,
245 kSecAttrAccessGroup, kSOSInternalAccessGroup,
246 NULL);
247 require_action_quiet(errSecSuccess == SecItemCopyMatching(query, &queryResult), retOut, secnotice("ghostBust", "Can't get iCloud Identity private keys"));
248 require_action_quiet(CFGetTypeID(queryResult) == CFArrayGetTypeID(), retOut, cleaned = true);
249 CFArrayRef iCloudPrivKeys = queryResult;
250 secnotice("ghostBust", "Screening %ld icloud private keys", (long)CFArrayGetCount(iCloudPrivKeys));
251 CFArrayForEach(iCloudPrivKeys, ^(const void *value) {
252 CFDictionaryRef privkey = asDictionary(value, NULL);
253 if(privkey) {
254 CFDataRef candidate = asData(CFDictionaryGetValue(privkey, kSecAttrApplicationLabel), NULL);
255 if(candidate && !CFEqual(pubKeyHash, candidate)) {
256 CFStringRef label = asString(CFDictionaryGetValue(privkey, kSecAttrLabel), NULL);
257 if(label) {
258 if(CFStringHasPrefix(label, CFSTR("Cloud Identity"))) {
259
260 CFDictionaryRef delQuery = CFDictionaryCreateForCFTypes(kCFAllocatorDefault,
261 kSecClass, kSecClassKey,
262 kSecAttrKeyClass, kSecAttrKeyClassPrivate,
263 kSecAttrSynchronizable, kSecAttrSynchronizableAny,
264 kSecAttrApplicationLabel, candidate,
265 NULL);
266
267 OSStatus status = SecItemDelete(delQuery);
268 if(errSecSuccess == status) {
269 secnotice("ghostBust", "removed %@", label);
270 removed++;
271 cleaned = true;
272 } else {
273 secnotice("ghostbust", "Search for %@ returned %d", candidate, (int) status);
274 }
275 CFReleaseNull(delQuery);
276 }
277 }
278 }
279 }
280 });
281 secnotice("ghostBust", "Removed %d icloud private keys", removed);
282 retOut:
283 CFReleaseNull(query);
284 CFReleaseNull(queryResult);
285 CFReleaseNull(pubKeyHash);
286 CFReleaseNull(pubKey);
287 CFReleaseNull(iCloudIdentity);
288 return cleaned;
289 }
290
291 bool SOSAccountGhostBustCircle(SOSAccount *account, SOSAuthKitHelpers *akh, SOSAccountGhostBustingOptions options, int mincount) {
292 __block bool result = false;
293 CFErrorRef localError = NULL;
294 __block NSUInteger nbusted = 9999;
295 NSMutableDictionary *attributes =[NSMutableDictionary new];
296 int circleSize = SOSCircleCountPeers(account.trust.trustedCircle);
297
298 if ([akh isUseful] && [account isInCircle:nil] && circleSize > mincount) {
299 if(options & SOSGhostBustiCloudIdentities) {
300 secnotice("ghostBust", "Callout to cleanup icloud identities");
301 result = SOSGhostBustiCloudIdentityPrivateKeys(account);
302 } else {
303 [account.trust modifyCircle:account.circle_transport err:&localError action:^(SOSCircleRef circle) {
304 nbusted = SOSGhostBustThinByMIDList(circle, account.peerID, akh, options, attributes);
305 secnotice("ghostbust", "Removed %lu ghosts from circle by midlist && serialNumber", (unsigned long)nbusted);
306 if(options & SOSGhostBustSerialByAge) {
307 NSUInteger thinBusted = 9999;
308 thinBusted = SOSGhostBustThinSerialClones(circle, account.peerID);
309 nbusted += thinBusted;
310 attributes[@"byAge"] = @(thinBusted);
311 }
312 attributes[@"total"] = @(SecBucket1Significant(nbusted));
313 attributes[@"startCircleSize"] = @(SecBucket1Significant(circleSize));
314 result = nbusted > 0;
315 if(result) {
316 SOSAccountRestartPrivateCredentialTimer(account);
317 if((SOSAccountGetPrivateCredential(account, NULL) != NULL) || SOSAccountAssertStashedAccountCredential(account, NULL)) {
318 result = SOSCircleGenerationSign(circle, SOSAccountGetPrivateCredential(account, NULL), account.fullPeerInfo, NULL);
319 } else {
320 result = false;
321 }
322 }
323 return result;
324 }];
325 secnotice("circleOps", "Ghostbusting %@ (%@)", result ? CFSTR("Performed") : CFSTR("Not Performed"), localError);
326 }
327 } else {
328 secnotice("circleOps", "Ghostbusting skipped");
329 }
330 CFReleaseNull(localError);
331
332 if(result) {
333 [[SOSAnalytics logger] logSoftFailureForEventNamed:@"GhostBust" withAttributes:attributes];
334 } else if(nbusted == 0){
335 [[SOSAnalytics logger] logSuccessForEventNamed:@"GhostBust"];
336 } else {
337 [[SOSAnalytics logger] logHardFailureForEventNamed:@"GhostBust" withAttributes:nil];
338 }
339 return result;
340 }