X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/5dd5f9ec28f304ca377c42fd7f711d6cf12b90e1..5c19dc3ae3bd8e40a9c028b0deddd50ff337692c:/OSX/sec/ProjectHeaders/Security/SecureObjectSync/SOSEngine.c diff --git a/OSX/sec/ProjectHeaders/Security/SecureObjectSync/SOSEngine.c b/OSX/sec/ProjectHeaders/Security/SecureObjectSync/SOSEngine.c new file mode 100644 index 00000000..efa578a5 --- /dev/null +++ b/OSX/sec/ProjectHeaders/Security/SecureObjectSync/SOSEngine.c @@ -0,0 +1,2308 @@ +/* + * Copyright (c) 2012-2015 Apple Inc. All Rights Reserved. + * + * @APPLE_LICENSE_HEADER_START@ + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + * + * @APPLE_LICENSE_HEADER_END@ + */ + + +/* + * SOSEngine.c - Implementation of a secure object syncing engine + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // TODO: We can't leave this here. +#include // TODO: We can't leave this here. +#include // TODO: We can't leave this here. +#include // TODO: We can't leave this here. +#include +#include + +#include + +// +// MARK: SOSEngine The Keychain database with syncable keychain support. +// + +// Key in dataSource for general engine state file. +// This file only has digest entries in it, no manifests. +static const CFStringRef kSOSEngineState = CFSTR("engine-state"); + +// Keys in state dictionary +static CFStringRef kSOSEngineManifestCacheKey = CFSTR("manifestCache"); +static CFStringRef kSOSEnginePeerStateKey = CFSTR("peerState"); +static CFStringRef kSOSEnginePeerIDsKey = CFSTR("peerIDs"); +static CFStringRef kSOSEngineIDKey = CFSTR("id"); +static CFStringRef kSOSEngineTraceDateKey = CFSTR("traceDate"); + +/* SOSEngine implementation. */ +struct __OpaqueSOSEngine { + CFRuntimeBase _base; + SOSDataSourceRef dataSource; + CFStringRef myID; // My peerID in the circle + // We need to address the issues of corrupt keychain items + SOSManifestRef unreadable; // Possibly by having a set of unreadable items, to which we + // add any corrupted items in the db that have yet to be deleted. + // This happens if we notce corruption during a (read only) query. + // We would also perma-subtract unreadable from manifest whenever + // anyone asked for manifest. This result would be cached in + // The manifestCache below, so we just need a key into the cache + CFDataRef localMinusUnreadableDigest; // or a digest (CFDataRef of the right size). + + CFMutableDictionaryRef manifestCache; // digest -> ( refcount, manifest ) + //CFMutableDictionaryRef peerState; // peerId -> mutable array of digests + CFMutableDictionaryRef peerMap; // peerId -> SOSPeerRef + CFDictionaryRef viewNameSet2ChangeTracker; // CFSetRef of CFStringRef -> SOSChangeTrackerRef + CFDictionaryRef viewName2ChangeTracker; // CFStringRef -> SOSChangeTrackerRef + CFArrayRef peerIDs; + CFDateRef lastTraceDate; // Last time we did a CloudKeychainTrace + + dispatch_queue_t queue; // Engine queue + + dispatch_queue_t syncCompleteQueue; // Non-retained queue for async notificaion + CFMutableDictionaryRef syncCompleteListeners; // Map from PeerID->notification block +}; + +static bool SOSEngineLoad(SOSEngineRef engine, CFErrorRef *error); + + +static CFStringRef SOSPeerIDArrayCreateString(CFArrayRef peerIDs) { + return peerIDs ? CFStringCreateByCombiningStrings(kCFAllocatorDefault, peerIDs, CFSTR(" ")) : CFSTR(""); +} + +static CFStringRef SOSEngineCopyFormattingDesc(CFTypeRef cf, CFDictionaryRef formatOptions) { + SOSEngineRef engine = (SOSEngineRef)cf; + CFStringRef tpDesc = SOSPeerIDArrayCreateString(engine->peerIDs); + CFStringRef desc = CFStringCreateWithFormat(kCFAllocatorDefault, formatOptions, CFSTR(""), engine->myID, tpDesc, engine->manifestCache ? (int)CFDictionaryGetCount(engine->manifestCache) : 0, engine->peerMap ? (int)CFDictionaryGetCount(engine->peerMap) : 0); + CFReleaseSafe(tpDesc); + return desc; + } + +static CFStringRef SOSEngineCopyDebugDesc(CFTypeRef cf) { + return SOSEngineCopyFormattingDesc(cf, NULL); + } + +static dispatch_queue_t sEngineQueue; +static CFDictionaryRef sEngineMap; + +CFGiblisWithFunctions(SOSEngine, NULL, NULL, NULL, NULL, NULL, SOSEngineCopyFormattingDesc, SOSEngineCopyDebugDesc, NULL, NULL, ^{ + sEngineQueue = dispatch_queue_create("SOSEngine queue", DISPATCH_QUEUE_SERIAL); + sEngineMap = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); +}); + +#define _LOG_RAW_MESSAGES 0 +void logRawMessage(CFDataRef message, bool sending, uint64_t seqno) +{ +#if _LOG_RAW_MESSAGES + CFStringRef hexMessage = NULL; + if (message) { + hexMessage = CFDataCopyHexString(message); + if (sending) + secnoticeq("engine", "%s RAW%1d %@", sending ? "send" : "recv", seqno?2:0, hexMessage); + else + secnoticeq("engine", "%s RAWx %@", sending ? "send" : "recv", hexMessage); // we don't know vers of received msg here + } + CFReleaseSafe(hexMessage); +#endif +} +// +// Peer state layout. WRONG! It's an array now +// The peer state is an array. +// The first element of the array is a dictionary with any number of keys and +// values in it (for future expansion) such as changing the digest size or type +// or remembering boolean flags for a peers sake. +// The next three are special in that they are manifest digests with special +// meaning and rules as to how they are treated (These are dynamically updated +// based on database activity so they have a fully history of all changes made +// to the local db. The first is the manifest representing the pendingObjects +// to send to the other peer. This is normally only ever appending to, and in +// particular with transactions originating from the Keychain API that affect +// syncable items will need to add the new objects digests to the pendingObjects list +// while adding the digests of any tombstones encountered to the extra list. + +CFStringRef SOSEngineGetMyID(SOSEngineRef engine) { + // TODO: this should not be needed + return engine->myID; +} + +// TEMPORARY: Get the list of IDs for cleanup, this shouldn't be used instead it should iterate KVS. +CFArrayRef SOSEngineGetPeerIDs(SOSEngineRef engine) { + return engine->peerIDs; +} + +void SOSEngineClearCache(SOSEngineRef engine){ + CFReleaseNull(engine->manifestCache); + CFReleaseNull(engine->localMinusUnreadableDigest); + dispatch_release(engine->queue); +} + +static SOSPeerRef SOSEngineCopyPeerWithMapEntry_locked(SOSEngineRef engine, CFStringRef peerID, CFTypeRef mapEntry, CFErrorRef *error) { + SOSPeerRef peer = NULL; + if (mapEntry && CFGetTypeID(mapEntry) == SOSPeerGetTypeID()) { + // The mapEntry is an SOSPeer, so we're done. + peer = (SOSPeerRef)CFRetain(mapEntry); + } else { + // The mapEntry is a peerState, attempt to initialize a new + // peer iff peerID is in the set of trusted peerIDs + if (engine->peerIDs && CFArrayContainsValue(engine->peerIDs, CFRangeMake(0, CFArrayGetCount(engine->peerIDs)), peerID)) { + CFErrorRef localError = NULL; + peer = SOSPeerCreateWithState(engine, peerID, mapEntry, &localError); + if (!peer) { + secerror("error inflating peer: %@: %@ from state: %@", peerID, localError, mapEntry); + CFReleaseNull(localError); + peer = SOSPeerCreateWithState(engine, peerID, NULL, error); + } + if (peer) { + // Replace the map entry with the inflated peer. + CFDictionarySetValue(engine->peerMap, peerID, peer); + } + } else { + SOSErrorCreate(kSOSErrorPeerNotFound, error, NULL, CFSTR("peer: %@ is untrusted inflating not allowed"), peerID); + } + } + return peer; +} + +static SOSPeerRef SOSEngineCopyPeerWithID_locked(SOSEngineRef engine, CFStringRef peerID, CFErrorRef *error) { + CFTypeRef mapEntry = CFDictionaryGetValue(engine->peerMap, peerID); + SOSPeerRef peer = NULL; + if (mapEntry) { + peer = SOSEngineCopyPeerWithMapEntry_locked(engine, peerID, mapEntry, error); + } else { + peer = NULL; + SOSErrorCreate(kSOSErrorPeerNotFound, error, NULL, CFSTR("peer: %@ not found"), peerID); + } + return peer; +} + +struct SOSEngineWithPeerContext { + SOSEngineRef engine; + void (^with)(SOSPeerRef peer); +}; + +static void SOSEngineWithPeerMapEntry_locked(const void *peerID, const void *mapEntry, void *context) { + struct SOSEngineWithPeerContext *ewp = context; + SOSPeerRef peer = SOSEngineCopyPeerWithMapEntry_locked(ewp->engine, peerID, mapEntry, NULL); + if (peer) { + ewp->with(peer); + CFRelease(peer); + } +} + +static void SOSEngineForEachPeer_locked(SOSEngineRef engine, void (^with)(SOSPeerRef peer)) { + struct SOSEngineWithPeerContext ewp = { .engine = engine, .with = with }; + CFDictionaryRef peerMapCopy = CFDictionaryCreateCopy(NULL, engine->peerMap); + CFDictionaryApplyFunction(peerMapCopy, SOSEngineWithPeerMapEntry_locked, &ewp); + CFRelease(peerMapCopy); +} + +static void SOSEngineWithBackupPeerMapEntry_locked(const void *peerID, const void *mapEntry, void *context) { + struct SOSEngineWithPeerContext *ewp = context; + // v0 backup peer is always in map but we only consider it a backup peer if it has a keybag. + if (SOSPeerMapEntryIsBackup(mapEntry)) { + SOSPeerRef peer = SOSEngineCopyPeerWithMapEntry_locked(ewp->engine, peerID, mapEntry, NULL); + if (peer) { + ewp->with(peer); + CFRelease(peer); + } + } +} + +static void SOSEngineForEachBackupPeer_locked(SOSEngineRef engine, void (^with)(SOSPeerRef peer)) { + struct SOSEngineWithPeerContext ewp = { .engine = engine, .with = with }; + CFDictionaryRef peerMapCopy = CFDictionaryCreateCopy(NULL, engine->peerMap); + CFDictionaryApplyFunction(peerMapCopy, SOSEngineWithBackupPeerMapEntry_locked, &ewp); + CFRelease(peerMapCopy); +} + +// +// Manifest cache +// +SOSManifestRef SOSEngineGetManifestForDigest(SOSEngineRef engine, CFDataRef digest) { + if (!engine->manifestCache || !digest) return NULL; + SOSManifestRef manifest = (SOSManifestRef)CFDictionaryGetValue(engine->manifestCache, digest); + if (!manifest) return NULL; + if (CFGetTypeID(manifest) != SOSManifestGetTypeID()) { + secerror("dropping corrupt manifest for %@ from cache", digest); + CFDictionaryRemoveValue(engine->manifestCache, digest); + return NULL; + } + + return manifest; +} + +void SOSEngineAddManifest(SOSEngineRef engine, SOSManifestRef manifest) { + CFDataRef digest = SOSManifestGetDigest(manifest, NULL); + if (digest) { + if (!engine->manifestCache) + engine->manifestCache = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + CFDictionaryAddValue(engine->manifestCache, digest, manifest); + } +} + +CFDataRef SOSEnginePatchRecordAndCopyDigest(SOSEngineRef engine, SOSManifestRef base, SOSManifestRef removals, SOSManifestRef additions, CFErrorRef *error) { + CFDataRef digest = NULL; + SOSManifestRef manifest = SOSManifestCreateWithPatch(base, removals, additions, error); + if (manifest) { + SOSEngineAddManifest(engine, manifest); + digest = CFRetainSafe(SOSManifestGetDigest(manifest, NULL)); + } + CFReleaseSafe(manifest); + return digest; +} + +SOSManifestRef SOSEngineCopyPersistedManifest(SOSEngineRef engine, CFDictionaryRef persisted, CFStringRef key) { + return CFRetainSafe(SOSEngineGetManifestForDigest(engine, asData(CFDictionaryGetValue(persisted, key), NULL))); +} + +CFMutableArrayRef SOSEngineCopyPersistedManifestArray(SOSEngineRef engine, CFDictionaryRef persisted, CFStringRef key, CFErrorRef *error) { + CFMutableArrayRef manifests = NULL; + CFArrayRef digests = NULL; + CFDataRef digest; + if (asArrayOptional(CFDictionaryGetValue(persisted, key), &digests, error)) + manifests = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + if (digests) CFArrayForEachC(digests, digest) { + SOSManifestRef manifest = SOSEngineGetManifestForDigest(engine, digest); + if (manifest) + CFArrayAppendValue(manifests, manifest); + } + return manifests; +} + +static CFDictionaryRef SOSEngineCopyEncodedManifestCache_locked(SOSEngineRef engine, CFErrorRef *error) { + CFMutableDictionaryRef mfc = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + SOSEngineForEachPeer_locked(engine, ^(SOSPeerRef peer) { + SOSPeerAddManifestsInUse(peer, mfc); + }); + return mfc; +} + +#if 0 +static bool SOSEngineGCManifests_locked(SOSEngineRef engine, CFErrorRef *error) { + __block struct SOSDigestVector mdInCache = SOSDigestVectorInit; + __block struct SOSDigestVector mdInUse = SOSDigestVectorInit; + struct SOSDigestVector mdUnused = SOSDigestVectorInit; + struct SOSDigestVector mdMissing = SOSDigestVectorInit; + bool ok = true; + + SOSEngineForEachPeer_locked(engine, ^(SOSPeerRef peer) { + SOSPeerMarkDigestsInUse(peer, &mdInUse); + }); + + if (engine->manifestCache) { + CFDictionaryForEach(engine->manifestCache, ^(const void *key, const void *value) { + CFDataRef digest = (CFDataRef)key; + if (isData(digest)) + SOSDigestVectorAppend(&mdInCache, CFDataGetBytePtr(digest)); + }); + + // Delete unused manifests. + SOSDigestVectorDiff(&mdInCache, &mdInUse, &mdUnused, &mdMissing); + SOSManifestRef unused = SOSManifestCreateWithDigestVector(&mdUnused, NULL); + SOSManifestForEach(unused, ^(CFDataRef digest, bool *stop) { + if (digest) + CFDictionaryRemoveValue(engine->manifestCache, digest); + }); + CFReleaseSafe(unused); + } + + SOSDigestVectorFree(&mdInCache); + SOSDigestVectorFree(&mdInUse); + SOSDigestVectorFree(&mdUnused); + SOSDigestVectorFree(&mdMissing); + return ok; +} +#endif + +// +// End of Manifest cache +// + +static bool SOSEngineGCPeerState_locked(SOSEngineRef engine, CFErrorRef *error) { + bool ok = true; + + //require_quiet(ok = SOSEngineGCManifests_locked(engine, error), exit); + +//exit: + return ok; +} + +static CFMutableDictionaryRef SOSEngineCopyPeerState_locked(SOSEngineRef engine, CFErrorRef *error) { + CFMutableDictionaryRef peerState = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + CFDictionaryForEach(engine->peerMap, ^(const void *key, const void *value) { + CFDictionaryRef state = NULL; + if (value && CFGetTypeID(value) == SOSPeerGetTypeID()) { + CFErrorRef localError = NULL; + // Inflated peer + state = SOSPeerCopyState((SOSPeerRef)value, &localError); + if (!state) + secnotice("engine", "%@ failed to encode peer: %@", key, localError); + CFReleaseNull(localError); + // TODO: Potentially replace inflated peer with deflated peer in peerMap + } else if (value) { + // We have a deflated peer. + state = CFRetainSafe(value); + } + + if (state) { + CFDictionarySetValue(peerState, key, state); + CFReleaseSafe(state); + } + }); + return peerState; +} + +static CFDataRef SOSEngineCopyState(SOSEngineRef engine, CFErrorRef *error) { + CFDataRef der = NULL; + CFMutableDictionaryRef state = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + if (engine->myID) CFDictionarySetValue(state, kSOSEngineIDKey, engine->myID); + if (engine->peerIDs) CFDictionarySetValue(state, kSOSEnginePeerIDsKey, engine->peerIDs); + if (engine->lastTraceDate) CFDictionarySetValue(state, kSOSEngineTraceDateKey, engine->lastTraceDate); + CFTypeRef peerState = SOSEngineCopyPeerState_locked(engine, error); + if (peerState) CFDictionarySetValue(state, kSOSEnginePeerStateKey, peerState); + CFReleaseSafe(peerState); + CFDictionaryRef mfc = SOSEngineCopyEncodedManifestCache_locked(engine, error); + if (mfc) { + CFDictionarySetValue(state, kSOSEngineManifestCacheKey, mfc); + CFReleaseSafe(mfc); + } + der = CFPropertyListCreateDERData(kCFAllocatorDefault, state, error); + CFReleaseSafe(state); + secnotice("engine", "%@", engine); + return der; +} + +static bool SOSEngineSave(SOSEngineRef engine, SOSTransactionRef txn, CFErrorRef *error) { + CFDataRef derState = SOSEngineCopyState(engine, error); + bool ok = derState && SOSDataSourceSetStateWithKey(engine->dataSource, txn, kSOSEngineState, kSecAttrAccessibleAlways, derState, error); + CFReleaseSafe(derState); + return ok; +} + +static SOSManifestRef SOSEngineCreateManifestWithViewNameSet_locked(SOSEngineRef engine, CFSetRef viewNameSet, CFErrorRef *error) { + // TODO: Potentially tell all changeTrackers to track manifests ( //forall ct do SOSChangeTrackerSetConcrete(ct, true); + // and read the entire dataSource and pass all objects though the filter here, instead of + // forcing the datasource to be able to do "smart" queries + return SOSDataSourceCopyManifestWithViewNameSet(engine->dataSource, viewNameSet, error); +} + +static SOSChangeTrackerRef SOSEngineCopyChangeTrackerWithViewNameSet_locked(SOSEngineRef engine, CFSetRef viewNameSet, CFErrorRef *error) { + SOSChangeTrackerRef ct = (SOSChangeTrackerRef)CFDictionaryGetValue(engine->viewNameSet2ChangeTracker, viewNameSet); + if (!ct) + SOSErrorCreate(kSOSErrorPeerNotFound, error, NULL, CFSTR("no change tracker for view set %@"), viewNameSet); + return CFRetainSafe(ct); +} + +static SOSManifestRef SOSEngineCopyManifestWithViewNameSet_locked(SOSEngineRef engine, CFSetRef viewNameSet, CFErrorRef *error) { + SOSChangeTrackerRef ct = SOSEngineCopyChangeTrackerWithViewNameSet_locked(engine, viewNameSet, error); + if (!ct) + return NULL; + + SOSManifestRef manifest = SOSChangeTrackerCopyManifest(ct, NULL); + if (!manifest) { + manifest = SOSEngineCreateManifestWithViewNameSet_locked(engine, viewNameSet, error); // Do the SQL query + SOSChangeTrackerSetManifest(ct, manifest); + } + CFReleaseSafe(ct); + return manifest; +} + +SOSManifestRef SOSEngineCopyLocalPeerManifest_locked(SOSEngineRef engine, SOSPeerRef peer, CFErrorRef *error) { + return SOSEngineCopyManifestWithViewNameSet_locked(engine, SOSPeerGetViewNameSet(peer), error); +} + +#define withViewAndBackup(VIEW) do { with(VIEW); if (!isTomb) with(VIEW ## _tomb); } while(0) + + +// Invoke with once for each view an object is in. +// TODO: Move this function into the DataSource +static void SOSEngineObjectWithView(SOSEngineRef engine, SOSObjectRef object, void (^with)(CFStringRef view)) { + // Filter items into v0 only view here + SecDbItemRef item = (SecDbItemRef)object; // TODO: Layer violation, breaks tests + if (isDictionary(object)) { + CFTypeRef isTombValue = CFDictionaryGetValue((CFDictionaryRef)object, kSecAttrTombstone); + bool isTomb = isTombValue && CFBooleanGetValue(isTombValue); + // We are in the test just assume v0 and v2 views. + withViewAndBackup(kSOSViewKeychainV0); + } else if (SecDbItemIsSyncableOrCorrupted(item)) { + const SecDbClass *iclass = SecDbItemGetClass(item); + CFTypeRef pdmn = SecDbItemGetCachedValueWithName(item, kSecAttrAccessible); + if ((iclass == &genp_class || iclass == &inet_class || iclass == &keys_class || iclass == &cert_class) + && isString(pdmn) + && (CFEqual(pdmn, kSecAttrAccessibleWhenUnlocked) + || CFEqual(pdmn, kSecAttrAccessibleAfterFirstUnlock) + || CFEqual(pdmn, kSecAttrAccessibleAlways) + || CFEqual(pdmn, kSecAttrAccessibleWhenUnlockedThisDeviceOnly) + || CFEqual(pdmn, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) + || CFEqual(pdmn, kSecAttrAccessibleAlwaysThisDeviceOnly))) + { + CFTypeRef tomb = SecDbItemGetCachedValueWithName(item, kSecAttrTombstone); + char cvalue = 0; + bool isTomb = (isNumber(tomb) && CFNumberGetValue(tomb, kCFNumberCharType, &cvalue) && cvalue == 1); + CFTypeRef viewHint = SecDbItemGetCachedValueWithName(item, kSecAttrSyncViewHint); + if (viewHint == NULL) { + if (iclass == &cert_class) { + withViewAndBackup(kSOSViewOtherSyncable); + } else { + if (!SecDbItemGetCachedValueWithName(item, kSecAttrTokenID)) { + withViewAndBackup(kSOSViewKeychainV0); + } + CFTypeRef agrp = SecDbItemGetCachedValueWithName(item, kSecAttrAccessGroup); + if (iclass == &keys_class && CFEqualSafe(agrp, CFSTR("com.apple.security.sos"))) { + withViewAndBackup(kSOSViewiCloudIdentity); + } else if (CFEqualSafe(agrp, CFSTR("com.apple.cfnetwork"))) { + withViewAndBackup(kSOSViewAutofillPasswords); + } else if (CFEqualSafe(agrp, CFSTR("com.apple.safari.credit-cards"))) { + withViewAndBackup(kSOSViewSafariCreditCards); + } else if (iclass == &genp_class) { + if (CFEqualSafe(agrp, CFSTR("apple")) && + CFEqualSafe(SecDbItemGetCachedValueWithName(item, kSecAttrService), CFSTR("AirPort"))) { + withViewAndBackup(kSOSViewWiFi); + } else if (CFEqualSafe(agrp, CFSTR("com.apple.sbd"))) { + withViewAndBackup(kSOSViewBackupBagV0); + } else { + withViewAndBackup(kSOSViewOtherSyncable); // (genp) + } + } else { + withViewAndBackup(kSOSViewOtherSyncable); // (inet || keys) + } + } + } else { + with(viewHint); + if (!isTomb) { + CFStringRef viewHintTomb = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%@-tomb"), viewHint); + if (viewHintTomb) { + with(viewHintTomb); + CFRelease(viewHintTomb); + } + } + } + } + } else { + // TODO: general queries +#if 0 + SOSViewRef view; + CFArrayForEachC(engine->views, view) { + bool inView = SOSViewQueryMatchItem(view, item); + if (inView) { + CFStringRef viewName = SOSViewCopyName(view); + with(viewName); + CFReleaseSafe(viewName); + } + } +#endif + } +} + +// +// SOSChangeMapper - Helper for SOSEngineUpdateChanges_locked +// +struct SOSChangeMapper { + SOSEngineRef engine; + SOSTransactionRef txn; + SOSDataSourceTransactionPhase phase; + SOSDataSourceTransactionSource source; + CFMutableDictionaryRef ct2changes; +}; + +static void SOSChangeMapperInit(struct SOSChangeMapper *cm, SOSEngineRef engine, SOSTransactionRef txn, SOSDataSourceTransactionPhase phase, SOSDataSourceTransactionSource source) { + cm->engine = engine; + cm->txn = txn; + cm->phase = phase; + cm->source = source; + cm->ct2changes = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); +} + +static void SOSChangeMapperFree(struct SOSChangeMapper *cm) { + CFReleaseSafe(cm->ct2changes); +} + +static void SOSChangeMapperAppendObject(struct SOSChangeMapper *cm, SOSChangeTrackerRef ct, bool isAdd, CFTypeRef object) { + CFMutableArrayRef changes = (CFMutableArrayRef)CFDictionaryGetValue(cm->ct2changes, ct); + if (!changes) { + changes = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + CFDictionarySetValue(cm->ct2changes, ct, changes); + CFReleaseSafe(changes); + } + isAdd ? SOSChangesAppendAdd(changes, object) : SOSChangesAppendDelete(changes, object); +} + +static bool SOSChangeMapperIngestChange(struct SOSChangeMapper *cm, bool isAdd, CFTypeRef change) { + bool someoneCares = false; + if (isData(change)) { + // TODO: Reenable assertion once the tests have been updated + //assert(!isAdd); + // We got a digest for a deleted object. Our dataSource probably couldn't find + // an object with this digest, probably because it went missing, or it was + // discovered to be corrupted. + // Tell all our changeTrackers about this digest since we don't know who might need it. + CFDictionaryForEach(cm->engine->viewNameSet2ChangeTracker, ^(const void *viewNameSet, const void *ct) { + SOSChangeMapperAppendObject(cm, (SOSChangeTrackerRef)ct, isAdd, change); + }); + someoneCares = CFDictionaryGetCount(cm->engine->viewNameSet2ChangeTracker); + } else { + // We got an object let's figure out which views it's in and schedule it for + // delivery to all changeTrackers interested in any of those views. + SOSObjectRef object = (SOSObjectRef)change; + CFMutableSetRef changeTrackerSet = CFSetCreateMutableForCFTypes(kCFAllocatorDefault); + // First gather all the changeTrackers interested in this object (eleminating dupes by collecting them in a set) + SOSEngineObjectWithView(cm->engine, object, ^(CFStringRef viewName) { + const void *ctorset = CFDictionaryGetValue(cm->engine->viewName2ChangeTracker, viewName); + if (isSet(ctorset)) { + CFSetForEach((CFSetRef)ctorset, ^(const void *ct) { CFSetAddValue(changeTrackerSet, ct); }); + } else if (ctorset) { + CFSetAddValue(changeTrackerSet, ctorset); + } + }); + // Then append the object to the changes array in the ct2changes dictionary keyed by viewSet + CFSetForEach(changeTrackerSet, ^(const void *ct) { + SOSChangeMapperAppendObject(cm, (SOSChangeTrackerRef)ct, isAdd, object); + }); + someoneCares = CFSetGetCount(changeTrackerSet); + CFReleaseSafe(changeTrackerSet); + } + return someoneCares; +} + +static bool SOSChangeMapperSend(struct SOSChangeMapper *cm, CFErrorRef *error) { + __block bool ok = true; + CFDictionaryForEach(cm->ct2changes, ^(const void *ct, const void *changes) { + ok &= SOSChangeTrackerTrackChanges((SOSChangeTrackerRef)ct, cm->engine, cm->txn, cm->source, cm->phase, (CFArrayRef)changes, error); + }); + return ok; +} + +static bool SOSEngineUpdateChanges_locked(SOSEngineRef engine, SOSTransactionRef txn, SOSDataSourceTransactionPhase phase, SOSDataSourceTransactionSource source, CFArrayRef changes, CFErrorRef *error) +{ + secnoticeq("engine", "%@: %s %s %ld changes", engine->myID, phase == kSOSDataSourceTransactionWillCommit ? "will-commit" : phase == kSOSDataSourceTransactionDidCommit ? "did-commit" : "did-rollback", source == kSOSDataSourceSOSTransaction ? "sos" : "api", CFArrayGetCount(changes)); + bool ok = true; + switch (phase) { + case kSOSDataSourceTransactionDidRollback: + ok &= SOSEngineLoad(engine, error); + break; + case kSOSDataSourceTransactionWillCommit: + case kSOSDataSourceTransactionDidCommit: + { + struct SOSChangeMapper cm; + SOSChangeMapperInit(&cm, engine, txn, phase, source); + SecDbEventRef event; + CFArrayForEachC(changes, event) { + CFTypeRef deleted = NULL; + CFTypeRef inserted = NULL; + SecDbEventGetComponents(event, &deleted, &inserted, error); + if (deleted) + SOSChangeMapperIngestChange(&cm, false, deleted); + if (inserted) { + bool someoneCares = SOSChangeMapperIngestChange(&cm, true, inserted); + if (!someoneCares && !isData(inserted) && SecDbItemIsTombstone((SecDbItemRef)inserted) && !CFEqualSafe(SecDbItemGetValue((SecDbItemRef)inserted, &v7utomb, NULL), kCFBooleanTrue)) { + CFErrorRef localError = NULL; + // A tombstone was inserted but there is no changetracker that + // cares about it. + if (!SecDbItemDoDeleteSilently((SecDbItemRef)inserted, (SecDbConnectionRef)txn, &localError)) { + secerror("failed to delete tombstone %@ that no one cares about: %@", inserted, localError); + CFReleaseNull(localError); + } + } + } + } + + ok &= SOSChangeMapperSend(&cm, error); + SOSChangeMapperFree(&cm); + + if (phase == kSOSDataSourceTransactionDidCommit) { + // We are being called outside a transaction, beware, that any + // db changes we attempt to make here will cause deadlock! + } else { + // Write SOSEngine and SOSPeer state to disk + // TODO: Only do this if dirty + ok &= SOSEngineSave(engine, txn, error); + } + break; + } + } + return ok; +} + +static void SOSEngineSetNotifyPhaseBlock(SOSEngineRef engine) { + SOSDataSourceSetNotifyPhaseBlock(engine->dataSource, engine->queue, ^(SOSDataSourceRef ds, SOSTransactionRef txn, SOSDataSourceTransactionPhase phase, SOSDataSourceTransactionSource source, CFArrayRef changes) { + CFErrorRef localError = NULL; + if (!SOSEngineUpdateChanges_locked(engine, txn, phase, source, changes, &localError)) { + secerror("updateChanged failed: %@", localError); + } + CFReleaseSafe(localError); + }); +} + +#if 0 // TODO: update these checks +static void SOSEngineCircleChanged_sanitycheck(SOSEngineRef engine, CFStringRef myPeerID, CFArrayRef trustedPeers, CFArrayRef untrustedPeers) { + // Logging code + CFMutableArrayRef addedPeers = CFArrayCreateDifference(kCFAllocatorDefault, trustedPeers, engine->peerIDs); + CFMutableArrayRef deletedPeers = CFArrayCreateDifference(kCFAllocatorDefault, engine->peerIDs, trustedPeers); + CFMutableArrayRef addedUntrustedPeers = CFArrayCreateDifference(kCFAllocatorDefault, untrustedPeers, engine->peerIDs); + CFMutableArrayRef deletedUntrustedPeers = CFArrayCreateDifference(kCFAllocatorDefault, engine->peerIDs, untrustedPeers); + + CFStringRef tpDesc = SOSPeerIDArrayCreateString(trustedPeers); + CFStringRef apDesc = SOSPeerIDArrayCreateString(addedPeers); + CFStringRef dpDesc = SOSPeerIDArrayCreateString(deletedPeers); + CFStringRef aupDesc = SOSPeerIDArrayCreateString(addedUntrustedPeers); + CFStringRef dupDesc = SOSPeerIDArrayCreateString(deletedUntrustedPeers); + secnotice("engine", "trusted %@ added %@ removed %@ add ut: %@ rem ut: %@", tpDesc, apDesc, dpDesc, aupDesc, dupDesc); + CFReleaseSafe(dupDesc); + CFReleaseSafe(aupDesc); + CFReleaseSafe(dpDesc); + CFReleaseSafe(apDesc); + CFReleaseSafe(tpDesc); + + // Assertions: + // Ensure SOSAccount isn't giving us the runaround. + // Assert that trustedPeers, untrustedPeers and myPeerId are disjoint sets + if (trustedPeers) { + CFMutableArrayRef allTrustedPeers = CFArrayCreateDifference(kCFAllocatorDefault, trustedPeers, untrustedPeers); + assert(CFEqual(trustedPeers, allTrustedPeers)); + CFReleaseSafe(allTrustedPeers); + assert(!CFArrayContainsValue(trustedPeers, CFRangeMake(0, CFArrayGetCount(trustedPeers)), myPeerID)); + } + if (untrustedPeers) { + CFMutableArrayRef allUntrustedPeers = CFArrayCreateDifference(kCFAllocatorDefault, untrustedPeers, trustedPeers); + assert(CFEqual(untrustedPeers, allUntrustedPeers)); + CFReleaseSafe(allUntrustedPeers); + assert(!CFArrayContainsValue(untrustedPeers, CFRangeMake(0, CFArrayGetCount(trustedPeers)), myPeerID)); + } + + CFReleaseNull(deletedUntrustedPeers); + CFReleaseNull(addedUntrustedPeers); + CFReleaseNull(deletedPeers); + CFReleaseNull(addedPeers); + + // End of logging and asertions, actual code here. +} +#endif + +static SOSChangeTrackerRef SOSReferenceAndGetChangeTracker(CFDictionaryRef lookup, CFMutableDictionaryRef referenced, CFSetRef viewNameSet) { + SOSChangeTrackerRef ct = (SOSChangeTrackerRef)CFDictionaryGetValue(referenced, viewNameSet); + if (!ct) { + ct = (SOSChangeTrackerRef)CFDictionaryGetValue(lookup, viewNameSet); + if (ct) { + SOSChangeTrackerResetRegistration(ct); + CFDictionarySetValue(referenced, viewNameSet, ct); + } else { + ct = SOSChangeTrackerCreate(kCFAllocatorDefault, false, NULL, NULL); + CFDictionarySetValue(referenced, viewNameSet, ct); + CFReleaseSafe(ct); + } + } + return ct; +} + +static CFStringRef CFStringCreateWithViewNameSet(CFSetRef vns); + +static void CFStringAppendPeerIDAndViews(CFMutableStringRef desc, CFStringRef peerID, CFSetRef vns) { + if (vns) { + CFStringRef vnsDesc = CFStringCreateWithViewNameSet(vns); + CFStringAppendFormat(desc, NULL, CFSTR(" %@ (%@)"), peerID, vnsDesc); + CFReleaseSafe(vnsDesc); + } else { + CFStringAppendFormat(desc, NULL, CFSTR(" %@ NULL"), peerID); + } +} + +// Must be called after updating viewNameSet2ChangeTracker +static void SOSEngineUpdateViewName2ChangeTracker(SOSEngineRef engine) { + // Create the mapping from viewName -> ChangeTracker used for lookup during change notification + CFMutableDictionaryRef newViewName2ChangeTracker = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + CFDictionaryForEach(engine->viewNameSet2ChangeTracker, ^(const void *viewNameSet, const void *ct) { + CFSetForEach(viewNameSet, ^(const void *viewName) { + const void *ctorset = NULL; + if (CFDictionaryGetValueIfPresent(newViewName2ChangeTracker, viewName, &ctorset)) { + if (isSet(ctorset)) { + CFSetAddValue((CFMutableSetRef)ctorset, ct); + } else if (!CFEqual(ct, ctorset)) { + CFMutableSetRef set = CFSetCreateMutableForCFTypes(kCFAllocatorDefault); + CFSetAddValue(set, ctorset); + CFSetAddValue(set, ct); + CFDictionaryReplaceValue(newViewName2ChangeTracker, viewName, set); + CFRelease(set); + } + } else { + CFDictionarySetValue(newViewName2ChangeTracker, viewName, ct); + } + }); + }); + CFAssignRetained(engine->viewName2ChangeTracker, newViewName2ChangeTracker); +} + +static void SOSEngineSetBackupBag(SOSEngineRef engine, SOSObjectRef bagItem); + +// This is called only if we are in a circle and we should listen for keybag changes +static void SOSEngineRegisterBackupBagV0Tracker(SOSEngineRef engine, CFMutableDictionaryRef newViewNameSet2ChangeTracker, CFMutableStringRef desc) { + SOSChangeTrackerRef bbct = SOSReferenceAndGetChangeTracker(engine->viewNameSet2ChangeTracker, newViewNameSet2ChangeTracker, SOSViewsGetV0BackupBagViewSet()); + SOSChangeTrackerRegisterChangeUpdate(bbct, ^bool(SOSChangeTrackerRef ct, SOSEngineRef engine, SOSTransactionRef txn, SOSDataSourceTransactionSource source, SOSDataSourceTransactionPhase phase, CFArrayRef changes, CFErrorRef *error) { + SOSChangeRef change; + CFArrayForEachC(changes, change) { + CFTypeRef object = NULL; + bool isAdd = SOSChangeGetObject(change, &object); + SecDbItemRef dbi = (SecDbItemRef)object; + if (!isData(object) && + CFEqualSafe(SecDbItemGetCachedValueWithName(dbi, kSecAttrService), CFSTR("SecureBackupService")) && + CFEqualSafe(SecDbItemGetCachedValueWithName(dbi, kSecAttrAccessible), kSecAttrAccessibleWhenUnlocked) && + CFEqualSafe(SecDbItemGetCachedValueWithName(dbi, kSecAttrAccount), CFSTR("SecureBackupPublicKeybag"))) { + SOSEngineSetBackupBag(engine, isAdd ? (SOSObjectRef)object : NULL); + } + } + return true; + }); +} + +static void SOSEngineReferenceBackupPeer(SOSEngineRef engine, CFStringRef peerID, CFSetRef viewNameSet, CFDataRef keyBag, CFMutableDictionaryRef newViewNameSet2ChangeTracker, CFMutableDictionaryRef newPeerMap) { + CFTypeRef oldEntry = CFDictionaryGetValue(engine->peerMap, peerID); + CFTypeRef newEntry = SOSPeerOrStateSetViewsKeyBagAndCreateCopy(oldEntry, viewNameSet, keyBag); + if (newEntry) { + if (isDictionary(newEntry)) { + // Backup peers, are always inflated + CFAssignRetained(newEntry, SOSPeerCreateWithState(engine, peerID, newEntry, NULL)); + // If !oldEntry this is an edge (first creation of a peer). + if (!oldEntry) { + SOSPeerKeyBagDidChange((SOSPeerRef)newEntry); + } + } + CFDictionarySetValue(newPeerMap, peerID, newEntry); + CFRelease(newEntry); + + if (keyBag) { + SOSChangeTrackerRef ct = SOSReferenceAndGetChangeTracker(engine->viewNameSet2ChangeTracker, newViewNameSet2ChangeTracker, viewNameSet); + + SOSChangeTrackerUpdatesChanges child = Block_copy(^bool(SOSChangeTrackerRef ct, SOSEngineRef engine, SOSTransactionRef txn, SOSDataSourceTransactionSource source, SOSDataSourceTransactionPhase phase, CFArrayRef changes, CFErrorRef *error) { + return SOSPeerDataSourceWillChange((SOSPeerRef)newEntry, SOSEngineGetDataSource(engine), source, changes, error); + }); + + SOSChangeTrackerRegisterChangeUpdate(ct, child); + Block_release(child); + } + } +} + +static void SOSEngineReferenceSyncPeer(SOSEngineRef engine, CFStringRef peerID, CFSetRef viewNameSet, CFMutableDictionaryRef newViewNameSet2ChangeTracker, CFMutableDictionaryRef newPeerMap) { + CFTypeRef newEntry = SOSPeerOrStateSetViewsKeyBagAndCreateCopy(CFDictionaryGetValue(engine->peerMap, peerID), viewNameSet, NULL); + if (newEntry) { + SOSChangeTrackerRef ct = SOSReferenceAndGetChangeTracker(engine->viewNameSet2ChangeTracker, newViewNameSet2ChangeTracker, viewNameSet); + // Standard peer, inflated on demand + SOSChangeTrackerUpdatesManifests trackManifest; + if (isDictionary(newEntry)) { + // Uninflated peer, inflate on first notification. + trackManifest = ^bool(SOSChangeTrackerRef ct, SOSEngineRef engine, SOSTransactionRef txn, SOSDataSourceTransactionSource source, SOSDataSourceTransactionPhase phase, SOSManifestRef removals, SOSManifestRef additions, CFErrorRef *error) { + CFErrorRef localError = NULL; + SOSPeerRef peer = SOSEngineCopyPeerWithID_locked(engine, peerID, &localError); + bool ok; + if (!peer) { + secerror("%@: peer failed to inflate: %@", peerID, localError); + CFReleaseSafe(localError); + ok = false; + } else { + ok = SOSPeerDataSourceWillCommit(peer, source, removals, additions, error); + } + CFReleaseSafe(peer); + return ok; + }; + } else { + // Inflated peer, just forward the changes to the peer + trackManifest = ^bool(SOSChangeTrackerRef ct, SOSEngineRef engine, SOSTransactionRef txn, SOSDataSourceTransactionSource source, SOSDataSourceTransactionPhase phase, SOSManifestRef removals, SOSManifestRef additions, CFErrorRef *error) { + return SOSPeerDataSourceWillCommit((SOSPeerRef)newEntry, source, removals, additions, error); + }; + } + SOSChangeTrackerUpdatesManifests trackManifestCopy = Block_copy(trackManifest); + SOSChangeTrackerRegisterManifestUpdate(ct, trackManifestCopy); + Block_release(trackManifestCopy); + + CFDictionarySetValue(newPeerMap, peerID, newEntry); + CFRelease(newEntry); + } +} + + +static void SOSEngineReferenceTrustedPeer(SOSEngineRef engine, SOSPeerMetaRef peerMeta, CFMutableDictionaryRef newViewNameSet2ChangeTracker, CFMutableDictionaryRef newPeerMap, CFMutableArrayRef peerIDs, CFMutableStringRef desc) { + CFSetRef viewNameSet = NULL; + CFDataRef keyBag = NULL; + CFStringRef peerID = SOSPeerMetaGetComponents(peerMeta, &viewNameSet, &keyBag, NULL); + // We trust peerID so append it to peerIDs + CFArrayAppendValue(peerIDs, peerID); + if (desc) CFStringAppendPeerIDAndViews(desc, peerID, viewNameSet); + // Update the viewNameSet for this peer, to appease tests, default to a viewset of the V0 view. + if (!viewNameSet) + viewNameSet = SOSViewsGetV0ViewSet(); + + // Always inflate backup peers, since they need to register with their changeTrackers right away. + if (keyBag) { + SOSEngineReferenceBackupPeer(engine, peerID, viewNameSet, keyBag, newViewNameSet2ChangeTracker, newPeerMap); + } else { + SOSEngineReferenceSyncPeer(engine, peerID, viewNameSet, newViewNameSet2ChangeTracker, newPeerMap); + } +} + +static CFDataRef SOSEngineLoadV0KeyBag(SOSEngineRef engine, CFErrorRef *error) { + // Return the keybag for the given peerID. + /* + Values for V0 are: + kSecAttrAccessGroup ==> CFSTR("com.apple.sbd") + kSecAttrAccessible ==> kSecAttrAccessibleWhenUnlocked + kSecAttrAccount ==> CFSTR("SecureBackupPublicKeybag") + kSecAttrService ==> CFSTR("SecureBackupService") + */ + + CFMutableDictionaryRef keys = CFDictionaryCreateMutableForCFTypesWith(kCFAllocatorDefault, + kSecAttrAccessGroup, CFSTR("com.apple.sbd"), + kSecAttrAccount, CFSTR("SecureBackupPublicKeybag"), + kSecAttrService, CFSTR("SecureBackupService"), + kSecAttrAccessible, kSecAttrAccessibleWhenUnlocked, + kSecAttrSynchronizable, kCFBooleanTrue, + NULL); + + CFDataRef keybag = engine->dataSource->dsCopyItemDataWithKeys(engine->dataSource, keys, error); + CFReleaseSafe(keys); + + return keybag; +} + +static void SOSEngineReferenceBackupV0Peer(SOSEngineRef engine, CFMutableDictionaryRef newViewNameSet2ChangeTracker, CFMutableDictionaryRef newPeerMap, CFMutableArrayRef newPeerIDs, CFMutableStringRef desc) { + SOSPeerRef backupPeer = (SOSPeerRef)CFDictionaryGetValue(engine->peerMap, kSOSViewKeychainV0_tomb); + CFDataRef bag = NULL; + if (backupPeer && CFGetTypeID(backupPeer) == SOSPeerGetTypeID()) { + bag = SOSPeerGetKeyBag(backupPeer); + } else { + CFErrorRef localError = NULL; + if (!(bag = SOSEngineLoadV0KeyBag(engine, &localError))) { + secnotice("engine", "No keybag found for v0 backup peer: %@", localError); + CFReleaseSafe(localError); + } + } + SOSEngineReferenceBackupPeer(engine, kSOSViewKeychainV0_tomb, SOSViewsGetV0BackupViewSet(), bag, newViewNameSet2ChangeTracker, newPeerMap); +} + +static void SOSEngineReferenceTrustedPeers(SOSEngineRef engine, CFMutableDictionaryRef newViewNameSet2ChangeTracker, CFMutableDictionaryRef newPeerMap, CFMutableArrayRef newPeerIDs, CFArrayRef trustedPeerMetas, CFMutableStringRef desc) { + // Then update the views for all trusted peers and add them to newPeerMap. + if (trustedPeerMetas != NULL && CFArrayGetCount(trustedPeerMetas) != 0) { + if (desc) CFStringAppend(desc, CFSTR(" trusted")); + // Remake engine->peerIDs + SOSPeerMetaRef peerMeta; + CFArrayForEachC(trustedPeerMetas, peerMeta) { + SOSEngineReferenceTrustedPeer(engine, peerMeta, newViewNameSet2ChangeTracker, newPeerMap, newPeerIDs, desc); + } + } +} + +static void SOSEngineReferenceUntrustedPeers(SOSEngineRef engine, CFMutableDictionaryRef newPeerMap, CFArrayRef untrustedPeerMetas, CFMutableStringRef description) { + // Copy any untrustedPeers to newPeerMap as well if we have a state + // for them, if not no big deal. We also serialize all the untrustedPeers + // since they don't need to be deserializable + if (untrustedPeerMetas != NULL && CFArrayGetCount(untrustedPeerMetas) != 0) { + if (description) CFStringAppend(description, CFSTR(" untrusted")); + SOSPeerMetaRef peerMeta; + CFArrayForEachC(untrustedPeerMetas, peerMeta) { + CFSetRef views = NULL; + CFStringRef peerID = SOSPeerMetaGetComponents(peerMeta, &views, NULL, NULL); + if (description) CFStringAppendPeerIDAndViews(description, peerID, views); + CFSetRef nviews = NULL; + if (!views) + views = nviews = CFSetCreate(kCFAllocatorDefault, NULL, 0, &kCFTypeSetCallBacks); + CFTypeRef newEntry = SOSPeerOrStateSetViewsAndCopyState(CFDictionaryGetValue(engine->peerMap, peerID), views); + CFReleaseSafe(nviews); + if (newEntry) { + CFDictionarySetValue(newPeerMap, peerID, newEntry); + CFReleaseSafe(newEntry); + } + } + } +} + +static void SOSEngineReferenceChangeTrackers(SOSEngineRef engine, CFArrayRef trustedPeerMetas, CFArrayRef untrustedPeerMetas, CFMutableStringRef desc) { + CFMutableArrayRef newPeerIDs = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + CFMutableDictionaryRef newPeerMap = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + CFMutableDictionaryRef newViewNameSet2ChangeTracker = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + + if (engine->myID) { + // We have an engineID => in a circle (with 0 or more peers) + // Ensure we have a v0 backup peer and it's listening for backup bag changes + SOSEngineReferenceBackupV0Peer(engine, newViewNameSet2ChangeTracker, newPeerMap, newPeerIDs, desc); + SOSEngineRegisterBackupBagV0Tracker(engine, newViewNameSet2ChangeTracker, desc); + } + SOSEngineReferenceTrustedPeers(engine, newViewNameSet2ChangeTracker, newPeerMap, newPeerIDs, trustedPeerMetas, desc); + SOSEngineReferenceUntrustedPeers(engine, newPeerMap, untrustedPeerMetas, desc); + + CFAssignRetained(engine->peerIDs, newPeerIDs); + CFAssignRetained(engine->peerMap, newPeerMap); + CFAssignRetained(engine->viewNameSet2ChangeTracker, newViewNameSet2ChangeTracker); + SOSEngineUpdateViewName2ChangeTracker(engine); +} + +// Return true iff peers or views changed +static bool SOSEngineSetPeers_locked(SOSEngineRef engine, SOSPeerMetaRef myPeerMeta, CFArrayRef trustedPeerMetas, CFArrayRef untrustedPeerMetas) { + CFErrorRef error = NULL; + CFSetRef myViews = NULL; + CFDataRef myKeyBag = NULL; + CFMutableStringRef desc = CFStringCreateMutableCopy(kCFAllocatorDefault, 0, CFSTR("me")); + CFStringRef myPeerID = myPeerMeta ? SOSPeerMetaGetComponents(myPeerMeta, &myViews, &myKeyBag, &error) : NULL; + if (desc) CFStringAppendPeerIDAndViews(desc, myPeerID, myViews); + + CFRetainAssign(engine->myID, myPeerID); + + // Remake engine->peerMap from both trusted and untrusted peers + SOSEngineReferenceChangeTrackers(engine, trustedPeerMetas, untrustedPeerMetas, desc); + + secnotice("engine", "%@", desc); + CFReleaseSafe(desc); + return true; +} + +static void SOSEngineApplyPeerState(SOSEngineRef engine, CFDictionaryRef peerStateMap) { + if (peerStateMap) CFDictionaryForEach(peerStateMap, ^(const void *peerID, const void *peerState) { + CFTypeRef mapEntry = CFDictionaryGetValue(engine->peerMap, peerID); + if (mapEntry && CFGetTypeID(mapEntry) == SOSPeerGetTypeID()) { + // Update the state of any already inflated peers + SOSPeerRef peer = (SOSPeerRef)mapEntry; + CFErrorRef localError = NULL; + if (!SOSPeerSetState(peer, engine, peerState, &localError)) { + CFStringRef stateHex = NULL; + stateHex = CFDataCopyHexString(peerState); + secerror("peer: %@: bad state: %@ in engine state: %@", peerID, localError, stateHex); + CFReleaseSafe(stateHex); + CFReleaseNull(localError); + // Possibly ask for an ensurePeerRegistration so we have a good list or peers again. + } + } else { + // Just record the state for non inflated peers for now. + CFDictionarySetValue(engine->peerMap, peerID, peerState); + } + }); +} + +static void SOSEngineSynthesizePeerMetas(SOSEngineRef engine, CFMutableArrayRef trustedPeersMetas, CFMutableArrayRef untrustedPeers) { + CFSetRef trustedPeerSet = engine->peerIDs ? CFSetCreateCopyOfArrayForCFTypes(engine->peerIDs) : NULL; + CFDictionaryForEach(engine->peerMap, ^(const void *peerID, const void *peerState) { + SOSPeerMetaRef meta = NULL; + if (peerState && CFGetTypeID(peerState) == SOSPeerGetTypeID()) { + SOSPeerRef peer = (SOSPeerRef)peerState; + meta = SOSPeerMetaCreateWithComponents(peerID, SOSPeerGetViewNameSet(peer), SOSPeerGetKeyBag(peer)); + } else { + // We don't need to add the meta for the backup case, since + // SOSEngineReferenceBackupV0Peer will do the right thing + if (!CFEqualSafe(peerID, kSOSViewKeychainV0_tomb)) { + meta = SOSPeerMetaCreateWithState(peerID, peerState); + } + } + // Any peer in peerStateMap that is not in trustedPeers is an untrustedPeer unless it's the v0 backup peer + if ((trustedPeerSet && CFSetContainsValue(trustedPeerSet, peerID)) || CFEqualSafe(peerID, kSOSViewKeychainV0_tomb)) { + if (meta) { + CFArrayAppendValue(trustedPeersMetas, meta); + } + } else { + CFArrayAppendValue(untrustedPeers, peerID); + } + CFReleaseNull(meta); + }); + CFReleaseNull(trustedPeerSet); +} + +static void SOSEngineSetBackupBag(SOSEngineRef engine, SOSObjectRef bagItem) { + CFMutableStringRef desc = NULL; + SOSPeerRef backupPeer = SOSEngineCopyPeerWithID_locked(engine, kSOSViewKeychainV0_tomb, NULL); + CFDataRef keybag = NULL; + if (bagItem) { + keybag = SecDbItemGetValue((SecDbItemRef)bagItem, &v6v_Data, NULL); + } + + // Since SOSPeerSetKeyBag() doesn't notify on the edge from NULL->initial keybag, since + // that is the right behaviour for non v0 backup peers, we need to do it here for the v0 peer. + bool hadBag = SOSPeerGetKeyBag(backupPeer); + SOSPeerSetKeyBag(backupPeer, keybag); + if (!hadBag) + SOSPeerKeyBagDidChange(backupPeer); + + CFReleaseSafe(backupPeer); + + CFMutableArrayRef untrustedPeerMetas = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + CFMutableArrayRef trustedPeersMetas = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + SOSEngineSynthesizePeerMetas(engine, trustedPeersMetas, untrustedPeerMetas); + SOSEngineReferenceChangeTrackers(engine, trustedPeersMetas, untrustedPeerMetas, desc); + CFReleaseSafe(trustedPeersMetas); + CFReleaseSafe(untrustedPeerMetas); +} + +#define SECONDS_PER_DAY (86400.0) + +#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) +#define TRACE_INTERVAL (7 * SECONDS_PER_DAY) +#elif (TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) +#define TRACE_INTERVAL (1 * SECONDS_PER_DAY) +#endif + +#ifdef TRACE_INTERVAL +static void SOSEngineCloudKeychainTrace(SOSEngineRef engine, CFAbsoluteTime now) { + CFAssignRetained(engine->lastTraceDate, CFDateCreate(kCFAllocatorDefault, now)); + CFIndex num_peers = engine->peerIDs ? 1 + CFArrayGetCount(engine->peerIDs) : 1; + SOSManifestRef manifest = SOSEngineCopyManifestWithViewNameSet_locked(engine, SOSViewsGetV0ViewSet(), NULL); + if (!manifest) + manifest = SOSDataSourceCopyManifestWithViewNameSet(engine->dataSource, SOSViewsGetV0ViewSet(), NULL); + size_t num_items = SOSManifestGetCount(manifest); + CFReleaseSafe(manifest); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + CloudKeychainTrace(num_peers, num_items); + }); +} +#endif + +static void SOSEngineCloudKeychainTraceIfNeeded(SOSEngineRef engine) { +#ifdef TRACE_INTERVAL + if (!engine->myID) + return; + CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); + if (engine->lastTraceDate) { + CFAbsoluteTime lastTraceTime = CFDateGetAbsoluteTime(engine->lastTraceDate); + if ((now - lastTraceTime) >= TRACE_INTERVAL) { + SOSEngineCloudKeychainTrace(engine, now); + } + } else { + SOSEngineCloudKeychainTrace(engine, now); + } +#endif +} + +// Restore the in-memory state of engine from saved state loaded from the db +static bool SOSEngineSetState(SOSEngineRef engine, CFDataRef state, CFErrorRef *error) { + __block bool ok = true; + if (state) { + CFMutableDictionaryRef dict = NULL; + const uint8_t *der = CFDataGetBytePtr(state); + const uint8_t *der_end = der + CFDataGetLength(state); + ok = der = der_decode_dictionary(kCFAllocatorDefault, kCFPropertyListMutableContainers, (CFDictionaryRef *)&dict, error, der, der_end); + if (der && der != der_end) { + ok = SOSErrorCreate(kSOSErrorDecodeFailure, error, NULL, CFSTR("trailing %td bytes at end of state"), der_end - der); + } else if (ok) { + CFReleaseNull(engine->manifestCache); + CFMutableDictionaryRef mfc = (CFMutableDictionaryRef)CFDictionaryGetValue(dict, kSOSEngineManifestCacheKey); + if (mfc) { + engine->manifestCache = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + CFDictionaryForEach(mfc, ^(const void *key, const void *value) { + CFDataRef data = (CFDataRef)value; + if (isData(data)) { + SOSManifestRef mf = SOSManifestCreateWithData(data, NULL); + if (mf) + CFDictionarySetValue(engine->manifestCache, key, mf); + CFReleaseSafe(mf); + } + }); + } + CFMutableArrayRef untrustedPeers = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + CFMutableArrayRef trustedPeersMetas = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + CFRetainAssign(engine->peerIDs, asArray(CFDictionaryGetValue(dict, kSOSEnginePeerIDsKey), NULL)); + CFRetainAssign(engine->lastTraceDate, asDate(CFDictionaryGetValue(dict, kSOSEngineTraceDateKey), NULL)); + SOSEngineApplyPeerState(engine, asDictionary(CFDictionaryGetValue(dict, kSOSEnginePeerStateKey), NULL)); + SOSEngineSynthesizePeerMetas(engine, trustedPeersMetas, untrustedPeers); + SOSEngineSetPeers_locked(engine, (CFStringRef)CFDictionaryGetValue(dict, kSOSEngineIDKey), + trustedPeersMetas, untrustedPeers); + CFReleaseNull(trustedPeersMetas); + CFReleaseNull(untrustedPeers); + } + CFReleaseNull(dict); + } + secnotice("engine", "%@", engine); + return ok; +} + +static bool SOSEngineLoad(SOSEngineRef engine, CFErrorRef *error) { + CFDataRef state = SOSDataSourceCopyStateWithKey(engine->dataSource, kSOSEngineState, kSecAttrAccessibleAlways, error); + bool ok = state && SOSEngineSetState(engine, state, error); + CFReleaseSafe(state); + return ok; +} + +static CFStringRef accountStatusFileName = CFSTR("accountStatus.plist"); + +static bool SOSEngineCircleChanged_locked(SOSEngineRef engine, SOSPeerMetaRef myPeerMeta, CFArrayRef trustedPeers, CFArrayRef untrustedPeers) { + // Sanity check params +// SOSEngineCircleChanged_sanitycheck(engine, myPeerID, trustedPeers, untrustedPeers); + + // Transform from SOSPeerInfoRefs to CFDictionaries with the info we want per peer. + // Or, Tell the real SOSPeerRef what the SOSPeerInfoRef is and have it copy out the data it needs. + bool peersOrViewsChanged = SOSEngineSetPeers_locked(engine, myPeerMeta, trustedPeers, untrustedPeers); + + // Run though all peers and only cache manifests for peers we still have + CFErrorRef localError = NULL; + if (!SOSEngineGCPeerState_locked(engine, &localError)) { + secerror("SOSEngineGCPeerState_locked failed: %@", localError); + CFReleaseNull(localError); + } + return peersOrViewsChanged; +} + +// Initialize the engine if a load fails. Basically this is our first time setup +static bool SOSEngineInit(SOSEngineRef engine, CFErrorRef *error) { + bool ok = true; + secnotice("engine", "new engine for datasource named %@", SOSDataSourceGetName(engine->dataSource)); + CFAssignRetained(engine->peerMap, CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault)); + CFAssignRetained(engine->viewNameSet2ChangeTracker, CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault)); + CFAssignRetained(engine->viewName2ChangeTracker, CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault)); + CFReleaseNull(engine->manifestCache); + CFReleaseNull(engine->peerIDs); + // TODO: We shouldn't need to load the backup bag if there was no engine + // state (load failed), since that means there was no circle nor were we an applicant. + + // Set up change trackers so we know when a backup peer needs to be created? + // no, since myID is not set, we are not in a circle, so no need to back up + SOSEngineSetPeers_locked(engine, NULL, NULL, NULL); + return ok; +} + +// Called by our DataSource in its constructor +SOSEngineRef SOSEngineCreate(SOSDataSourceRef dataSource, CFErrorRef *error) { + SOSEngineRef engine = NULL; + engine = CFTypeAllocate(SOSEngine, struct __OpaqueSOSEngine, kCFAllocatorDefault); + engine->dataSource = dataSource; + engine->queue = dispatch_queue_create("engine", DISPATCH_QUEUE_SERIAL); + + engine->peerMap = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + engine->viewNameSet2ChangeTracker = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + engine->viewName2ChangeTracker = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + //engine->syncCompleteQueue = NULL; + engine->syncCompleteListeners = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + CFErrorRef engineError = NULL; + if (!SOSEngineLoad(engine, &engineError)) { + secwarning("engine failed load state starting with nothing %@", engineError); + CFReleaseNull(engineError); + if (!SOSEngineInit(engine, error)) { + secerror("engine failed to initialze %@ giving up", error ? *error : NULL); + } + } else { + // Successfully loaded engine state, let's trace if we haven't in a while + SOSEngineCloudKeychainTraceIfNeeded(engine); + } + SOSEngineSetNotifyPhaseBlock(engine); + return engine; +} + +// --- Called from off the queue, need to move to on the queue + +static void SOSEngineDoOnQueue(SOSEngineRef engine, dispatch_block_t action) +{ + dispatch_sync(engine->queue, action); +} + +static bool SOSEngineDoTxnOnQueue(SOSEngineRef engine, CFErrorRef *error, void(^transaction)(SOSTransactionRef txn, bool *commit)) +{ + return SOSDataSourceWith(engine->dataSource, error, ^(SOSTransactionRef txn, bool *commit) { + SOSEngineDoOnQueue(engine, ^{ transaction(txn, commit); }); + }); +} + +// +// MARK: SOSEngine API +// + +void SOSEngineDispose(SOSEngineRef engine) { + // NOOP Engines stick around forever to monitor dataSource changes. +} + +void SOSEngineForEachPeer(SOSEngineRef engine, void (^with)(SOSPeerRef peer)) { + SOSEngineDoOnQueue(engine, ^{ + SOSEngineForEachPeer_locked(engine, with); + }); +} + +static void SOSEngineForEachBackupPeer(SOSEngineRef engine, void (^with)(SOSPeerRef peer)) { + SOSEngineDoOnQueue(engine, ^{ + SOSEngineForEachBackupPeer_locked(engine, with); + }); +} + + +/* Handle incoming message from peer p. Return false if there was an error, true otherwise. */ +bool SOSEngineHandleMessage_locked(SOSEngineRef engine, CFStringRef peerID, SOSMessageRef message, + SOSTransactionRef txn, bool *commit, bool *somethingChanged, CFErrorRef *error) { + SOSPeerRef peer = SOSEngineCopyPeerWithID_locked(engine, peerID, error); + if (!peer) return false; + CFStringRef peerDesc = NULL; + SOSManifestRef localManifest = NULL; + SOSManifestRef allAdditions = NULL; + SOSManifestRef unwanted = NULL; + SOSManifestRef confirmed = NULL; + SOSManifestRef base = NULL; + SOSManifestRef confirmedRemovals = NULL, confirmedAdditions = NULL; + __block struct SOSDigestVector receivedObjects = SOSDigestVectorInit; + __block struct SOSDigestVector unwantedObjects = SOSDigestVectorInit; + + // Check for unknown criticial extensions in the message, and handle + // any other extensions we support + __block bool ok = true; + CFMutableArrayRef changes = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + + require_action_quiet(peer, exit, ok = SOSErrorCreate(errSecParam, error, NULL, CFSTR("Couldn't create peer with Engine for %@"), peerID)); + peerDesc = CFCopyDescription(peer); + + SOSMessageWithExtensions(message, true, ^(CFDataRef oid, bool isCritical, CFDataRef extension, bool *stop) { + // OMFG a Critical extension what shall I do! + ok = SOSErrorCreate(kSOSErrorNotReady, error, NULL, CFSTR("Unknown criticial extension in peer message")); + *stop = true; + }); + require_quiet(ok, exit); + + // Merge Objects from the message into our DataSource. + // Should we move the transaction to the SOSAccount level? + // TODO: Filter incoming objects + //if (!SOSDataSourceForEachObjectInViewSet(engine->dataSource, pendingObjects, SOSPeerGetViewNameSet(peer), error, ^void(CFDataRef key, SOSObjectRef object, bool *stop) { + require_quiet(ok &= SOSMessageWithSOSObjects(message, engine->dataSource, error, ^(SOSObjectRef peersObject, bool *stop) { + CFDataRef digest = SOSObjectCopyDigest(engine->dataSource, peersObject, error); + if (!digest) { + *stop = true; + *commit = false; + secerror("%@ peer sent bad object: %@, rolling back changes", SOSPeerGetID(peer), error ? *error : NULL); + return; + } + SOSDigestVectorAppend(&receivedObjects, CFDataGetBytePtr(digest)); + SOSObjectRef mergedObject = NULL; + SOSMergeResult mr = SOSDataSourceMergeObject(engine->dataSource, txn, peersObject, &mergedObject, error); + // TODO: If the mr is kSOSMergeLocalObject most of the time (or all of the time), + // consider asking the peer to stop sending us objects, and send it objects instead. + ok &= (mr != kSOSMergeFailure); + if (!ok) { + *stop = true; + *commit = false; + // TODO: Might want to change to warning since the race of us locking after ckd sends us a message could cause db locked errors here. + secerror("%@ SOSDataSourceMergeObject failed %@ rolling back changes", SOSPeerGetID(peer), error ? *error : NULL); + } else if (mr==kSOSMergePeersObject || mr==kSOSMergeCreatedObject) { + *somethingChanged = true; + } else { + // mr == kSOSMergeLocalObject + if (!CFEqual(mergedObject, peersObject)) { + // Record this object as something we don't want peer to ever send us again. By adding it to + // unwantedObjects we'll falsely claim to peer we have it until they tell us they don't have it anymore. + SOSDigestVectorAppend(&unwantedObjects, CFDataGetBytePtr(digest)); + } + // Ensure localObject is in local manifest (possible corruption) by posting an update when we are done. + SOSChangesAppendAdd(changes, mergedObject); + } + CFReleaseSafe(mergedObject); + CFReleaseSafe(digest); + }), exit); + struct SOSDigestVector dvunion = SOSDigestVectorInit; + SOSDigestVectorSort(&receivedObjects); + SOSDigestVectorUnionSorted(SOSManifestGetDigestVector(SOSMessageGetAdditions(message)), &receivedObjects, &dvunion); + allAdditions = SOSManifestCreateWithDigestVector(&dvunion, error); + SOSDigestVectorFree(&receivedObjects); + SOSDigestVectorFree(&dvunion); + + unwanted = SOSManifestCreateWithDigestVector(&unwantedObjects, error); + SOSDigestVectorFree(&unwantedObjects); + + if (CFArrayGetCount(changes)) { + // NOTE: This is always notifiying of all additions that end up choosing local, which should be rare, since we shouldn't + // be receiving objects we already have. When we do we tell ourselves to add them all again so our views will properly + // reflect that we actually have these objects if we didn't already. + + // Ensure any objects that we received and have locally already are actually in our local manifest + SOSEngineUpdateChanges_locked(engine, NULL, kSOSDataSourceTransactionDidCommit, kSOSDataSourceSOSTransaction, changes, error); + } + CFReleaseSafe(changes); + + // ---- Don't use local or peer manifests from above this line, + // ---- since commiting the SOSDataSourceWith transaction might change them --- + + // Take a snapshot of our dataSource's local manifest. + require_quiet(ok = localManifest = SOSEngineCopyLocalPeerManifest_locked(engine, peer, error), exit); + + CFDataRef baseDigest = SOSMessageGetBaseDigest(message); + CFDataRef proposedDigest = SOSMessageGetProposedDigest(message); + +#if 0 + // I believe this is no longer needed now that we have eliminated extra, + // since this is handled below once we get a confirmed manifest from our + // peer. + + // If we just received a L00 reset pendingObjects to localManifest + if (!baseDigest && !proposedDigest) { + // TODO: This is definitely busted for v0 peers since v0 peers always send a + // L00 (ManifestDigestMessage as an ack) whereas in v2 this is a protocol restart + // However if we can still find a confirmed manifest below we probably + // don't want to do this even for v2. + // Also I don't think we will ever send a ManifestMessage right now in + // response to a ManifestDigest + SOSPeerSetPendingObjects(peer, localManifest); + secnoticeq("engine", "%@:%@ SOSPeerSetPendingObjects: %@", engine->myID, peerID, localManifest); + } +#endif + + base = SOSPeerCopyManifestForDigest(peer, baseDigest); + confirmed = SOSPeerCopyManifestForDigest(peer, SOSMessageGetSenderDigest(message)); + if (!confirmed) { + if (SOSManifestGetCount(SOSMessageGetRemovals(message)) || SOSManifestGetCount(allAdditions)) { + if (base || !baseDigest) { + confirmed = SOSManifestCreateWithPatch(base, SOSMessageGetRemovals(message), allAdditions, error); + } + if (!confirmed) { + confirmedRemovals = CFRetainSafe(SOSMessageGetRemovals(message)); + confirmedAdditions = CFRetainSafe(allAdditions); + } + } else if (baseDigest) { + confirmed = CFRetainSafe(base); + secerror("%@:%@ Protocol error send L00 - figure out later base: %@", engine->myID, peerID, base); + } + } + secnoticeq("engine", "%@:%@ confirmed: %@ base: %@", engine->myID, peerID, confirmed, base); + if (confirmed) { + ok &= SOSManifestDiff(SOSPeerGetConfirmedManifest(peer), confirmed, &confirmedRemovals, &confirmedAdditions, error); + if (SOSManifestGetCount(SOSMessageGetRemovals(message))) + CFAssignRetained(confirmedRemovals, SOSManifestCreateUnion(confirmedRemovals, SOSMessageGetRemovals(message), error)); + } + if (SOSManifestGetCount(confirmedRemovals) || SOSManifestGetCount(confirmedAdditions) || SOSManifestGetCount(unwanted)) + ok &= SOSPeerDidReceiveRemovalsAndAdditions(peer, confirmedRemovals, confirmedAdditions, unwanted, localManifest, error); + // TODO: We should probably remove the if below and always call SOSPeerSetConfirmedManifest, + // since having a NULL confirmed will force us to send a manifest message to get in sync again. + if (confirmed) + SOSPeerSetConfirmedManifest(peer, confirmed); + else if (SOSPeerGetConfirmedManifest(peer)) { + secnoticeq("engine", "%@:%@ unable to find confirmed in %@, sync protocol reset", engine->myID, peer, message); + + SOSPeerSetConfirmedManifest(peer, NULL); + //SOSPeerSetSendObjects(peer, true); + } + + // ---- SendObjects and extra->pendingObjects promotion dance ---- + + // The first block of code below sets peer.sendObjects to true when we receive a L00 and the second block + // moves extra to pendingObjects once we receive a confirmed manifest in or after the L00. + if (!baseDigest && !proposedDigest) { + SOSPeerSetSendObjects(peer, true); + } + + if (0 /* confirmed && SOSPeerSendObjects(peer) */) { + SOSManifestRef allExtra = NULL; + ok &= SOSManifestDiff(confirmed, localManifest, NULL, &allExtra, error); + secnoticeq("engine", "%@:%@ confirmed %@ (re)setting O:%@", engine->myID, SOSPeerGetID(peer), confirmed, allExtra); + SOSPeerSetPendingObjects(peer, allExtra); + CFReleaseSafe(allExtra); + } + +exit: + secinfo("engine", "recv %@:%@ %@", engine->myID, SOSPeerGetID(peer), message); + secinfo("peer", "recv %@ -> %@", peerDesc, peer); + + CFReleaseNull(base); + CFReleaseSafe(confirmed); + CFReleaseSafe(localManifest); + CFReleaseSafe(peerDesc); + CFReleaseSafe(allAdditions); + CFReleaseSafe(unwanted); + CFReleaseSafe(confirmedRemovals); + CFReleaseSafe(confirmedAdditions); + CFReleaseSafe(peer); + return ok; +} + +static CFDataRef SOSEngineCopyObjectDER(SOSEngineRef engine, SOSObjectRef object, CFErrorRef *error) { + CFDataRef der = NULL; + CFDictionaryRef plist = SOSObjectCopyPropertyList(engine->dataSource, object, error); + if (plist) { + der = CFPropertyListCreateDERData(kCFAllocatorDefault, plist, error); + CFRelease(plist); + } + return der; +} + + +/* + + +-----------------------------+_ + | | | \ + | A | T | \ + | | | \ + _+=============================+ } L + / | | / + / | S | / + / | |_/ + / +============================== + / | | + C { | | + \ | M +------------| + \ | | | + \ | | U | + \ | | | + \_+-------------+---------------+ + +A assumed +T to be sent +S shared +M missing +U unwanted +L local +C confirmed + +*/ +#if 0 +static bool SOSAppendRemoveToPatch(CFTypeRef remove, CFMutableDictionaryRef patch, CFErrorRef *error) { +} + +static bool SOSAppendAddToPatch(CFTypeRef add, CFMutableDictionaryRef patch, CFErrorRef *error) { +} + +static bool SOSAppendDiffToPatch(CFTypeRef left, CFTypeRef right, CFMutableDictionaryRef patch, CFErrorRef *error) { + bool ok = true; + if (!left && right) { + SOSAppendAddToPatch(right, patch, error); + } else if (left && !right) { + SOSAppendRemoveToPatch(left, patch, error); + } else if (left && right) { + CFTypeID ltype = CFGetTypeID(left); + CFTypeID rtype = CFGetTypeID(right); + if (ltype == rtype) { + if (CFArrayGetTypeID() == ltype) { + ok = SecError(errSecParam, error, CFSTR("unsupported type array"), ltype); + } else if (CFBooleanGetTypeID == ltype) { + ok = SecError(errSecParam, error, CFSTR("unsupported type boolean"), ltype); + } else if (CFDataGetTypeID == ltype) { + ok = SecError(errSecParam, error, CFSTR("unsupported type data"), ltype); + } else if (CFDictionaryGetTypeID == ltype) { + __block CFMutableDictionaryRef leftnotright = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + __block CFMutableDictionaryRef rightnotleft = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, right); + + CFDictionaryForEach(left, ^(const void *key, const void *lvalue) { + const void *rvalue = NULL; + if (CFDictionaryGetValueIfPresent(right, key, &rvalue)) { + CFDictionaryRemoveValue(rightnotleft, key); + + CFMutableDictionaryRef subpatch = CFDictionaryCreateForCFTypes(kCFAllocatorDefault); + CFDictionaryAddValue(patch, key, subpatch); + SOSAppendDiffToPatch(lvalue, rvalue, subpatch, error); + CFReleaseSafe(subpatch); + } else { + CFDictionaryAddValue(leftnotright, key, lvalue); + } + }); + // Proccess leftnotright and rightnotleft + CFReleaseSafe(leftnotright); + CFReleaseSafe(rightnotleft); + } else if (SOSManifestGetTypeID == ltype) { + SOSManifestRef removed = NULL, added = NULL; + ok &= SOSManifestDiff(left, right, &removed, &added, error); + if (SOSManifestGetCount(removed) || SOSManifestGetCount(added)) { + SOSAppendDiffToPatch(lvalue, rvalue, subpatch, error); + CFStringAppend(, <#CFStringRef appendedString#>) + } + CFReleaseSafe(removed); + CFReleaseSafe(added); + } else if (CFNumberGetTypeID == ltype) { + ok = SecError(errSecParam, error, CFSTR("unsupported type number"), ltype); + } else if (CFSetGetTypeID == ltype) { + ok = SecError(errSecParam, error, CFSTR("unsupported type set"), ltype); + } else if (CFStringGetTypeID == ltype) { + ok = SecError(errSecParam, error, CFSTR("unsupported type string"), ltype); + } else { + ok = SecError(errSecParam, error, CFSTR("unknown type %lu"), ltype); + } + } + } else if (!left && !right) { + // NOOP + } +} +#endif + +static __unused bool SOSEngineCheckPeerIntegrity(SOSEngineRef engine, SOSPeerRef peer, CFErrorRef *error) { +#if 0 + //static CFMutableDictionaryRef p2amtu; + if (!engine->p2amtu) + engine->p2amtu = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault); + CFDictionaryRef amtu = CFDictionaryGetValue(engine->p2amtu, SOSPeerGetID(peer)); +#endif + + // Inputs + SOSManifestRef L = SOSEngineCopyLocalPeerManifest_locked(engine, peer, error); + SOSManifestRef T = SOSPeerGetPendingObjects(peer); + SOSManifestRef C = SOSPeerGetConfirmedManifest(peer); + SOSManifestRef U = SOSPeerGetUnwantedManifest(peer); + + // Computed + SOSManifestRef CunionU = SOSManifestCreateUnion(C, U, error); + SOSManifestRef S = SOSManifestCreateIntersection(L, CunionU, error); + + SOSManifestRef AunionT = NULL, MunionU = NULL; + SOSManifestDiff(L, C, &AunionT, &MunionU, error); + + SOSManifestRef A = SOSManifestCreateComplement(T, AunionT, error); + SOSManifestRef M = SOSManifestCreateComplement(U, MunionU, error); + + SOSManifestRef SunionAunionT = SOSManifestCreateUnion(S, AunionT, error); + SOSManifestRef SunionMunionU = SOSManifestCreateUnion(S, MunionU, error); + + SOSManifestRef AintersectM = SOSManifestCreateIntersection(A, M, error); + SOSManifestRef AintersectS = SOSManifestCreateIntersection(A, S, error); + SOSManifestRef AintersectT = SOSManifestCreateIntersection(A, T, error); + SOSManifestRef AintersectU = SOSManifestCreateIntersection(A, U, error); + SOSManifestRef MintersectS = SOSManifestCreateIntersection(M, S, error); + SOSManifestRef MintersectT = SOSManifestCreateIntersection(M, T, error); + SOSManifestRef MintersectU = SOSManifestCreateIntersection(M, U, error); + SOSManifestRef SintersectT = SOSManifestCreateIntersection(S, T, error); + SOSManifestRef SintersectU = SOSManifestCreateIntersection(S, U, error); + SOSManifestRef TintersectU = SOSManifestCreateIntersection(T, U, error); + +#if 0 + CFDictionaryRef newAmtu = CFDictionaryCreateForCFTypes(kCFAllocatorDefault, CFSTR("A"), A, CFSTR("M"), M, CFSTR("T"), T, CFSTR("U") U, NULL); + CFDictionarySetValue(engine->p2amtu, SOSPeerGetID(peer), newAmtu); + CFMutableStringRef amtuChanges = CFStringCreateMutable(kCFAllocatorDefault, 0); + SOSAppendDiffToString(amtu, newAmtu, amtuChanges); + secnotice("engine", "%@: %@", SOSPeerGetID(peer), amtuChanges); +#endif + +#define SOSASSERT(e) (__builtin_expect(!(e), 0) ? secnotice("engine", "state-assertion %s", #e), assert(e) : (void)0) + + SOSASSERT(L ? CFEqual(L, SunionAunionT) : SOSManifestGetCount(SunionAunionT) == 0); + SOSASSERT(C ? CFEqual(C, SunionMunionU) : SOSManifestGetCount(SunionMunionU) == 0); + + SOSASSERT(SOSManifestGetCount(AintersectM) == 0); + SOSASSERT(SOSManifestGetCount(AintersectS) == 0); + SOSASSERT(SOSManifestGetCount(AintersectT) == 0); + SOSASSERT(SOSManifestGetCount(AintersectU) == 0); + SOSASSERT(SOSManifestGetCount(MintersectS) == 0); + SOSASSERT(SOSManifestGetCount(MintersectT) == 0); + SOSASSERT(SOSManifestGetCount(MintersectU) == 0); + SOSASSERT(SOSManifestGetCount(SintersectT) == 0); + SOSASSERT(SOSManifestGetCount(SintersectU) == 0); + SOSASSERT(SOSManifestGetCount(TintersectU) == 0); + + CFReleaseSafe(AintersectM); + CFReleaseSafe(AintersectS); + CFReleaseSafe(AintersectT); + CFReleaseSafe(AintersectU); + CFReleaseSafe(MintersectS); + CFReleaseSafe(MintersectT); + CFReleaseSafe(MintersectU); + CFReleaseSafe(SintersectT); + CFReleaseSafe(SintersectU); + CFReleaseSafe(TintersectU); + + CFReleaseSafe(AunionT); + CFReleaseSafe(MunionU); + + + CFReleaseSafe(A); + CFReleaseSafe(M); + CFReleaseSafe(S); + //CFReleaseSafe(T); // Get + //CFReleaseSafe(U); // Get + //CFReleaseSafe(C); // Get + CFReleaseSafe(L); + return true; +} + +void SOSEngineSetSyncCompleteListener(SOSEngineRef engine, CFStringRef peerID, dispatch_block_t notify_block) { + dispatch_block_t copy = Block_copy(notify_block); + SOSEngineDoOnQueue(engine, ^{ + if (notify_block) { + CFDictionarySetValue(engine->syncCompleteListeners, peerID, copy); + } else { + CFDictionaryRemoveValue(engine->syncCompleteListeners, peerID); + } + }); + CFReleaseNull(copy); +} + +void SOSEngineSetSyncCompleteListenerQueue(SOSEngineRef engine, dispatch_queue_t notify_queue) { + SOSEngineDoOnQueue(engine, ^{ + engine->syncCompleteQueue = notify_queue; + }); +} + +static void SOSEngineCompletedSyncWithPeer(SOSEngineRef engine, SOSPeerRef peer) { + dispatch_block_t notify = CFDictionaryGetValue(engine->syncCompleteListeners, SOSPeerGetID(peer)); + if (notify && engine->syncCompleteQueue) + dispatch_async(engine->syncCompleteQueue, notify); + + // Delete dictionary entry? +} + + +CFDataRef SOSEngineCreateMessage_locked(SOSEngineRef engine, SOSPeerRef peer, + CFErrorRef *error, SOSEnginePeerMessageSentBlock *sent) { + SOSManifestRef local = SOSEngineCopyLocalPeerManifest_locked(engine, peer, error); + __block SOSMessageRef message = SOSMessageCreate(kCFAllocatorDefault, SOSPeerGetMessageVersion(peer), error); + SOSManifestRef confirmed = SOSPeerGetConfirmedManifest(peer); + SOSManifestRef pendingObjects = SOSPeerGetPendingObjects(peer); + SOSManifestRef objectsSent = NULL; + SOSManifestRef proposed = NULL; + SOSManifestRef allMissing = NULL; + SOSManifestRef allExtra = NULL; + SOSManifestRef extra = NULL; + SOSManifestRef excessPending = NULL; + SOSManifestRef missing = NULL; + SOSManifestRef unwanted = SOSPeerGetUnwantedManifest(peer); + SOSManifestRef excessUnwanted = NULL; + CFDataRef result = NULL; + + // Given (C, L, T, U) compute (T, U, M, A) + // (C \ L) \ U => M + // (L \ C) \ T => A + // we also compute + // U \ (C \ L) => EU + // T \ (L \ C) => ET + // And assert that both EU and ET are empty and if not remove them from U and T respectively + SOSManifestDiff(confirmed, local, &allMissing, &allExtra, error); + SOSManifestDiff(allExtra, pendingObjects, &extra, &excessPending, error); + if (SOSManifestGetCount(excessPending)) { + // T \ (L \ C) => excessPending (items both in L and C or in neither that are still pending) + // Can only happen if a member of T was removed from L without us having a chance to update T + secerror("%@ ASSERTION FAILURE purging excess pendingObjects: %@", peer, excessPending); + SOSManifestRef newPendingObjects = SOSManifestCreateComplement(excessPending, pendingObjects, error); + SOSPeerSetPendingObjects(peer, newPendingObjects); + CFReleaseSafe(newPendingObjects); + pendingObjects = SOSPeerGetPendingObjects(peer); + } + SOSManifestDiff(allMissing, unwanted, &missing, &excessUnwanted, error); + if (SOSManifestGetCount(excessUnwanted)) { + // U \ (C \ L) => excessUnwanted (items both in L and C or in neither that are still unwanted) + // Can only happen if a member of U was added to L without us having a chance to update U. + // Since U only contains items the conflict resolver rejected, this implies L somehow got rolled back + // The other option (and more likely) is a member of U was removed from C and not from U. + secerror("%@ ASSERTION FAILURE purging excess unwanted: %@", peer, excessUnwanted); + SOSManifestRef newUnwanted = SOSManifestCreateComplement(excessUnwanted, unwanted, error); + SOSPeerSetUnwantedManifest(peer, newUnwanted); + CFReleaseSafe(newUnwanted); + unwanted = SOSPeerGetUnwantedManifest(peer); + } + + CFReleaseNull(allExtra); + CFReleaseNull(excessPending); + CFReleaseNull(allMissing); + CFReleaseNull(excessUnwanted); + + secnoticeq("engine", "%@:%@: send state for peer [%s%s%s][%s%s] P:%zu, E:%zu, M:%zu U:%zu", engine->myID, SOSPeerGetID(peer), + local ? "L":"l", + confirmed ? "C":"0", + pendingObjects ? "P":"0", + SOSPeerSendObjects(peer) ? "O":"o", + SOSPeerMustSendMessage(peer) ? "S":"s", + SOSManifestGetCount(pendingObjects), + SOSManifestGetCount(extra), + SOSManifestGetCount(missing), + SOSManifestGetCount(unwanted) + ); + + if (confirmed) { + // TODO: Because of not letting things terminate while we have extra left + // we might send objects when we didn't need to, but there is always an + // extra roundtrip required for objects that we assume the other peer + // should have already. + // TODO: If there are extra objects left, calling this function is not + // idempotent we should check if pending is what we are about to send and not send anything in this case. + if (SOSManifestGetCount(pendingObjects) == 0 && SOSManifestGetCount(extra) == 0) + SOSPeerSetSendObjects(peer, false); + + if (CFEqualSafe(local, SOSPeerGetProposedManifest(peer)) && !SOSPeerMustSendMessage(peer)) { + bool send = false; + if (CFEqual(confirmed, local)) { + SOSEngineCompletedSyncWithPeer(engine, peer); + secnoticeq("engine", "synced %@:%@", engine->myID, peer); + } else if (SOSManifestGetCount(pendingObjects) == 0 /* TODO: No entries moved from extra to pendingObjects. */ + && SOSManifestGetCount(missing) == 0) { + secnoticeq("engine", "waiting %@:%@ extra: %@", engine->myID, peer, extra); + } else { + send = true; + } + if (!send) { + CFReleaseSafe(local); + CFReleaseSafe(message); + CFReleaseNull(extra); + CFReleaseNull(missing); + return CFDataCreate(kCFAllocatorDefault, NULL, 0); + } + } + + if (SOSManifestGetCount(pendingObjects)) { + // If we have additions and we need to send objects, do so. + __block size_t objectsSize = 0; + __block struct SOSDigestVector dv = SOSDigestVectorInit; + CFMutableArrayRef changes = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + if (!SOSDataSourceForEachObject(engine->dataSource, pendingObjects, error, ^void(CFDataRef key, SOSObjectRef object, bool *stop) { + CFErrorRef localError = NULL; + CFDataRef digest = NULL; + CFDataRef der = NULL; + if (!object) { + const uint8_t *d = CFDataGetBytePtr(key); + secnoticeq("engine", "%@:%@ object %02X%02X%02X%02X dropping from manifest: not found in datasource", + engine->myID, SOSPeerGetID(peer), d[0], d[1], d[2], d[3]); + SOSChangesAppendDelete(changes, key); + } else if (!(der = SOSEngineCopyObjectDER(engine, object, &localError)) + || !(digest = SOSObjectCopyDigest(engine->dataSource, object, &localError))) { + if (SecErrorGetOSStatus(localError) == errSecDecode) { + // Decode error, we need to drop these objects from our manifests + const uint8_t *d = CFDataGetBytePtr(key); + secnoticeq("engine", "%@:%@ object %02X%02X%02X%02X dropping from manifest: %@", + engine->myID, SOSPeerGetID(peer), d[0], d[1], d[2], d[3], localError); + SOSChangesAppendDelete(changes, key); + CFRelease(localError); + } else { + // Stop iterating and propagate out all other errors. + const uint8_t *d = CFDataGetBytePtr(key); + secnoticeq("engine", "%@:%@ object %02X%02X%02X%02X in SOSDataSourceForEachObject: %@", + engine->myID, SOSPeerGetID(peer), d[0], d[1], d[2], d[3], localError); + *stop = true; + CFErrorPropagate(localError, error); + CFReleaseNull(message); + } + } else { + if (!CFEqual(key, digest)) { + const uint8_t *d = CFDataGetBytePtr(key); + const uint8_t *e = CFDataGetBytePtr(digest); + secnoticeq("engine", "%@:%@ object %02X%02X%02X%02X is really %02X%02X%02X%02X dropping from local manifest", + engine->myID, SOSPeerGetID(peer), d[0], d[1], d[2], d[3], e[0], e[1], e[2], e[3]); + SOSChangesAppendDelete(changes, key); + SOSChangesAppendAdd(changes, object); // This is new behaviour but we think it's more correct + } + + size_t objectLen = (size_t)CFDataGetLength(der); + if (SOSMessageAppendObject(message, der, &localError)) { + SOSDigestVectorAppend(&dv, CFDataGetBytePtr(digest)); + } else { + const uint8_t *d = CFDataGetBytePtr(digest); + CFStringRef hexder = CFDataCopyHexString(der); + secnoticeq("engine", "%@:%@ object %02X%02X%02X%02X der: %@ dropping from manifest: %@", + engine->myID, SOSPeerGetID(peer), d[0], d[1], d[2], d[3], hexder, localError); + CFReleaseNull(hexder); + CFReleaseNull(message); + // Since we can't send these objects let's assume they are bad too? + SOSChangesAppendDelete(changes, digest); + } + objectsSize += objectLen; + if (objectsSize > kSOSMessageMaxObjectsSize) + *stop = true; + } + CFReleaseSafe(der); + CFReleaseSafe(digest); + })) { + CFReleaseNull(message); + } + if (dv.count) + objectsSent = SOSManifestCreateWithDigestVector(&dv, error); + if (CFArrayGetCount(changes)) { + CFErrorRef localError = NULL; + if (!SOSEngineUpdateChanges_locked(engine, NULL, kSOSDataSourceTransactionDidCommit, kSOSDataSourceSOSTransaction, changes, &localError)) + secerror("SOSEngineUpdateChanges_locked: %@ failed: %@", changes, localError); + CFReleaseSafe(localError); + CFAssignRetained(local, SOSEngineCopyLocalPeerManifest_locked(engine, peer, error)); + } + CFReleaseSafe(changes); + } + } else { + // If we have no confirmed manifest, we want all pendedObjects going out as a manifest + objectsSent = CFRetainSafe(pendingObjects); + } + + if (confirmed || SOSManifestGetCount(missing) || SOSManifestGetCount(extra) || objectsSent) { + SOSManifestRef allExtra = SOSManifestCreateUnion(extra, objectsSent, error); + proposed = SOSManifestCreateWithPatch(confirmed, missing, allExtra, error); + CFReleaseNull(allExtra); + } + + if (!SOSMessageSetManifests(message, local, confirmed, proposed, proposed, confirmed ? objectsSent : NULL, error)) + CFReleaseNull(message); + + CFReleaseNull(objectsSent); + + if (message) { + result = SOSMessageCreateData(message, SOSPeerNextSequenceNumber(peer), error); + } + + if (result) { + // Capture the peer in our block (SOSEnginePeerMessageSentBlock) + CFRetainSafe(peer); + *sent = Block_copy(^(bool success) { + dispatch_async(engine->queue, ^{ + if (success) { + SOSPeerSetMustSendMessage(peer, false); + if (!confirmed && !proposed) { + SOSPeerSetSendObjects(peer, true); + secnoticeq("engine", "%@:%@ sendObjects=true L:%@", engine->myID, SOSPeerGetID(peer), local); + } + SOSPeerAddLocalManifest(peer, local); + SOSPeerAddProposedManifest(peer, proposed); + secnoticeq("engine", "send %@:%@ %@", engine->myID, SOSPeerGetID(peer), message); + //SOSEngineCheckPeerIntegrity(engine, peer, NULL); + } else { + secerror("%@:%@ failed to send %@", engine->myID, SOSPeerGetID(peer), message); + } + CFReleaseSafe(peer); + CFReleaseSafe(local); + CFReleaseSafe(proposed); + CFReleaseSafe(message); + }); + }); + } else { + CFReleaseSafe(local); + CFReleaseSafe(proposed); + CFReleaseSafe(message); + } + CFReleaseNull(extra); + CFReleaseNull(missing); + if (error && *error) + secerror("%@:%@ error in send: %@", engine->myID, SOSPeerGetID(peer), *error); + + return result; +} + +static void SOSEngineLogItemError(SOSEngineRef engine, CFStringRef peerID, CFDataRef key, CFDataRef optionalDigest, const char *where, CFErrorRef error) { + if (!optionalDigest) { + const uint8_t *d = CFDataGetBytePtr(key); + secwarning("%@:%@ object %02X%02X%02X%02X %s: %@", engine->myID, peerID, d[0], d[1], d[2], d[3], where, error ? (CFTypeRef)error : CFSTR("")); + } else { + const uint8_t *d = CFDataGetBytePtr(key); + const uint8_t *e = CFDataGetBytePtr(optionalDigest); + secwarning("%@:%@ object %02X%02X%02X%02X is really %02X%02X%02X%02X dropping from local manifest", engine->myID, peerID, d[0], d[1], d[2], d[3], e[0], e[1], e[2], e[3]); + } +} + +static bool SOSEngineWriteToBackup_locked(SOSEngineRef engine, SOSPeerRef peer, bool rewriteComplete, bool *didWrite, bool *incomplete, CFErrorRef *error) { + __block bool ok = SOSPeerWritePendingReset(peer, error); + if (!ok || !SOSPeerGetKeyBag(peer)) + return ok; + __block SOSManifestRef local = SOSEngineCopyLocalPeerManifest_locked(engine, peer, error); + __block SOSManifestRef proposed = SOSPeerGetProposedManifest(peer); + __block bool notify = true; + SOSManifestRef pendingObjects = NULL; + SOSManifestRef missing = NULL; + CFStringRef peerID = SOSPeerGetID(peer); + + ok &= SOSManifestDiff(proposed, local, &missing, &pendingObjects, error); + + secnoticeq("engine", "%@:%@: Send state for peer [%s%s%s] O: %zu, M: %zu", engine->myID, peerID, + local ? "L":"l", + proposed ? "P":"0", + pendingObjects ? "O":"0", + SOSManifestGetCount(pendingObjects), + SOSManifestGetCount(missing)); + + if (SOSManifestGetCount(missing) == 0 && SOSManifestGetCount(pendingObjects) == 0) { + // proposed == local (faster test than CFEqualSafe above), since we + // already did the SOSManifestDiff + if (rewriteComplete) { + notify = false; + } else { + secnoticeq("engine", "%@:%@ backup still done", engine->myID, peer); + goto done; + } + } + ok &= SOSPeerAppendToJournal(peer, error, ^(FILE *journalFile, keybag_handle_t kbhandle) { + SOSManifestRef objectsSent = NULL; + __block struct SOSDigestVector dvdel = SOSDigestVectorInit; + __block struct SOSDigestVector dvadd = SOSDigestVectorInit; + SOSManifestForEach(missing, ^(CFDataRef key, bool *stop) { + CFErrorRef localError = NULL; + if (ftello(journalFile) > kSOSBackupMaxFileSize) { + // Highwatermark hit on file. + *stop = true; + } else if (SOSBackupEventWriteDelete(journalFile, key, &localError)) { + SOSDigestVectorAppend(&dvdel, CFDataGetBytePtr(key)); + } else { + SOSEngineLogItemError(engine, peerID, key, NULL, "in SOSPeerWriteDelete", localError); + CFErrorPropagate(localError, error); + // TODO: Update of missing so proposed is updated properly + *stop = true; // Disk full? + ok = false; + } + }); + if (ok && SOSManifestGetCount(pendingObjects)) { + CFMutableArrayRef changes = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + ok &= SOSDataSourceForEachObject(engine->dataSource, pendingObjects, error, ^void(CFDataRef key, SOSObjectRef object, bool *stop) { + CFErrorRef localError = NULL; + CFDataRef digest = NULL; + CFDictionaryRef backupItem = NULL; + if (ftello(journalFile) > kSOSBackupMaxFileSize) { + // Highwatermark hit on file. + *stop = true; + } else if (!object) { + SOSEngineLogItemError(engine, peerID, key, NULL, "dropping from manifest: not found in datasource", localError); + SOSChangesAppendDelete(changes, key); + } else if (!(backupItem = SOSObjectCopyBackup(engine->dataSource, object, kbhandle, &localError)) + || !(digest = SOSObjectCopyDigest(engine->dataSource, object, &localError))) { + if (SecErrorGetOSStatus(localError) == errSecDecode) { + // Decode error, we need to drop these objects from our manifests + SOSEngineLogItemError(engine, peerID, key, NULL, "dropping from manifest", localError); + SOSChangesAppendDelete(changes, key); + CFRelease(localError); + } else { + // Stop iterating and propagate out all other errors. + SOSEngineLogItemError(engine, peerID, key, NULL, "in SOSDataSourceForEachObject", localError); + *stop = true; + CFErrorPropagate(localError, error); + ok = false; + } + } else { + if (!CFEqual(key, digest)) { + SOSEngineLogItemError(engine, peerID, key, digest, "", NULL); + SOSChangesAppendDelete(changes, key); + SOSChangesAppendAdd(changes, object); // This is new behaviour but we think it's more correct + } + + if (SOSBackupEventWriteAdd(journalFile, backupItem, &localError)) { + SOSDigestVectorAppend(&dvadd, CFDataGetBytePtr(digest)); + } else { + SOSEngineLogItemError(engine, peerID, key, NULL, "in SOSPeerWriteAdd", localError); + *stop = true; // Disk full? + CFErrorPropagate(localError, error); + ok = false; + } + } + CFReleaseSafe(backupItem); + CFReleaseSafe(digest); + }); + if (CFArrayGetCount(changes)) { + CFErrorRef localError = NULL; + if (!SOSEngineUpdateChanges_locked(engine, NULL, kSOSDataSourceTransactionDidCommit, kSOSDataSourceSOSTransaction, changes, &localError)) + secerror("SOSEngineUpdateChanges_locked: %@ failed: %@", changes, localError); + CFReleaseSafe(localError); + // Since calling SOSEngineUpdateChanges_locked might cause local to change and might cause the backup peer to update proposed, refetch them here. + CFAssignRetained(local, SOSEngineCopyLocalPeerManifest_locked(engine, peer, error)); + proposed = SOSPeerGetProposedManifest(peer); + } + CFReleaseSafe(changes); + } + + if (dvadd.count || (proposed && dvdel.count)) { + *didWrite = true; + SOSManifestRef deleted = SOSManifestCreateWithDigestVector(&dvdel, error); + SOSManifestRef objectsSent = SOSManifestCreateWithDigestVector(&dvadd, error); + SOSManifestRef newProposed = SOSManifestCreateWithPatch(proposed, deleted, objectsSent, error); + CFReleaseSafe(deleted); + CFReleaseSafe(objectsSent); + SOSPeerSetProposedManifest(peer, newProposed); + CFReleaseSafe(newProposed); + proposed = SOSPeerGetProposedManifest(peer); + } + SOSDigestVectorFree(&dvdel); + SOSDigestVectorFree(&dvadd); + + // TODO: If proposed is NULL, and local is empty we should still consider ourselves done. + // It so happens this can't happen in practice today since there is at least a backupbag + // in the backup, but this is a bug waiting to rear its head in the future. + if (ok && CFEqualSafe(local, proposed)) { + CFErrorRef localError = NULL; + if (SOSBackupEventWriteCompleteMarker(journalFile, 899, &localError)) { + SOSPeerSetSendObjects(peer, true); + *didWrite = true; + secnoticeq("backup", "%@:%@ backup done%s", engine->myID, peerID, notify ? " notifying sbd" : ""); + // TODO: Now switch to changes based writing to backup sync. + // Currently we leave changes enabled but we probably shouldn't + } else { + secwarning("%@:%@ in SOSBackupPeerWriteCompleteMarker: %@", engine->myID, peerID, localError); + ok = false; + *incomplete = true; + CFErrorPropagate(localError, error); + } + } else { + secnoticeq("backup", "%@:%@ backup incomplete [%zu/%zu]%s", engine->myID, peerID, SOSManifestGetCount(local), SOSManifestGetCount(proposed), notify ? " notifying sbd" : ""); + *incomplete = true; + } + CFReleaseNull(objectsSent); + }); + if (notify) + SOSBackupPeerPostNotification("writing changes to backup"); + +done: + CFReleaseSafe(local); + CFReleaseNull(pendingObjects); + CFReleaseNull(missing); + + return ok; +} + +bool SOSEngineSyncWithPeers(SOSEngineRef engine, CFTypeRef idsTransport, CFTypeRef kvsTransport, CFErrorRef *error) { + __block bool ok = true; + __block bool incomplete = false; + ok &= SOSEngineDoTxnOnQueue(engine, error, ^(SOSTransactionRef txn, bool *commit) { + __block bool dirty = false; + SOSEngineForEachBackupPeer_locked(engine, ^(SOSPeerRef peer) { + ok = SOSEngineWriteToBackup_locked(engine, peer, false, &dirty, &incomplete, error); + }); + + if (dirty) + ok = SOSEngineSave(engine, txn, error); + }); + if (incomplete) { + // Ensure we get called again in a while (after a backup timeout) + // sbd will do this since we never wrote a complete marker. + // TODO: This relies on us not writing complete marker for update + // event while we havn't finished a full backup, which we currently still do. + } + return ok; +} + +bool SOSEngineHandleMessage(SOSEngineRef engine, CFStringRef peerID, + CFDataRef raw_message, CFErrorRef *error) +{ + __block bool result = true; + __block bool somethingChanged = false; + SOSMessageRef message = SOSMessageCreateWithData(kCFAllocatorDefault, raw_message, error); + result &= message && SOSEngineDoTxnOnQueue(engine, error, ^(SOSTransactionRef txn, bool *commit) { + result = SOSEngineHandleMessage_locked(engine, peerID, message, txn, commit, &somethingChanged, error); + }); + CFReleaseSafe(message); + if (somethingChanged) + SecKeychainChanged(false); + return result; +} + +void SOSEngineCircleChanged(SOSEngineRef engine, CFStringRef myPeerID, CFArrayRef trustedPeers, CFArrayRef untrustedPeers) { + __block bool peersOrViewsChanged = false; + SOSEngineDoOnQueue(engine, ^{ + peersOrViewsChanged = SOSEngineCircleChanged_locked(engine, myPeerID, trustedPeers, untrustedPeers); + }); + + __block bool ok = true; + __block CFErrorRef localError = NULL; + ok &= SOSEngineDoTxnOnQueue(engine, &localError, ^(SOSTransactionRef txn, bool *commit) { + ok = *commit = SOSEngineSave(engine, txn, &localError); + }); + if (!ok) { + secerror("failed to save engine state: %@", localError); + CFReleaseSafe(localError); + } + + if (peersOrViewsChanged) + SOSCCSyncWithAllPeers(); +} + +SOSManifestRef SOSEngineCopyManifest(SOSEngineRef engine, CFErrorRef *error) { + __block SOSManifestRef result = NULL; + SOSEngineDoOnQueue(engine, ^{ + result = SOSEngineCopyManifestWithViewNameSet_locked(engine, SOSViewsGetV0ViewSet(), error); + }); + return result; +} + +SOSManifestRef SOSEngineCopyLocalPeerManifest(SOSEngineRef engine, SOSPeerRef peer, CFErrorRef *error) { + __block SOSManifestRef result = NULL; + SOSEngineDoOnQueue(engine, ^{ + result = SOSEngineCopyLocalPeerManifest_locked(engine, peer, error); + }); + return result; +} + +bool SOSEngineUpdateChanges(SOSEngineRef engine, SOSDataSourceTransactionSource source, CFArrayRef changes, CFErrorRef *error) { + __block bool result = true; + SOSEngineDoOnQueue(engine, ^{ + result = SOSEngineUpdateChanges_locked(engine, NULL, kSOSDataSourceTransactionDidCommit, source, changes, error); + }); + return result; +} + +// +// Peer state layout. WRONG! It's an array now +// The peer state is an array. +// The first element of the array is a dictionary with any number of keys and +// values in it (for future expansion) such as changing the digest size or type +// or remebering boolean flags for a peers sake. +// The next three are special in that they are manifest digests with special +// meaning and rules as to how they are treated (These are dynamically updated +// based on database activity so they have a fully history of all changes made +// to the local db. The first is the manifest representing the pendingObjects +// to send to the other peer. This is normally only ever appending to, and in +// particular with transactions originating from the Keychain API that affect +// syncable items will need to add the new objects digests to the pendingObjects list +// while adding the digests of any tombstones encountered to the extra list. + +SOSPeerRef SOSEngineCopyPeerWithID(SOSEngineRef engine, CFStringRef peer_id, CFErrorRef *error) { + __block SOSPeerRef peer = NULL; + SOSEngineDoOnQueue(engine, ^{ + peer = SOSEngineCopyPeerWithID_locked(engine, peer_id, error); + }); + return peer; +} + +bool SOSEngineForPeerID(SOSEngineRef engine, CFStringRef peerID, CFErrorRef *error, void (^forPeer)(SOSPeerRef peer)) { + __block bool ok = true; + SOSEngineDoOnQueue(engine, ^{ + SOSPeerRef peer = SOSEngineCopyPeerWithID_locked(engine, peerID, error); + if (peer) { + forPeer(peer); + CFRelease(peer); + } else { + ok = false; + } + }); + return ok; +} + +bool SOSEngineWithPeerID(SOSEngineRef engine, CFStringRef peerID, CFErrorRef *error, void (^with)(SOSPeerRef peer, SOSDataSourceRef dataSource, SOSTransactionRef txn, bool *forceSaveState)) { + __block bool result = true; + result &= SOSEngineDoTxnOnQueue(engine, error, ^(SOSTransactionRef txn, bool *commit) { + SOSPeerRef peer = SOSEngineCopyPeerWithID_locked(engine, peerID, error); + if (!peer) { + result = SOSErrorCreate(kSOSErrorPeerNotFound, error, NULL, CFSTR("Engine has no peer for %@"), peerID); + } else { + bool saveState = false; + with(peer, engine->dataSource, txn, &saveState); + CFReleaseSafe(peer); + if (saveState) + result = SOSEngineSave(engine, txn, error); + // TODO: Don't commit if engineSave fails? + } + }); + + return result; +} + +CFDataRef SOSEngineCreateMessageToSyncToPeer(SOSEngineRef engine, CFStringRef peerID, SOSEnginePeerMessageSentBlock *sentBlock, CFErrorRef *error) { + __block CFDataRef message = NULL; + SOSEngineForPeerID(engine, peerID, error, ^(SOSPeerRef peer) { + message = SOSEngineCreateMessage_locked(engine, peer, error, sentBlock); + }); + return message; +} + +bool SOSEnginePeerDidConnect(SOSEngineRef engine, CFStringRef peerID, CFErrorRef *error) { + return SOSEngineWithPeerID(engine, peerID, error, ^(SOSPeerRef peer, SOSDataSourceRef dataSource, SOSTransactionRef txn, bool *saveState) { + *saveState = SOSPeerDidConnect(peer); + }); +} + +bool SOSEngineSetPeerConfirmedManifest(SOSEngineRef engine, CFStringRef backupName, + CFDataRef keybagDigest, CFDataRef manifestData, CFErrorRef *error) { + __block bool ok = true; + + ok &= SOSEngineForPeerID(engine, backupName, error, ^(SOSPeerRef peer) { + bool dirty = false; + bool incomplete = false; + SOSManifestRef confirmed = NULL; + CFDataRef keybag = SOSPeerGetKeyBag(peer); + CFDataRef computedKeybagDigest = keybag ? CFDataCopySHA1Digest(keybag, NULL) : NULL; + if (CFEqualSafe(keybagDigest, computedKeybagDigest)) { + ok = confirmed = SOSManifestCreateWithData(manifestData, error); + if (ok) { + // Set both confirmed and proposed (confirmed is just + // for debug status, proposed is actually what's used + // by the backup peer). + SOSPeerSetConfirmedManifest(peer, confirmed); + SOSPeerSetProposedManifest(peer, confirmed); + } + } else { + // sbd missed a reset event, send it again + // Force SOSEngineWriteToBackup_locked to call SOSPeerWriteReset, which clears + // confirmed and proposed manifests and writes the keybag to the journal. + SOSPeerSetMustSendMessage(peer, true); + } + + // Stop changes from writing complete markers, unless SOSEngineWriteToBackup_locked() detects we are in sync + SOSPeerSetSendObjects(peer, false); + // Write data for this peer if we can, technically not needed for non legacy protocol support all the time. + ok = SOSEngineWriteToBackup_locked(engine, peer, true, &dirty, &incomplete, error); + + CFReleaseSafe(confirmed); + CFReleaseSafe(computedKeybagDigest); + }); + return ok; +} + +CFArrayRef SOSEngineCopyBackupPeerNames(SOSEngineRef engine, CFErrorRef *error) { + __block CFMutableArrayRef backupNames = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + SOSEngineForEachBackupPeer(engine, ^(SOSPeerRef peer) { + CFArrayAppendValue(backupNames, SOSPeerGetID(peer)); + }); + return backupNames; +} + +static CFStringRef CFStringCreateWithViewNameSet(CFSetRef vns) { + CFIndex count = CFSetGetCount(vns); + CFMutableArrayRef mvn = CFArrayCreateMutableForCFTypesWithCapacity(kCFAllocatorDefault, count); + CFSetForEach(vns, ^(const void *value) { + CFArrayAppendValue(mvn, value); + }); + CFArraySortValues(mvn, CFRangeMake(0, count), (CFComparatorFunction)CFStringCompare, 0); + CFStringRef string = CFStringCreateByCombiningStrings(kCFAllocatorDefault, mvn, CFSTR(":")); + CFRelease(mvn); + return string; +} + +static CFStringRef CFStringCreateWithLabelAndViewNameSetDescription(CFStringRef label, CFStringRef peerID, CFSetRef vns, SOSManifestRef manifest) { + CFStringRef vnsDesc = CFStringCreateWithViewNameSet(vns); + CFStringRef desc; + if (manifest) + desc = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%@ %@ (%@) [%lu]"), label, peerID, vnsDesc, SOSManifestGetCount(manifest)); + else + desc = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%@ %@ (%@)"), label, peerID, vnsDesc); + CFReleaseSafe(vnsDesc); + return desc; +} + +static void CFArrayAppendConfirmedDigestsEntry(CFMutableArrayRef array, CFStringRef label, CFStringRef peerID, CFSetRef vns, SOSManifestRef manifest) { + CFStringRef desc = CFStringCreateWithLabelAndViewNameSetDescription(label, peerID, vns, manifest); + CFArrayAppendValue(array, desc); + CFReleaseSafe(desc); + CFDataRef digest = SOSManifestGetDigest(manifest, NULL); + if (digest) { + CFArrayAppendValue(array, digest); + } else { + CFDataRef nullDigest = CFDataCreate(kCFAllocatorDefault, NULL, 0); + CFArrayAppendValue(array, nullDigest); + CFReleaseSafe(nullDigest); + } +} + +static CFArrayRef SOSEngineCopyPeerConfirmedDigests_locked(SOSEngineRef engine, CFErrorRef *error) { + CFMutableArrayRef result = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault); + CFDictionaryForEach(engine->viewNameSet2ChangeTracker, ^(const void *vns, const void *ct) { + SOSManifestRef manifest = SOSEngineCopyManifestWithViewNameSet_locked(engine, vns, error); + CFArrayAppendConfirmedDigestsEntry(result, CFSTR("local "), engine->myID, (CFSetRef)vns, manifest); + CFReleaseSafe(manifest); + }); + + // Copy other peers even if we aren't in the circle, since we're observing it. + SOSEngineForEachPeer_locked(engine, ^(SOSPeerRef peer) { + CFArrayAppendConfirmedDigestsEntry(result, CFSTR("remote"), SOSPeerGetID(peer), SOSPeerGetViewNameSet(peer), + SOSPeerGetConfirmedManifest(peer)); + }); + return result; +} + +CFArrayRef SOSEngineCopyPeerConfirmedDigests(SOSEngineRef engine, CFErrorRef *error) { + __block CFArrayRef result = NULL; + SOSEngineDoOnQueue(engine, ^{ + result = SOSEngineCopyPeerConfirmedDigests_locked(engine, error); + }); + return result; +} + +SOSDataSourceRef SOSEngineGetDataSource(SOSEngineRef engine) { + return engine->dataSource; +}