// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2012-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #include #import "ServerConnector.h" #import "NSString+Hex.h" #import "BoxedMessage.h" #import "MyIdentityStore.h" #import "ProtocolDefines.h" #import "Reachability.h" #import "MessageQueue.h" #import "MessageProcessorProxy.h" #import "Utils.h" #import "ContactStore.h" #import "UserSettings.h" #import "BundleUtil.h" #import "AppGroup.h" #import "LicenseStore.h" #import "PushPayloadDecryptor.h" #import "ValidationLogger.h" #import "GCDAsyncSocketFactory.h" #ifdef DEBUG static const DDLogLevel ddLogLevel = DDLogLevelVerbose; #else static const DDLogLevel ddLogLevel = DDLogLevelWarning; #endif #define LOG_KEY_INFO 0 @implementation ServerConnector { NSData *clientTempKeyPub; NSData *clientTempKeySec; time_t clientTempKeyGenTime; NSData *clientCookie; NSData *serverCookie; NSData *serverTempKeyPub; NSString *serverNamePrefix; NSString *serverNamePrefixv6; NSString *serverNameSuffix; NSArray *serverPorts; int curServerPortIndex; NSData *chosenServerKeyPub; NSData *serverKeyPub; NSData *serverAltKeyPub; uint64_t serverNonce; uint64_t clientNonce; dispatch_queue_t queue; dispatch_source_t keepalive_timer; NSCondition *disconnectCondition; GCDAsyncSocket *socket; int reconnectAttempts; enum ConnectionState connectionState; BOOL autoReconnect; CFTimeInterval lastRead; NSDate *lastErrorDisplay; CFTimeInterval lastEchoSendTime; uint64_t lastSentEchoSeq; uint64_t lastRcvdEchoSeq; Reachability *internetReachability; NetworkStatus lastInternetStatus; NSMutableSet *displayedServerAlerts; int anotherConnectionCount; BOOL serverInInitialQueueSend; BOOL isWaitingForReconnect; } @synthesize connectionState; @synthesize lastRtt; #pragma pack(push, 1) #pragma pack(1) struct pktClientHello { unsigned char client_tempkey_pub[kNaClCryptoPubKeySize]; unsigned char client_cookie[kCookieLen]; }; struct pktServerHelloBox { unsigned char server_tempkey_pub[kNaClCryptoPubKeySize]; unsigned char client_cookie[kCookieLen]; }; struct pktServerHello { unsigned char server_cookie[kCookieLen]; char box[sizeof(struct pktServerHelloBox) + kNaClBoxOverhead]; }; struct pktVouch { unsigned char client_tempkey_pub[kNaClCryptoPubKeySize]; }; struct pktLogin { char identity[kIdentityLen]; char client_version[kClientVersionLen]; unsigned char server_cookie[kCookieLen]; unsigned char vouch_nonce[kNaClCryptoNonceSize]; char vouch_box[sizeof(struct pktVouch) + kNaClBoxOverhead]; }; struct pktLoginAck { char reserved[kLoginAckReservedLen]; }; struct pktPayload { uint8_t type; uint8_t reserved[3]; char data[]; }; #pragma pack(pop) #define TAG_CLIENT_HELLO_SENT 1 #define TAG_SERVER_HELLO_READ 2 #define TAG_LOGIN_SENT 3 #define TAG_LOGIN_ACK_READ 4 #define TAG_PAYLOAD_SENT 5 #define TAG_PAYLOAD_LENGTH_READ 6 #define TAG_PAYLOAD_READ 7 + (ServerConnector*)sharedServerConnector { static ServerConnector *instance; @synchronized (self) { if (!instance) instance = [[ServerConnector alloc] init]; } return instance; } - (id)init { self = [super init]; if (self) { /* Read server info */ if ([LicenseStore requiresLicenseKey]) { serverNamePrefix = [BundleUtil objectForInfoDictionaryKey:@"ThreemaWorkServerNamePrefix"]; serverNamePrefixv6 = [BundleUtil objectForInfoDictionaryKey:@"ThreemaWorkServerNamePrefixv6"]; } else { serverNamePrefix = [BundleUtil objectForInfoDictionaryKey:@"ThreemaServerNamePrefix"]; serverNamePrefixv6 = [BundleUtil objectForInfoDictionaryKey:@"ThreemaServerNamePrefixv6"]; } serverNameSuffix = [BundleUtil objectForInfoDictionaryKey:@"ThreemaServerNameSuffix"]; serverPorts = [BundleUtil objectForInfoDictionaryKey:@"ThreemaServerPorts"]; curServerPortIndex = 0; serverKeyPub = [BundleUtil objectForInfoDictionaryKey:@"ThreemaServerPublicKey"]; serverAltKeyPub = [BundleUtil objectForInfoDictionaryKey:@"ThreemaServerAltPublicKey"]; queue = dispatch_queue_create("ch.threema.SocketQueue", NULL); disconnectCondition = [[NSCondition alloc] init]; reconnectAttempts = 0; lastSentEchoSeq = 0; lastRcvdEchoSeq = 0; self.connectionState = ConnectionStateDisconnected; displayedServerAlerts = [NSMutableSet set]; /* register with reachability API */ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkStatusDidChange:) name:kReachabilityChangedNotification object:nil]; internetReachability = [Reachability reachabilityForInternetConnection]; lastInternetStatus = [internetReachability currentReachabilityStatus]; [internetReachability startNotifier]; isWaitingForReconnect = false; /* listen for identity changes */ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(identityCreated:) name:kNotificationCreatedIdentity object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(identityDestroyed:) name:kNotificationDestroyedIdentity object:nil]; } return self; } - (void)connect { dispatch_async(queue, ^{ lastErrorDisplay = nil; [self _connect]; }); } - (void)connectWait { dispatch_sync(queue, ^{ lastErrorDisplay = nil; [self _connect]; }); } - (void)_connect { if ([[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) { return; } if (![[MyIdentityStore sharedMyIdentityStore] isProvisioned]) { DDLogInfo(@"Cannot connect - missing identity or key"); [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Cannot connect - missing identity or key"]]; return; } if (self.connectionState == ConnectionStateDisconnecting) { // The socketDidDisconnect callback has not been called yet; ensure that we reconnect // as soon as the previous disconnect has finished. reconnectAttempts = 1; autoReconnect = YES; return; } else if (self.connectionState != ConnectionStateDisconnected) { if (self.connectionState == ConnectionStateLoggedIn) { return; } NSString *error = [NSString stringWithFormat:@"Cannot connect - invalid connection state (actual state: %u)", self.connectionState]; DDLogInfo(@"%@", error); [[ValidationLogger sharedValidationLogger] logString:error]; autoReconnect = YES; [self reconnectAfterDelay]; return; } if ([AppGroup amIActive] == NO) { DDLogInfo(@"Not active -> don't connect now, retry later"); [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Not active -> don't connect now, retry later"]]; // keep delay at constant rate to avoid too long waits when becoming active again reconnectAttempts = 1; autoReconnect = YES; [self reconnectAfterDelay]; return; } LicenseStore *licenseStore = [LicenseStore sharedLicenseStore]; if ([licenseStore isValid] == NO) { [licenseStore performLicenseCheckWithCompletion:^(BOOL success) { if (success) { [self connect]; } else { // don't show license warning for connection errors [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"License check failed: %@", licenseStore.error]]; if ([licenseStore.error.domain hasPrefix:@"NSURL"] == NO) { // License check failed permanently; need to inform user and ask for new license username/password [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationLicenseMissing object:nil]; } else { // License check failed due to connection error – try again later autoReconnect = YES; [self reconnectAfterDelay]; } } }]; return; } self.connectionState = ConnectionStateConnecting; autoReconnect = YES; self.lastRtt = -1; lastRead = CACurrentMediaTime(); serverInInitialQueueSend = YES; /* Generate a new key pair for the server connection. */ time_t uptime = [Utils systemUptime]; DDLogVerbose(@"System uptime is %ld", uptime); if (clientTempKeyPub == nil || clientTempKeySec == nil || uptime <= 0 || (uptime - clientTempKeyGenTime) > kClientTempKeyMaxAge) { NSData *publicKey, *secretKey; [[NaClCrypto sharedCrypto] generateKeyPairPublicKey:&publicKey secretKey:&secretKey]; clientTempKeyPub = publicKey; clientTempKeySec = secretKey; clientTempKeyGenTime = uptime; #if LOG_KEY_INFO DDLogVerbose(@"Client tempkey_pub = %@, tempkey_sec = %@", clientTempKeyPub, clientTempKeySec); #endif } /* Determine server host name */ NSString *serverHost; NSTimeInterval timeout = kConnectTimeout; if ([UserSettings sharedUserSettings].enableIPv6) { serverHost = [NSString stringWithFormat:@"%@%@%@", serverNamePrefixv6, [MyIdentityStore sharedMyIdentityStore].serverGroup, serverNameSuffix]; } else { serverHost = [NSString stringWithFormat:@"%@%@%@", serverNamePrefix, [MyIdentityStore sharedMyIdentityStore].serverGroup, serverNameSuffix]; } NSNumber* serverPort = [serverPorts objectAtIndex:curServerPortIndex]; socket = [GCDAsyncSocketFactory proxyAwareAsyncSocketForHost:serverHost port:serverPort delegate:self delegateQueue:queue]; if ([UserSettings sharedUserSettings].enableIPv6) { [socket setIPv4PreferredOverIPv6:NO]; } else { [socket setIPv4PreferredOverIPv6:YES]; } DDLogInfo(@"Connecting to %@:%@...", serverHost, serverPort); [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Connecting to %@:%@...", serverHost, serverPort]]; NSError *error; if (![socket connectToHost:serverHost onPort:[serverPort intValue] withTimeout:timeout error:&error]) { DDLogWarn(@"Connect failed: %@", error); [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Connect failed: %@", error]]; self.connectionState = ConnectionStateDisconnected; [self reconnectAfterDelay]; return; } /* Reset nonces for new connection */ clientNonce = 1; serverNonce = 1; } - (void)_disconnect { if (connectionState == ConnectionStateDisconnected) { return; } /* disconnect socket and make sure we don't reconnect */ autoReconnect = NO; self.connectionState = ConnectionStateDisconnecting; [self _disconnectSocketWithTimeout]; } - (void)_disconnectSocketWithTimeout { // Give the socket time for pending writes, but force disconnect if it takes too long for them to complete [socket disconnectAfterWriting]; GCDAsyncSocket *socketToDisconnect = socket; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDisconnectTimeout * NSEC_PER_SEC)), queue, ^{ if (socket == socketToDisconnect) { DDLogInfo(@"Socket still not disconnected - forcing disconnect now"); [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Socket still not disconnected - forcing disconnect now"]]; [socket disconnect]; } }); } - (void)disconnect { dispatch_async(queue, ^{ [self _disconnect]; }); } - (void)disconnectWait { dispatch_sync(queue, ^{ [self _disconnect]; }); [disconnectCondition lock]; if (connectionState != ConnectionStateDisconnected) { [disconnectCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:kDisconnectTimeout]]; } // Note: it's not guaranteed that the state is actually disconnected at this point, but it's good enough for our purposes [disconnectCondition unlock]; } - (void)reconnect { dispatch_async(queue, ^{ if (connectionState == ConnectionStateDisconnected) { [self _connect]; } else if (connectionState == ConnectionStateConnecting) { DDLogVerbose(@"Connection already in progress, not reconnecting"); } else { autoReconnect = YES; self.connectionState = ConnectionStateDisconnecting; [self _disconnectSocketWithTimeout]; } }); } - (void)setConnectionState:(enum ConnectionState)newConnectionState { [disconnectCondition lock]; connectionState = newConnectionState; if (connectionState == ConnectionStateDisconnected) { [disconnectCondition broadcast]; } [disconnectCondition unlock]; } - (void)processPayload:(struct pktPayload*)pl datalen:(int)datalen { switch (pl->type) { case PLTYPE_ECHO_REPLY: { self.lastRtt = CACurrentMediaTime() - lastEchoSendTime; if (datalen == sizeof(lastRcvdEchoSeq)) { memcpy(&lastRcvdEchoSeq, pl->data, sizeof(lastRcvdEchoSeq)); } else { DDLogError(@"Bad echo reply datalen %d", datalen); [socket disconnect]; break; } DDLogInfo(@"Received echo reply (seq %llu, RTT %.1f ms)", lastRcvdEchoSeq, self.lastRtt * 1000); break; } case PLTYPE_ERROR: { if (datalen < sizeof(struct plError)) { DDLogError(@"Bad error payload datalen %d", datalen); [socket disconnect]; break; } struct plError *plerr = (struct plError*)pl->data; NSData *errorMessageData = [NSData dataWithBytes:plerr->err_message length:datalen - sizeof(struct plError)]; NSString *errorMessage = [[NSString alloc] initWithData:errorMessageData encoding:NSUTF8StringEncoding]; DDLogError(@"Received error message from server: %@", errorMessage); if ([errorMessage rangeOfString:@"Another connection"].location != NSNotFound) { // extension took over connection if ([AppGroup amIActive] == NO) { break; } // ignore first few occurrences of "Another connection" messages to gracefully handle network switches if (anotherConnectionCount < 5) { anotherConnectionCount++; break; } } if (!plerr->reconnect_allowed) { autoReconnect = NO; } if (lastErrorDisplay == nil || ((-[lastErrorDisplay timeIntervalSinceNow]) > kErrorDisplayInterval)) { lastErrorDisplay = [NSDate date]; NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys: errorMessage, kKeyMessage, nil]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationErrorConnectionFailed object:nil userInfo:info]; } break; } case PLTYPE_ALERT: { NSData *alertData = [NSData dataWithBytes:pl->data length:datalen]; NSString *alertText = [[NSString alloc] initWithData:alertData encoding:NSUTF8StringEncoding]; [self displayServerAlert:alertText]; break; } case PLTYPE_OUTGOING_MESSAGE_ACK: { if (datalen != sizeof(struct plMessageAck)) { DDLogError(@"Bad ACK payload datalen %d", datalen); [socket disconnect]; break; } struct plMessageAck *ack = (struct plMessageAck*)pl->data; /* ignore from identity, as it must be ours */ NSData *messageId = [NSData dataWithBytes:ack->message_id length:kMessageIdLen]; [[MessageQueue sharedMessageQueue] processAck:messageId]; break; } case PLTYPE_INCOMING_MESSAGE: { if (datalen <= sizeof(struct plMessage)) { DDLogError(@"Bad message payload datalen %d", datalen); [socket disconnect]; break; } struct plMessage *plmsg = (struct plMessage*)pl->data; BoxedMessage *boxmsg = [[BoxedMessage alloc] init]; boxmsg.fromIdentity = [[NSString alloc] initWithData:[NSData dataWithBytes:plmsg->from_identity length:kIdentityLen] encoding:NSASCIIStringEncoding]; boxmsg.toIdentity = [[NSString alloc] initWithData:[NSData dataWithBytes:plmsg->to_identity length:kIdentityLen] encoding:NSASCIIStringEncoding]; boxmsg.messageId = [NSData dataWithBytes:plmsg->message_id length:kMessageIdLen]; boxmsg.date = [NSDate dateWithTimeIntervalSince1970:plmsg->date]; boxmsg.flags = plmsg->flags; char pushFromNameT[kPushFromNameLen+1]; memcpy(pushFromNameT, plmsg->push_from_name, kPushFromNameLen); pushFromNameT[kPushFromNameLen] = 0; boxmsg.pushFromName = [NSString stringWithCString:pushFromNameT encoding:NSUTF8StringEncoding]; boxmsg.nonce = [NSData dataWithBytes:plmsg->nonce length:kNonceLen]; boxmsg.box = [NSData dataWithBytes:plmsg->box length:(datalen - sizeof(struct plMessage))]; [MessageProcessorProxy processIncomingMessage:boxmsg receivedAfterInitialQueueSend:!serverInInitialQueueSend onCompletion:^{ [self completedProcessingMessage:boxmsg]; } onError:^(NSError *err) { [self failedProcessingMessage:boxmsg error:err]; }]; break; } case PLTYPE_QUEUE_SEND_COMPLETE: DDLogInfo(@"Queue send complete"); serverInInitialQueueSend = NO; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationQueueSendComplete object:nil userInfo:nil]; break; default: DDLogWarn(@"Unsupported payload type %d", pl->type); break; } } - (void)completedProcessingMessage:(BoxedMessage *)boxmsg { if (!(boxmsg.flags & MESSAGE_FLAG_NOACK)) { /* send ACK to server */ [self ackMessage:boxmsg.messageId fromIdentity:boxmsg.fromIdentity]; } } - (void)completedProcessingAbstractMessage:(AbstractGroupMessage *)abstractGroupMsg { uint8_t flags = abstractGroupMsg.flags.unsignedCharValue; if (!(flags & MESSAGE_FLAG_NOACK)) { /* send ACK to server */ [self ackMessage:abstractGroupMsg.messageId fromIdentity:abstractGroupMsg.fromIdentity]; } } - (void)failedProcessingMessage:(BoxedMessage *)boxmsg error:(NSError *)err { if (err.code == kBlockUnknownContactErrorCode) { DDLogVerbose(@"Message processing error due to block contacts - acking anyway"); [self ackMessage:boxmsg.messageId fromIdentity:boxmsg.fromIdentity]; } else if (err.code == kBadMessageErrorCode) { DDLogVerbose(@"Message processing error due to bad message format or decryption failure - acking anyway"); [self ackMessage:boxmsg.messageId fromIdentity:boxmsg.fromIdentity]; } else if (err.code == kMessageProcessingErrorCode) { DDLogError(@"Message processing error due to being unable to handle message: %@", err); } else { DDLogInfo(@"Could not process incoming message: %@", err); } } - (void)reconnectAfterDelay { if (!autoReconnect) { return; } /* calculate delay using bound exponential backoff */ float reconnectDelay = powf(kReconnectBaseInterval, MIN(reconnectAttempts - 1, 10)); if (reconnectDelay > kReconnectMaxInterval) { reconnectDelay = kReconnectMaxInterval; } if (!isWaitingForReconnect) { isWaitingForReconnect = true; [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Waiting %f seconds before reconnecting", reconnectDelay]]; reconnectAttempts++; DDLogInfo(@"Waiting %f seconds before reconnecting", reconnectDelay); dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, reconnectDelay * NSEC_PER_SEC); dispatch_after(popTime, queue, ^(void){ isWaitingForReconnect = false; [self _connect]; }); } } - (void)sendPayloadWithType:(uint8_t)type data:(NSData*)data { if (connectionState != ConnectionStateLoggedIn) { DDLogVerbose(@"Cannot send payload - not logged in"); return; } dispatch_async(queue, ^{ [self sendPayloadAsyncWithType:type data:data]; }); } - (void)sendPayloadAsyncWithType:(uint8_t)type data:(NSData*)data { /* Make encrypted box */ unsigned long pllen = sizeof(struct pktPayload) + data.length; struct pktPayload *pl = malloc(pllen); if (!pl) { return; } bzero(pl, pllen); pl->type = type; memcpy(pl->data, data.bytes, data.length); NSData *plData = [NSData dataWithBytesNoCopy:pl length:pllen]; NSData *nextClientNonce = [self nextClientNonce]; NSData *plBox = [[NaClCrypto sharedCrypto] encryptData:plData withPublicKey:serverTempKeyPub signKey:clientTempKeySec nonce:nextClientNonce]; if (plBox == nil) { DDLogError(@"Payload encryption failed!"); return; } /* prepend length - make one NSData object to pass to socket to ensure it is sent in a single TCP segment */ uint16_t pktlen = plBox.length; if (pktlen > kMaxPktLen) { DDLogError(@"Packet is too big (%d) - cannot send", pktlen); return; } NSMutableData *sendData = [NSMutableData dataWithCapacity:plBox.length + sizeof(uint16_t)]; [sendData appendBytes:&pktlen length:sizeof(uint16_t)]; [sendData appendData:plBox]; [socket writeData:sendData withTimeout:kWriteTimeout tag:TAG_PAYLOAD_SENT]; return; } - (void)sendMessage:(BoxedMessage*)message { unsigned long msglen = sizeof(struct plMessage) + message.box.length; struct plMessage *plmsg = malloc(msglen); if (!plmsg) { return; } DDLogInfo(@"Sending message from %@ to %@ (ID %@), box length %lu", message.fromIdentity, message.toIdentity, message.messageId, (unsigned long)message.box.length); memcpy(plmsg->from_identity, [message.fromIdentity dataUsingEncoding:NSASCIIStringEncoding].bytes, kIdentityLen); memcpy(plmsg->to_identity, [message.toIdentity dataUsingEncoding:NSASCIIStringEncoding].bytes, kIdentityLen); memcpy(plmsg->message_id, message.messageId.bytes, kMessageIdLen); plmsg->date = [message.date timeIntervalSince1970]; plmsg->flags = message.flags; plmsg->reserved[0] = 0; plmsg->reserved[1] = 0; plmsg->reserved[2] = 0; bzero(plmsg->push_from_name, kPushFromNameLen); if (message.pushFromName != nil) { NSData *encodedPushFromName = [Utils truncatedUTF8String:message.pushFromName maxLength:kPushFromNameLen]; strncpy(plmsg->push_from_name, encodedPushFromName.bytes, encodedPushFromName.length); } memcpy(plmsg->nonce, message.nonce.bytes, kNaClCryptoNonceSize); memcpy(plmsg->box, message.box.bytes, message.box.length); [self sendPayloadWithType:PLTYPE_OUTGOING_MESSAGE data:[NSData dataWithBytesNoCopy:plmsg length:msglen]]; } - (void)ackMessage:(NSData*)messageId fromIdentity:(NSString*)fromIdentity { int msglen = sizeof(struct plMessageAck); struct plMessageAck *plmsgack = malloc(msglen); if (!plmsgack) return; DDLogInfo(@"Sending ack for message ID %@ from %@", messageId, fromIdentity); memcpy(plmsgack->from_identity, [fromIdentity dataUsingEncoding:NSASCIIStringEncoding].bytes, kIdentityLen); memcpy(plmsgack->message_id, messageId.bytes, kMessageIdLen); [self sendPayloadWithType:PLTYPE_INCOMING_MESSAGE_ACK data:[NSData dataWithBytesNoCopy:plmsgack length:msglen]]; } - (void)ping { dispatch_async(queue, ^{ [self sendEchoRequest]; }); } - (void)sendEchoRequest { if (connectionState != ConnectionStateLoggedIn) return; lastSentEchoSeq++; DDLogInfo(@"Sending echo request (seq %llu)", lastSentEchoSeq); lastEchoSendTime = CACurrentMediaTime(); [self sendPayloadAsyncWithType:PLTYPE_ECHO_REQUEST data:[NSData dataWithBytes:&lastSentEchoSeq length:sizeof(lastSentEchoSeq)]]; GCDAsyncSocket *curSocket = socket; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kReadTimeout * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void){ if (curSocket == socket && lastRcvdEchoSeq < lastSentEchoSeq) { DDLogInfo(@"No reply to echo payload; disconnecting"); [socket disconnect]; } }); } - (void)cleanPushToken { if ([[AppGroup userDefaults] objectForKey:kPushNotificationDeviceToken] != nil) { [[AppGroup userDefaults] setObject:nil forKey:kPushNotificationDeviceToken]; [[AppGroup userDefaults] synchronize]; } [self sendPushToken]; [self sendPushAllowedIdentities]; [self sendPushSound]; } - (void)setVoIPPushToken:(NSData *)voIPPushToken { [[AppGroup userDefaults] setObject:voIPPushToken forKey:kVoIPPushNotificationDeviceToken]; [[AppGroup userDefaults] synchronize]; [self sendVoIPPushToken]; } - (void)sendPushToken { if ([self shouldRegisterPush] == NO) { return; } DDLogInfo(@"Sending push notification token"); uint8_t pushTokenType = PUSHTOKEN_TYPE_NONE; NSMutableData *payloadData = [NSMutableData dataWithBytes:&pushTokenType length:1]; [self sendPayloadWithType:PLTYPE_PUSH_NOTIFICATION_TOKEN data:payloadData]; } - (void)sendVoIPPushToken { if ([self shouldRegisterVoIP] == NO) { return; } NSData *voIPPushToken = [[AppGroup userDefaults] objectForKey:kVoIPPushNotificationDeviceToken]; DDLogInfo(@"Sending VoIP push notification token"); uint8_t voIPPushTokenType; #ifdef DEBUG voIPPushTokenType = PUSHTOKEN_TYPE_APPLE_SANDBOX; #else voIPPushTokenType = PUSHTOKEN_TYPE_APPLE_PROD; #endif NSMutableData *payloadData = [NSMutableData dataWithBytes:&voIPPushTokenType length:1]; [payloadData appendData:voIPPushToken]; [payloadData appendData:[@"|" dataUsingEncoding:NSUTF8StringEncoding]]; [payloadData appendData:[[[NSBundle mainBundle] bundleIdentifier] dataUsingEncoding:NSASCIIStringEncoding]]; [payloadData appendData:[@"|" dataUsingEncoding:NSUTF8StringEncoding]]; [payloadData appendData:[PushPayloadDecryptor pushEncryptionKey]]; [self sendPayloadWithType:PLTYPE_VOIP_PUSH_NOTIFICATION_TOKEN data:payloadData]; } - (void)sendPushAllowedIdentities { if ([self shouldRegisterPush] == NO) { return; } // Disable filter by allowing all IDs; we filter pushes in our own logic now DDLogInfo(@"Sending push allowed identities"); dispatch_async(dispatch_get_main_queue(), ^{ NSData *iddata = [NSData dataWithBytes:"\0" length:1]; DDLogVerbose(@"Sending allowed identities: %@", iddata); [self sendPayloadWithType:PLTYPE_PUSH_ALLOWED_IDENTITIES data:iddata]; }); } - (BOOL)shouldRegisterPush { if (connectionState != ConnectionStateLoggedIn) { return NO; } if ([AppGroup getCurrentType] != AppGroupTypeApp) { // only register within main app for pushes return NO; } return YES; } - (BOOL)shouldRegisterVoIP { if (connectionState != ConnectionStateLoggedIn) { return NO; } if ([[AppGroup userDefaults] objectForKey:kVoIPPushNotificationDeviceToken] == nil) { return NO; } if ([AppGroup getCurrentType] != AppGroupTypeApp) { // only register within main app for pushes return NO; } return YES; } - (void)sendPushSound{ if ([self shouldRegisterPush] == NO) { return; } NSString *pushSound = @""; DDLogInfo(@"Sending push sound: %@", pushSound); [self sendPayloadWithType:PLTYPE_PUSH_SOUND data:[pushSound dataUsingEncoding:NSASCIIStringEncoding]]; } - (void)sendPushGroupSound { if ([self shouldRegisterPush] == NO) { return; } NSString *pushGroupSound = @""; DDLogInfo(@"Sending push group sound: %@", pushGroupSound); [self sendPayloadWithType:PLTYPE_PUSH_GROUP_SOUND data:[pushGroupSound dataUsingEncoding:NSASCIIStringEncoding]]; } - (NSData*)nextClientNonce { char nonce[kNaClCryptoNonceSize]; memcpy(nonce, clientCookie.bytes, kCookieLen); memcpy(&nonce[kCookieLen], &clientNonce, sizeof(clientNonce)); clientNonce++; return [NSData dataWithBytes:nonce length:kNaClCryptoNonceSize]; } - (NSData*)nextServerNonce { char nonce[kNaClCryptoNonceSize]; memcpy(nonce, serverCookie.bytes, kCookieLen); memcpy(&nonce[kCookieLen], &serverNonce, sizeof(serverNonce)); serverNonce++; return [NSData dataWithBytes:nonce length:kNaClCryptoNonceSize]; } - (NSString*)nameForConnectionState:(enum ConnectionState)_connectionState { switch (_connectionState) { case ConnectionStateDisconnected: return @"disconnected"; case ConnectionStateConnecting: return @"connecting"; case ConnectionStateConnected: return @"connected"; case ConnectionStateLoggedIn: return @"loggedin"; case ConnectionStateDisconnecting: return @"disconnecting"; } return nil; } - (BOOL)isIPv6Connection { return [socket isIPv6]; } - (BOOL)isProxyConnection { return (socket != nil && ![socket isMemberOfClass:[GCDAsyncSocket class]]); } - (void)displayServerAlert:(NSString*)alertText { if ([displayedServerAlerts containsObject:alertText]) return; /* not shown before */ NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys: alertText, kKeyMessage, nil]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationServerMessage object:nil userInfo:info]; [displayedServerAlerts addObject:alertText]; } - (void)networkStatusDidChange:(NSNotification *)notice { NetworkStatus internetStatus = [internetReachability currentReachabilityStatus]; switch (internetStatus) { case NotReachable: DDLogInfo(@"Internet is not reachable"); [[ValidationLogger sharedValidationLogger] logString:@"Internet is not reachable"]; break; case ReachableViaWiFi: DDLogInfo(@"Internet is reachable via WiFi"); [[ValidationLogger sharedValidationLogger] logString:@"Internet is reachable via WiFi"]; break; case ReachableViaWWAN: DDLogInfo(@"Internet is reachable via WWAN"); [[ValidationLogger sharedValidationLogger] logString:@"Internet is reachable via WWAN"]; break; } if (internetStatus != lastInternetStatus) { DDLogInfo(@"Internet status changed - forcing reconnect"); [[ValidationLogger sharedValidationLogger] logString:@"Internet status changed - forcing reconnect"]; curServerPortIndex = 0; [self reconnect]; lastInternetStatus = internetStatus; } } - (void)setServerPorts:(NSArray *)ports { serverPorts = ports; } - (void)sendPushOverrideTimeout { DDLogInfo(@"Sending set push override timeout"); NSUserDefaults *defaults = [AppGroup userDefaults]; NSDate *lastSendDate = [defaults objectForKey:kLastPushOverrideSendDate]; if (lastSendDate == nil) { [self setPushOverrideTimeout]; } else { NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitMinute fromDate:[NSDate date] toDate:lastSendDate options:0]; NSInteger minutes = [components minute]; if (minutes > 60 || lastSendDate == nil) { [self setPushOverrideTimeout]; } } } - (void)setPushOverrideTimeout { NSUserDefaults *defaults = [AppGroup userDefaults]; NSTimeInterval secondsInEightHours = 8 * 60 * 60; NSDate *pushOverrideEndDate = [[NSDate date] dateByAddingTimeInterval:secondsInEightHours]; uint64_t timestamp = [pushOverrideEndDate timeIntervalSince1970]; NSData *payloadData = [NSData dataWithBytes:×tamp length:sizeof(timestamp)]; [self sendPayloadWithType:PLTYPE_PUSH_OVERRIDE_TIMEOUT data:payloadData]; [defaults setObject:[NSDate date] forKey:kLastPushOverrideSendDate]; [defaults synchronize]; } - (void)resetPushOverrideTimeout { DDLogInfo(@"Reset push override timeout"); NSUserDefaults *defaults = [AppGroup userDefaults]; uint64_t timestamp = 0; NSData *data = [NSData dataWithBytes:×tamp length:sizeof(timestamp)]; [self sendPayloadWithType:PLTYPE_PUSH_OVERRIDE_TIMEOUT data:data]; [defaults setObject:nil forKey:kLastPushOverrideSendDate]; [defaults synchronize]; } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sender didConnectToHost:(NSString *)host port:(UInt16)port { if (sender != socket) { DDLogWarn(@"didConnectToHost from old socket"); return; } DDLogInfo(@"Connected to %@:%d", host, port); [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Connected to %@:%d", host, port]]; self.connectionState = ConnectionStateConnected; /* Send client hello packet with temporary public key and client cookie */ clientCookie = [[NaClCrypto sharedCrypto] randomBytes:kCookieLen]; DDLogVerbose(@"Client cookie = %@", clientCookie); /* Make sure to pass everything in one writeData call, or we will get two separate TCP segments */ struct pktClientHello clientHello; memcpy(clientHello.client_tempkey_pub, clientTempKeyPub.bytes, sizeof(clientHello.client_tempkey_pub)); memcpy(clientHello.client_cookie, clientCookie.bytes, sizeof(clientHello.client_cookie)); [socket writeData:[NSData dataWithBytes:&clientHello length:sizeof(clientHello)] withTimeout:kWriteTimeout tag:TAG_CLIENT_HELLO_SENT]; /* Prepare to receive server hello packet */ [socket readDataToLength:sizeof(struct pktServerHello) withTimeout:kReadTimeout tag:TAG_SERVER_HELLO_READ]; } - (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag { //DDLogVerbose(@"Read data (%d bytes) with tag: %ld", data.length, tag); if (sender != socket) { DDLogWarn(@"didReadData from old socket"); return; } switch (tag) { case TAG_SERVER_HELLO_READ: { DDLogVerbose(@"Got server hello!"); const struct pktServerHello* serverHello = data.bytes; serverCookie = [NSData dataWithBytes:serverHello->server_cookie length:sizeof(serverHello->server_cookie)]; DDLogVerbose(@"Server cookie = %@", serverCookie); /* decrypt server hello box */ chosenServerKeyPub = serverKeyPub; NSData *serverHelloBox = [NSData dataWithBytes:serverHello->box length:sizeof(serverHello->box)]; NSData *nonce = [self nextServerNonce]; NSData *serverHelloBoxOpen = [[NaClCrypto sharedCrypto] decryptData:serverHelloBox withSecretKey:clientTempKeySec signKey:chosenServerKeyPub nonce:nonce]; if (serverHelloBoxOpen == nil) { /* try alternate key */ chosenServerKeyPub = serverAltKeyPub; serverHelloBoxOpen = [[NaClCrypto sharedCrypto] decryptData:serverHelloBox withSecretKey:clientTempKeySec signKey:chosenServerKeyPub nonce:nonce]; if (serverHelloBoxOpen == nil) { DDLogError(@"Decryption of server hello box failed"); [socket disconnect]; return; } else { DDLogWarn(@"Using alternate server key!"); } } const struct pktServerHelloBox *serverHelloBoxU = (struct pktServerHelloBox*)serverHelloBoxOpen.bytes; /* verify client cookie */ NSData *clientCookieFromServer = [NSData dataWithBytes:serverHelloBoxU->client_cookie length:sizeof(serverHelloBoxU->client_cookie)]; if (![clientCookieFromServer isEqualToData:clientCookie]) { DDLogError(@"Client cookie mismatch (mine: %@, server: %@)", clientCookie, clientCookieFromServer); [socket disconnect]; return; } /* copy temporary server key */ serverTempKeyPub = [NSData dataWithBytes:serverHelloBoxU->server_tempkey_pub length:sizeof(serverHelloBoxU->server_tempkey_pub)]; DDLogInfo(@"Server hello successful, tempkey_pub = %@", serverTempKeyPub); /* now prepare login packet */ NSData *vouchNonce = [[NaClCrypto sharedCrypto] randomBytes:kNaClCryptoNonceSize]; struct pktLogin login; memcpy(login.identity, [[MyIdentityStore sharedMyIdentityStore].identity dataUsingEncoding:NSASCIIStringEncoding].bytes, kIdentityLen); bzero(login.client_version, kClientVersionLen); NSData *clientVersion = [[Utils getClientVersion] dataUsingEncoding:NSASCIIStringEncoding]; memcpy(login.client_version, clientVersion.bytes, MIN(clientVersion.length, kClientVersionLen)); memcpy(login.server_cookie, serverCookie.bytes, kCookieLen); memcpy(login.vouch_nonce, vouchNonce.bytes, kNaClCryptoNonceSize); /* vouch subpacket */ struct pktVouch vouch; memcpy(vouch.client_tempkey_pub, clientTempKeyPub.bytes, kNaClCryptoPubKeySize); NSData *vouchBox = [[MyIdentityStore sharedMyIdentityStore] encryptData:[NSData dataWithBytes:&vouch length:sizeof(vouch)] withNonce:vouchNonce publicKey:chosenServerKeyPub]; memcpy(login.vouch_box, vouchBox.bytes, sizeof(login.vouch_box)); /* encrypt login packet */ NSData *loginBox = [[NaClCrypto sharedCrypto] encryptData:[NSData dataWithBytes:&login length:sizeof(login)] withPublicKey:serverTempKeyPub signKey:clientTempKeySec nonce:[self nextClientNonce]]; /* send it! */ [socket writeData:loginBox withTimeout:kWriteTimeout tag:TAG_LOGIN_SENT]; /* Prepare to receive login ack packet */ [socket readDataToLength:sizeof(struct pktLoginAck) + kNaClBoxOverhead withTimeout:kReadTimeout tag:TAG_LOGIN_ACK_READ]; break; } case TAG_LOGIN_ACK_READ: { DDLogInfo(@"Login ack received"); lastRead = CACurrentMediaTime(); /* decrypt server hello box */ NSData *loginAckBox = data; loginAckBox = [[NaClCrypto sharedCrypto] decryptData:loginAckBox withSecretKey:clientTempKeySec signKey:serverTempKeyPub nonce:[self nextServerNonce]]; if (loginAckBox == nil) { DDLogError(@"Decryption of login ack failed"); [socket disconnect]; return; } /* Don't care about the contents of the login ACK for now; it only needs to decrypt correctly */ reconnectAttempts = 0; self.connectionState = ConnectionStateLoggedIn; /* Clean and send nil push token info */ [self cleanPushToken]; /* Send voIP push token info */ [self sendVoIPPushToken]; /* Schedule task for keepalive */ keepalive_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_source_set_event_handler(keepalive_timer, ^{ [self sendEchoRequest]; }); dispatch_source_set_timer(keepalive_timer, dispatch_time(DISPATCH_TIME_NOW, kKeepAliveInterval * NSEC_PER_SEC), kKeepAliveInterval * NSEC_PER_SEC, NSEC_PER_SEC); dispatch_resume(keepalive_timer); /* Receive next payload header */ [socket readDataToLength:sizeof(uint16_t) withTimeout:-1 tag:TAG_PAYLOAD_LENGTH_READ]; break; } case TAG_PAYLOAD_LENGTH_READ: { uint16_t msglen = *((uint16_t*)data.bytes); [socket readDataToLength:msglen withTimeout:-1 tag:TAG_PAYLOAD_READ]; break; } case TAG_PAYLOAD_READ: { DDLogVerbose(@"Payload (%lu bytes) received", (unsigned long)data.length); lastRead = CACurrentMediaTime(); dispatch_source_set_timer(keepalive_timer, dispatch_time(DISPATCH_TIME_NOW, kKeepAliveInterval * NSEC_PER_SEC), kKeepAliveInterval * NSEC_PER_SEC, NSEC_PER_SEC); /* Decrypt payload */ NSData *plData = [[NaClCrypto sharedCrypto] decryptData:data withSecretKey:clientTempKeySec signKey:serverTempKeyPub nonce:[self nextServerNonce]]; if (plData == nil) { DDLogError(@"Payload decryption failed"); [socket disconnect]; return; } struct pktPayload *pl = (struct pktPayload*)plData.bytes; int datalen = (int)plData.length - (int)sizeof(struct pktPayload); DDLogInfo(@"Decrypted payload (type %02x, data %@)", pl->type, [NSData dataWithBytes:pl->data length:datalen]); [self processPayload:pl datalen:datalen]; /* Receive next payload header */ [socket readDataToLength:sizeof(uint16_t) withTimeout:-1 tag:TAG_PAYLOAD_LENGTH_READ]; break; } } } - (void)socketDidDisconnect:(GCDAsyncSocket *)sender withError:(NSError *)error { [[ValidationLogger sharedValidationLogger] logString:@"socketDidDisconnect called"]; if (sender != socket) { DDLogWarn(@"socketDidDisconnect from old socket"); [[ValidationLogger sharedValidationLogger] logString:@"socketDidDisconnect from old socket"]; return; } if (error != nil) { DDLogWarn(@"Socket disconnected, error = %@", error); [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Socket disconnected, error = %@", error]]; /* try next port */ curServerPortIndex++; if (curServerPortIndex >= serverPorts.count) curServerPortIndex = 0; } self.connectionState = ConnectionStateDisconnected; if (keepalive_timer != nil) { dispatch_source_cancel(keepalive_timer); keepalive_timer = nil; } socket = nil; [self reconnectAfterDelay]; } - (NSTimeInterval)socket:(GCDAsyncSocket *)sender shouldTimeoutReadWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length { if (sender != socket) { DDLogWarn(@"shouldTimeoutReadWithTag from old socket"); return 0; } DDLogInfo(@"Read timeout, tag = %ld", tag); [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Read timeout"]]; [socket disconnect]; return 0; } - (NSTimeInterval)socket:(GCDAsyncSocket *)sender shouldTimeoutWriteWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length { if (sender != socket) { DDLogWarn(@"shouldTimeoutWriteWithTag from old socket"); return 0; } DDLogInfo(@"Write timeout, tag = %ld", tag); [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Write timeout"]]; [socket disconnect]; return 0; } #pragma mark - Notifications - (void)identityCreated:(NSNotification*)notification { /* when the identity is created, we should connect */ [self connect]; } - (void)identityDestroyed:(NSNotification*)notification { /* when the identity is destroyed, we must disconnect */ if (connectionState != ConnectionStateDisconnected) { DDLogInfo(@"Disconnecting because identity destroyed"); /* Clear push token on server now to reduce occurrence of push messages being delivered to devices that don't use that particular identity anymore */ DDLogInfo(@"Clearing push notification token"); uint8_t pushTokenType; #ifdef DEBUG pushTokenType = PUSHTOKEN_TYPE_APPLE_SANDBOX_MC; #else pushTokenType = PUSHTOKEN_TYPE_APPLE_PROD_MC; #endif NSMutableData *payloadData = [NSMutableData dataWithBytes:&pushTokenType length:1]; NSData *pushToken = [[NaClCrypto sharedCrypto] zeroBytes:32]; [payloadData appendData:pushToken]; [self sendPayloadWithType:PLTYPE_PUSH_NOTIFICATION_TOKEN data:payloadData]; DDLogInfo(@"Sending VoIP push notification token"); uint8_t voIPPushTokenType; #ifdef DEBUG voIPPushTokenType = PUSHTOKEN_TYPE_APPLE_SANDBOX; #else voIPPushTokenType = PUSHTOKEN_TYPE_APPLE_PROD; #endif NSMutableData *voipPayloadData = [NSMutableData dataWithBytes:&voIPPushTokenType length:1]; NSData *voipPushToken = [[NaClCrypto sharedCrypto] zeroBytes:32]; [voipPayloadData appendData:voipPushToken]; [self sendPayloadWithType:PLTYPE_VOIP_PUSH_NOTIFICATION_TOKEN data:voipPayloadData]; [self disconnect]; } /* destroy temporary keys, as we cannot reuse them for the new identity */ dispatch_async(queue, ^{ clientTempKeyPub = nil; clientTempKeySec = nil; }); /* also flush the queue so that messages stuck in it don't later cause problems because they have the wrong from identity */ [[MessageQueue sharedMessageQueue] flush]; } @end