SOSPeer.c   [plain text]


/*
 * Created by Michael Brouwer on 6/22/12.
 * Copyright 2012 Apple Inc. All Rights Reserved.
 */

/*
 * SOSPeer.c -  Implementation of a secure object syncing peer
 */
#include <SecureObjectSync/SOSPeer.h>
#include <SecureObjectSync/SOSEngine.h>
#include <SecureObjectSync/SOSFullPeerInfo.h>
#include <SecureObjectSync/SOSPeerInfo.h>
#include <SecureObjectSync/SOSCoder.h>
#include <SecureObjectSync/SOSInternal.h>
#include <utilities/SecCFRelease.h>
#include <CommonCrypto/CommonDigest.h>
#include <CommonCrypto/CommonDigestSPI.h>
#include <utilities/SecCFError.h>
#include <utilities/SecCFWrappers.h>
#include <utilities/debugging.h>
#include <utilities/SecFileLocations.h>
#include <utilities/der_plist.h>
#include <utilities/der_plist_internal.h>

#include <utilities/SecDb.h>

#include <securityd/SOSCloudCircleServer.h>

#include <CoreFoundation/CoreFoundation.h>

#include <stdlib.h>

#include <AssertMacros.h>

//
//
//
static CFStringRef sErrorDomain = CFSTR("com.apple.security.sos.peer.error");

static CFMutableDictionaryRef sPersistenceCache = NULL;
static CFStringRef peerFile = CFSTR("PeerManifestCache.plist");

static CFMutableDictionaryRef SOSPeerGetPersistenceCache(CFStringRef my_id)
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        CFErrorRef localError = NULL;
        CFMutableDictionaryRef peerDict = NULL;
        CFDataRef dictAsData = SOSItemGet(kSOSPeerDataLabel, &localError);

        if (dictAsData) {
            der_decode_dictionary(kCFAllocatorDefault, kCFPropertyListMutableContainers, (CFDictionaryRef*)&peerDict, &localError,
                                  CFDataGetBytePtr(dictAsData),
                                  CFDataGetBytePtr(dictAsData) + CFDataGetLength(dictAsData));
        }
        
        if (!isDictionary(peerDict)) {
            CFReleaseNull(peerDict);
            secnotice("peer", "Error finding persisted peer data %@, using empty", localError);
            peerDict = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault);
            CFReleaseNull(localError);
        }
        
        if (CFDictionaryGetValue(peerDict, my_id) != NULL) {
            CFMutableDictionaryRef mySubDictionary = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault);

            CFDictionaryForEach(peerDict, ^(const void *key, const void *value) {
                if (!isDictionary(value)) {
                    CFDictionaryAddValue(mySubDictionary, key, value);
                };
            });
            
            CFDictionaryForEach(mySubDictionary, ^(const void *key, const void *value) {
                CFDictionaryRemoveValue(peerDict, key);
            });
            
            CFDictionaryAddValue(peerDict, my_id, mySubDictionary);
        }
        sPersistenceCache = peerDict;
    });

    return sPersistenceCache;
}

static void SOSPeerFlushPersistenceCache()
{
    if (!sPersistenceCache)
        return;

    CFErrorRef localError = NULL;
    CFIndex size = der_sizeof_dictionary(sPersistenceCache, &localError);
    CFMutableDataRef dataToStore = CFDataCreateMutableWithScratch(kCFAllocatorDefault, size);

    if (size == 0) {
        secerror("Error calculating size of persistence cache: %@", localError);
        goto fail;
    }

    uint8_t *der = NULL;
    if (CFDataGetBytePtr(dataToStore) != (der = der_encode_dictionary(sPersistenceCache, &localError,
					                                                  CFDataGetBytePtr(dataToStore),
                                                                      CFDataGetMutableBytePtr(dataToStore) + CFDataGetLength(dataToStore)))) {
        secerror("Error flattening peer cache: %@", localError);
        secerror("ERROR flattening peer cache (%@): size=%zd %@ (%p %p)", sPersistenceCache, size, dataToStore, CFDataGetBytePtr(dataToStore), der);
        goto fail;
}

    if (!SOSItemUpdateOrAdd(kSOSPeerDataLabel, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, dataToStore, &localError)) {
        secerror("Peer cache item save failed: %@", localError);
        goto fail;
    }

fail:
    CFReleaseNull(localError);
    CFReleaseNull(dataToStore);
}

void SOSPeerPurge(SOSPeerRef peer) {
    // TODO: Do we use this or some other end-around for PurgeAll?
}

void SOSPeerPurgeAllFor(CFStringRef my_id)
{
    if (!my_id)
        return;
    
    CFMutableDictionaryRef persistenceCache = SOSPeerGetPersistenceCache(my_id);
    
    CFMutableDictionaryRef myPeerIDs = (CFMutableDictionaryRef) CFDictionaryGetValue(persistenceCache, my_id);
    if (myPeerIDs)
    {
        CFRetainSafe(myPeerIDs);

        CFDictionaryRemoveValue(myPeerIDs, my_id);

        if (isDictionary(myPeerIDs)) {
            CFDictionaryForEach(myPeerIDs, ^(const void *key, const void *value) {
                // TODO: Inflate each and purge its keys.
            });
        }

        CFReleaseNull(myPeerIDs);
    }
}

static bool SOSPeerFindDataFor(CFTypeRef *peerData, CFStringRef my_id, CFStringRef peer_id, CFErrorRef *error)
{
    CFDictionaryRef table = (CFDictionaryRef) CFDictionaryGetValue(SOSPeerGetPersistenceCache(my_id), my_id);

    *peerData = isDictionary(table) ? CFDictionaryGetValue(table, peer_id) : NULL;

    return true;
}

static bool SOSPeerCopyPersistedManifest(SOSManifestRef* manifest, CFStringRef my_id, CFStringRef peer_id, CFErrorRef *error)
{
    CFTypeRef persistedObject = NULL;
    
    require(SOSPeerFindDataFor(&persistedObject, my_id, peer_id, error), fail);
    
    CFDataRef persistedData = NULL;
    
    if (isData(persistedObject))
        persistedData = (CFDataRef)persistedObject;
    else if (isArray(persistedObject) && (CFArrayGetCount((CFArrayRef) persistedObject) == 2))
        persistedData = CFArrayGetValueAtIndex((CFArrayRef) persistedObject, 1);
    
    if (isData(persistedData)) {
        SOSManifestRef createdManifest = SOSManifestCreateWithData(persistedData, error);

        require(createdManifest, fail);

        *manifest = createdManifest;
}

    return true;

fail:
    return false;
}


static bool SOSPeerCopyCoderData(CFDataRef *data, CFStringRef my_id, CFStringRef peer_id, CFErrorRef *error)
{    
    CFTypeRef persistedObject = NULL;
    
    require(SOSPeerFindDataFor(&persistedObject, my_id, peer_id, error), fail);
    
    CFDataRef persistedData = NULL;
    
    if (isArray(persistedObject))
        persistedData = CFArrayGetValueAtIndex((CFArrayRef) persistedObject, 0);
    
    if (isData(persistedData)) {
        CFRetainSafe(persistedData);
        *data = persistedData;
    }

    return true;
    
fail:
    return false;
}


static void SOSPeerPersistData(CFStringRef my_id, CFStringRef peer_id, SOSManifestRef manifest, CFDataRef coderData)
{
    CFMutableArrayRef data_array = CFArrayCreateMutableForCFTypes(0);
	if (coderData) {
    CFArrayAppendValue(data_array, coderData);
    } else {
        CFDataRef nullData = CFDataCreate(kCFAllocatorDefault, NULL, 0);
        CFArrayAppendValue(data_array, nullData);
        CFReleaseNull(nullData);
	}

    if (manifest) {
        CFArrayAppendValue(data_array, SOSManifestGetData(manifest));
    }

    CFMutableDictionaryRef mySubDict = (CFMutableDictionaryRef) CFDictionaryGetValue(SOSPeerGetPersistenceCache(my_id), my_id);

    if (mySubDict == NULL) {
        mySubDict = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault);
        CFDictionaryAddValue(SOSPeerGetPersistenceCache(my_id), my_id, mySubDict);
    }

    CFDictionarySetValue(mySubDict, peer_id, data_array);
    
    CFReleaseNull(data_array);

    SOSPeerFlushPersistenceCache();
}

struct __OpaqueSOSPeer {
    SOSPeerSendBlock send_block;
    CFStringRef my_id;
    CFStringRef peer_id;
    CFIndex version;
    SOSManifestRef manifest;
    CFDataRef manifest_digest;
    SOSCoderRef coder; // Currently will be used for OTR stuff.
};

static SOSPeerRef SOSPeerCreate_Internal(CFStringRef myPeerID, CFStringRef theirPeerID, CFIndex version, CFErrorRef *error,
                                         SOSPeerSendBlock sendBlock) {
    SOSPeerRef p = calloc(1, sizeof(struct __OpaqueSOSPeer));
    p->send_block = sendBlock;
    p->peer_id = theirPeerID;
    CFRetainSafe(p->peer_id);

    p->version = version;
    
    p->my_id = myPeerID;
    CFRetainSafe(myPeerID);
    
    require(SOSPeerCopyPersistedManifest(&p->manifest, p->my_id, p->peer_id, error), fail);

    return p;

fail:
    CFReleaseSafe(p->peer_id);
    CFReleaseSafe(p->my_id);
    free(p);
    return NULL;
}


SOSPeerRef SOSPeerCreate(SOSFullPeerInfoRef myPeerInfo, SOSPeerInfoRef peerInfo,
                         CFErrorRef *error, SOSPeerSendBlock sendBlock) {
    
    if (myPeerInfo == NULL) {
        SOSCreateError(kSOSErrorUnsupported, CFSTR("Can't create peer without my peer info!"), NULL, error);
        return NULL;
    }
    if (peerInfo == NULL) {
        SOSCreateError(kSOSErrorUnsupported, CFSTR("Can't create peer without their peer info!"), NULL, error);
        return NULL;
    }

    SOSPeerRef result = NULL;
    SOSPeerRef p = SOSPeerCreate_Internal(SOSPeerInfoGetPeerID(SOSFullPeerInfoGetPeerInfo(myPeerInfo)),
                                          SOSPeerInfoGetPeerID(peerInfo),
                                          SOSPeerInfoGetVersion(peerInfo),
                                          error, sendBlock);
    
    require(p, fail);

    CFDataRef coderData = NULL;
    CFErrorRef coderError = NULL;

    if (SOSPeerCopyCoderData(&coderData, p->my_id, p->peer_id, &coderError)
        && coderData && CFDataGetLength(coderData) != 0) {
        p->coder = SOSCoderCreateFromData(coderData, &coderError);
    }

    if (p->coder) {
        secnotice("peer", "Old coder for me: %@ to peer: %@", p->my_id, p->peer_id);
    } else {
        secnotice("peer", "New coder for me: %@ to peer: %@ [Got error: %@]", p->my_id, p->peer_id, coderError);

        p->coder = SOSCoderCreate(peerInfo, myPeerInfo, error);
        
        if (!p->coder) {
            SOSPeerDispose(p);
            p = NULL;
        }
    }

    CFReleaseNull(coderData);
    CFReleaseNull(coderError);

    result = p;
    p = NULL;
    
fail:
    CFReleaseNull(p);
    return result;
}

SOSPeerRef SOSPeerCreateSimple(CFStringRef peer_id, CFIndex version, CFErrorRef *error,
                               SOSPeerSendBlock sendBlock) {
    return SOSPeerCreate_Internal(CFSTR("FakeTestID"), peer_id, version, error, sendBlock);
}

void SOSPeerDispose(SOSPeerRef peer) {
        CFErrorRef error = NULL;
    CFDataRef coderData = NULL;
    if (peer->coder) {
        coderData = SOSCoderCopyDER(peer->coder, &error);
        if (coderData == NULL) {
			secerror("Coder data failed to export (%@), zapping data for me: %@ to peer: %@", error, peer->my_id, peer->peer_id);
		}
		CFReleaseNull(error);
    }
        
        if (!coderData) {
            coderData = CFDataCreate(NULL, NULL, 0);
        }
        
        SOSPeerPersistData(peer->my_id, peer->peer_id, peer->manifest, coderData);
        
        CFReleaseNull(coderData);
    CFReleaseSafe(peer->peer_id);
    CFReleaseSafe(peer->my_id);
    if (peer->manifest)
        SOSManifestDispose(peer->manifest);
    CFReleaseSafe(peer->manifest_digest);
    if (peer->coder)
        SOSCoderDispose(peer->coder);

    free(peer);
}

SOSPeerCoderStatus SOSPeerHandleMessage(SOSPeerRef peer, SOSEngineRef engine, CFDataRef codedMessage, CFErrorRef *error) {
    CFMutableDataRef message = NULL;
    SOSPeerCoderStatus coderStatus = kSOSPeerCoderDataReturned;

    if (peer->coder) {
        coderStatus = SOSCoderUnwrap(peer->coder, peer->send_block, codedMessage, &message, peer->peer_id, error);
    } else {
        message = CFDataCreateMutableCopy(kCFAllocatorDefault, 0, codedMessage);
    }

    switch(coderStatus) {
        case kSOSPeerCoderDataReturned: {
            CFStringRef description = SOSMessageCopyDescription(message);
            secnotice("peer", "Got message from %@: %@", peer->peer_id, description);
            CFReleaseSafe(description);
            coderStatus = (SOSEngineHandleMessage(engine, peer, message, error)) ? coderStatus: kSOSPeerCoderFailure;
            break;
        }
        case kSOSPeerCoderNegotiating:  // Sent message already in Unwrap.
            secnotice("peer", "Negotiating with %@: Got: %@", peer->peer_id, codedMessage);
            break;
        case kSOSPeerCoderNegotiationCompleted:
            if (SOSEngineSyncWithPeer(engine, peer, true, error)) {
                secnotice("peer", "Negotiating with %@ completed: %@" , peer->peer_id, codedMessage);
            } else {
                secerror("Negotiating with %@ completed syncWithPeer: %@ calling syncWithAllPeers" , peer->peer_id, error ? *error : NULL);
                // Clearing the manifest forces SOSEngineSyncWithPeer(engine, peer, false, error) to send a message no matter what.
                // This is needed because that's what gets called by SOSPeerStartSync, which is what SOSCCSyncWithAllPeers triggers.
                SOSPeerSetManifest(peer, NULL, NULL);
                SOSCCSyncWithAllPeers();
                coderStatus = kSOSPeerCoderFailure;
            }
            break;
        case kSOSPeerCoderFailure:      // Probably restart coder
            secnotice("peer", "Failed handling message from %@: Got: %@", peer->peer_id, codedMessage);
            SOSCoderReset(peer->coder);
            coderStatus = SOSCoderStart(peer->coder, peer->send_block, peer->peer_id, error);
            break;
        case kSOSPeerCoderStaleEvent:   // We received an event we have already processed in the past.
            secnotice("peer", "StaleEvent from %@: Got: %@", peer->peer_id, codedMessage);
            break;
        default:
            assert(false);
            break;
    }

    CFReleaseNull(message);

    return coderStatus;
}

SOSPeerCoderStatus SOSPeerStartSync(SOSPeerRef peer, SOSEngineRef engine, CFErrorRef *error) {
    SOSPeerCoderStatus coderStatus = kSOSPeerCoderDataReturned;

    if (peer->coder) {
        coderStatus = SOSCoderStart(peer->coder, peer->send_block, peer->peer_id, error);
    }

    switch(coderStatus) {
        case kSOSPeerCoderDataReturned:         // fallthrough
        case kSOSPeerCoderNegotiationCompleted: // fallthrough
            coderStatus = (SOSEngineSyncWithPeer(engine, peer, false, error)) ? coderStatus: kSOSPeerCoderFailure;
            break;
        case kSOSPeerCoderNegotiating: // Sent message already in Unwrap.
            secnotice("peer", "Started sync with %@", peer->peer_id);
            break;
        case kSOSPeerCoderFailure: // Probably restart coder
            break;
        default:
            assert(false);
            break;
    }
    return coderStatus;
}

bool SOSPeerSendMessage(SOSPeerRef peer, CFDataRef message, CFErrorRef *error) {
    CFMutableDataRef codedMessage = NULL;
    CFStringRef description = SOSMessageCopyDescription(message);

    SOSPeerCoderStatus coderStatus = kSOSPeerCoderDataReturned;

    if (peer->coder) {
        coderStatus = SOSCoderWrap(peer->coder, message, &codedMessage, peer->peer_id, error);
    } else {
        codedMessage = CFDataCreateMutableCopy(kCFAllocatorDefault, 0, message);
    }
    bool ok = true;
    switch(coderStatus) {
        case kSOSPeerCoderDataReturned:
            secnotice("peer", "%@ message: %@", peer->peer_id, description);
            peer->send_block(codedMessage, error);
            break;
        case kSOSPeerCoderNegotiating:
            secnotice("peer", "%@ coder Negotiating - message not sent", peer->peer_id);
            ok = SOSCreateErrorWithFormat(kSOSCCError, NULL, error, NULL, CFSTR("%@ failed to send message peer still negotiating"), peer->peer_id);
            break;
        default: // includes kSOSPeerCoderFailure
            secerror("%@ coder failure - message not sent %@", peer->peer_id, error ? *error : NULL);
            ok = false;
            break;
    }
    CFReleaseSafe(description);
    return ok;
}

bool SOSPeerCanSendMessage(SOSPeerRef peer) {
    return (!peer->coder || SOSCoderCanWrap(peer->coder));
}

CFIndex SOSPeerGetVersion(SOSPeerRef peer) {
    return peer->version;
}

CFStringRef SOSPeerGetID(SOSPeerRef peer) {
    return peer->peer_id;
}

bool SOSPeersEqual(SOSPeerRef peerA, SOSPeerRef peerB)
{
    // Use mainly to see if peerB is actually this device (peerA)
    return CFStringCompare(SOSPeerGetID(peerA), SOSPeerGetID(peerB), 0) == kCFCompareEqualTo;
}

bool SOSPeerSetManifest(SOSPeerRef peer, SOSManifestRef manifest, CFErrorRef *error __unused) {
    CFRetainSafe(manifest);
    CFReleaseSafe(peer->manifest);
    peer->manifest = manifest;

    CFReleaseNull(peer->manifest_digest);
    return true;
}

SOSManifestRef SOSPeerCopyManifest(SOSPeerRef peer, CFErrorRef *error __unused) {
    if (!peer->manifest) {
        SecCFCreateError(kSOSPeerHasNoManifest, sErrorDomain, CFSTR("failed to find peer manifest - not yet implemented"), NULL, error);
        return NULL;
    }

    CFRetain(peer->manifest);
    return peer->manifest;
}

CFDataRef SOSPeerCopyManifestDigest(SOSPeerRef peer, CFErrorRef *error) {
    if (peer->manifest_digest) {
        CFRetain(peer->manifest_digest);
    } else {
        if (peer->manifest) {
            CFMutableDataRef data = CFDataCreateMutable(NULL, CC_SHA1_DIGEST_LENGTH);
            if (data) {
                CFDataSetLength(data, CC_SHA1_DIGEST_LENGTH);
                CCDigest(kCCDigestSHA1, SOSManifestGetBytePtr(peer->manifest), (CC_LONG)SOSManifestGetSize(peer->manifest), CFDataGetMutableBytePtr(data));
                peer->manifest_digest = data;
                CFRetain(peer->manifest_digest);
            } else {
                SecCFCreateError(kSOSPeerDigestFailure, sErrorDomain, CFSTR("failed to create digest"), NULL, error);
            }
        } else {
            SecCFCreateError(kSOSPeerHasNoManifest, sErrorDomain, CFSTR("peer has no manifest, can't create digest"), NULL, error);
        }
    }

    return peer->manifest_digest;
}