From dedfea78da196cea0f8bdb7f30695af256f38f60 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 20 Jul 2018 09:25:53 -0600 Subject: [PATCH 1/4] callback handlers for remote attestation --- .../src/Contacts/ContactDiscoveryService.h | 5 +- .../src/Contacts/ContactDiscoveryService.m | 48 ++++++++++++++----- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/SignalServiceKit/src/Contacts/ContactDiscoveryService.h b/SignalServiceKit/src/Contacts/ContactDiscoveryService.h index 929e7bfcb..79cb40e60 100644 --- a/SignalServiceKit/src/Contacts/ContactDiscoveryService.h +++ b/SignalServiceKit/src/Contacts/ContactDiscoveryService.h @@ -4,6 +4,8 @@ NS_ASSUME_NONNULL_BEGIN +@class RemoteAttestation; + @interface ContactDiscoveryService : NSObject - (instancetype)init NS_UNAVAILABLE; @@ -11,7 +13,8 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)sharedService; - (void)testService; - +- (void)performRemoteAttestationWithSuccess:(void (^)(RemoteAttestation *_Nonnull remoteAttestation))successHandler + failure:(void (^)(NSError *_Nonnull error))failureHandler; @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Contacts/ContactDiscoveryService.m b/SignalServiceKit/src/Contacts/ContactDiscoveryService.m index 2acf7e5d5..f034b680b 100644 --- a/SignalServiceKit/src/Contacts/ContactDiscoveryService.m +++ b/SignalServiceKit/src/Contacts/ContactDiscoveryService.m @@ -8,6 +8,7 @@ #import "Cryptography.h" #import "NSData+OWS.h" #import "NSDate+OWS.h" +#import "OWSError.h" #import "OWSRequestFactory.h" #import "TSNetworkManager.h" #import @@ -219,34 +220,48 @@ NS_ASSUME_NONNULL_BEGIN - (void)testService { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self performRemoteAttestation]; + [self + performRemoteAttestationWithSuccess:^(RemoteAttestation *_Nonnull remoteAttestation) { + DDLogDebug(@"%@ in %s succeeded", self.logTag, __PRETTY_FUNCTION__); + } + failure:^(NSError *_Nonnull error) { + DDLogDebug(@"%@ in %s failed with error: %@", self.logTag, __PRETTY_FUNCTION__, error); + }]; }); } -- (void)performRemoteAttestation +- (void)performRemoteAttestationWithSuccess:(void (^)(RemoteAttestation *_Nonnull remoteAttestation))successHandler + failure:(void (^)(NSError *_Nonnull error))failureHandler { - [self performRemoteAttestationAuth]; + [self + getRemoteAttestationAuthWithSuccess:^(RemoteAttestationAuth *_Nonnull auth) { + [self performRemoteAttestationWithAuth:auth success:successHandler failure:failureHandler]; + } + failure:failureHandler]; } -// TODO: Add success and failure? -- (void)performRemoteAttestationAuth +- (void)getRemoteAttestationAuthWithSuccess:(void (^)(RemoteAttestationAuth *))successHandler + failure:(void (^)(NSError *_Nonnull error))failureHandler { TSRequest *request = [OWSRequestFactory remoteAttestationAuthRequest]; [[TSNetworkManager sharedManager] makeRequest:request success:^(NSURLSessionDataTask *task, id responseDict) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ RemoteAttestationAuth *_Nullable auth = [self parseAuthToken:responseDict]; if (!auth) { DDLogError(@"%@ remote attestation auth could not be parsed: %@", self.logTag, responseDict); + NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); + failureHandler(error); return; } - [self performRemoteAttestationWithAuth:auth]; + + successHandler(auth); }); } failure:^(NSURLSessionDataTask *task, NSError *error) { NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response; DDLogVerbose(@"%@ remote attestation auth failure: %zd", self.logTag, response.statusCode); + failureHandler(error); }]; } @@ -276,6 +291,8 @@ NS_ASSUME_NONNULL_BEGIN } - (void)performRemoteAttestationWithAuth:(RemoteAttestationAuth *)auth + success:(void (^)(RemoteAttestation *_Nonnull remoteAttestation))successHandler + failure:(void (^)(NSError *_Nonnull error))failureHandler { ECKeyPair *keyPair = [Curve25519 generateKeyPair]; @@ -290,15 +307,24 @@ NS_ASSUME_NONNULL_BEGIN success:^(NSURLSessionDataTask *task, id responseJson) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // TODO: Handle result. - [self parseAttestationResponseJson:responseJson - response:task.response - keyPair:keyPair - enclaveId:enclaveId]; + RemoteAttestation *_Nullable attestation = [self parseAttestationResponseJson:responseJson + response:task.response + keyPair:keyPair + enclaveId:enclaveId]; + + if (!attestation) { + NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); + failureHandler(error); + return; + } + + successHandler(attestation); }); } failure:^(NSURLSessionDataTask *task, NSError *error) { NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response; DDLogVerbose(@"%@ remote attestation failure: %zd", self.logTag, response.statusCode); + failureHandler(error); }]; } From a611625691b80a30bcf8810103aba7b6f8e2b574 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 20 Jul 2018 13:34:02 -0600 Subject: [PATCH 2/4] fixup lookup threading --- SignalServiceKit/src/Contacts/ContactsUpdater.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SignalServiceKit/src/Contacts/ContactsUpdater.m b/SignalServiceKit/src/Contacts/ContactsUpdater.m index f89157f3c..8ac744224 100644 --- a/SignalServiceKit/src/Contacts/ContactsUpdater.m +++ b/SignalServiceKit/src/Contacts/ContactsUpdater.m @@ -108,7 +108,9 @@ NS_ASSUME_NONNULL_BEGIN } }]; - success([recipients copy]); + dispatch_async(dispatch_get_main_queue(), ^{ + success([recipients copy]); + }); }); } From b42f5287134fab89384ae7400d326f475da2c626 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 20 Jul 2018 08:45:46 -0600 Subject: [PATCH 3/4] Integrate with new contact discovery endpoint Also: * use system cookie parsing * add AESGCM additional authenticated data parameter // FREEBIE --- Pods | 2 +- Signal.xcodeproj/project.pbxproj | 4 + .../ContactDiscoveryOperationTest.swift | 60 ++++++ SignalMessaging/profiles/OWSProfileManager.m | 4 +- .../src/Contacts/ContactDiscoveryService.h | 25 ++- .../src/Contacts/ContactDiscoveryService.m | 75 ++++--- .../OWSContactDiscoveryOperation.swift | 197 +++++++++++++++++- .../API/Requests/CDSAttestationRequest.h | 24 --- .../API/Requests/CDSAttestationRequest.m | 29 --- .../Network/API/Requests/OWSRequestFactory.h | 14 +- .../Network/API/Requests/OWSRequestFactory.m | 62 ++++-- .../src/Network/API/Requests/TSRequest.h | 2 + .../src/Network/API/Requests/TSRequest.m | 2 + .../src/Network/API/TSNetworkManager.m | 20 +- SignalServiceKit/src/Util/Cryptography.h | 26 ++- SignalServiceKit/src/Util/Cryptography.m | 124 ++++++++--- 16 files changed, 524 insertions(+), 146 deletions(-) create mode 100644 Signal/test/contact/ContactDiscoveryOperationTest.swift delete mode 100644 SignalServiceKit/src/Network/API/Requests/CDSAttestationRequest.h delete mode 100644 SignalServiceKit/src/Network/API/Requests/CDSAttestationRequest.m diff --git a/Pods b/Pods index a2394bbaf..5dc9c23dc 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit a2394bbafc099db434ee91e7a617c412750c44b9 +Subproject commit 5dc9c23dc3229ab6a884372a0e2cf62cb0904be6 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index f93c298df..f6ae7640f 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -433,6 +433,7 @@ 4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF51208B82E9007B4E76 /* ThreadViewModel.swift */; }; 4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */; }; 4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */; }; + 4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */; }; 4C6F527C20FFE8400097DEEE /* SignalUBSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */; }; 4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; }; 4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; }; @@ -1112,6 +1113,7 @@ 4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = ""; }; 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearchViewController.swift; sourceTree = ""; }; 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableTextField.swift; sourceTree = ""; }; + 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContactDiscoveryOperationTest.swift; path = contact/ContactDiscoveryOperationTest.swift; sourceTree = ""; }; 4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalUBSan.supp; sourceTree = ""; }; 4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = ""; }; 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationConfigurationSyncOperation.swift; sourceTree = ""; }; @@ -2072,6 +2074,7 @@ 458E38381D6699110094BD24 /* Models */ = { isa = PBXGroup; children = ( + 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */, 458E38391D6699FA0094BD24 /* OWSDeviceProvisioningURLParserTest.m */, 458967101DC117CC00E9DD21 /* AccountManagerTest.swift */, ); @@ -3452,6 +3455,7 @@ B660F6DB1C29868000687D6E /* FunctionalUtilTest.m in Sources */, 45E7A6A81E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift in Sources */, 452D1AF12081059C00A67F7F /* StringAdditionsTest.swift in Sources */, + 4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */, B660F6BB1C29868000687D6E /* OWSContactsManagerTest.m in Sources */, B660F6D21C29868000687D6E /* PushManagerTest.m in Sources */, 455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */, diff --git a/Signal/test/contact/ContactDiscoveryOperationTest.swift b/Signal/test/contact/ContactDiscoveryOperationTest.swift new file mode 100644 index 000000000..75c76b5c2 --- /dev/null +++ b/Signal/test/contact/ContactDiscoveryOperationTest.swift @@ -0,0 +1,60 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import XCTest +@testable import SignalServiceKit + +class ContactDiscoveryOperationTest: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func tesBoolArrayFromEmptyData() { + let data = Data() + let bools = CDSBatchOperation.boolArray(data: data) + XCTAssert(bools == []) + } + + func testBoolArrayFromFalseByte() { + let data = Data(repeating: 0x00, count: 4) + let bools = CDSBatchOperation.boolArray(data: data) + XCTAssert(bools == [false, false, false, false]) + } + + func testBoolArrayFromTrueByte() { + let data = Data(repeating: 0x01, count: 4) + let bools = CDSBatchOperation.boolArray(data: data) + XCTAssert(bools == [true, true, true, true]) + } + + func testBoolArrayFromMixedBytes() { + let data = Data(bytes: [0x01, 0x00, 0x01, 0x01]) + let bools = CDSBatchOperation.boolArray(data: data) + XCTAssert(bools == [true, false, true, true]) + } + + func testEncodeNumber() { + let recipientIds = [ "+1011" ] + let actual = try! CDSBatchOperation.encodePhoneNumbers(recipientIds: recipientIds) + let expected: Data = Data(bytes: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf3]) + + XCTAssertEqual(expected, actual) + } + + func testEncodeMultipleNumber() { + let recipientIds = [ "+1011", "+15551231234"] + let actual = try! CDSBatchOperation.encodePhoneNumbers(recipientIds: recipientIds) + let expected: Data = Data(bytes: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf3, + 0x00, 0x00, 0x00, 0x03, 0x9e, 0xec, 0xf5, 0x02]) + + XCTAssertEqual(expected, actual) + } +} diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m index 246819050..3f006061d 100644 --- a/SignalMessaging/profiles/OWSProfileManager.m +++ b/SignalMessaging/profiles/OWSProfileManager.m @@ -994,7 +994,7 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640; return nil; } - return [Cryptography encryptAESGCMWithData:encryptedData key:profileKey]; + return [Cryptography encryptAESGCMWithProfileData:encryptedData key:profileKey]; } - (nullable NSData *)decryptProfileData:(nullable NSData *)encryptedData profileKey:(OWSAES256Key *)profileKey @@ -1005,7 +1005,7 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640; return nil; } - return [Cryptography decryptAESGCMWithData:encryptedData key:profileKey]; + return [Cryptography decryptAESGCMWithProfileData:encryptedData key:profileKey]; } - (nullable NSString *)decryptProfileNameData:(nullable NSData *)encryptedData profileKey:(OWSAES256Key *)profileKey diff --git a/SignalServiceKit/src/Contacts/ContactDiscoveryService.h b/SignalServiceKit/src/Contacts/ContactDiscoveryService.h index 79cb40e60..7701eece7 100644 --- a/SignalServiceKit/src/Contacts/ContactDiscoveryService.h +++ b/SignalServiceKit/src/Contacts/ContactDiscoveryService.h @@ -4,7 +4,30 @@ NS_ASSUME_NONNULL_BEGIN -@class RemoteAttestation; +@class ECKeyPair; +@class OWSAES256Key; + +@interface RemoteAttestationKeys : NSObject + +@property (nonatomic, readonly) ECKeyPair *keyPair; +@property (nonatomic, readonly) NSData *serverEphemeralPublic; +@property (nonatomic, readonly) NSData *serverStaticPublic; + +@property (nonatomic, readonly) OWSAES256Key *clientKey; +@property (nonatomic, readonly) OWSAES256Key *serverKey; + +@end + +@interface RemoteAttestation : NSObject + +@property (nonatomic, readonly) RemoteAttestationKeys *keys; +@property (nonatomic, readonly) NSArray *cookies; +@property (nonatomic, readonly) NSData *requestId; +@property (nonatomic, readonly) NSString *enclaveId; +@property (nonatomic, readonly) NSString *authUsername; +@property (nonatomic, readonly) NSString *authToken; + +@end @interface ContactDiscoveryService : NSObject diff --git a/SignalServiceKit/src/Contacts/ContactDiscoveryService.m b/SignalServiceKit/src/Contacts/ContactDiscoveryService.m index f034b680b..0498fee2b 100644 --- a/SignalServiceKit/src/Contacts/ContactDiscoveryService.m +++ b/SignalServiceKit/src/Contacts/ContactDiscoveryService.m @@ -31,14 +31,14 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - -@interface RemoteAttestationKeys : NSObject +@interface RemoteAttestationKeys () @property (nonatomic) ECKeyPair *keyPair; @property (nonatomic) NSData *serverEphemeralPublic; @property (nonatomic) NSData *serverStaticPublic; -@property (nonatomic) NSData *clientKey; -@property (nonatomic) NSData *serverKey; +@property (nonatomic) OWSAES256Key *clientKey; +@property (nonatomic) OWSAES256Key *serverKey; @end @@ -74,7 +74,8 @@ NS_ASSUME_NONNULL_BEGIN NSData *_Nullable derivedMaterial; @try { - derivedMaterial = [HKDFKit deriveKey:masterSecret info:nil salt:publicKeys outputSize:ECCKeyLength * 2]; + derivedMaterial = + [HKDFKit deriveKey:masterSecret info:nil salt:publicKeys outputSize:(int)kAES256_KeyByteLength * 2]; } @catch (NSException *exception) { DDLogError(@"%@ could not derive service key: %@", self.logTag, exception); return NO; @@ -84,17 +85,23 @@ NS_ASSUME_NONNULL_BEGIN OWSProdLogAndFail(@"%@ missing derived service key.", self.logTag); return NO; } - if (derivedMaterial.length != ECCKeyLength * 2) { + if (derivedMaterial.length != kAES256_KeyByteLength * 2) { OWSProdLogAndFail(@"%@ derived service key has unexpected length.", self.logTag); return NO; } - NSData *_Nullable clientKey = [derivedMaterial subdataWithRange:NSMakeRange(ECCKeyLength * 0, ECCKeyLength)]; - NSData *_Nullable serverKey = [derivedMaterial subdataWithRange:NSMakeRange(ECCKeyLength * 1, ECCKeyLength)]; - if (clientKey.length != ECCKeyLength) { + + NSData *_Nullable clientKeyData = + [derivedMaterial subdataWithRange:NSMakeRange(kAES256_KeyByteLength * 0, kAES256_KeyByteLength)]; + OWSAES256Key *_Nullable clientKey = [OWSAES256Key keyWithData:clientKeyData]; + if (!clientKey) { OWSProdLogAndFail(@"%@ clientKey has unexpected length.", self.logTag); return NO; } - if (serverKey.length != ECCKeyLength) { + + NSData *_Nullable serverKeyData = + [derivedMaterial subdataWithRange:NSMakeRange(kAES256_KeyByteLength * 1, kAES256_KeyByteLength)]; + OWSAES256Key *_Nullable serverKey = [OWSAES256Key keyWithData:serverKeyData]; + if (!serverKey) { OWSProdLogAndFail(@"%@ serverKey has unexpected length.", self.logTag); return NO; } @@ -109,12 +116,13 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - -@interface RemoteAttestation : NSObject +@interface RemoteAttestation () @property (nonatomic) RemoteAttestationKeys *keys; -// TODO: Do we need to support multiple cookies? -@property (nonatomic) NSString *cookie; +@property (nonatomic) NSArray *cookies; @property (nonatomic) NSData *requestId; +@property (nonatomic) NSString *enclaveId; +@property (nonatomic) RemoteAttestationAuth *auth; @end @@ -122,6 +130,16 @@ NS_ASSUME_NONNULL_BEGIN @implementation RemoteAttestation +- (NSString *)authUsername +{ + return self.auth.username; +} + +- (NSString *)authToken +{ + return self.auth.authToken; +} + @end #pragma mark - @@ -301,16 +319,17 @@ NS_ASSUME_NONNULL_BEGIN TSRequest *request = [OWSRequestFactory remoteAttestationRequest:keyPair enclaveId:enclaveId - username:auth.username - authToken:auth.authToken]; + authUsername:auth.username + authPassword:auth.authToken]; + [[TSNetworkManager sharedManager] makeRequest:request success:^(NSURLSessionDataTask *task, id responseJson) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - // TODO: Handle result. RemoteAttestation *_Nullable attestation = [self parseAttestationResponseJson:responseJson response:task.response keyPair:keyPair - enclaveId:enclaveId]; + enclaveId:enclaveId + auth:auth]; if (!attestation) { NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); @@ -332,6 +351,7 @@ NS_ASSUME_NONNULL_BEGIN response:(NSURLResponse *)response keyPair:(ECKeyPair *)keyPair enclaveId:(NSString *)enclaveId + auth:(RemoteAttestationAuth *)auth { OWSAssert(responseJson); OWSAssert(response); @@ -342,22 +362,14 @@ NS_ASSUME_NONNULL_BEGIN OWSProdLogAndFail(@"%@ unexpected response type.", self.logTag); return nil; } - NSDictionary *responseHeaders = ((NSHTTPURLResponse *)response).allHeaderFields; - - NSString *_Nullable cookie = [responseHeaders stringForKey:@"Set-Cookie"]; - if (cookie.length < 1) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + NSArray *cookies = + [NSHTTPCookie cookiesWithResponseHeaderFields:httpResponse.allHeaderFields forURL:[NSURL new]]; + if (cookies.count < 1) { OWSProdLogAndFail(@"%@ couldn't parse cookie.", self.logTag); return nil; } - // The cookie header will have this form: - // Set-Cookie: __NSCFString, c2131364675-413235ic=c1656171-249545-958227; Path=/; Secure - // We want to strip everything after the semicolon (;). - NSRange cookieRange = [cookie rangeOfString:@";"]; - if (cookieRange.length != NSNotFound) { - cookie = [cookie substringToIndex:cookieRange.location]; - } - if (![responseJson isKindOfClass:[NSDictionary class]]) { return nil; } @@ -450,9 +462,11 @@ NS_ASSUME_NONNULL_BEGIN } RemoteAttestation *result = [RemoteAttestation new]; - result.cookie = cookie; + result.cookies = cookies; result.keys = keys; result.requestId = requestId; + result.enclaveId = enclaveId; + result.auth = auth; DDLogVerbose(@"%@ remote attestation complete.", self.logTag); @@ -648,13 +662,14 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(encryptedRequestTag.length > 0); OWSAssert(keys); - OWSAES256Key *_Nullable key = [OWSAES256Key keyWithData:keys.serverKey]; + OWSAES256Key *_Nullable key = keys.serverKey; if (!key) { OWSProdLogAndFail(@"%@ invalid server key.", self.logTag); return nil; } NSData *_Nullable decryptedData = [Cryptography decryptAESGCMWithInitializationVector:encryptedRequestIv ciphertext:encryptedRequestId + additionalAuthenticatedData:nil authTag:encryptedRequestTag key:key]; if (!decryptedData) { diff --git a/SignalServiceKit/src/Contacts/OWSContactDiscoveryOperation.swift b/SignalServiceKit/src/Contacts/OWSContactDiscoveryOperation.swift index 018054f7c..1d4b7cfcb 100644 --- a/SignalServiceKit/src/Contacts/OWSContactDiscoveryOperation.swift +++ b/SignalServiceKit/src/Contacts/OWSContactDiscoveryOperation.swift @@ -128,7 +128,6 @@ class LegacyContactDiscoveryBatchOperation: OWSOperation { } self.reportError(error) - }) } @@ -191,15 +190,29 @@ class LegacyContactDiscoveryBatchOperation: OWSOperation { } +public class CDSBatchOperation: OWSOperation { + enum CDSBatchOperationError: Error { + case parseError(description: String) + case assertionError(description: String) + } + private let recipientIdsToLookup: [String] var registeredRecipientIds: Set + private var networkManager: TSNetworkManager { + return TSNetworkManager.shared() + } + + private var contactDiscoveryService: ContactDiscoveryService { + return ContactDiscoveryService.shared() + } + // MARK: Initializers - required init(recipientIdsToLookup: [String]) { - self.recipientIdsToLookup = recipientIdsToLookup + public required init(recipientIdsToLookup: [String]) { + self.recipientIdsToLookup = Set(recipientIdsToLookup).map { $0 } self.registeredRecipientIds = Set() super.init() @@ -210,12 +223,162 @@ class CDSBatchOperation: OWSOperation { // MARK: OWSOperationOverrides // Called every retry, this is where the bulk of the operation's work should go. - override func run() { + override public func run() { Logger.debug("\(logTag) in \(#function)") - Logger.debug("\(logTag) in \(#function) FAKING intersection (TODO)") - self.registeredRecipientIds = Set(self.recipientIdsToLookup) - self.reportSuccess() + guard !isCancelled else { + Logger.info("\(logTag) in \(#function) no work to do, since we were canceled") + self.reportCancelled() + return + } + + contactDiscoveryService.performRemoteAttestation(success: { (remoteAttestation: RemoteAttestation) in + self.makeContactDiscoveryRequest(remoteAttestation: remoteAttestation) + }, + failure: self.reportError) + } + + private func makeContactDiscoveryRequest(remoteAttestation: RemoteAttestation) { + + guard !isCancelled else { + Logger.info("\(logTag) in \(#function) no work to do, since we were canceled") + self.reportCancelled() + return + } + + let encryptionResult: AES25GCMEncryptionResult + do { + encryptionResult = try encryptAddresses(recipientIds: recipientIdsToLookup, remoteAttestation: remoteAttestation) + } catch { + reportError(error) + return + } + + let request = OWSRequestFactory.enclaveContactDiscoveryRequest(withId: remoteAttestation.requestId, + addressCount: UInt(recipientIdsToLookup.count), + encryptedAddressData: encryptionResult.ciphertext, + cryptIv: encryptionResult.initializationVector, + cryptMac: encryptionResult.authTag, + enclaveId: remoteAttestation.enclaveId, + authUsername: remoteAttestation.authUsername, + authPassword: remoteAttestation.authToken, + cookies: remoteAttestation.cookies) + + self.networkManager.makeRequest(request, + success: { (task, responseDict) in + do { + self.registeredRecipientIds = try self.handle(response: responseDict, remoteAttestation: remoteAttestation) + self.reportSuccess() + } catch { + self.reportError(error) + } + }, + failure: { (task, error) in + guard let response = task.response as? HTTPURLResponse else { + let responseError: NSError = OWSErrorMakeUnableToProcessServerResponseError() as NSError + responseError.isRetryable = true + self.reportError(responseError) + return + } + + guard response.statusCode != 413 else { + let rateLimitError = OWSErrorWithCodeDescription(OWSErrorCode.contactsUpdaterRateLimit, "Contacts Intersection Rate Limit") + self.reportError(rateLimitError) + return + } + + self.reportError(error) + }) + } + + func encryptAddresses(recipientIds: [String], remoteAttestation: RemoteAttestation) throws -> AES25GCMEncryptionResult { + + let addressPlainTextData = try type(of: self).encodePhoneNumbers(recipientIds: recipientIds) + + guard let encryptionResult = Cryptography.encryptAESGCM(plainTextData: addressPlainTextData, + additionalAuthenticatedData: remoteAttestation.requestId, + key: remoteAttestation.keys.clientKey) else { + + throw CDSBatchOperationError.assertionError(description: "Encryption failure") + } + + return encryptionResult + } + + class func encodePhoneNumbers(recipientIds: [String]) throws -> Data { + var output = Data() + + for recipientId in recipientIds { + guard recipientId.prefix(1) == "+" else { + throw CDSBatchOperationError.assertionError(description: "unexpected id format") + } + + let numericPortionIndex = recipientId.index(after: recipientId.startIndex) + let numericPortion = recipientId.suffix(from: numericPortionIndex) + + guard let numericIdentifier = UInt64(numericPortion), numericIdentifier > 99 else { + throw CDSBatchOperationError.assertionError(description: "unexpectedly short identifier") + } + + var bigEndian: UInt64 = CFSwapInt64HostToBig(numericIdentifier) + let buffer = UnsafeBufferPointer(start: &bigEndian, count: 1) + output.append(buffer) + } + + return output + } + + func handle(response: Any?, remoteAttestation: RemoteAttestation) throws -> Set { + let isIncludedData: Data = try parseAndDecrypt(response: response, remoteAttestation: remoteAttestation) + guard let isIncluded: [Bool] = type(of: self).boolArray(data: isIncludedData) else { + throw CDSBatchOperationError.assertionError(description: "isIncluded was unexpectedly nil") + } + + return try match(recipientIds: self.recipientIdsToLookup, isIncluded: isIncluded) + } + + class func boolArray(data: Data) -> [Bool]? { + var bools: [Bool]? = nil + data.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + let buffer = UnsafeBufferPointer(start: bytes, count: data.count) + bools = Array(buffer) + } + + return bools + } + + func match(recipientIds: [String], isIncluded: [Bool]) throws -> Set { + guard recipientIds.count == isIncluded.count else { + throw CDSBatchOperationError.assertionError(description: "length mismatch for isIncluded/recipientIds") + } + + let includedRecipientIds: [String] = (0.. Data { + + guard let responseDict = response as? [String: AnyObject] else { + throw CDSBatchOperationError.parseError(description: "missing response dict") + } + + let cipherText = try responseDict.expectBase64EncodedData(key: "data") + let initializationVector = try responseDict.expectBase64EncodedData(key: "iv") + let authTag = try responseDict.expectBase64EncodedData(key: "mac") + + guard let plainText = Cryptography.decryptAESGCM(withInitializationVector: initializationVector, + ciphertext: cipherText, + additionalAuthenticatedData: nil, + authTag: authTag, + key: remoteAttestation.keys.serverKey) else { + + throw CDSBatchOperationError.parseError(description: "decryption failed") + } + + return plainText } } @@ -279,3 +442,23 @@ extension Array { } } } + +extension Dictionary where Key: Hashable { + + enum DictionaryError: Error { + case missingField(Key) + case invalidFormat(Key) + } + + public func expectBase64EncodedData(key: Key) throws -> Data { + guard let encodedData = self[key] as? String else { + throw DictionaryError.missingField(key) + } + + guard let data = Data(base64Encoded: encodedData) else { + throw DictionaryError.invalidFormat(key) + } + + return data + } +} diff --git a/SignalServiceKit/src/Network/API/Requests/CDSAttestationRequest.h b/SignalServiceKit/src/Network/API/Requests/CDSAttestationRequest.h deleted file mode 100644 index f34a0cffd..000000000 --- a/SignalServiceKit/src/Network/API/Requests/CDSAttestationRequest.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "TSRequest.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface CDSAttestationRequest : TSRequest - -@property (nonatomic, readonly) NSString *authToken; -@property (nonatomic, readonly) NSString *username; - -- (instancetype)init NS_UNAVAILABLE; - -- (TSRequest *)initWithURL:(NSURL *)URL - method:(NSString *)method - parameters:(nullable NSDictionary *)parameters - username:(NSString *)username - authToken:(NSString *)authToken; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/API/Requests/CDSAttestationRequest.m b/SignalServiceKit/src/Network/API/Requests/CDSAttestationRequest.m deleted file mode 100644 index fd769a6b0..000000000 --- a/SignalServiceKit/src/Network/API/Requests/CDSAttestationRequest.m +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "CDSAttestationRequest.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation CDSAttestationRequest - -- (TSRequest *)initWithURL:(NSURL *)URL - method:(NSString *)method - parameters:(nullable NSDictionary *)parameters - username:(NSString *)username - authToken:(NSString *)authToken -{ - OWSAssert(authToken.length > 0); - - if (self = [super initWithURL:URL method:method parameters:parameters]) { - _username = username; - _authToken = authToken; - } - - return self; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h index 9b5442505..4dd117da6 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h @@ -72,8 +72,18 @@ typedef NS_ENUM(NSUInteger, TSVerificationTransport) { TSVerificationTransportVo + (TSRequest *)remoteAttestationRequest:(ECKeyPair *)keyPair enclaveId:(NSString *)enclaveId - username:(NSString *)username - authToken:(NSString *)authToken; + authUsername:(NSString *)authUsername + authPassword:(NSString *)authPassword; + ++ (TSRequest *)enclaveContactDiscoveryRequestWithId:(NSData *)requestId + addressCount:(NSUInteger)addressCount + encryptedAddressData:(NSData *)encryptedAddressData + cryptIv:(NSData *)cryptIv + cryptMac:(NSData *)cryptMac + enclaveId:(NSString *)enclaveId + authUsername:(NSString *)authUsername + authPassword:(NSString *)authPassword + cookies:(NSArray *)cookies; + (TSRequest *)remoteAttestationAuthRequest; diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m index 3a51dd89b..6ac5d57e1 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m @@ -3,7 +3,6 @@ // #import "OWSRequestFactory.h" -#import "CDSAttestationRequest.h" #import "NSData+Base64.h" #import "OWS2FAManager.h" #import "OWSDevice.h" @@ -278,24 +277,61 @@ NS_ASSUME_NONNULL_BEGIN + (TSRequest *)remoteAttestationRequest:(ECKeyPair *)keyPair enclaveId:(NSString *)enclaveId - username:(NSString *)username - authToken:(NSString *)authToken + authUsername:(NSString *)authUsername + authPassword:(NSString *)authPassword { OWSAssert(keyPair); OWSAssert(enclaveId.length > 0); - OWSAssert(username.length > 0); - OWSAssert(authToken.length > 0); + OWSAssert(authUsername.length > 0); + OWSAssert(authPassword.length > 0); NSString *path = [NSString stringWithFormat:@"https://api.contact-discovery.acton-signal.org/v1/attestation/%@", enclaveId]; - return [[CDSAttestationRequest alloc] initWithURL:[NSURL URLWithString:path] - method:@"PUT" - parameters:@{ - // We DO NOT prepend the "key type" byte. - @"clientPublic" : [keyPair.publicKey base64EncodedStringWithOptions:0], - } - username:username - authToken:authToken]; + TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] + method:@"PUT" + parameters:@{ + // We DO NOT prepend the "key type" byte. + @"clientPublic" : [keyPair.publicKey base64EncodedStringWithOptions:0], + }]; + request.authUsername = authUsername; + request.authPassword = authPassword; + + return request; +} + ++ (TSRequest *)enclaveContactDiscoveryRequestWithId:(NSData *)requestId + addressCount:(NSUInteger)addressCount + encryptedAddressData:(NSData *)encryptedAddressData + cryptIv:(NSData *)cryptIv + cryptMac:(NSData *)cryptMac + enclaveId:(NSString *)enclaveId + authUsername:(NSString *)authUsername + authPassword:(NSString *)authPassword + cookies:(NSArray *)cookies +{ + NSString *path = + [NSString stringWithFormat:@"https://api.contact-discovery.acton-signal.org/v1/discovery/%@", enclaveId]; + + TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] + method:@"PUT" + parameters:@{ + @"requestId" : requestId.base64EncodedString, + @"addressCount" : @(addressCount), + @"data" : encryptedAddressData.base64EncodedString, + @"iv" : cryptIv.base64EncodedString, + @"mac" : cryptMac.base64EncodedString, + }]; + + request.authUsername = authUsername; + request.authPassword = authPassword; + + NSDictionary *cookieHeaders = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + for (NSString *cookieHeader in cookieHeaders) { + NSString *cookieValue = cookieHeaders[cookieHeader]; + [request setValue:cookieValue forHTTPHeaderField:cookieHeader]; + } + + return request; } + (TSRequest *)remoteAttestationAuthRequest diff --git a/SignalServiceKit/src/Network/API/Requests/TSRequest.h b/SignalServiceKit/src/Network/API/Requests/TSRequest.h index eaae5f82f..19fbca461 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSRequest.h +++ b/SignalServiceKit/src/Network/API/Requests/TSRequest.h @@ -5,6 +5,8 @@ @interface TSRequest : NSMutableURLRequest @property (nonatomic) BOOL shouldHaveAuthorizationHeaders; +@property (nullable) NSString *authUsername; +@property (nullable) NSString *authPassword; @property (nonatomic, readonly) NSDictionary *parameters; diff --git a/SignalServiceKit/src/Network/API/Requests/TSRequest.m b/SignalServiceKit/src/Network/API/Requests/TSRequest.m index 27937ba7d..78d1fa195 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSRequest.m +++ b/SignalServiceKit/src/Network/API/Requests/TSRequest.m @@ -58,6 +58,8 @@ _parameters = parameters ?: @{}; [self setHTTPMethod:method]; self.shouldHaveAuthorizationHeaders = YES; + _authUsername = [TSAccountManager localNumber]; + _authPassword = [TSAccountManager serverAuthToken]; return self; } diff --git a/SignalServiceKit/src/Network/API/TSNetworkManager.m b/SignalServiceKit/src/Network/API/TSNetworkManager.m index 123868117..5180de37a 100644 --- a/SignalServiceKit/src/Network/API/TSNetworkManager.m +++ b/SignalServiceKit/src/Network/API/TSNetworkManager.m @@ -4,7 +4,7 @@ #import "TSNetworkManager.h" #import "AppContext.h" -#import "CDSAttestationRequest.h" +#import "NSError+messageSending.h" #import "NSURLSessionDataTask+StatusCode.h" #import "OWSSignalService.h" #import "TSAccountManager.h" @@ -114,14 +114,9 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error); [parameters removeObjectForKey:@"AuthKey"]; [sessionManager PUT:request.URL.absoluteString parameters:parameters success:success failure:failure]; } else { - if ([request isKindOfClass:[CDSAttestationRequest class]]) { - CDSAttestationRequest *attestationRequest = (CDSAttestationRequest *)request; - [sessionManager.requestSerializer setAuthorizationHeaderFieldWithUsername:attestationRequest.username - password:attestationRequest.authToken]; - } else if (request.shouldHaveAuthorizationHeaders) { - [sessionManager.requestSerializer - setAuthorizationHeaderFieldWithUsername:[TSAccountManager localNumber] - password:[TSAccountManager serverAuthToken]]; + if (request.shouldHaveAuthorizationHeaders) { + [sessionManager.requestSerializer setAuthorizationHeaderFieldWithUsername:request.authUsername + password:request.authPassword]; } if ([request.HTTPMethod isEqualToString:@"GET"]) { @@ -170,6 +165,8 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error); switch (statusCode) { case 0: { + error.isRetryable = YES; + DDLogWarn(@"The network request failed because of a connectivity error: %@", request); failureBlock(task, [self errorWithHTTPCode:statusCode @@ -183,6 +180,10 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error); case 400: { DDLogError(@"The request contains an invalid parameter : %@, %@", networkError.debugDescription, request); + error.isRetryable = NO; + + // TODO distinguish CDS requests. we don't want a bad CDS request to trigger "Signal deauth" logic. + // also, shouldn't this be under 403, not 400? [TSAccountManager.sharedInstance setIsDeregistered:YES]; failureBlock(task, error); @@ -192,6 +193,7 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error); DDLogError(@"The server returned an error about the authorization header: %@, %@", networkError.debugDescription, request); + error.isRetryable = NO; failureBlock(task, error); break; } diff --git a/SignalServiceKit/src/Util/Cryptography.h b/SignalServiceKit/src/Util/Cryptography.h index efd67f99c..3f9a48832 100755 --- a/SignalServiceKit/src/Util/Cryptography.h +++ b/SignalServiceKit/src/Util/Cryptography.h @@ -6,7 +6,7 @@ NS_ASSUME_NONNULL_BEGIN extern const NSUInteger kAES256_KeyByteLength; -/// Key appropriate for use in AES128 crypto +/// Key appropriate for use in AES256-GCM @interface OWSAES256Key : NSObject /// Generates new secure random key @@ -16,7 +16,7 @@ extern const NSUInteger kAES256_KeyByteLength; /** * @param data representing the raw key bytes * - * @returns a new instance if key is of appropriate length for AES128 crypto + * @returns a new instance if key is of appropriate length for AES256-GCM * else returns nil. */ + (nullable instancetype)keyWithData:(NSData *)data; @@ -26,6 +26,18 @@ extern const NSUInteger kAES256_KeyByteLength; @end +@interface AES25GCMEncryptionResult : NSObject + +@property (nonatomic, readonly) NSData *ciphertext; +@property (nonatomic, readonly) NSData *initializationVector; +@property (nonatomic, readonly) NSData *authTag; + +- (nullable instancetype)initWithCipherText:(NSData *)cipherText + initializationVector:(NSData *)initializationVector + authTag:(NSData *)authTag NS_DESIGNATED_INITIALIZER; + +@end + @interface Cryptography : NSObject typedef NS_ENUM(NSInteger, TSMACType) { @@ -69,14 +81,20 @@ typedef NS_ENUM(NSInteger, TSMACType) { outKey:(NSData *_Nonnull *_Nullable)outKey outDigest:(NSData *_Nonnull *_Nullable)outDigest; -+ (nullable NSData *)encryptAESGCMWithData:(NSData *)plaintextData key:(OWSAES256Key *)key; -+ (nullable NSData *)decryptAESGCMWithData:(NSData *)encryptedData key:(OWSAES256Key *)key; ++ (nullable AES25GCMEncryptionResult *)encryptAESGCMWithData:(NSData *)plaintext + additionalAuthenticatedData:(nullable NSData *)additionalAuthenticatedData + key:(OWSAES256Key *)key + NS_SWIFT_NAME(encryptAESGCM(plainTextData:additionalAuthenticatedData:key:)); + (nullable NSData *)decryptAESGCMWithInitializationVector:(NSData *)initializationVector ciphertext:(NSData *)ciphertext + additionalAuthenticatedData:(nullable NSData *)additionalAuthenticatedData authTag:(NSData *)authTagFromEncrypt key:(OWSAES256Key *)key; ++ (nullable NSData *)encryptAESGCMWithProfileData:(NSData *)plaintextData key:(OWSAES256Key *)key; ++ (nullable NSData *)decryptAESGCMWithProfileData:(NSData *)encryptedData key:(OWSAES256Key *)key; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/Cryptography.m b/SignalServiceKit/src/Util/Cryptography.m index 514b2869a..3261302b9 100755 --- a/SignalServiceKit/src/Util/Cryptography.m +++ b/SignalServiceKit/src/Util/Cryptography.m @@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN // Returned by many OpenSSL functions - indicating success const int kOpenSSLSuccess = 1; -// length of initialization nonce +// length of initialization nonce for AES256-GCM static const NSUInteger kAESGCM256_IVLength = 12; // length of authentication tag for AES256-GCM @@ -35,7 +35,7 @@ const NSUInteger kAES256_KeyByteLength = 32; + (nullable instancetype)keyWithData:(NSData *)data { if (data.length != kAES256_KeyByteLength) { - OWSFail(@"Invalid key length for AES128: %lu", (unsigned long)data.length); + OWSFail(@"%@ Invalid key length: %lu", self.logTag, (unsigned long)data.length); return nil; } @@ -96,6 +96,30 @@ const NSUInteger kAES256_KeyByteLength = 32; @end +@implementation AES25GCMEncryptionResult + +- (nullable instancetype)initWithCipherText:(NSData *)cipherText + initializationVector:(NSData *)initializationVector + authTag:(NSData *)authTag +{ + self = [super init]; + if (!self) { + return self; + } + + _ciphertext = [cipherText copy]; + _initializationVector = [initializationVector copy]; + _authTag = [authTag copy]; + + if (_ciphertext == nil || _initializationVector == nil || _authTag == nil) { + return nil; + } + + return self; +} + +@end + @implementation Cryptography #pragma mark random bytes methods @@ -464,7 +488,9 @@ const NSUInteger kAES256_KeyByteLength = 32; return [encryptedPaddedData copy]; } -+ (nullable NSData *)encryptAESGCMWithData:(NSData *)plaintext key:(OWSAES256Key *)key ++ (nullable AES25GCMEncryptionResult *)encryptAESGCMWithData:(NSData *)plaintext + additionalAuthenticatedData:(nullable NSData *)additionalAuthenticatedData + key:(OWSAES256Key *)key { NSData *initializationVector = [Cryptography generateRandomBytes:kAESGCM256_IVLength]; NSMutableData *ciphertext = [NSMutableData dataWithLength:plaintext.length]; @@ -496,6 +522,26 @@ const NSUInteger kAES256_KeyByteLength = 32; int bytesEncrypted = 0; + // Provide any AAD data. This can be called zero or more times as + // required + if (additionalAuthenticatedData != nil) { + if (additionalAuthenticatedData.length >= INT32_MAX) { + OWSFail(@"%@ additionalAuthenticatedData too large", self.logTag); + return nil; + } + if (EVP_EncryptUpdate( + ctx, NULL, &bytesEncrypted, additionalAuthenticatedData.bytes, (int)additionalAuthenticatedData.length) + != kOpenSSLSuccess) { + OWSFail(@"%@ encryptUpdate failed", self.logTag); + return nil; + } + } + + if (plaintext.length >= UINT32_MAX) { + OWSFail(@"%@ plaintext too large", self.logTag); + return nil; + } + // Provide the message to be encrypted, and obtain the encrypted output. // // If we wanted to save memory, we could encrypt piece-wise from a plaintext iostream - @@ -532,31 +578,17 @@ const NSUInteger kAES256_KeyByteLength = 32; // Clean up EVP_CIPHER_CTX_free(ctx); - // build up return value: initializationVector || ciphertext || authTag - NSMutableData *encryptedData = [initializationVector mutableCopy]; - [encryptedData appendData:ciphertext]; - [encryptedData appendData:authTag]; + AES25GCMEncryptionResult *_Nullable result = + [[AES25GCMEncryptionResult alloc] initWithCipherText:ciphertext + initializationVector:initializationVector + authTag:authTag]; - return [encryptedData copy]; -} - -+ (nullable NSData *)decryptAESGCMWithData:(NSData *)encryptedData key:(OWSAES256Key *)key -{ - OWSAssert(encryptedData.length > kAESGCM256_IVLength + kAESGCM256_TagLength); - NSUInteger cipherTextLength = encryptedData.length - kAESGCM256_IVLength - kAESGCM256_TagLength; - - // encryptedData layout: initializationVector || ciphertext || authTag - NSData *initializationVector = [encryptedData subdataWithRange:NSMakeRange(0, kAESGCM256_IVLength)]; - NSData *ciphertext = [encryptedData subdataWithRange:NSMakeRange(kAESGCM256_IVLength, cipherTextLength)]; - NSData *authTag = - [encryptedData subdataWithRange:NSMakeRange(kAESGCM256_IVLength + cipherTextLength, kAESGCM256_TagLength)]; - - return - [self decryptAESGCMWithInitializationVector:initializationVector ciphertext:ciphertext authTag:authTag key:key]; + return result; } + (nullable NSData *)decryptAESGCMWithInitializationVector:(NSData *)initializationVector ciphertext:(NSData *)ciphertext + additionalAuthenticatedData:(nullable NSData *)additionalAuthenticatedData authTag:(NSData *)authTagFromEncrypt key:(OWSAES256Key *)key { @@ -593,12 +625,27 @@ const NSUInteger kAES256_KeyByteLength = 32; return nil; } + int decryptedBytes = 0; + + // Provide any AAD data. This can be called zero or more times as + // required + if (additionalAuthenticatedData) { + if (additionalAuthenticatedData.length >= INT32_MAX) { + OWSFail(@"%@ additionalAuthenticatedData too large", self.logTag); + return nil; + } + if (!EVP_DecryptUpdate( + ctx, NULL, &decryptedBytes, additionalAuthenticatedData.bytes, additionalAuthenticatedData.length)) { + OWSFail(@"%@ failed during additionalAuthenticatedData", self.logTag); + return nil; + } + } + // Provide the message to be decrypted, and obtain the plaintext output. // // If we wanted to save memory, we could decrypt piece-wise from an iostream - // feeding each chunk to EVP_DecryptUpdate, which can be called multiple times. // For simplicity, we currently decrypt the entire ciphertext in one shot. - int decryptedBytes = 0; if (EVP_DecryptUpdate(ctx, plaintext.mutableBytes, &decryptedBytes, ciphertext.bytes, (int)ciphertext.length) != kOpenSSLSuccess) { OWSFail(@"%@ decryptUpdate failed", self.logTag); @@ -638,6 +685,35 @@ const NSUInteger kAES256_KeyByteLength = 32; } } ++ (nullable NSData *)encryptAESGCMWithProfileData:(NSData *)plaintext key:(OWSAES256Key *)key +{ + AES25GCMEncryptionResult *result = [self encryptAESGCMWithData:plaintext additionalAuthenticatedData:nil key:key]; + + NSMutableData *encryptedData = [result.initializationVector mutableCopy]; + [encryptedData appendData:result.ciphertext]; + [encryptedData appendData:result.authTag]; + + return [encryptedData copy]; +} + ++ (nullable NSData *)decryptAESGCMWithProfileData:(NSData *)encryptedData key:(OWSAES256Key *)key +{ + OWSAssert(encryptedData.length > kAESGCM256_IVLength + kAESGCM256_TagLength); + NSUInteger cipherTextLength = encryptedData.length - kAESGCM256_IVLength - kAESGCM256_TagLength; + + // encryptedData layout: initializationVector || ciphertext || authTag + NSData *initializationVector = [encryptedData subdataWithRange:NSMakeRange(0, kAESGCM256_IVLength)]; + NSData *ciphertext = [encryptedData subdataWithRange:NSMakeRange(kAESGCM256_IVLength, cipherTextLength)]; + NSData *authTag = + [encryptedData subdataWithRange:NSMakeRange(kAESGCM256_IVLength + cipherTextLength, kAESGCM256_TagLength)]; + + return [self decryptAESGCMWithInitializationVector:initializationVector + ciphertext:ciphertext + additionalAuthenticatedData:nil + authTag:authTag + key:key]; +} + @end NS_ASSUME_NONNULL_END From 8c5d6ba9bbace1e78c936884fc7af51c72675f26 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 23 Jul 2018 10:35:35 -0600 Subject: [PATCH 4/4] Respond to code review. --- .../src/Network/API/Requests/TSRequest.h | 4 ++-- .../src/Network/API/Requests/TSRequest.m | 17 +++++++++++++++-- SignalServiceKit/src/Util/Cryptography.h | 1 + SignalServiceKit/src/Util/Cryptography.m | 3 ++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/SignalServiceKit/src/Network/API/Requests/TSRequest.h b/SignalServiceKit/src/Network/API/Requests/TSRequest.h index 19fbca461..a56ba703d 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSRequest.h +++ b/SignalServiceKit/src/Network/API/Requests/TSRequest.h @@ -5,8 +5,8 @@ @interface TSRequest : NSMutableURLRequest @property (nonatomic) BOOL shouldHaveAuthorizationHeaders; -@property (nullable) NSString *authUsername; -@property (nullable) NSString *authPassword; +@property (atomic, nullable) NSString *authUsername; +@property (atomic, nullable) NSString *authPassword; @property (nonatomic, readonly) NSDictionary *parameters; diff --git a/SignalServiceKit/src/Network/API/Requests/TSRequest.m b/SignalServiceKit/src/Network/API/Requests/TSRequest.m index 78d1fa195..159b20b6a 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSRequest.m +++ b/SignalServiceKit/src/Network/API/Requests/TSRequest.m @@ -58,8 +58,6 @@ _parameters = parameters ?: @{}; [self setHTTPMethod:method]; self.shouldHaveAuthorizationHeaders = YES; - _authUsername = [TSAccountManager localNumber]; - _authPassword = [TSAccountManager serverAuthToken]; return self; } @@ -71,4 +69,19 @@ return [[TSRequest alloc] initWithURL:url method:method parameters:parameters]; } +#pragma mark - Authorization + +- (NSString *)authUsername +{ + OWSAssert(self.shouldHaveAuthorizationHeaders); + return (_authUsername ?: [TSAccountManager localNumber]); +} + +- (NSString *)authPassword +{ + OWSAssert(self.shouldHaveAuthorizationHeaders); + return (_authPassword ?: [TSAccountManager serverAuthToken]); +} + + @end diff --git a/SignalServiceKit/src/Util/Cryptography.h b/SignalServiceKit/src/Util/Cryptography.h index 3f9a48832..a04e2e0e8 100755 --- a/SignalServiceKit/src/Util/Cryptography.h +++ b/SignalServiceKit/src/Util/Cryptography.h @@ -32,6 +32,7 @@ extern const NSUInteger kAES256_KeyByteLength; @property (nonatomic, readonly) NSData *initializationVector; @property (nonatomic, readonly) NSData *authTag; +- (instancetype)init NS_UNAVAILABLE; - (nullable instancetype)initWithCipherText:(NSData *)cipherText initializationVector:(NSData *)initializationVector authTag:(NSData *)authTag NS_DESIGNATED_INITIALIZER; diff --git a/SignalServiceKit/src/Util/Cryptography.m b/SignalServiceKit/src/Util/Cryptography.m index 3261302b9..d618e1ae7 100755 --- a/SignalServiceKit/src/Util/Cryptography.m +++ b/SignalServiceKit/src/Util/Cryptography.m @@ -111,7 +111,8 @@ const NSUInteger kAES256_KeyByteLength = 32; _initializationVector = [initializationVector copy]; _authTag = [authTag copy]; - if (_ciphertext == nil || _initializationVector == nil || _authTag == nil) { + if (_ciphertext == nil || _initializationVector.length != kAESGCM256_IVLength + || _authTag.length != kAESGCM256_TagLength) { return nil; }