Integrate with new contact discovery endpoint

Also:
* use system cookie parsing
* add AESGCM additional authenticated data parameter

// FREEBIE
This commit is contained in:
Michael Kirk 2018-07-20 08:45:46 -06:00
parent a611625691
commit b42f528713
16 changed files with 524 additions and 146 deletions

2
Pods

@ -1 +1 @@
Subproject commit a2394bbafc099db434ee91e7a617c412750c44b9
Subproject commit 5dc9c23dc3229ab6a884372a0e2cf62cb0904be6

View File

@ -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 = "<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>"; };
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>"; };
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>"; };
@ -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 */,

View 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)
}
}

View File

@ -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

View File

@ -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<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

View File

@ -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<NSHTTPCookie *> *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<NSHTTPCookie *> *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) {

View File

@ -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<String>
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<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
}
}

View File

@ -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

View File

@ -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

View File

@ -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<NSHTTPCookie *> *)cookies;
+ (TSRequest *)remoteAttestationAuthRequest;

View File

@ -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<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];
}
return request;
}
+ (TSRequest *)remoteAttestationAuthRequest

View File

@ -5,6 +5,8 @@
@interface TSRequest : NSMutableURLRequest
@property (nonatomic) BOOL shouldHaveAuthorizationHeaders;
@property (nullable) NSString *authUsername;
@property (nullable) NSString *authPassword;
@property (nonatomic, readonly) NSDictionary *parameters;

View File

@ -58,6 +58,8 @@
_parameters = parameters ?: @{};
[self setHTTPMethod:method];
self.shouldHaveAuthorizationHeaders = YES;
_authUsername = [TSAccountManager localNumber];
_authPassword = [TSAccountManager serverAuthToken];
return self;
}

View File

@ -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;
}

View File

@ -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 <NSSecureCoding>
/// 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

View File

@ -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