PairingChannel.m   [plain text]

//
// Security
//

#import "PairingChannel.h"
#import <Foundation/NSXPCConnection_Private.h>
#import <CoreFoundation/CoreFoundation.h>
#import <CoreFoundation/CFPropertyList_Private.h>
#import <Security/Security.h>
#import <Security/SecItemPriv.h>
#import <Security/SecureObjectSync/SOSTypes.h>
#import <utilities/debugging.h>
#import <utilities/SecCFWrappers.h>
#import <ipc/securityd_client.h>

#import <compression.h>
#if TARGET_OS_EMBEDDED
#import <MobileGestalt.h>
#endif

#import "SecADWrapper.h"

typedef void(^KCPairingInternalCompletion)(BOOL complete, NSDictionary *outdict, NSError *error);
typedef void(^KCNextState)(NSDictionary *indict, KCPairingInternalCompletion complete);

NSString *kKCPairingChannelErrorDomain = @"com.apple.security.kcparingchannel";

@implementation KCPairingChannelContext
@end

@interface KCPairingChannel ()
@property (assign) KCPairingChannelContext *peerVersionContext;
@property (assign) bool initator;
@property (assign) unsigned counter;
@property (assign) bool acceptorWillSendInitialSyncCredentials;
@property (strong) NSXPCConnection *connection;

@property (strong) KCNextState nextState;
@end

@implementation KCPairingChannel

+ (instancetype)pairingChannelInitiator:(KCPairingChannelContext *)peerVersionContext
{
return [[KCPairingChannel alloc] initAsInitiator:true version:peerVersionContext];
}

+ (instancetype)pairingChannelAcceptor:(KCPairingChannelContext *)peerVersionContext
{
return [[KCPairingChannel alloc] initAsInitiator:false version:peerVersionContext];
}

- (instancetype)initAsInitiator:(bool)initator version:(KCPairingChannelContext *)peerVersionContext
{
if (![KCPairingChannel isSupportedPlatform])
return NULL;

if (self = [super init]) {
__weak typeof(self) weakSelf = self;
_initator = initator;
_peerVersionContext = peerVersionContext;
if (_initator) {
_nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf initatorFirstPacket:nsdata complete:kscomplete];
};
} else {
_nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf acceptorFirstPacket:nsdata complete:kscomplete];
};
}
_needInitialSync = true;
}
return self;
}

+ (bool)isSupportedPlatform
{
CFStringRef deviceClass = NULL;
#if TARGET_OS_EMBEDDED && !RC_HIDE_HARDWARE_WINTER_2017_IOS
deviceClass = MGCopyAnswer(kMGQDeviceClass, NULL);
if (deviceClass && CFEqual(deviceClass, kMGDeviceClassAudioAccessory)){
CFReleaseNull(deviceClass);
return false;
}
#endif
CFReleaseNull(deviceClass);
return true;
}

- (void)oneStepTooMany:(NSDictionary * __unused)indata complete:(KCPairingInternalCompletion)complete
{
secerror("pairingchannel: one step too many");
complete(false, NULL, [NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorTooManySteps userInfo:NULL]);
}

- (void)setNextStateError:(NSError *)error complete:(KCPairingInternalCompletion)complete
{
__weak typeof(self) weakSelf = self;
self.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf oneStepTooMany:nsdata complete:kscomplete];
};
if (complete) {
if (error)
secerror("pairingchannel: failed pairing with: %@", error);
complete(false, NULL, error);
}
}

//MARK: - Compression

const compression_algorithm pairingCompression = COMPRESSION_LZFSE;
#define EXTRA_SIZE 100

- (NSData *)compressData:(NSData *)data
{
NSMutableData *scratch = [NSMutableData dataWithLength:compression_encode_scratch_buffer_size(pairingCompression)];

NSUInteger outLength = [data length];
if (outLength > NSUIntegerMax - EXTRA_SIZE)
return nil;
outLength += EXTRA_SIZE;

NSMutableData *o = [NSMutableData dataWithLength:outLength];
size_t result = compression_encode_buffer([o mutableBytes], [o length], [data bytes], [data length], [scratch mutableBytes], pairingCompression);
if (result == 0)
return nil;

[o setLength:result];

return o;
}

- (NSData *)decompressData:(NSData *)data
{
NSMutableData *scratch = [NSMutableData dataWithLength:compression_decode_scratch_buffer_size(pairingCompression)];

size_t outLength = [data length];
size_t result;
NSMutableData *o = NULL;

do {
size_t size;
if (__builtin_umull_overflow(outLength, 2, &size))
return nil;
outLength = size;
o = [NSMutableData dataWithLength:outLength];

result = compression_decode_buffer([o mutableBytes], outLength, [data bytes], [data length], [scratch mutableBytes], pairingCompression);
if (result == 0)
return nil;
} while(result == outLength);

[o setLength:result];

return o;
}

//MARK: - Initiator

- (void)initatorFirstPacket:(NSDictionary * __unused)indata complete:(KCPairingInternalCompletion)complete
{
secnotice("pairing", "initator packet 1");

if (![self ensureControlChannel]) {
[self setNextStateError:[NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorNoControlChannel userInfo:NULL] complete:complete];
return;
}

__weak typeof(self) weakSelf = self;
self.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf initatorSecondPacket:nsdata complete:kscomplete];
};
complete(false, @{ @"d" : @YES }, NULL);
}

- (void)initatorSecondPacket:(NSDictionary *)indata complete:(KCPairingInternalCompletion)complete
{
secnotice("pairing", "initator packet 2");

NSData *credential = indata[@"c"];
if (credential == NULL) {
secnotice("pairing", "no credential");
[self setNextStateError:[NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorAccountCredentialMissing userInfo:NULL] complete:complete];
return;
}

if (indata[@"d"]) {
secnotice("pairing", "acceptor will send send initial credentials");
self.acceptorWillSendInitialSyncCredentials = true;
}

[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] stashAccountCredential:credential complete:^(bool success, NSError *error) {
[self setNextStateError:NULL complete:NULL];
if (!success) {
secnotice("pairing", "failed stash credentials: %@", error);
complete(true, NULL, error);
return;
}
[self initatorCompleteSecondPacket:complete];
}];
}

- (void)initatorCompleteSecondPacket:(KCPairingInternalCompletion)complete
{
__weak typeof(self) weakSelf = self;
secnotice("pairing", "initator complete second packet 2");

[self setNextStateError:NULL complete:NULL];

[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] myPeerInfo:^(NSData *application, NSError *error) {
if (application) {
complete(false, @{ @"p" : application }, error);

weakSelf.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf initatorThirdPacket:nsdata complete:kscomplete];
};

} else {
secnotice("pairing", "failed getting application: %@", error);
complete(true, @{}, error);
}
}];
}

- (void)initatorThirdPacket:(NSDictionary *)indata complete:(KCPairingInternalCompletion)complete
{
__weak typeof(self) weakSelf = self;
secnotice("pairing", "initator packet 3");

[self setNextStateError:NULL complete:NULL];

NSData *circleBlob = indata[@"b"];

if (circleBlob == NULL) {
complete(true, NULL, NULL);
return;
}

[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] joinCircleWithBlob:circleBlob version:kPiggyV1 complete:^(bool success, NSError *error){
typeof(self) strongSelf = weakSelf;
secnotice("pairing", "initator cirle join complete, more data: %s: %@",
strongSelf->_acceptorWillSendInitialSyncCredentials ? "yes" : "no", error);

if (strongSelf->_acceptorWillSendInitialSyncCredentials) {
strongSelf.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf initatorFourthPacket:nsdata complete:kscomplete];
};

complete(false, @{}, NULL);
} else {
complete(true, NULL, NULL);
}
}];
}

- (void)initatorFourthPacket:(NSDictionary *)indata complete:(KCPairingInternalCompletion)complete
{
secnotice("pairing", "initator packet 4");

[self setNextStateError:NULL complete:NULL];

NSArray *items = indata[@"d"];
if (![items isKindOfClass:[NSArray class]]) {
secnotice("pairing", "initator no items to import");
complete(true, NULL, NULL);
return;
}

secnotice("pairing", "importing %lu items", (unsigned long)[items count]);

[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] importInitialSyncCredentials:items complete:^(bool success, NSError *error) {
secnotice("pairing", "initator importInitialSyncCredentials: %s: %@", success ? "yes" : "no", error);
if (success)
self->_needInitialSync = false;
complete(true, NULL, NULL);
}];
}

//MARK: - Acceptor

- (void)acceptorFirstPacket:(NSDictionary *)indata complete:(KCPairingInternalCompletion)complete
{
__weak typeof(self) weakSelf = self;

secnotice("pairing", "acceptor packet 1");

[self setNextStateError:NULL complete:NULL];

if (![self ensureControlChannel]) {
[self setNextStateError:[NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorNoControlChannel userInfo:NULL] complete:complete];
return;
}

if (indata[@"d"]) {
secnotice("pairing", "acceptor initialSyncCredentials requested");
self.acceptorWillSendInitialSyncCredentials = true;
}

[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] validatedStashedAccountCredential:^(NSData *credential, NSError *error) {
if (!credential) {
secnotice("pairing", "acceptor doesn't have a stashed credential: %@", error);
[self setNextStateError:error complete:complete];
return;
}

self.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf acceptorSecondPacket:nsdata complete:kscomplete];
};

NSMutableDictionary *reply = [@{
@"c" : credential,
} mutableCopy];

if (self.acceptorWillSendInitialSyncCredentials) {
reply[@"d"] = @YES;
};

secnotice("pairing", "acceptor reply to packet 1");
complete(false, reply, NULL);
}];
}

- (void)acceptorSecondPacket:(NSDictionary *)indata complete:(KCPairingInternalCompletion)complete
{
__weak typeof(self) weakSelf = self;

[self setNextStateError:NULL complete:NULL];

secnotice("pairing", "acceptor packet 2");

[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] circleJoiningBlob:indata[@"p"] complete:^(NSData *blob, NSError *error){
NSMutableDictionary *reply = [NSMutableDictionary dictionary];

if (blob) {
secnotice("pairing", "acceptor pairing complete (will send: %s): %@",
self.acceptorWillSendInitialSyncCredentials ? "YES" : "NO",
error);

reply[@"b"] = blob;

if (self.acceptorWillSendInitialSyncCredentials) {
self.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf acceptorThirdPacket:nsdata complete:kscomplete];
};

complete(false, reply, NULL);
} else {
complete(true, reply, NULL);
}
} else {
complete(true, reply, error);
}
secnotice("pairing", "acceptor reply to packet 2");
}];
}

- (void)acceptorThirdPacket:(NSDictionary *)indata complete:(KCPairingInternalCompletion)complete
{
secnotice("pairing", "acceptor packet 3");

const uint32_t initialSyncCredentialsFlags =
SOSControlInitialSyncFlagTLK|
SOSControlInitialSyncFlagPCS|
SOSControlInitialSyncFlagBluetoothMigration;

[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] initialSyncCredentials:initialSyncCredentialsFlags complete:^(NSArray *items, NSError *error2) {
NSMutableDictionary *reply = [NSMutableDictionary dictionary];

secnotice("pairing", "acceptor initialSyncCredentials complete: items %u: %@", (unsigned)[items count], error2);
if (items) {
reply[@"d"] = items;
}
secnotice("pairing", "acceptor reply to packet 3");
complete(true, reply, NULL);
}];
}

//MARK: - Helper

- (bool)ensureControlChannel
{
if (self.connection)
return true;

NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SOSControlProtocol)];

self.connection = [[NSXPCConnection alloc] initWithMachServiceName:@(kSecuritydSOSServiceName) options:0];
if (self.connection == NULL)
return false;

self.connection.remoteObjectInterface = interface;

[self.connection resume];

return true;
}

- (void)validateStart:(void(^)(bool result, NSError *error))complete
{
if (!self.initator) {
[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(false, error);
}] stashedCredentialPublicKey:^(NSData *publicKey, NSError *error) {
complete(publicKey != NULL, error);
}];
} else {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
complete(true, NULL);
});
}
}

- (void)exchangePacket:(NSData *)inputCompressedData complete:(KCPairingChannelCompletion)complete
{
NSDictionary *indict = NULL;

if (inputCompressedData) {

NSData *data = [self decompressData:inputCompressedData];
if (data == NULL) {
secnotice("pairing", "failed to decompress");
complete(true, NULL, NULL);
}

NSError *error = NULL;
indict = [NSPropertyListSerialization propertyListWithData:data
options:(NSPropertyListReadOptions)kCFPropertyListSupportedFormatBinary_v1_0
format:NULL
error:&error];
if (indict == NULL) {
secnotice("pairing", "failed to deserialize");
complete(true, NULL, error);
return;
}
}
self.nextState(indict, ^(BOOL completed, NSDictionary *outdict, NSError *error) {
NSData *outdata = NULL, *compressedData = NULL;
if (outdict) {
NSError *error2 = NULL;
outdata = [NSPropertyListSerialization dataWithPropertyList:outdict format:NSPropertyListBinaryFormat_v1_0 options:0 error:&error2];
if (outdata == NULL && error)
error = error2;
if (outdata)
compressedData = [self compressData:outdata];

if (compressedData) {
NSString *key = [NSString stringWithFormat:@"com.apple.ckks.pairing.packet-size.%s.%u",
self->_initator ? "initator" : "acceptor", self->_counter];
SecADClientPushValueForDistributionKey((__bridge CFStringRef)key, [compressedData length]);
secnotice("pairing", "pairing packet size %lu", (unsigned long)[compressedData length]);
}
}
complete(completed, compressedData, error);
});
}

- (NSData *)exchangePacket:(NSData *)data complete:(bool *)complete error:(NSError * __autoreleasing *)error
{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block NSData *rData = NULL;
__block NSError* processingError;
[self exchangePacket:data complete:^(BOOL cComplete, NSData *cData, NSError *cError) {
self.counter++;
*complete = cComplete;
rData = cData;
processingError = cError;
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
if (error)
*error = processingError;
return rData;
}

- (void)setXPCConnectionObject:(NSXPCConnection *)connection
{
self.connection = connection;
}

@end