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