KeychainSyncingOverIDSProxy+SendMessage.m [plain text]
/*
* Copyright (c) 2012-2016 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@
*/
#import <Foundation/NSArray.h>
#import <Foundation/Foundation.h>
#import <Security/SecBasePriv.h>
#import <Security/SecItemPriv.h>
#import <utilities/debugging.h>
#import <notify.h>
#include <Security/CKBridge/SOSCloudKeychainConstants.h>
#include <Security/SecureObjectSync/SOSARCDefines.h>
#include <Security/SecureObjectSync/SOSCloudCircle.h>
#include <Security/SecureObjectSync/SOSCloudCircleInternal.h>
#import <IDS/IDS.h>
#import <os/activity.h>
#include <utilities/SecAKSWrappers.h>
#include <utilities/SecCFRelease.h>
#include <AssertMacros.h>
#import "IDSProxy.h"
#import "IDSPersistentState.h"
#import "KeychainSyncingOverIDSProxy+SendMessage.h"
#import "KeychainSyncingOverIDSProxy+Throttle.h"
#include <Security/SecItemInternal.h>
static NSString *const IDSSendMessageOptionForceEncryptionOffKey = @"IDSSendMessageOptionForceEncryptionOff";
static NSString *const kIDSNumberOfFragments = @"NumberOfIDSMessageFragments";
static NSString *const kIDSFragmentIndex = @"kFragmentIndex";
static NSString *const kIDSOperationType = @"IDSMessageOperation";
static NSString *const kIDSMessageToSendKey = @"MessageToSendKey";
static NSString *const kIDSMessageUniqueID = @"MessageID";
static NSString *const kIDSMessageRecipientPeerID = @"RecipientPeerID";
static NSString *const kIDSMessageRecipientDeviceID = @"RecipientDeviceID";
static NSString *const kIDSMessageUseACKModel = @"UsesAckModel";
static NSString *const kIDSDeviceID = @"deviceID";
static const int64_t kRetryTimerLeeway = (NSEC_PER_MSEC * 250); // 250ms leeway for handling unhandled messages.
static const int64_t timeout = 3ull;
static const int64_t KVS_BACKOFF = 5;
static const NSUInteger kMaxIDSMessagePayloadSize = 64000;
@implementation KeychainSyncingOverIDSProxy (SendMessage)
-(bool) chunkAndSendKeychainPayload:(NSData*)keychainData deviceID:(NSString*)deviceName ourPeerID:(NSString*)ourPeerID theirPeerID:(NSString*) theirPeerID operation:(NSString*)operationTypeAsString uuid:(NSString*)uuidString error:(NSError**) error
{
__block BOOL result = true;
NSUInteger keychainDataLength = [keychainData length];
int fragmentIndex = 0;
int startingPosition = 0;
NSUInteger totalNumberOfFragments = (keychainDataLength + kMaxIDSMessagePayloadSize - 1)/kMaxIDSMessagePayloadSize;
NSMutableDictionary* fragmentDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:
deviceName, kIDSDeviceID,
[NSNumber numberWithUnsignedInteger:totalNumberOfFragments], kIDSNumberOfFragments,
[NSNumber numberWithInt:fragmentIndex], kIDSFragmentIndex,
deviceName, kIDSMessageRecipientDeviceID, theirPeerID, kIDSMessageRecipientPeerID,
operationTypeAsString, kIDSOperationType,
uuidString, kIDSMessageUniqueID,
nil];
NSUInteger remainingLength = keychainDataLength;
while(remainingLength > 0 && result == true){
NSUInteger fragmentLength = MIN(remainingLength, kMaxIDSMessagePayloadSize);
NSData *fragment = [keychainData subdataWithRange:NSMakeRange(startingPosition, fragmentLength)];
// Insert the current fragment data in dictionary with key peerID and message key.
[fragmentDictionary setObject:@{theirPeerID:fragment}
forKey:kIDSMessageToSendKey];
// Insert the fragment number in the dictionary
[fragmentDictionary setObject:[NSNumber numberWithInt:fragmentIndex]
forKey:kIDSFragmentIndex];
result = [self sendIDSMessage:fragmentDictionary name:deviceName peer:ourPeerID];
if(!result)
secerror("Could not send fragmented message");
startingPosition+=fragmentLength;
remainingLength-=fragmentLength;
fragmentIndex++;
}
return result;
}
- (void)sendToKVS: (NSString*) theirPeerID message: (NSData*) message
{
[self sendKeysCallout:^NSMutableDictionary *(NSMutableDictionary *pending, NSError** error) {
CFErrorRef cf_error = NULL;
bool success = SOSCCRequestSyncWithPeerOverKVS(((__bridge CFStringRef)theirPeerID), (__bridge CFDataRef)message, &cf_error);
if(success){
secnotice("IDSPing", "sent peerID: }
else{
secerror("Could not hand peerID: }
CFReleaseNull(cf_error);
return NULL;
}];
}
- (void) sendMessageToKVS: (NSDictionary<NSString*, NSDictionary*>*) encapsulatedKeychainMessage
{
[encapsulatedKeychainMessage enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
if ([key isKindOfClass: [NSString class]] && [obj isKindOfClass:[NSData class]]) {
[self sendToKVS:key message:obj];
} else {
secerror("Couldn't send to KVS key: }
}];
}
- (void)pingTimerFired:(NSString*)IDSid peerID:(NSString*)peerID identifier:(NSString*)identifier
{
//setting next time to send
[self updateNextTimeToSendFor5Minutes:IDSid];
secnotice("IDS Transport", "device ID: //call securityd to sync with device over KVS
__block CFErrorRef cf_error = NULL;
__block bool success = kHandleIDSMessageSuccess;
dispatch_source_t timer = [[KeychainSyncingOverIDSProxy idsProxy].pingTimers objectForKey:IDSid]; //remove timer
dispatch_cancel(timer); //cancel timer
[[KeychainSyncingOverIDSProxy idsProxy].pingTimers removeObjectForKey:IDSid];
[self sendKeysCallout:^NSMutableDictionary *(NSMutableDictionary *pending, NSError** error) {
success = SOSCCRequestSyncWithPeerOverKVSUsingIDOnly(((__bridge CFStringRef)IDSid), &cf_error);
if(success){
secnotice("IDSPing", "sent peerID: }
else{
secerror("Could not hand peerID: }
return NULL;
}];
CFReleaseSafe(cf_error);
}
-(void) pingDevices:(NSArray*)list peerID:(NSString*)peerID
{
NSDictionary *messageDictionary = @{kIDSOperationType : [NSString stringWithFormat:@"
[list enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * top) {
NSString* IDSid = (NSString*)obj;
NSString* identifier = [NSString string];
bool result = false;
secnotice("IDS Transport", "sending to id:
[self recordTimestampOfWriteToIDS: messageDictionary deviceName:IDSid peerID:peerID]; //add pings to throttling
NSDictionary *safeValues = [self filterForWritableValues:messageDictionary];
if(safeValues != nil && [safeValues count] > 0){
result = [self sendIDSMessage:safeValues name:IDSid peer:peerID];
if(!result){
secerror("Could not send message over IDS");
[self sendKeysCallout:^NSMutableDictionary *(NSMutableDictionary *pending, NSError** error) {
CFErrorRef kvsError = nil;
bool success = SOSCCRequestSyncWithPeerOverKVSUsingIDOnly(((__bridge CFStringRef)IDSid), &kvsError);
if(success){
secnotice("IDSPing", "sent peerID: }
else{
secerror("Could not hand peerID: }
CFReleaseNull(kvsError);
return NULL;
}];
}
else{
dispatch_source_t timer = nil;
if( [self.pingTimers objectForKey:IDSid] == nil){
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, kRetryTimerLeeway);
dispatch_source_set_event_handler(timer, ^{
[self pingTimerFired:IDSid peerID:peerID identifier:identifier];
});
dispatch_resume(timer);
[self.pingTimers setObject:timer forKey:IDSid];
}
}
}
}];
}
-(BOOL) shouldProxySendMessage:(NSString*)deviceName
{
BOOL result = false;
//checking peer cache to see if the message should be sent over IDS or back to KVS
if(self.peerNextSendCache == nil)
{
self.peerNextSendCache = [[NSMutableDictionary alloc]initWithCapacity:0];
}
NSDate *nextTimeToSend = [self.peerNextSendCache objectForKey:deviceName];
if(nextTimeToSend != nil)
{
//check if the timestamp is stale or set sometime in the future
NSDate *currentTime = [[NSDate alloc] init];
//if the current time is greater than the next time to send -> time to send!
if([[nextTimeToSend laterDate:currentTime] isEqual:currentTime]){
result = true;
}
}
else{ //next time to send is not set yet
result = true;
}
return result;
}
-(BOOL) isMessageAPing:(NSDictionary*)data
{
NSDictionary *messageDictionary = [data objectForKey: kIDSMessageToSendKey];
BOOL isPingMessage = false;
if(messageDictionary && ![messageDictionary isKindOfClass:[NSDictionary class]])
{
NSString* messageString = [data objectForKey: kIDSMessageToSendKey];
if(messageString && [messageString isKindOfClass:[NSString class]])
isPingMessage = true;
}
else if(!messageDictionary){
secerror("IDS Transport: message is null?");
}
return isPingMessage;
}
-(BOOL) sendFragmentedIDSMessages:(NSDictionary*)data name:(NSString*) deviceName peer:(NSString*) ourPeerID error:(NSError**) error
{
BOOL result = false;
BOOL isPingMessage = false;
NSError* localError = nil;
NSString* operationTypeAsString = [data objectForKey: kIDSOperationType];
NSMutableDictionary *messageDictionary = [data objectForKey: kIDSMessageToSendKey];
isPingMessage = [self isMessageAPing:data];
//check the peer cache for the next time to send timestamp
//if the timestamp is set in the future, reroute the message to KVS
//otherwise send the message over IDS
if(![self shouldProxySendMessage:deviceName])
{
if(isPingMessage){
secnotice("IDS Transport", "peer negative cache check: peer cannot send yet. not sending ping message");
return true;
}
else{
secnotice("IDS Transport", "peer negative cache check: peer cannot send yet. rerouting message to be sent over KVS: [self sendMessageToKVS:messageDictionary];
return true;
}
}
if(isPingMessage){ //foward the ping message, no processing
result = [self sendIDSMessage:data
name:deviceName
peer:ourPeerID];
if(!result){
secerror("Could not send ping message");
}
return result;
}
NSString *localMessageIdentifier = [[NSUUID UUID] UUIDString];
bool fragment = [operationTypeAsString intValue] == kIDSKeychainSyncIDSFragmentation;
bool useAckModel = fragment && [[data objectForKey:kIDSMessageUseACKModel] compare: @"YES"] == NSOrderedSame;
__block NSData *keychainData = nil;
__block NSString *theirPeerID = nil;
[messageDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if ([key isKindOfClass:[NSString class]] && [obj isKindOfClass:[NSData class]]) {
theirPeerID = (NSString*)key;
keychainData = (NSData*)obj;
}
*stop = YES;
}];
if(fragment && keychainData && [keychainData length] >= kMaxIDSMessagePayloadSize){
result = [self chunkAndSendKeychainPayload:keychainData
deviceID:deviceName
ourPeerID:ourPeerID
theirPeerID:theirPeerID
operation:operationTypeAsString
uuid:localMessageIdentifier
error:&localError];
}
else{
NSMutableDictionary* dataCopy = [NSMutableDictionary dictionaryWithDictionary:data];
[dataCopy setObject:localMessageIdentifier forKey:kIDSMessageUniqueID];
result = [self sendIDSMessage:dataCopy
name:deviceName
peer:ourPeerID];
}
if(result && useAckModel){
secnotice("IDS Transport", "setting ack timer");
[self setMessageTimer:localMessageIdentifier deviceID:deviceName message:data];
}
secnotice("IDS Transport","returning result: return result;
}
-(void) updateNextTimeToSendFor5Minutes:(NSString*)ID
{
secnotice("IDS Transport", "Setting next time to send in 5 minutes for device:
NSTimeInterval backOffInterval = (KVS_BACKOFF * 60);
NSDate *nextTimeToTransmit = [NSDate dateWithTimeInterval:backOffInterval sinceDate:[NSDate date]];
[self.peerNextSendCache setObject:nextTimeToTransmit forKey:ID];
}
- (void)ackTimerFired:(NSString*)identifier deviceID:(NSString*)ID
{
secnotice("IDS Transport", "IDS device id:
//call securityd to sync with device over KVS
NSMutableDictionary *message = [[KeychainSyncingOverIDSProxy idsProxy].messagesInFlight objectForKey:identifier];
if(!message){
return;
}
NSDictionary *encapsulatedKeychainMessage = [message objectForKey:kIDSMessageToSendKey];
[[KeychainSyncingOverIDSProxy idsProxy].messagesInFlight removeObjectForKey:identifier];
secnotice("IDS Transport", "Encapsulated message: dispatch_source_t timer = [[KeychainSyncingOverIDSProxy idsProxy].pingTimers objectForKey:identifier]; //remove timer
dispatch_cancel(timer); //cancel timer
[[KeychainSyncingOverIDSProxy idsProxy].pingTimers removeObjectForKey:identifier];
[self sendMessageToKVS:encapsulatedKeychainMessage];
//setting next time to send
[self updateNextTimeToSendFor5Minutes:ID];
[[KeychainSyncingOverIDSProxy idsProxy] persistState];
}
-(void) setMessageTimer:(NSString*)identifier deviceID:(NSString*)ID message:(NSDictionary*)message
{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, kRetryTimerLeeway);
dispatch_source_set_event_handler(timer, ^{
[self ackTimerFired:identifier deviceID:ID];
});
dispatch_resume(timer);
//restructure message in flight
[[KeychainSyncingOverIDSProxy idsProxy].messagesInFlight setObject:message forKey:identifier];
[self.pingTimers setObject:timer forKey:identifier];
[[KeychainSyncingOverIDSProxy idsProxy] persistState];
}
-(BOOL) sendIDSMessage:(NSDictionary*)data name:(NSString*) deviceName peer:(NSString*) peerID
{
if(!self->_service){
secerror("Could not send message to peer: return NO;
}
dispatch_async(self.calloutQueue, ^{
IDSMessagePriority priority = IDSMessagePriorityHigh;
BOOL encryptionOff = YES;
NSString *sendersPeerIDKey = [ NSString stringWithUTF8String: kMessageKeySendersPeerID];
secnotice("backoff","!!writing these keys to IDS!!:
NSDictionary *options = @{IDSSendMessageOptionForceEncryptionOffKey : [NSNumber numberWithBool:encryptionOff] };
NSMutableDictionary *dataCopy = [NSMutableDictionary dictionaryWithDictionary: data];
//set our peer id and a unique id for this message
[dataCopy setObject:peerID forKey:sendersPeerIDKey];
secnotice("IDS Transport", "
NSDictionary *info;
NSInteger errorCode = 0;
NSInteger numberOfDevices = 0;
NSString *errMessage = nil;
NSMutableSet *destinations = nil;
NSError *localError = nil;
NSString *identifier = nil;
IDSDevice *device = nil;
numberOfDevices = [self.listOfDevices count];
require_action_quiet(numberOfDevices > 0, fail, errorCode = kSecIDSErrorNotRegistered; errMessage=createErrorString(@"Could not send message to peer: secnotice("IDS Transport","List of devices:
destinations = [NSMutableSet set];
for(NSUInteger i = 0; i < [ self.listOfDevices count ]; i++){
device = self.listOfDevices[i];
if( [ deviceName compare:device.uniqueID ] == 0){
[destinations addObject: IDSCopyIDForDevice(device)];
}
}
require_action_quiet([destinations count] != 0, fail, errorCode = kSecIDSErrorCouldNotFindMatchingAuthToken; errMessage = createErrorString(@"Could not send message to peer:
bool result = [self->_service sendMessage:dataCopy toDestinations:destinations priority:priority options:options identifier:&identifier error:&localError ] ;
require_action_quiet(localError == nil && result, fail, errorCode = kSecIDSErrorFailedToSend; errMessage = createErrorString(@"Had an error sending IDS message to peer:
secnotice("IDS Transport","successfully sent to peer: fail:
info = [ NSDictionary dictionaryWithObjectsAndKeys:errMessage, NSLocalizedDescriptionKey, nil ];
if(localError != nil){
localError = [[NSError alloc] initWithDomain:@"com.apple.security.ids.error" code:errorCode userInfo:info ];
secerror(" }
if(localError != nil)
secerror("
});
return YES;
}
@end