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 "utilities/SecCoreAnalytics.h"
#import <ipc/securityd_client.h>
#import "keychain/ot/OTManager.h"
#import "keychain/ot/OctagonControlServer.h"
#import "keychain/ot/OTControl.h"
#import "keychain/ot/OctagonControlServer.h"
#import "keychain/ot/OTJoiningConfiguration.h"
#import "keychain/ot/proto/generated_source/OTApplicantToSponsorRound2M1.h"
#import "keychain/ot/proto/generated_source/OTSponsorToApplicantRound2M2.h"
#import "keychain/ot/proto/generated_source/OTSponsorToApplicantRound1M2.h"
#import "keychain/ot/proto/generated_source/OTGlobalEnums.h"
#import "keychain/ot/proto/generated_source/OTSupportSOSMessage.h"
#import "keychain/ot/proto/generated_source/OTSupportOctagonMessage.h"
#import "keychain/ot/proto/generated_source/OTPairingMessage.h"
#include <notify.h>

#import <compression.h>
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
#import <MobileGestalt.h>
#endif

KCPairingIntent_Type KCPairingIntent_Type_None = @"none";
KCPairingIntent_Type KCPairingIntent_Type_SilentRepair = @"repair";
KCPairingIntent_Type KCPairingIntent_Type_UserDriven = @"userdriven";

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

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

const char* pairingScope = "ot-pairing";

typedef void(^OTPairingInternalCompletion)(BOOL complete, NSData * _Nullable outData, NSError * _Nullable error);
typedef void(^OTNextState)(NSData *inData, OTPairingInternalCompletion complete);

@implementation KCPairingChannelContext

+ (BOOL)supportsSecureCoding {
return YES;
}

- (BOOL)isEqual:(id)object
{
KCPairingChannelContext *other = (KCPairingChannelContext *)object;

return [other isMemberOfClass:[self class]]
&& ((!self->_model && !other->_model) || [self->_model isEqual:other->_model])
&& ((!self->_modelVersion && !other->_modelVersion) || [self->_modelVersion isEqual:other->_modelVersion])
&& ((!self->_modelClass && !other->_modelClass) || [self->_modelClass isEqual:other->_modelClass])
&& ((!self->_osVersion && !other->_osVersion) || [self->_osVersion isEqual:other->_osVersion])
&& ((!self->_uniqueDeviceID && !other->_uniqueDeviceID) || [self->_uniqueDeviceID isEqual:other->_uniqueDeviceID])
&& ((!self->_uniqueClientID && !other->_uniqueClientID) || [self->_uniqueClientID isEqual:other->_uniqueClientID])
&& ((!self->_intent && !other->_intent) || [self->_intent isEqual:other->_intent])
;
}

- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:_model forKey:@"model"];
[coder encodeObject:_modelVersion forKey:@"modelVersion"];
[coder encodeObject:_modelClass forKey:@"modelClass"];
[coder encodeObject:_osVersion forKey:@"osVersion"];
[coder encodeObject:_uniqueDeviceID forKey:@"uniqueDeviceID"];
[coder encodeObject:_uniqueClientID forKey:@"uniqueClientID"];
[coder encodeObject:_intent forKey:@"intent"];
}

- (nullable instancetype)initWithCoder:(NSCoder *)decoder
{
if ((self = [super init])) {
_model = [decoder decodeObjectOfClass:[NSString class] forKey:@"model"];
_modelVersion = [decoder decodeObjectOfClass:[NSString class] forKey:@"modelVersion"];
_modelClass = [decoder decodeObjectOfClass:[NSString class] forKey:@"modelClass"];
_osVersion = [decoder decodeObjectOfClass:[NSString class] forKey:@"osVersion"];
_uniqueDeviceID = [decoder decodeObjectOfClass:[NSString class] forKey:@"uniqueDeviceID"];
_uniqueClientID = [decoder decodeObjectOfClass:[NSString class] forKey:@"uniqueClientID"];
_intent = [decoder decodeObjectOfClass:[NSString class] forKey:@"intent"];

/* validate intent if we have one */
if (_intent != NULL &&
!([_intent isEqualToString:KCPairingIntent_Type_None] ||
[_intent isEqualToString:KCPairingIntent_Type_SilentRepair] ||
[_intent isEqualToString:KCPairingIntent_Type_UserDriven]))
{
return nil;
}
}
return self;
}

@end

@interface KCPairingChannel ()
@property (assign) KCPairingChannelContext *peerVersionContext;
@property (assign) bool initiator;
@property (assign) unsigned counter;
@property (assign) bool acceptorWillSendInitialSyncCredentials;
@property (assign) uint32_t acceptorInitialSyncCredentialsFlags;
@property (strong) NSXPCConnection *connection;
@property (strong) OTControl *otControl;
@property (strong) NSString* contextID;
@property (strong) OTNextState nextOctagonState;
@property (strong) KCNextState nextState;
@property (nonatomic, strong) OTJoiningConfiguration* joiningConfiguration;
@property (nonatomic) bool testFailSOS;
@property (nonatomic) bool testFailOctagon;

@property (assign) bool sessionSupportsSOS;
@property (assign) bool sessionSupportsOctagon;
@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)initiator version:(KCPairingChannelContext *)peerVersionContext
{
if (![KCPairingChannel isSupportedPlatform]) {
secerror("platform not supported for pairing");
return NULL;
}

if (self = [super init]) {
__weak typeof(self) weakSelf = self;
_initiator = initiator;
_peerVersionContext = peerVersionContext;
if (_initiator) {
_nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf initiatorFirstPacket:nsdata complete:kscomplete];
};
} else {
_nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf acceptorFirstPacket:nsdata complete:kscomplete];
};
}
_needInitialSync = true;
_testFailSOS = false;
_contextID = OTDefaultContext;

/* only apply to acceptor right now */
_sessionSupportsSOS = OctagonPlatformSupportsSOS();
_sessionSupportsOctagon = OctagonIsEnabled();
NSError *localError = nil;
_otControl = [OTControl controlObject:true error:&localError];
if(localError){
secerror("could not stand up otcontrol connection");
}
_joiningConfiguration = [[OTJoiningConfiguration alloc]initWithProtocolType:OTProtocolPairing
uniqueDeviceID:peerVersionContext.uniqueDeviceID
uniqueClientID:peerVersionContext.uniqueClientID
pairingUUID:[[NSUUID UUID] UUIDString]
containerName:nil
contextID:OTDefaultContext
epoch:0
isInitiator:initiator];
}
return self;
}

+ (bool)isSupportedPlatform
{
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 *)pairingChannelCompressData:(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 *)pairingChannelDecompressData:(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) waitForOctagonUpgrade
{
[self.otControl waitForOctagonUpgrade:nil context:self.contextID reply:^(NSError *error) {
if(error){
secerror("pairing: failed to upgrade initiator into Octagon: %@", error);
}
}];
}

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

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

if(self.sessionSupportsOctagon && self.sessionSupportsSOS && !self.testFailOctagon) {
__weak typeof(self) weakSelf = self;
self.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf initiatorSecondPacket:nsdata complete:kscomplete];
};
complete(false, @{ @"d" : @YES, @"o" : @{@"v" : @"O"} }, NULL);
return;
} else if (self.sessionSupportsOctagon && self.testFailOctagon) {
complete(true, nil, NULL);
return;
} else if (self.sessionSupportsOctagon && !self.sessionSupportsSOS) {
__weak typeof(self) weakSelf = self;
self.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf initiatorSecondPacket:nsdata complete:kscomplete];
};
complete(false, @{ @"o" : @{@"v" : @"O"} }, NULL);
return;
}
else {
__weak typeof(self) weakSelf = self;
self.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf initiatorSecondPacket:nsdata complete:kscomplete];
};
complete(false, @{ @"d" : @YES }, NULL);
}
}

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

NSData *octagonData = indata[@"o"];

if(octagonData == nil) {
secnotice("pairing", "acceptor didn't send a octagon packet, so skipping all octagon flows");
self.sessionSupportsOctagon = false;
}

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

if (OctagonPlatformSupportsSOS() && indata[@"d"]) {
secnotice("pairing", "acceptor will send send initial credentials");
self.acceptorWillSendInitialSyncCredentials = true;
}
if(OctagonPlatformSupportsSOS()) {
[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] stashAccountCredential:credential complete:^(bool success, NSError *error) {
[self setNextStateError:NULL complete:NULL];
if (!success || self.testFailSOS) {
secnotice("pairing", "failed stash credentials: %@", error);
if(!OctagonIsEnabled() || !self.sessionSupportsOctagon){
complete(true, NULL, error);
return;
} else {
[self initiatorCompleteSecondPacketOctagon:indata application:nil complete:complete];
}
} else{
[self initiatorCompleteSecondPacketWithSOS:indata complete:complete];
}
}];
} else if(OctagonIsEnabled() && self.sessionSupportsOctagon) {
[self initiatorCompleteSecondPacketOctagon:indata application:nil complete:complete];
return;
}
}

- (void)initiatorCompleteSecondPacketWithSOS:(NSDictionary *)indata complete:(KCPairingInternalCompletion)complete
{
secnotice("pairing", "initiator complete second packet 2");

__weak typeof(self) weakSelf = self;

[self setNextStateError:NULL complete:NULL];

[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] myPeerInfo:^(NSData *application, NSError *error) {
if (application && !self.testFailSOS) {
if(OctagonIsEnabled() && self.sessionSupportsOctagon) {
[self initiatorCompleteSecondPacketOctagon:indata application:application complete:complete];
} else{
complete(false, @{ @"p" : application }, error);
weakSelf.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf initiatorThirdPacket:nsdata complete:kscomplete];
};
}
} else {
if(OctagonIsEnabled() && self.sessionSupportsOctagon){
[self initiatorCompleteSecondPacketOctagon:indata application:application complete:complete];
} else {
secnotice("pairing", "failed getting application: %@", error);
complete(true, @{}, error);
}
}
}];
}

- (void)initiatorCompleteSecondPacketOctagon:(NSDictionary*)indata application:(NSData*)application complete:(KCPairingInternalCompletion)complete
{
secnotice("pairing", "initiator complete second packet 2 with octagon");

__weak typeof(self) weakSelf = self;
NSData *octagonData = indata[@"o"];

if(![octagonData isKindOfClass:[NSData class]]) {
secnotice(pairingScope, "initiatorCompleteSecondPacketOctagon octagonData missing or wrong class");
[self setNextStateError:[NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorOctagonMessageMissing userInfo:NULL] complete:complete];
return;
}

//handle epoch and create identity message
[self.otControl rpcPrepareIdentityAsApplicantWithConfiguration:self.joiningConfiguration
reply:^(NSString *peerID,
NSData *permanentInfo,
NSData *permanentInfoSig,
NSData *stableInfo,
NSData *stableInfoSig,
NSError *error) {
if (error || self.testFailOctagon) {
secerror("ot-pairing: failed to create %d message: %@", self.counter, error);
complete(true, nil, error);
return;
} else {
OTPairingMessage *octagonMessage = [[OTPairingMessage alloc]init];
octagonMessage.supportsSOS = [[OTSupportSOSMessage alloc] init];
octagonMessage.supportsOctagon = [[OTSupportOctagonMessage alloc] init];
OTApplicantToSponsorRound2M1 *prepare = [[OTApplicantToSponsorRound2M1 alloc] init];
prepare.peerID = peerID;
prepare.permanentInfo = permanentInfo;
prepare.permanentInfoSig = permanentInfoSig;
prepare.stableInfo = stableInfo;
prepare.stableInfoSig = stableInfoSig;

octagonMessage.supportsSOS.supported = OctagonPlatformSupportsSOS() ? OTSupportType_supported : OTSupportType_not_supported;
octagonMessage.supportsOctagon.supported = OTSupportType_supported;
octagonMessage.prepare = prepare;
if(application){
secnotice(pairingScope, "initiatorCompleteSecondPacketOctagon returning octagon and sos data");
complete(false, @{ @"p" : application, @"o" : octagonMessage.data }, nil);
} else {
secnotice(pairingScope, "initiatorCompleteSecondPacketOctagon returning octagon data");
complete(false, @{ @"o" : octagonMessage.data }, nil);
}
weakSelf.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf initiatorThirdPacket:nsdata complete:kscomplete];
};
}
}];
}

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

[self setNextStateError:NULL complete:NULL];

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

if (circleBlob == NULL) {
if(!OctagonIsEnabled() && OctagonPlatformSupportsSOS()) {
secnotice("pairing", "no sos circle");
complete(true, NULL, NULL);
return;
}
} else if(OctagonPlatformSupportsSOS()) {
if(![circleBlob isKindOfClass:[NSData class]]) {
complete(true, NULL, [NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorTypeConfusion userInfo:NULL]);
return;
}

[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] joinCircleWithBlob:circleBlob version:kPiggyV1 complete:^(bool success, NSError *error){
if(error || self.testFailSOS) {
if(OctagonIsEnabled() && self.sessionSupportsOctagon) {
secnotice("pairing", "failed to join circle with blob, continuing to handle octagon protocol");
} else {
secnotice("pairing", "failed to join circle with blob");
complete(true, NULL, NULL);
}
} else{
if(OctagonIsEnabled() && self.sessionSupportsOctagon) {
secnotice("pairing","initiator circle join complete");
} else {
//kick off SOS ugprade
if(OctagonIsEnabled() && !self.sessionSupportsOctagon) {
[self waitForOctagonUpgrade];
}
typeof(self) strongSelf = weakSelf;
secnotice("pairing", "initiator circle join complete, more data: %s: %@",
strongSelf->_acceptorWillSendInitialSyncCredentials ? "yes" : "no", error);

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

complete(false, @{}, NULL);
} else {
complete(true, NULL, NULL);
}
}
}
}];
}
if(OctagonIsEnabled() && self.sessionSupportsOctagon){
NSData *octagonData = indata[@"o"];
if(![octagonData isKindOfClass:[NSData class]]) {
secnotice(pairingScope, "initiatorThirdPacket octagonData missing or wrong class");
[self setNextStateError:[NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorOctagonMessageMissing userInfo:NULL] complete:complete];
return;
}
OTPairingMessage *pairingMessage = [[OTPairingMessage alloc] initWithData:octagonData];
if(!pairingMessage.hasVoucher){
secnotice(pairingScope, "initiatorThirdPacket pairingMessage has no voucher");
[self setNextStateError:[NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorOctagonMessageMissing userInfo:NULL] complete:complete];
return;
}
OTSponsorToApplicantRound2M2 *voucher = pairingMessage.voucher;

//handle voucher and join octagon
[self.otControl rpcJoinWithConfiguration:self.joiningConfiguration
vouchData:voucher.voucher
vouchSig:voucher.voucherSignature
reply:^(NSError *error) {
if (error || self.testFailOctagon) {
secerror("ot-pairing: failed to create %d message: %@", self.counter, error);
complete(true, NULL, error);
return;
}else{
secnotice(pairingScope, "initiatorThirdPacket successfully joined Octagon");
typeof(self) strongSelf = weakSelf;
if(OctagonPlatformSupportsSOS() && strongSelf->_acceptorWillSendInitialSyncCredentials) {
strongSelf.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf initiatorFourthPacket:nsdata complete:kscomplete];
};
complete(false, @{}, nil);

} else {
complete(true, nil, nil);

}
}
}];
}
}

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

[self setNextStateError:NULL complete:NULL];

NSArray *items = indata[@"d"];
if (![items isKindOfClass:[NSArray class]]) {
secnotice("pairing", "initiator 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", "initiator importInitialSyncCredentials: %s: %@", success ? "yes" : "no", error);
if (success)
self->_needInitialSync = false;
complete(true, nil, nil);
}];
}

//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.sessionSupportsSOS && ![self ensureControlChannel]) {
secnotice("pairing", "unable to establish a channel to sos control");
[self setNextStateError:[NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorNoControlChannel userInfo:NULL] complete:complete];
return;
}

if (self.sessionSupportsSOS && indata[@"d"]) {
secnotice("pairing", "acceptor initialSyncCredentials requested");
self.acceptorWillSendInitialSyncCredentials = true;
self.acceptorInitialSyncCredentialsFlags =
SOSControlInitialSyncFlagTLK|
SOSControlInitialSyncFlagPCS|
SOSControlInitialSyncFlagBluetoothMigration;

}

if (indata[@"o"] == nil) {
secnotice("pairing", "initiator didn't send a octagon packet, so skipping all octagon flows");
self.sessionSupportsOctagon = false;
}
// XXX Before we go here we should check if we are trusted or not, if we are not, there is no point proposing octagon

NSMutableDictionary *reply = [NSMutableDictionary dictionary];

if(self.sessionSupportsSOS) {
[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] validatedStashedAccountCredential:^(NSData *credential, NSError *error) {
secnotice("pairing", "acceptor validatedStashedAccountCredential: %@ (%@)", credential != NULL ? @"yes" : @"no", error);
if(credential){
reply[@"c"] = credential;

if (self.acceptorWillSendInitialSyncCredentials) {
reply[@"d"] = @YES;
};
if(self.sessionSupportsOctagon) {
[self acceptorFirstOctagonPacket:indata reply:reply complete:complete];
} else {
self.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf acceptorSecondPacket:nsdata complete:kscomplete];
};
secnotice("pairing", "acceptor reply to packet 1");
complete(false, reply, NULL);
}
}
else if ((!credential && !self.sessionSupportsOctagon) || self.testFailSOS) {
secnotice("pairing", "acceptor doesn't have a stashed credential: %@", error);
NSError *noStashCredentail = [NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorNoStashedCredential userInfo:NULL];
[self setNextStateError:noStashCredentail complete:complete];
return;
}
else if(self.sessionSupportsOctagon) {
[self acceptorFirstOctagonPacket:indata reply:reply complete:complete];
}
}];
} else if(self.sessionSupportsOctagon){
[self acceptorFirstOctagonPacket:indata reply:reply complete:complete];
} else {
secnotice("pairing", "acceptor neither of octagon nor SOS");
NSError *notSupported = [NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorNoTrustAvailable userInfo:NULL];
[self setNextStateError:notSupported complete:complete];
}
}
- (void)acceptorFirstOctagonPacket:(NSDictionary *)indata reply:(NSMutableDictionary*)reply complete:(KCPairingInternalCompletion)complete
{
__weak typeof(self) weakSelf = self;
NSDictionary *octagonData = indata[@"o"];

if(![octagonData isKindOfClass:[NSDictionary class]]) {
secnotice(pairingScope, "acceptorFirstOctagonPacket octagon data missing");
[self setNextStateError:[NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorOctagonMessageMissing userInfo:NULL] complete:complete];
return;
}

NSDictionary *unpackedMessage = octagonData;

if(![unpackedMessage[@"v"] isEqualToString:@"O"]){
secnotice(pairingScope, "acceptorFirstOctagonPacket 'v' contents wrong");
[self setNextStateError:[NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorOctagonMessageMissing userInfo:NULL] complete:complete];
return;
}

//handle epoch request and fetch epoch
[self.otControl rpcEpochWithConfiguration:self.joiningConfiguration reply:^(uint64_t epoch, NSError * _Nullable error) {
secnotice("pairing", "acceptor rpcEpochWithConfiguration: %ld (%@)", (long)epoch, error);
if(error || self.testFailOctagon){
secerror("error acceptor handling packet %d", self.counter);
complete(true, nil, error);
}else{
secnotice(pairingScope, "acceptor handled packet %d", self.counter);
self.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf acceptorSecondPacket:nsdata complete:kscomplete];
};
OTPairingMessage *response = [[OTPairingMessage alloc] init];
response.supportsSOS = [[OTSupportSOSMessage alloc] init];
response.supportsOctagon = [[OTSupportOctagonMessage alloc] init];
response.epoch = [[OTSponsorToApplicantRound1M2 alloc] init];
response.epoch.epoch = epoch;
response.supportsSOS.supported = OctagonPlatformSupportsSOS() ? OTSupportType_supported : OTSupportType_not_supported;
response.supportsOctagon.supported = OTSupportType_supported;
reply[@"o"] = response.data;
secnotice("pairing", "acceptor reply to packet 1");
complete(false, reply, error);
}
}];
}

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

[self setNextStateError:NULL complete:NULL];

secnotice("pairing", "acceptor packet 2");
__block NSMutableDictionary *reply = [NSMutableDictionary dictionary];

NSData *peerJoinBlob = indata[@"p"];

if(self.sessionSupportsSOS && [peerJoinBlob isKindOfClass:[NSData class]]) {
[[self.connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
complete(true, NULL, error);
}] circleJoiningBlob:peerJoinBlob complete:^(NSData *blob, NSError *error){

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

reply[@"b"] = blob;
}
if(self.sessionSupportsOctagon) {
[self acceptorSecondOctagonPacket:indata reply:reply complete:complete];
} else {
secnotice("pairing", "posting kSOSCCCircleOctagonKeysChangedNotification");
notify_post(kSOSCCCircleOctagonKeysChangedNotification);
if (self.acceptorWillSendInitialSyncCredentials) {
self.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf acceptorThirdPacket:nsdata complete:kscomplete];
};

complete(false, reply, NULL);
} else {
complete(true, reply, NULL);
}
secnotice("pairing", "acceptor reply to packet 2");
}
}];
} else if(self.sessionSupportsOctagon){
[self acceptorSecondOctagonPacket:indata reply:reply complete:complete];
}
}

- (void)acceptorSecondOctagonPacket:(NSDictionary*)indata reply:(NSMutableDictionary*)reply complete:(KCPairingInternalCompletion)complete
{
__weak typeof(self) weakSelf = self;
NSData *octagonData = indata[@"o"];

if(![octagonData isKindOfClass:[NSData class]]) {
secnotice(pairingScope, "acceptorSecondOctagonPacket octagon data missing");
[self setNextStateError:[NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorOctagonMessageMissing userInfo:NULL] complete:complete];
return;
}

OTPairingMessage *pairingMessage = [[OTPairingMessage alloc] initWithData:octagonData];
if(!pairingMessage.hasPrepare){
secerror("ot-pairing: acceptorSecondOctagonPacket: no octagon message");
[self setNextStateError:[NSError errorWithDomain:kKCPairingChannelErrorDomain code:KCPairingErrorOctagonMessageMissing userInfo:NULL] complete:complete];
return;
}
OTApplicantToSponsorRound2M1 *prepare = pairingMessage.prepare;

//handle identity and fetch voucher
[self.otControl rpcVoucherWithConfiguration:self.joiningConfiguration
peerID:prepare.peerID
permanentInfo:prepare.permanentInfo
permanentInfoSig:prepare.permanentInfoSig
stableInfo:prepare.stableInfo
stableInfoSig:prepare.stableInfoSig
reply:^(NSData *voucher,
NSData *voucherSig,
NSError *error) {
if(error || self.testFailOctagon){
secerror("error acceptor handling octagon packet %d", self.counter);
complete(true, nil, error);
return;
} else {
bool finished = true;

secnotice(pairingScope, "acceptor handled octagon packet %d", self.counter);
if (OctagonPlatformSupportsSOS() && self.acceptorWillSendInitialSyncCredentials) {
self.nextState = ^(NSDictionary *nsdata, KCPairingInternalCompletion kscomplete){
[weakSelf acceptorThirdPacket:nsdata complete:kscomplete];
};
finished = false;
}
OTPairingMessage *response = [[OTPairingMessage alloc] init];
response.supportsSOS = [[OTSupportSOSMessage alloc] init];
response.supportsOctagon = [[OTSupportOctagonMessage alloc] init];
response.voucher = [[OTSponsorToApplicantRound2M2 alloc] init];
response.voucher.voucher = voucher;
response.voucher.voucherSignature = voucherSig;
response.supportsSOS.supported = OctagonPlatformSupportsSOS() ? OTSupportType_supported : OTSupportType_not_supported;
response.supportsOctagon.supported = OTSupportType_supported;

if (self.acceptorWillSendInitialSyncCredentials) {
// no need to share TLKs over the pairing channel, that's provided by octagon
self.acceptorInitialSyncCredentialsFlags &= ~(SOSControlInitialSyncFlagTLK | SOSControlInitialSyncFlagPCS);
}

reply[@"o"] = response.data;

secnotice("pairing", "acceptor reply to packet 2");
complete(finished ? true : false, reply, error);
}
}];
}

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

const uint32_t initialSyncCredentialsFlags = self.acceptorInitialSyncCredentialsFlags;

[[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.initiator) {
[[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;
secnotice("pairing", "Exchange packet: %u", self.counter);
self.counter++;

if (inputCompressedData) {

NSData *data = [[self class] pairingChannelDecompressData: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 class] pairingChannelCompressData:outdata];

if (compressedData) {
NSString *key = [NSString stringWithFormat:@"com.apple.ckks.pairing.packet-size.%s.%u",
self->_initiator ? "initiator" : "acceptor", self->_counter];
[SecCoreAnalytics sendEvent:key event:@{SecCoreAnalyticsValue: [NSNumber numberWithUnsignedInteger:[compressedData length]]}];
secnotice("pairing", "pairing packet size %lu", (unsigned long)[compressedData length]);
}
}
secnotice("pairing", "Exchange packet complete data: %@: %@", outdata ? @"YES" : @"NO", error);
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) {
*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;
}

- (void)setControlObject:(OTControl *)control
{
self.otControl = control;
}

- (void)setConfiguration:(OTJoiningConfiguration *)config
{
self.joiningConfiguration = config;
}

- (void)setSOSMessageFailForTesting:(BOOL)value
{
self.testFailSOS = value;
}
- (void)setOctagonMessageFailForTesting:(BOOL)value
{
self.testFailOctagon = value;
}

- (void)setSessionSupportsOctagonForTesting:(bool)value
{
self.sessionSupportsOctagon = value;
}

@end