mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Integrate with new contact discovery endpoint
Also: * use system cookie parsing * add AESGCM additional authenticated data parameter // FREEBIE
This commit is contained in:
parent
a611625691
commit
b42f528713
2
Pods
2
Pods
|
@ -1 +1 @@
|
||||||
Subproject commit a2394bbafc099db434ee91e7a617c412750c44b9
|
Subproject commit 5dc9c23dc3229ab6a884372a0e2cf62cb0904be6
|
|
@ -433,6 +433,7 @@
|
||||||
4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF51208B82E9007B4E76 /* ThreadViewModel.swift */; };
|
4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF51208B82E9007B4E76 /* ThreadViewModel.swift */; };
|
||||||
4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */; };
|
4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */; };
|
||||||
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4AEC4420EC343B0020E72B /* DismissableTextField.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 */; };
|
4C6F527C20FFE8400097DEEE /* SignalUBSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */; };
|
||||||
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; };
|
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; };
|
||||||
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.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 = "<group>"; };
|
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = "<group>"; };
|
||||||
4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearchViewController.swift; sourceTree = "<group>"; };
|
4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearchViewController.swift; sourceTree = "<group>"; };
|
||||||
4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableTextField.swift; sourceTree = "<group>"; };
|
4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableTextField.swift; sourceTree = "<group>"; };
|
||||||
|
4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContactDiscoveryOperationTest.swift; path = contact/ContactDiscoveryOperationTest.swift; sourceTree = "<group>"; };
|
||||||
4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalUBSan.supp; sourceTree = "<group>"; };
|
4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalUBSan.supp; sourceTree = "<group>"; };
|
||||||
4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = "<group>"; };
|
4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = "<group>"; };
|
||||||
4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationConfigurationSyncOperation.swift; sourceTree = "<group>"; };
|
4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationConfigurationSyncOperation.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2072,6 +2074,7 @@
|
||||||
458E38381D6699110094BD24 /* Models */ = {
|
458E38381D6699110094BD24 /* Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */,
|
||||||
458E38391D6699FA0094BD24 /* OWSDeviceProvisioningURLParserTest.m */,
|
458E38391D6699FA0094BD24 /* OWSDeviceProvisioningURLParserTest.m */,
|
||||||
458967101DC117CC00E9DD21 /* AccountManagerTest.swift */,
|
458967101DC117CC00E9DD21 /* AccountManagerTest.swift */,
|
||||||
);
|
);
|
||||||
|
@ -3452,6 +3455,7 @@
|
||||||
B660F6DB1C29868000687D6E /* FunctionalUtilTest.m in Sources */,
|
B660F6DB1C29868000687D6E /* FunctionalUtilTest.m in Sources */,
|
||||||
45E7A6A81E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift in Sources */,
|
45E7A6A81E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift in Sources */,
|
||||||
452D1AF12081059C00A67F7F /* StringAdditionsTest.swift in Sources */,
|
452D1AF12081059C00A67F7F /* StringAdditionsTest.swift in Sources */,
|
||||||
|
4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */,
|
||||||
B660F6BB1C29868000687D6E /* OWSContactsManagerTest.m in Sources */,
|
B660F6BB1C29868000687D6E /* OWSContactsManagerTest.m in Sources */,
|
||||||
B660F6D21C29868000687D6E /* PushManagerTest.m in Sources */,
|
B660F6D21C29868000687D6E /* PushManagerTest.m in Sources */,
|
||||||
455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */,
|
455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */,
|
||||||
|
|
60
Signal/test/contact/ContactDiscoveryOperationTest.swift
Normal file
60
Signal/test/contact/ContactDiscoveryOperationTest.swift
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -994,7 +994,7 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [Cryptography encryptAESGCMWithData:encryptedData key:profileKey];
|
return [Cryptography encryptAESGCMWithProfileData:encryptedData key:profileKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (nullable NSData *)decryptProfileData:(nullable NSData *)encryptedData profileKey:(OWSAES256Key *)profileKey
|
- (nullable NSData *)decryptProfileData:(nullable NSData *)encryptedData profileKey:(OWSAES256Key *)profileKey
|
||||||
|
@ -1005,7 +1005,7 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [Cryptography decryptAESGCMWithData:encryptedData key:profileKey];
|
return [Cryptography decryptAESGCMWithProfileData:encryptedData key:profileKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (nullable NSString *)decryptProfileNameData:(nullable NSData *)encryptedData profileKey:(OWSAES256Key *)profileKey
|
- (nullable NSString *)decryptProfileNameData:(nullable NSData *)encryptedData profileKey:(OWSAES256Key *)profileKey
|
||||||
|
|
|
@ -4,7 +4,30 @@
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
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<NSHTTPCookie *> *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
|
@interface ContactDiscoveryService : NSObject
|
||||||
|
|
||||||
|
|
|
@ -31,14 +31,14 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
#pragma mark -
|
#pragma mark -
|
||||||
|
|
||||||
@interface RemoteAttestationKeys : NSObject
|
@interface RemoteAttestationKeys ()
|
||||||
|
|
||||||
@property (nonatomic) ECKeyPair *keyPair;
|
@property (nonatomic) ECKeyPair *keyPair;
|
||||||
@property (nonatomic) NSData *serverEphemeralPublic;
|
@property (nonatomic) NSData *serverEphemeralPublic;
|
||||||
@property (nonatomic) NSData *serverStaticPublic;
|
@property (nonatomic) NSData *serverStaticPublic;
|
||||||
|
|
||||||
@property (nonatomic) NSData *clientKey;
|
@property (nonatomic) OWSAES256Key *clientKey;
|
||||||
@property (nonatomic) NSData *serverKey;
|
@property (nonatomic) OWSAES256Key *serverKey;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
@ -74,7 +74,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
NSData *_Nullable derivedMaterial;
|
NSData *_Nullable derivedMaterial;
|
||||||
@try {
|
@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) {
|
} @catch (NSException *exception) {
|
||||||
DDLogError(@"%@ could not derive service key: %@", self.logTag, exception);
|
DDLogError(@"%@ could not derive service key: %@", self.logTag, exception);
|
||||||
return NO;
|
return NO;
|
||||||
|
@ -84,17 +85,23 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
OWSProdLogAndFail(@"%@ missing derived service key.", self.logTag);
|
OWSProdLogAndFail(@"%@ missing derived service key.", self.logTag);
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
if (derivedMaterial.length != ECCKeyLength * 2) {
|
if (derivedMaterial.length != kAES256_KeyByteLength * 2) {
|
||||||
OWSProdLogAndFail(@"%@ derived service key has unexpected length.", self.logTag);
|
OWSProdLogAndFail(@"%@ derived service key has unexpected length.", self.logTag);
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
NSData *_Nullable clientKey = [derivedMaterial subdataWithRange:NSMakeRange(ECCKeyLength * 0, ECCKeyLength)];
|
|
||||||
NSData *_Nullable serverKey = [derivedMaterial subdataWithRange:NSMakeRange(ECCKeyLength * 1, ECCKeyLength)];
|
NSData *_Nullable clientKeyData =
|
||||||
if (clientKey.length != ECCKeyLength) {
|
[derivedMaterial subdataWithRange:NSMakeRange(kAES256_KeyByteLength * 0, kAES256_KeyByteLength)];
|
||||||
|
OWSAES256Key *_Nullable clientKey = [OWSAES256Key keyWithData:clientKeyData];
|
||||||
|
if (!clientKey) {
|
||||||
OWSProdLogAndFail(@"%@ clientKey has unexpected length.", self.logTag);
|
OWSProdLogAndFail(@"%@ clientKey has unexpected length.", self.logTag);
|
||||||
return NO;
|
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);
|
OWSProdLogAndFail(@"%@ serverKey has unexpected length.", self.logTag);
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
@ -109,12 +116,13 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
#pragma mark -
|
#pragma mark -
|
||||||
|
|
||||||
@interface RemoteAttestation : NSObject
|
@interface RemoteAttestation ()
|
||||||
|
|
||||||
@property (nonatomic) RemoteAttestationKeys *keys;
|
@property (nonatomic) RemoteAttestationKeys *keys;
|
||||||
// TODO: Do we need to support multiple cookies?
|
@property (nonatomic) NSArray<NSHTTPCookie *> *cookies;
|
||||||
@property (nonatomic) NSString *cookie;
|
|
||||||
@property (nonatomic) NSData *requestId;
|
@property (nonatomic) NSData *requestId;
|
||||||
|
@property (nonatomic) NSString *enclaveId;
|
||||||
|
@property (nonatomic) RemoteAttestationAuth *auth;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
@ -122,6 +130,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@implementation RemoteAttestation
|
@implementation RemoteAttestation
|
||||||
|
|
||||||
|
- (NSString *)authUsername
|
||||||
|
{
|
||||||
|
return self.auth.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)authToken
|
||||||
|
{
|
||||||
|
return self.auth.authToken;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#pragma mark -
|
#pragma mark -
|
||||||
|
@ -301,16 +319,17 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
TSRequest *request = [OWSRequestFactory remoteAttestationRequest:keyPair
|
TSRequest *request = [OWSRequestFactory remoteAttestationRequest:keyPair
|
||||||
enclaveId:enclaveId
|
enclaveId:enclaveId
|
||||||
username:auth.username
|
authUsername:auth.username
|
||||||
authToken:auth.authToken];
|
authPassword:auth.authToken];
|
||||||
|
|
||||||
[[TSNetworkManager sharedManager] makeRequest:request
|
[[TSNetworkManager sharedManager] makeRequest:request
|
||||||
success:^(NSURLSessionDataTask *task, id responseJson) {
|
success:^(NSURLSessionDataTask *task, id responseJson) {
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
// TODO: Handle result.
|
|
||||||
RemoteAttestation *_Nullable attestation = [self parseAttestationResponseJson:responseJson
|
RemoteAttestation *_Nullable attestation = [self parseAttestationResponseJson:responseJson
|
||||||
response:task.response
|
response:task.response
|
||||||
keyPair:keyPair
|
keyPair:keyPair
|
||||||
enclaveId:enclaveId];
|
enclaveId:enclaveId
|
||||||
|
auth:auth];
|
||||||
|
|
||||||
if (!attestation) {
|
if (!attestation) {
|
||||||
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
|
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
|
||||||
|
@ -332,6 +351,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
response:(NSURLResponse *)response
|
response:(NSURLResponse *)response
|
||||||
keyPair:(ECKeyPair *)keyPair
|
keyPair:(ECKeyPair *)keyPair
|
||||||
enclaveId:(NSString *)enclaveId
|
enclaveId:(NSString *)enclaveId
|
||||||
|
auth:(RemoteAttestationAuth *)auth
|
||||||
{
|
{
|
||||||
OWSAssert(responseJson);
|
OWSAssert(responseJson);
|
||||||
OWSAssert(response);
|
OWSAssert(response);
|
||||||
|
@ -342,22 +362,14 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
OWSProdLogAndFail(@"%@ unexpected response type.", self.logTag);
|
OWSProdLogAndFail(@"%@ unexpected response type.", self.logTag);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
NSDictionary *responseHeaders = ((NSHTTPURLResponse *)response).allHeaderFields;
|
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
||||||
|
NSArray<NSHTTPCookie *> *cookies =
|
||||||
NSString *_Nullable cookie = [responseHeaders stringForKey:@"Set-Cookie"];
|
[NSHTTPCookie cookiesWithResponseHeaderFields:httpResponse.allHeaderFields forURL:[NSURL new]];
|
||||||
if (cookie.length < 1) {
|
if (cookies.count < 1) {
|
||||||
OWSProdLogAndFail(@"%@ couldn't parse cookie.", self.logTag);
|
OWSProdLogAndFail(@"%@ couldn't parse cookie.", self.logTag);
|
||||||
return nil;
|
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]]) {
|
if (![responseJson isKindOfClass:[NSDictionary class]]) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
@ -450,9 +462,11 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteAttestation *result = [RemoteAttestation new];
|
RemoteAttestation *result = [RemoteAttestation new];
|
||||||
result.cookie = cookie;
|
result.cookies = cookies;
|
||||||
result.keys = keys;
|
result.keys = keys;
|
||||||
result.requestId = requestId;
|
result.requestId = requestId;
|
||||||
|
result.enclaveId = enclaveId;
|
||||||
|
result.auth = auth;
|
||||||
|
|
||||||
DDLogVerbose(@"%@ remote attestation complete.", self.logTag);
|
DDLogVerbose(@"%@ remote attestation complete.", self.logTag);
|
||||||
|
|
||||||
|
@ -648,13 +662,14 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
OWSAssert(encryptedRequestTag.length > 0);
|
OWSAssert(encryptedRequestTag.length > 0);
|
||||||
OWSAssert(keys);
|
OWSAssert(keys);
|
||||||
|
|
||||||
OWSAES256Key *_Nullable key = [OWSAES256Key keyWithData:keys.serverKey];
|
OWSAES256Key *_Nullable key = keys.serverKey;
|
||||||
if (!key) {
|
if (!key) {
|
||||||
OWSProdLogAndFail(@"%@ invalid server key.", self.logTag);
|
OWSProdLogAndFail(@"%@ invalid server key.", self.logTag);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
NSData *_Nullable decryptedData = [Cryptography decryptAESGCMWithInitializationVector:encryptedRequestIv
|
NSData *_Nullable decryptedData = [Cryptography decryptAESGCMWithInitializationVector:encryptedRequestIv
|
||||||
ciphertext:encryptedRequestId
|
ciphertext:encryptedRequestId
|
||||||
|
additionalAuthenticatedData:nil
|
||||||
authTag:encryptedRequestTag
|
authTag:encryptedRequestTag
|
||||||
key:key];
|
key:key];
|
||||||
if (!decryptedData) {
|
if (!decryptedData) {
|
||||||
|
|
|
@ -128,7 +128,6 @@ class LegacyContactDiscoveryBatchOperation: OWSOperation {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.reportError(error)
|
self.reportError(error)
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,15 +190,29 @@ class LegacyContactDiscoveryBatchOperation: OWSOperation {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public
|
||||||
class CDSBatchOperation: OWSOperation {
|
class CDSBatchOperation: OWSOperation {
|
||||||
|
|
||||||
|
enum CDSBatchOperationError: Error {
|
||||||
|
case parseError(description: String)
|
||||||
|
case assertionError(description: String)
|
||||||
|
}
|
||||||
|
|
||||||
private let recipientIdsToLookup: [String]
|
private let recipientIdsToLookup: [String]
|
||||||
var registeredRecipientIds: Set<String>
|
var registeredRecipientIds: Set<String>
|
||||||
|
|
||||||
|
private var networkManager: TSNetworkManager {
|
||||||
|
return TSNetworkManager.shared()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var contactDiscoveryService: ContactDiscoveryService {
|
||||||
|
return ContactDiscoveryService.shared()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Initializers
|
// MARK: Initializers
|
||||||
|
|
||||||
required init(recipientIdsToLookup: [String]) {
|
public required init(recipientIdsToLookup: [String]) {
|
||||||
self.recipientIdsToLookup = recipientIdsToLookup
|
self.recipientIdsToLookup = Set(recipientIdsToLookup).map { $0 }
|
||||||
self.registeredRecipientIds = Set()
|
self.registeredRecipientIds = Set()
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
@ -210,12 +223,162 @@ class CDSBatchOperation: OWSOperation {
|
||||||
// MARK: OWSOperationOverrides
|
// MARK: OWSOperationOverrides
|
||||||
|
|
||||||
// Called every retry, this is where the bulk of the operation's work should go.
|
// 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)")
|
||||||
|
|
||||||
Logger.debug("\(logTag) in \(#function) FAKING intersection (TODO)")
|
guard !isCancelled else {
|
||||||
self.registeredRecipientIds = Set(self.recipientIdsToLookup)
|
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()
|
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<String> {
|
||||||
|
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<Bool>) -> Void in
|
||||||
|
let buffer = UnsafeBufferPointer(start: bytes, count: data.count)
|
||||||
|
bools = Array(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bools
|
||||||
|
}
|
||||||
|
|
||||||
|
func match(recipientIds: [String], isIncluded: [Bool]) throws -> Set<String> {
|
||||||
|
guard recipientIds.count == isIncluded.count else {
|
||||||
|
throw CDSBatchOperationError.assertionError(description: "length mismatch for isIncluded/recipientIds")
|
||||||
|
}
|
||||||
|
|
||||||
|
let includedRecipientIds: [String] = (0..<recipientIds.count).compactMap { index in
|
||||||
|
isIncluded[index] ? recipientIds[index] : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Set(includedRecipientIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAndDecrypt(response: Any?, remoteAttestation: RemoteAttestation) throws -> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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<NSString *, id> *)parameters
|
|
||||||
username:(NSString *)username
|
|
||||||
authToken:(NSString *)authToken;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
|
@ -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<NSString *, id> *)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
|
|
|
@ -72,8 +72,18 @@ typedef NS_ENUM(NSUInteger, TSVerificationTransport) { TSVerificationTransportVo
|
||||||
|
|
||||||
+ (TSRequest *)remoteAttestationRequest:(ECKeyPair *)keyPair
|
+ (TSRequest *)remoteAttestationRequest:(ECKeyPair *)keyPair
|
||||||
enclaveId:(NSString *)enclaveId
|
enclaveId:(NSString *)enclaveId
|
||||||
username:(NSString *)username
|
authUsername:(NSString *)authUsername
|
||||||
authToken:(NSString *)authToken;
|
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<NSHTTPCookie *> *)cookies;
|
||||||
|
|
||||||
+ (TSRequest *)remoteAttestationAuthRequest;
|
+ (TSRequest *)remoteAttestationAuthRequest;
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "OWSRequestFactory.h"
|
#import "OWSRequestFactory.h"
|
||||||
#import "CDSAttestationRequest.h"
|
|
||||||
#import "NSData+Base64.h"
|
#import "NSData+Base64.h"
|
||||||
#import "OWS2FAManager.h"
|
#import "OWS2FAManager.h"
|
||||||
#import "OWSDevice.h"
|
#import "OWSDevice.h"
|
||||||
|
@ -278,24 +277,61 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
+ (TSRequest *)remoteAttestationRequest:(ECKeyPair *)keyPair
|
+ (TSRequest *)remoteAttestationRequest:(ECKeyPair *)keyPair
|
||||||
enclaveId:(NSString *)enclaveId
|
enclaveId:(NSString *)enclaveId
|
||||||
username:(NSString *)username
|
authUsername:(NSString *)authUsername
|
||||||
authToken:(NSString *)authToken
|
authPassword:(NSString *)authPassword
|
||||||
{
|
{
|
||||||
OWSAssert(keyPair);
|
OWSAssert(keyPair);
|
||||||
OWSAssert(enclaveId.length > 0);
|
OWSAssert(enclaveId.length > 0);
|
||||||
OWSAssert(username.length > 0);
|
OWSAssert(authUsername.length > 0);
|
||||||
OWSAssert(authToken.length > 0);
|
OWSAssert(authPassword.length > 0);
|
||||||
|
|
||||||
NSString *path =
|
NSString *path =
|
||||||
[NSString stringWithFormat:@"https://api.contact-discovery.acton-signal.org/v1/attestation/%@", enclaveId];
|
[NSString stringWithFormat:@"https://api.contact-discovery.acton-signal.org/v1/attestation/%@", enclaveId];
|
||||||
return [[CDSAttestationRequest alloc] initWithURL:[NSURL URLWithString:path]
|
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path]
|
||||||
method:@"PUT"
|
method:@"PUT"
|
||||||
parameters:@{
|
parameters:@{
|
||||||
// We DO NOT prepend the "key type" byte.
|
// We DO NOT prepend the "key type" byte.
|
||||||
@"clientPublic" : [keyPair.publicKey base64EncodedStringWithOptions:0],
|
@"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<NSHTTPCookie *> *)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<NSString *, NSString *> *cookieHeaders = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
|
||||||
|
for (NSString *cookieHeader in cookieHeaders) {
|
||||||
|
NSString *cookieValue = cookieHeaders[cookieHeader];
|
||||||
|
[request setValue:cookieValue forHTTPHeaderField:cookieHeader];
|
||||||
}
|
}
|
||||||
username:username
|
|
||||||
authToken:authToken];
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (TSRequest *)remoteAttestationAuthRequest
|
+ (TSRequest *)remoteAttestationAuthRequest
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
@interface TSRequest : NSMutableURLRequest
|
@interface TSRequest : NSMutableURLRequest
|
||||||
|
|
||||||
@property (nonatomic) BOOL shouldHaveAuthorizationHeaders;
|
@property (nonatomic) BOOL shouldHaveAuthorizationHeaders;
|
||||||
|
@property (nullable) NSString *authUsername;
|
||||||
|
@property (nullable) NSString *authPassword;
|
||||||
|
|
||||||
@property (nonatomic, readonly) NSDictionary *parameters;
|
@property (nonatomic, readonly) NSDictionary *parameters;
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,8 @@
|
||||||
_parameters = parameters ?: @{};
|
_parameters = parameters ?: @{};
|
||||||
[self setHTTPMethod:method];
|
[self setHTTPMethod:method];
|
||||||
self.shouldHaveAuthorizationHeaders = YES;
|
self.shouldHaveAuthorizationHeaders = YES;
|
||||||
|
_authUsername = [TSAccountManager localNumber];
|
||||||
|
_authPassword = [TSAccountManager serverAuthToken];
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
#import "TSNetworkManager.h"
|
#import "TSNetworkManager.h"
|
||||||
#import "AppContext.h"
|
#import "AppContext.h"
|
||||||
#import "CDSAttestationRequest.h"
|
#import "NSError+messageSending.h"
|
||||||
#import "NSURLSessionDataTask+StatusCode.h"
|
#import "NSURLSessionDataTask+StatusCode.h"
|
||||||
#import "OWSSignalService.h"
|
#import "OWSSignalService.h"
|
||||||
#import "TSAccountManager.h"
|
#import "TSAccountManager.h"
|
||||||
|
@ -114,14 +114,9 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error);
|
||||||
[parameters removeObjectForKey:@"AuthKey"];
|
[parameters removeObjectForKey:@"AuthKey"];
|
||||||
[sessionManager PUT:request.URL.absoluteString parameters:parameters success:success failure:failure];
|
[sessionManager PUT:request.URL.absoluteString parameters:parameters success:success failure:failure];
|
||||||
} else {
|
} else {
|
||||||
if ([request isKindOfClass:[CDSAttestationRequest class]]) {
|
if (request.shouldHaveAuthorizationHeaders) {
|
||||||
CDSAttestationRequest *attestationRequest = (CDSAttestationRequest *)request;
|
[sessionManager.requestSerializer setAuthorizationHeaderFieldWithUsername:request.authUsername
|
||||||
[sessionManager.requestSerializer setAuthorizationHeaderFieldWithUsername:attestationRequest.username
|
password:request.authPassword];
|
||||||
password:attestationRequest.authToken];
|
|
||||||
} else if (request.shouldHaveAuthorizationHeaders) {
|
|
||||||
[sessionManager.requestSerializer
|
|
||||||
setAuthorizationHeaderFieldWithUsername:[TSAccountManager localNumber]
|
|
||||||
password:[TSAccountManager serverAuthToken]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([request.HTTPMethod isEqualToString:@"GET"]) {
|
if ([request.HTTPMethod isEqualToString:@"GET"]) {
|
||||||
|
@ -170,6 +165,8 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error);
|
||||||
|
|
||||||
switch (statusCode) {
|
switch (statusCode) {
|
||||||
case 0: {
|
case 0: {
|
||||||
|
error.isRetryable = YES;
|
||||||
|
|
||||||
DDLogWarn(@"The network request failed because of a connectivity error: %@", request);
|
DDLogWarn(@"The network request failed because of a connectivity error: %@", request);
|
||||||
failureBlock(task,
|
failureBlock(task,
|
||||||
[self errorWithHTTPCode:statusCode
|
[self errorWithHTTPCode:statusCode
|
||||||
|
@ -183,6 +180,10 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error);
|
||||||
case 400: {
|
case 400: {
|
||||||
DDLogError(@"The request contains an invalid parameter : %@, %@", networkError.debugDescription, request);
|
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];
|
[TSAccountManager.sharedInstance setIsDeregistered:YES];
|
||||||
|
|
||||||
failureBlock(task, error);
|
failureBlock(task, error);
|
||||||
|
@ -192,6 +193,7 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error);
|
||||||
DDLogError(@"The server returned an error about the authorization header: %@, %@",
|
DDLogError(@"The server returned an error about the authorization header: %@, %@",
|
||||||
networkError.debugDescription,
|
networkError.debugDescription,
|
||||||
request);
|
request);
|
||||||
|
error.isRetryable = NO;
|
||||||
failureBlock(task, error);
|
failureBlock(task, error);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
extern const NSUInteger kAES256_KeyByteLength;
|
extern const NSUInteger kAES256_KeyByteLength;
|
||||||
|
|
||||||
/// Key appropriate for use in AES128 crypto
|
/// Key appropriate for use in AES256-GCM
|
||||||
@interface OWSAES256Key : NSObject <NSSecureCoding>
|
@interface OWSAES256Key : NSObject <NSSecureCoding>
|
||||||
|
|
||||||
/// Generates new secure random key
|
/// Generates new secure random key
|
||||||
|
@ -16,7 +16,7 @@ extern const NSUInteger kAES256_KeyByteLength;
|
||||||
/**
|
/**
|
||||||
* @param data representing the raw key bytes
|
* @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.
|
* else returns nil.
|
||||||
*/
|
*/
|
||||||
+ (nullable instancetype)keyWithData:(NSData *)data;
|
+ (nullable instancetype)keyWithData:(NSData *)data;
|
||||||
|
@ -26,6 +26,18 @@ extern const NSUInteger kAES256_KeyByteLength;
|
||||||
|
|
||||||
@end
|
@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
|
@interface Cryptography : NSObject
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, TSMACType) {
|
typedef NS_ENUM(NSInteger, TSMACType) {
|
||||||
|
@ -69,14 +81,20 @@ typedef NS_ENUM(NSInteger, TSMACType) {
|
||||||
outKey:(NSData *_Nonnull *_Nullable)outKey
|
outKey:(NSData *_Nonnull *_Nullable)outKey
|
||||||
outDigest:(NSData *_Nonnull *_Nullable)outDigest;
|
outDigest:(NSData *_Nonnull *_Nullable)outDigest;
|
||||||
|
|
||||||
+ (nullable NSData *)encryptAESGCMWithData:(NSData *)plaintextData key:(OWSAES256Key *)key;
|
+ (nullable AES25GCMEncryptionResult *)encryptAESGCMWithData:(NSData *)plaintext
|
||||||
+ (nullable NSData *)decryptAESGCMWithData:(NSData *)encryptedData key:(OWSAES256Key *)key;
|
additionalAuthenticatedData:(nullable NSData *)additionalAuthenticatedData
|
||||||
|
key:(OWSAES256Key *)key
|
||||||
|
NS_SWIFT_NAME(encryptAESGCM(plainTextData:additionalAuthenticatedData:key:));
|
||||||
|
|
||||||
+ (nullable NSData *)decryptAESGCMWithInitializationVector:(NSData *)initializationVector
|
+ (nullable NSData *)decryptAESGCMWithInitializationVector:(NSData *)initializationVector
|
||||||
ciphertext:(NSData *)ciphertext
|
ciphertext:(NSData *)ciphertext
|
||||||
|
additionalAuthenticatedData:(nullable NSData *)additionalAuthenticatedData
|
||||||
authTag:(NSData *)authTagFromEncrypt
|
authTag:(NSData *)authTagFromEncrypt
|
||||||
key:(OWSAES256Key *)key;
|
key:(OWSAES256Key *)key;
|
||||||
|
|
||||||
|
+ (nullable NSData *)encryptAESGCMWithProfileData:(NSData *)plaintextData key:(OWSAES256Key *)key;
|
||||||
|
+ (nullable NSData *)decryptAESGCMWithProfileData:(NSData *)encryptedData key:(OWSAES256Key *)key;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
|
@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
// Returned by many OpenSSL functions - indicating success
|
// Returned by many OpenSSL functions - indicating success
|
||||||
const int kOpenSSLSuccess = 1;
|
const int kOpenSSLSuccess = 1;
|
||||||
|
|
||||||
// length of initialization nonce
|
// length of initialization nonce for AES256-GCM
|
||||||
static const NSUInteger kAESGCM256_IVLength = 12;
|
static const NSUInteger kAESGCM256_IVLength = 12;
|
||||||
|
|
||||||
// length of authentication tag for AES256-GCM
|
// length of authentication tag for AES256-GCM
|
||||||
|
@ -35,7 +35,7 @@ const NSUInteger kAES256_KeyByteLength = 32;
|
||||||
+ (nullable instancetype)keyWithData:(NSData *)data
|
+ (nullable instancetype)keyWithData:(NSData *)data
|
||||||
{
|
{
|
||||||
if (data.length != kAES256_KeyByteLength) {
|
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;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,30 @@ const NSUInteger kAES256_KeyByteLength = 32;
|
||||||
|
|
||||||
@end
|
@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
|
@implementation Cryptography
|
||||||
|
|
||||||
#pragma mark random bytes methods
|
#pragma mark random bytes methods
|
||||||
|
@ -464,7 +488,9 @@ const NSUInteger kAES256_KeyByteLength = 32;
|
||||||
return [encryptedPaddedData copy];
|
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];
|
NSData *initializationVector = [Cryptography generateRandomBytes:kAESGCM256_IVLength];
|
||||||
NSMutableData *ciphertext = [NSMutableData dataWithLength:plaintext.length];
|
NSMutableData *ciphertext = [NSMutableData dataWithLength:plaintext.length];
|
||||||
|
@ -496,6 +522,26 @@ const NSUInteger kAES256_KeyByteLength = 32;
|
||||||
|
|
||||||
int bytesEncrypted = 0;
|
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.
|
// 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 -
|
// 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
|
// Clean up
|
||||||
EVP_CIPHER_CTX_free(ctx);
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
|
|
||||||
// build up return value: initializationVector || ciphertext || authTag
|
AES25GCMEncryptionResult *_Nullable result =
|
||||||
NSMutableData *encryptedData = [initializationVector mutableCopy];
|
[[AES25GCMEncryptionResult alloc] initWithCipherText:ciphertext
|
||||||
[encryptedData appendData:ciphertext];
|
initializationVector:initializationVector
|
||||||
[encryptedData appendData:authTag];
|
authTag:authTag];
|
||||||
|
|
||||||
return [encryptedData copy];
|
return result;
|
||||||
}
|
|
||||||
|
|
||||||
+ (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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (nullable NSData *)decryptAESGCMWithInitializationVector:(NSData *)initializationVector
|
+ (nullable NSData *)decryptAESGCMWithInitializationVector:(NSData *)initializationVector
|
||||||
ciphertext:(NSData *)ciphertext
|
ciphertext:(NSData *)ciphertext
|
||||||
|
additionalAuthenticatedData:(nullable NSData *)additionalAuthenticatedData
|
||||||
authTag:(NSData *)authTagFromEncrypt
|
authTag:(NSData *)authTagFromEncrypt
|
||||||
key:(OWSAES256Key *)key
|
key:(OWSAES256Key *)key
|
||||||
{
|
{
|
||||||
|
@ -593,12 +625,27 @@ const NSUInteger kAES256_KeyByteLength = 32;
|
||||||
return nil;
|
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.
|
// 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 -
|
// 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.
|
// feeding each chunk to EVP_DecryptUpdate, which can be called multiple times.
|
||||||
// For simplicity, we currently decrypt the entire ciphertext in one shot.
|
// 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)
|
if (EVP_DecryptUpdate(ctx, plaintext.mutableBytes, &decryptedBytes, ciphertext.bytes, (int)ciphertext.length)
|
||||||
!= kOpenSSLSuccess) {
|
!= kOpenSSLSuccess) {
|
||||||
OWSFail(@"%@ decryptUpdate failed", self.logTag);
|
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
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
Loading…
Reference in a new issue