Signal-iOS/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m
Adam Mork 728862da6f
[Payments] Biometry Payments Lock
* payment lock

* add 30 day timeout to showing the payment lock suggestion

* fix linting

* update comment

* Payment Lock: Fix project file & submodules (#4878)

* revert ringrtc changes

* revert pods changes

* fix project file

* PR Comment suggestions and fixes

* PR suggestions

* revert screen lock to previous implementation

* rename OWSPaymentLock to OWSPaymentsLock (missing plural s), remove
super-class dependency. Make OWSPaymentsLock more "swifty".

* revert changes to call-site for OWSScreenLock

* update call-sites for OWSPaymentsLock

* forgot to add PaymentOnboarding class and localization comment improvements

* change function call-site

* Remove OWSLocalAuthentication from project

* remove OWSLocalAuth from project file

* make edge-case default more secure if storage not ready

* fix project file

* lint fix

* fix linting errors

* Revert compact format style for OWSLocalizedString

* fix localization call-sites to use literals, update localization file after autogen

* Add new Payments Lock Prompt view to be shown after activating payments. Factor out the BiometryType "current biometry" into its own class with helpers to easily getting the devices current setup.

* fix linting issues, sort project file, and autogen localizations

* require payments lock to look at the recovery phrase

* fix linting issue

* fix missing localizations (caused by dynamic creation in previous commit)

* use Pods commit from signal/main

* re-run linter on latest commits

* new header comments for new branch files

* update submodule commits

* Use existing secondsInDay constant kDayInterval, use weak self guard statement instead of optional self in escaping closure.

* Revert submodule changes

* Remove duplicate copyright headers

* Revert copyright header changes

* Restore some missing translations

* Add localization for unknown LocalAuthentication error/state.

* remove Pods changes

* linting

* use submodule commits from main

* subclass the old Objc OWSViewController super-class, should fix CICD

* change capitalized Passcode to lower-case in non-title situation.

* remove early exit guard from biometryType computed function. Reason being that the type can be gathered from a policy that evaluates to false. In a case where the user has a FaceID phone but its disabled, the messaging would be incorrect. removing the early exit evaluates the biometryType which apple provides even if the policy returns false.

* inline snooze date

* inject write transaction to some convenience methods to reduce database overhead when multiple calls need to happen at the same time.

* make combined set and snooze function, update combined call-site

* rename long function

* use false instead of sender.on to avoid sneaky view issues

* add tryToUnlockPromise function to clean up call sites

* change superclass, fix Promise statement

* call super class function

* remove objc

* add unlock failed action sheet helper class and put at all relevant call-sites, still need to test though.

* add unlock failed action sheet helper class and put at all relevant call-sites, still need to test though. linting

* project file changes for new file

* re-render payments lock toggle after failure to change setting.

re-render payments lock toggle after failure to change setting.

* move around action sheet call-site to account for an action sheet already being presented. remove unecc. return

* fix merge conflicts, linting

* re-gen strings file

---------

Co-authored-by: Max Radermacher <max@signal.org>
2023-02-03 12:56:54 -08:00

958 lines
40 KiB
Objective-C

//
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
#import "OWSRequestFactory.h"
#import "NSData+keyVersionByte.h"
#import "OWS2FAManager.h"
#import "OWSDevice.h"
#import "OWSIdentityManager.h"
#import "ProfileManagerProtocol.h"
#import "SSKEnvironment.h"
#import "SignedPrekeyRecord.h"
#import "TSAccountManager.h"
#import "TSRequest.h"
#import <Curve25519Kit/Curve25519.h>
#import <SignalCoreKit/Cryptography.h>
#import <SignalCoreKit/NSData+OWS.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const OWSRequestKey_AuthKey = @"AuthKey";
static NSString *_Nullable queryParamForIdentity(OWSIdentity identity)
{
switch (identity) {
case OWSIdentityACI:
return nil;
case OWSIdentityPNI:
return @"identity=pni";
}
}
@implementation OWSRequestFactory
+ (TSRequest *)disable2FARequest
{
return [TSRequest requestWithUrl:[NSURL URLWithString:self.textSecure2FAAPI] method:@"DELETE" parameters:@{}];
}
+ (TSRequest *)acknowledgeMessageDeliveryRequestWithServerGuid:(NSString *)serverGuid
{
OWSAssertDebug(serverGuid.length > 0);
NSString *path = [NSString stringWithFormat:@"v1/messages/uuid/%@", serverGuid];
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"DELETE" parameters:@{}];
}
+ (TSRequest *)deleteDeviceRequestWithDevice:(OWSDevice *)device
{
OWSAssertDebug(device);
NSString *path = [NSString stringWithFormat:self.textSecureDevicesAPIFormat, @(device.deviceId)];
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"DELETE" parameters:@{}];
}
+ (TSRequest *)deviceProvisioningCodeRequest
{
return [TSRequest requestWithUrl:[NSURL URLWithString:self.textSecureDeviceProvisioningCodeAPI]
method:@"GET"
parameters:@{}];
}
+ (TSRequest *)getDevicesRequest
{
NSString *path = [NSString stringWithFormat:self.textSecureDevicesAPIFormat, @""];
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
}
+ (TSRequest *)getMessagesRequest
{
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:@"v1/messages"] method:@"GET" parameters:@{}];
[StoryManager appendStoryHeadersToRequest:request];
request.shouldMarkDeregisteredOn401 = YES;
return request;
}
+ (TSRequest *)getUnversionedProfileRequestWithAddress:(SignalServiceAddress *)address
udAccessKey:(nullable SMKUDAccessKey *)udAccessKey
{
OWSAssertDebug(address.isValid);
OWSAssertDebug(address.uuid != nil);
NSString *path = [NSString stringWithFormat:@"v1/profile/%@", address.uuidString];
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
if (udAccessKey != nil) {
[self useUDAuthWithRequest:request accessKey:udAccessKey];
}
return request;
}
+ (TSRequest *)getVersionedProfileRequestWithAddress:(SignalServiceAddress *)address
profileKeyVersion:(nullable NSString *)profileKeyVersion
credentialRequest:(nullable NSData *)credentialRequest
udAccessKey:(nullable SMKUDAccessKey *)udAccessKey
{
OWSAssertDebug(address.isValid);
OWSAssertDebug(address.uuid != nil);
NSString *uuidParam = address.uuid.UUIDString.lowercaseString;
NSString *_Nullable profileKeyVersionParam = profileKeyVersion.lowercaseString;
NSString *_Nullable credentialRequestParam = credentialRequest.hexadecimalString.lowercaseString;
// GET /v1/profile/{uuid}/{version}/{profile_key_credential_request}
NSString *path;
if (profileKeyVersion.length > 0 && credentialRequest.length > 0) {
path = [NSString stringWithFormat:@"v1/profile/%@/%@/%@?credentialType=expiringProfileKey",
uuidParam,
profileKeyVersionParam,
credentialRequestParam];
} else if (profileKeyVersion.length > 0) {
path = [NSString stringWithFormat:@"v1/profile/%@/%@", uuidParam, profileKeyVersionParam];
} else {
path = [NSString stringWithFormat:@"v1/profile/%@", uuidParam];
}
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
if (udAccessKey != nil) {
[self useUDAuthWithRequest:request accessKey:udAccessKey];
}
return request;
}
+ (TSRequest *)turnServerInfoRequest
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"v1/accounts/turn"] method:@"GET" parameters:@{}];
}
+ (TSRequest *)allocAttachmentRequestV2
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"v2/attachments/form/upload"] method:@"GET" parameters:@{}];
}
+ (TSRequest *)allocAttachmentRequestV3
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"v3/attachments/form/upload"] method:@"GET" parameters:@{}];
}
+ (TSRequest *)availablePreKeysCountRequestForIdentity:(OWSIdentity)identity
{
NSString *path = self.textSecureKeysAPI;
NSString *queryParam = queryParamForIdentity(identity);
if (queryParam != nil) {
path = [path stringByAppendingFormat:@"?%@", queryParam];
}
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
}
+ (TSRequest *)contactsIntersectionRequestWithHashesArray:(NSArray<NSString *> *)hashes
{
OWSAssertDebug(hashes.count > 0);
NSString *path = [NSString stringWithFormat:@"%@/%@", self.textSecureDirectoryAPI, @"tokens"];
return [TSRequest requestWithUrl:[NSURL URLWithString:path]
method:@"PUT"
parameters:@{
@"contacts" : hashes,
}];
}
+ (TSRequest *)currentSignedPreKeyRequest
{
NSString *path = self.textSecureSignedKeysAPI;
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
}
+ (TSRequest *)profileAvatarUploadFormRequest
{
NSString *path = self.textSecureProfileAvatarFormAPI;
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
}
+ (TSRequest *)recipientPreKeyRequestWithServiceId:(NSUUID *)serviceId
deviceId:(NSString *)deviceId
udAccessKey:(nullable SMKUDAccessKey *)udAccessKey
{
OWSAssertDebug(deviceId.length > 0);
NSString *path = [NSString stringWithFormat:@"%@/%@/%@", self.textSecureKeysAPI, serviceId.UUIDString, deviceId];
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
if (udAccessKey != nil) {
[self useUDAuthWithRequest:request accessKey:udAccessKey];
}
return request;
}
+ (TSRequest *)registerForPushRequestWithPushIdentifier:(NSString *)identifier
voipIdentifier:(nullable NSString *)voipId
{
OWSAssertDebug(identifier.length > 0);
NSString *path = [NSString stringWithFormat:@"%@/%@", self.textSecureAccountsAPI, @"apn"];
NSMutableDictionary *parameters = [@{ @"apnRegistrationId" : identifier } mutableCopy];
if (voipId.length > 0) {
parameters[@"voipRegistrationId"] = voipId;
} else {
OWSAssertDebug(SSKFeatureFlags.notificationServiceExtension);
}
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"PUT" parameters:parameters];
}
+ (TSRequest *)updatePrimaryDeviceAttributesRequest
{
// If you are updating capabilities for a secondary device, use `updateSecondaryDeviceCapabilities` instead
OWSAssertDebug(self.tsAccountManager.isPrimaryDevice);
NSString *authKey = self.tsAccountManager.storedServerAuthToken;
OWSAssertDebug(authKey.length > 0);
NSString *_Nullable pin = [self.ows2FAManager pinCode];
BOOL isManualMessageFetchEnabled = self.tsAccountManager.isManualMessageFetchEnabled;
NSDictionary<NSString *, id> *accountAttributes = [self accountAttributesWithAuthKey:authKey
pin:pin
encryptedDeviceName:nil
isManualMessageFetchEnabled:isManualMessageFetchEnabled
isSecondaryDevice:NO];
return [TSRequest requestWithUrl:[NSURL URLWithString:self.textSecureAttributesAPI]
method:@"PUT"
parameters:accountAttributes];
}
+ (TSRequest *)accountWhoAmIRequest
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"v1/accounts/whoami"] method:@"GET" parameters:@{}];
}
+ (TSRequest *)unregisterAccountRequest
{
NSString *path = [NSString stringWithFormat:@"%@/%@", self.textSecureAccountsAPI, @"me"];
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"DELETE" parameters:@{}];
}
+ (TSRequest *)requestVerificationCodeRequestWithE164:(NSString *)e164
preauthChallenge:(nullable NSString *)preauthChallenge
captchaToken:(nullable NSString *)captchaToken
transport:(TSVerificationTransport)transport
{
OWSAssertDebug(e164.length > 0);
NSMutableArray<NSURLQueryItem *> *queryItems = [NSMutableArray new];
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"client" value:@"ios"]];
if (captchaToken.length > 0) {
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"captcha" value:captchaToken]];
}
if (preauthChallenge.length > 0) {
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"challenge" value:preauthChallenge]];
}
NSString *path = [NSString
stringWithFormat:@"%@/%@/code/%@", self.textSecureAccountsAPI, [self stringForTransport:transport], e164];
NSURLComponents *components = [[NSURLComponents alloc] initWithString:path];
components.queryItems = queryItems;
TSRequest *request = [TSRequest requestWithUrl:components.URL method:@"GET" parameters:@{}];
request.shouldHaveAuthorizationHeaders = NO;
if (transport == TSVerificationTransportVoice) {
NSString *_Nullable localizationHeader = [self voiceCodeLocalizationHeader];
if (localizationHeader.length > 0) {
[request setValue:localizationHeader forHTTPHeaderField:@"Accept-Language"];
}
}
return request;
}
+ (nullable NSString *)voiceCodeLocalizationHeader
{
NSLocale *locale = [NSLocale currentLocale];
NSString *_Nullable languageCode = [locale objectForKey:NSLocaleLanguageCode];
NSString *_Nullable countryCode = [locale objectForKey:NSLocaleCountryCode];
if (!languageCode) {
return nil;
}
OWSAssertDebug([languageCode rangeOfString:@"-"].location == NSNotFound);
if (!countryCode) {
// In the absence of a country code, just send a language code.
return languageCode;
}
OWSAssertDebug(languageCode.length == 2);
OWSAssertDebug(countryCode.length == 2);
return [NSString stringWithFormat:@"%@-%@", languageCode, countryCode];
}
+ (NSString *)stringForTransport:(TSVerificationTransport)transport
{
switch (transport) {
case TSVerificationTransportSMS:
return @"sms";
case TSVerificationTransportVoice:
return @"voice";
}
}
+ (TSRequest *)verifyPrimaryDeviceRequestWithVerificationCode:(NSString *)verificationCode
phoneNumber:(NSString *)phoneNumber
authKey:(NSString *)authKey
pin:(nullable NSString *)pin
checkForAvailableTransfer:(BOOL)checkForAvailableTransfer
{
OWSAssertDebug(verificationCode.length > 0);
OWSAssertDebug(phoneNumber.length > 0);
OWSAssertDebug(authKey.length > 0);
NSString *path = [NSString stringWithFormat:@"%@/code/%@", self.textSecureAccountsAPI, verificationCode];
if (checkForAvailableTransfer) {
path = [path stringByAppendingString:@"?transfer=true"];
}
BOOL isManualMessageFetchEnabled = self.tsAccountManager.isManualMessageFetchEnabled;
NSMutableDictionary<NSString *, id> *accountAttributes =
[[self accountAttributesWithAuthKey:authKey
pin:pin
encryptedDeviceName:nil
isManualMessageFetchEnabled:isManualMessageFetchEnabled
isSecondaryDevice:NO] mutableCopy];
[accountAttributes removeObjectForKey:OWSRequestKey_AuthKey];
TSRequest *request =
[TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"PUT" parameters:accountAttributes];
// The "verify code" request handles auth differently.
request.authUsername = phoneNumber;
request.authPassword = authKey;
return request;
}
+ (TSRequest *)verifySecondaryDeviceRequestWithVerificationCode:(NSString *)verificationCode
phoneNumber:(NSString *)phoneNumber
authKey:(NSString *)authKey
encryptedDeviceName:(NSData *)encryptedDeviceName
{
OWSAssertDebug(verificationCode.length > 0);
OWSAssertDebug(phoneNumber.length > 0);
OWSAssertDebug(authKey.length > 0);
OWSAssertDebug(encryptedDeviceName.length > 0);
NSString *path = [NSString stringWithFormat:@"v1/devices/%@", verificationCode];
NSMutableDictionary<NSString *, id> *accountAttributes = [[self accountAttributesWithAuthKey:authKey
pin:nil
encryptedDeviceName:encryptedDeviceName
isManualMessageFetchEnabled:YES
isSecondaryDevice:YES] mutableCopy];
[accountAttributes removeObjectForKey:OWSRequestKey_AuthKey];
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path]
method:@"PUT"
parameters:accountAttributes];
// The "verify code" request handles auth differently.
request.authUsername = phoneNumber;
request.authPassword = authKey;
return request;
}
+ (TSRequest *)currencyConversionRequest NS_SWIFT_NAME(currencyConversionRequest())
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"v1/payments/conversions"] method:@"GET" parameters:@{}];
}
+ (NSDictionary<NSString *, id> *)accountAttributesWithAuthKey:(NSString *)authKey
pin:(nullable NSString *)pin
encryptedDeviceName:(nullable NSData *)encryptedDeviceName
isManualMessageFetchEnabled:(BOOL)isManualMessageFetchEnabled
isSecondaryDevice:(BOOL)isSecondaryDevice
{
OWSAssertDebug(authKey.length > 0);
__block uint32_t registrationId;
__block uint32_t pniRegistrationId;
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
registrationId = [self.tsAccountManager getOrGenerateRegistrationIdWithTransaction:transaction];
pniRegistrationId = [self.tsAccountManager getOrGeneratePniRegistrationIdWithTransaction:transaction];
});
OWSAES256Key *profileKey = [self.profileManager localProfileKey];
NSError *error;
SMKUDAccessKey *_Nullable udAccessKey = [[SMKUDAccessKey alloc] initWithProfileKey:profileKey.keyData error:&error];
if (error || udAccessKey.keyData.length < 1) {
// Crash app if UD cannot be enabled.
OWSFail(@"Could not determine UD access key: %@.", error);
}
BOOL allowUnrestrictedUD = [self.udManager shouldAllowUnrestrictedAccessLocal] && udAccessKey != nil;
// We no longer include the signalingKey.
NSMutableDictionary *accountAttributes = [@{
OWSRequestKey_AuthKey : authKey,
@"voice" : @(YES), // all Signal-iOS clients support voice
@"video" : @(YES), // all Signal-iOS clients support WebRTC-based voice and video calls.
@"fetchesMessages" : @(isManualMessageFetchEnabled), // devices that don't support push must tell the server
// they fetch messages manually
@"registrationId" : [NSString stringWithFormat:@"%i", registrationId],
@"pniRegistrationId" : [NSString stringWithFormat:@"%i", pniRegistrationId],
@"unidentifiedAccessKey" : udAccessKey.keyData.base64EncodedString,
@"unrestrictedUnidentifiedAccess" : @(allowUnrestrictedUD),
} mutableCopy];
NSString *_Nullable registrationLockToken = [KeyBackupServiceObjcBridge deriveRegistrationLockToken];
if (registrationLockToken.length > 0 && OWS2FAManager.shared.isRegistrationLockV2Enabled) {
accountAttributes[@"registrationLock"] = registrationLockToken;
} else if (pin.length > 0 && self.ows2FAManager.mode != OWS2FAMode_V2) {
accountAttributes[@"pin"] = pin;
}
if (encryptedDeviceName.length > 0) {
accountAttributes[@"name"] = encryptedDeviceName.base64EncodedString;
}
if (SSKFeatureFlags.phoneNumberDiscoverability) {
accountAttributes[@"discoverableByPhoneNumber"] = @(self.tsAccountManager.isDiscoverableByPhoneNumber);
}
accountAttributes[@"capabilities"] = [self deviceCapabilitiesWithIsSecondaryDevice:isSecondaryDevice];
return [accountAttributes copy];
}
+ (TSRequest *)updateSecondaryDeviceCapabilitiesRequest
{
// If you are updating capabilities for a primary device, use `updateAccountAttributes` instead
OWSAssertDebug(!self.tsAccountManager.isPrimaryDevice);
return [TSRequest requestWithUrl:[NSURL URLWithString:@"v1/devices/capabilities"]
method:@"PUT"
parameters:[self deviceCapabilitiesWithIsSecondaryDevice:YES]];
}
+ (NSDictionary<NSString *, NSNumber *> *)deviceCapabilitiesForLocalDevice
{
// tsAccountManager.isPrimaryDevice only has a valid value for registered
// devices.
OWSAssertDebug(self.tsAccountManager.isRegisteredAndReady);
BOOL isSecondaryDevice = !self.tsAccountManager.isPrimaryDevice;
return [self deviceCapabilitiesWithIsSecondaryDevice:isSecondaryDevice];
}
+ (NSDictionary<NSString *, NSNumber *> *)deviceCapabilitiesWithIsSecondaryDevice:(BOOL)isSecondaryDevice
{
NSMutableDictionary<NSString *, NSNumber *> *capabilities = [NSMutableDictionary new];
capabilities[@"gv2"] = @(YES);
capabilities[@"gv2-2"] = @(YES);
capabilities[@"gv2-3"] = @(YES);
capabilities[@"transfer"] = @(YES);
capabilities[@"announcementGroup"] = @(YES);
capabilities[@"senderKey"] = @(YES);
if (RemoteConfig.stories || isSecondaryDevice) {
capabilities[@"stories"] = @(YES);
}
if (RemoteConfig.canReceiveGiftBadges) {
capabilities[@"giftBadges"] = @(YES);
}
// If the storage service requires (or will require) secondary devices
// to have a capability in order to be linked, we might need to always
// set that capability here if isSecondaryDevice is true.
if (KeyBackupServiceObjcBridge.hasBackedUpMasterKey) {
capabilities[@"storage"] = @(YES);
}
capabilities[@"changeNumber"] = @(YES);
OWSLogInfo(@"local device capabilities: %@", capabilities);
return [capabilities copy];
}
+ (TSRequest *)submitMessageRequestWithServiceId:(NSUUID *)serviceId
messages:(NSArray *)messages
timestamp:(uint64_t)timestamp
udAccessKey:(nullable SMKUDAccessKey *)udAccessKey
isOnline:(BOOL)isOnline
isUrgent:(BOOL)isUrgent
isStory:(BOOL)isStory
{
// NOTE: messages may be empty; See comments in OWSDeviceManager.
OWSAssertDebug(timestamp > 0);
NSString *path = [self.textSecureMessagesAPI stringByAppendingString:serviceId.UUIDString];
path = [path stringByAppendingFormat:@"?story=%@", isStory ? @"true" : @"false"];
// Returns the per-account-message parameters used when submitting a message to
// the Signal Web Service.
// See
// <https://github.com/signalapp/Signal-Server/blob/65da844d70369cb8b44966cfb2d2eb9b925a6ba4/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java>.
NSDictionary *parameters =
@{ @"messages" : messages, @"timestamp" : @(timestamp), @"online" : @(isOnline), @"urgent" : @(isUrgent) };
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"PUT" parameters:parameters];
if (udAccessKey != nil) {
[self useUDAuthWithRequest:request accessKey:udAccessKey];
}
return request;
}
+ (TSRequest *)submitMultiRecipientMessageRequestWithCiphertext:(NSData *)ciphertext
compositeUDAccessKey:(SMKUDAccessKey *)udAccessKey
timestamp:(uint64_t)timestamp
isOnline:(BOOL)isOnline
isUrgent:(BOOL)isUrgent
isStory:(BOOL)isStory
{
OWSAssertDebug(ciphertext);
OWSAssertDebug(udAccessKey);
OWSAssertDebug(timestamp > 0);
// We build the URL by hand instead of passing the query parameters into the query parameters
// AFNetworking won't handle both query parameters and an httpBody (which we need here)
NSURLComponents *components = [[NSURLComponents alloc] initWithString:self.textSecureMultiRecipientMessageAPI];
components.queryItems = @[
[NSURLQueryItem queryItemWithName:@"ts" value:[@(timestamp) stringValue]],
[NSURLQueryItem queryItemWithName:@"online" value:isOnline ? @"true" : @"false"],
[NSURLQueryItem queryItemWithName:@"urgent" value:isUrgent ? @"true" : @"false"],
[NSURLQueryItem queryItemWithName:@"story" value:isStory ? @"true" : @"false"],
];
NSURL *url = [components URL];
TSRequest *request = [TSRequest requestWithUrl:url method:@"PUT" parameters:nil];
[request setValue:kSenderKeySendRequestBodyContentType forHTTPHeaderField:@"Content-Type"];
if (udAccessKey != nil) {
[self useUDAuthWithRequest:request accessKey:udAccessKey];
}
request.HTTPBody = [ciphertext copy];
return request;
}
+ (TSRequest *)registerSignedPrekeyRequestForIdentity:(OWSIdentity)identity
signedPreKey:(SignedPreKeyRecord *)signedPreKey
{
OWSAssertDebug(signedPreKey);
NSString *path = self.textSecureSignedKeysAPI;
NSString *queryParam = queryParamForIdentity(identity);
if (queryParam != nil) {
path = [path stringByAppendingFormat:@"?%@", queryParam];
}
return [TSRequest requestWithUrl:[NSURL URLWithString:path]
method:@"PUT"
parameters:[self dictionaryFromSignedPreKey:signedPreKey]];
}
+ (TSRequest *)registerPrekeysRequestForIdentity:(OWSIdentity)identity
prekeyArray:(NSArray *)prekeys
identityKey:(NSData *)identityKeyPublic
signedPreKey:(SignedPreKeyRecord *)signedPreKey
{
OWSAssertDebug(prekeys.count > 0);
OWSAssertDebug(identityKeyPublic.length > 0);
OWSAssertDebug(signedPreKey);
NSString *path = self.textSecureKeysAPI;
NSString *queryParam = queryParamForIdentity(identity);
if (queryParam != nil) {
path = [path stringByAppendingFormat:@"?%@", queryParam];
}
NSString *publicIdentityKey = [[identityKeyPublic prependKeyType] base64EncodedStringWithOptions:0];
NSMutableArray *serializedPrekeyList = [NSMutableArray array];
for (PreKeyRecord *preKey in prekeys) {
[serializedPrekeyList addObject:[self dictionaryFromPreKey:preKey]];
}
return [TSRequest requestWithUrl:[NSURL URLWithString:path]
method:@"PUT"
parameters:@{
@"preKeys" : serializedPrekeyList,
@"signedPreKey" : [self dictionaryFromSignedPreKey:signedPreKey],
@"identityKey" : publicIdentityKey
}];
}
+ (NSDictionary *)dictionaryFromPreKey:(PreKeyRecord *)preKey
{
return @{
@"keyId" : @(preKey.Id),
@"publicKey" : [[preKey.keyPair.publicKey prependKeyType] base64EncodedStringWithOptions:0],
};
}
+ (NSDictionary *)dictionaryFromSignedPreKey:(SignedPreKeyRecord *)preKey
{
return @{
@"keyId" : @(preKey.Id),
@"publicKey" : [[preKey.keyPair.publicKey prependKeyType] base64EncodedStringWithOptions:0],
@"signature" : [preKey.signature base64EncodedStringWithOptions:0]
};
}
#pragma mark - Storage Service
+ (TSRequest *)storageAuthRequest
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"v1/storage/auth"] method:@"GET" parameters:@{}];
}
#pragma mark - Remote Attestation
+ (TSRequest *)remoteAttestationAuthRequestForKeyBackup
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"v1/backup/auth"] method:@"GET" parameters:@{}];
}
+ (TSRequest *)remoteAttestationAuthRequestForContactDiscovery
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"v1/directory/auth"] method:@"GET" parameters:@{}];
}
+ (TSRequest *)remoteAttestationAuthRequestForCDSI
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"v2/directory/auth"] method:@"GET" parameters:@{}];
}
#pragma mark - KBS
+ (TSRequest *)kbsEnclaveTokenRequestWithEnclaveName:(NSString *)enclaveName
authUsername:(NSString *)authUsername
authPassword:(NSString *)authPassword
cookies:(NSArray<NSHTTPCookie *> *)cookies
{
NSString *path = [NSString stringWithFormat:@"v1/token/%@", enclaveName];
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
request.authUsername = authUsername;
request.authPassword = authPassword;
// Set the cookie header.
// OWSURLSession disables default cookie handling for all requests.
OWSAssertDebug(request.allHTTPHeaderFields.count == 0);
[request setAllHTTPHeaderFields:[NSHTTPCookie requestHeaderFieldsWithCookies:cookies]];
return request;
}
+ (TSRequest *)kbsEnclaveRequestWithRequestId:(NSData *)requestId
data:(NSData *)data
cryptIv:(NSData *)cryptIv
cryptMac:(NSData *)cryptMac
enclaveName:(NSString *)enclaveName
authUsername:(NSString *)authUsername
authPassword:(NSString *)authPassword
cookies:(NSArray<NSHTTPCookie *> *)cookies
requestType:(NSString *)requestType
{
NSString *path = [NSString stringWithFormat:@"v1/backup/%@", enclaveName];
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path]
method:@"PUT"
parameters:@{
@"requestId" : requestId.base64EncodedString,
@"data" : data.base64EncodedString,
@"iv" : cryptIv.base64EncodedString,
@"mac" : cryptMac.base64EncodedString,
@"type" : requestType
}];
request.authUsername = authUsername;
request.authPassword = authPassword;
// Set the cookie header.
// OWSURLSession disables default cookie handling for all requests.
OWSAssertDebug(request.allHTTPHeaderFields.count == 0);
[request setAllHTTPHeaderFields:[NSHTTPCookie requestHeaderFieldsWithCookies:cookies]];
return request;
}
#pragma mark - UD
+ (TSRequest *)udSenderCertificateRequestWithUuidOnly:(BOOL)uuidOnly
{
NSString *path = @"v1/certificate/delivery?includeUuid=true";
if (uuidOnly) {
path = [path stringByAppendingString:@"&includeE164=false"];
}
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
}
+ (void)useUDAuthWithRequest:(TSRequest *)request accessKey:(SMKUDAccessKey *)udAccessKey
{
OWSAssertDebug(request);
OWSAssertDebug(udAccessKey);
// Suppress normal auth headers.
request.shouldHaveAuthorizationHeaders = NO;
// Add UD auth header.
[request setValue:[udAccessKey.keyData base64EncodedString] forHTTPHeaderField:@"Unidentified-Access-Key"];
request.isUDRequest = YES;
}
#pragma mark - Usernames
+ (TSRequest *)getProfileRequestWithUsername:(NSString *)username
{
OWSAssertDebug(username.length > 0);
NSString *urlEncodedUsername =
[username stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLUserAllowedCharacterSet]];
OWSAssertDebug(urlEncodedUsername.length > 0);
NSString *path = [NSString stringWithFormat:@"v1/profile/username/%@", urlEncodedUsername];
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
}
#pragma mark - Profiles
+ (TSRequest *)profileNameSetRequestWithEncryptedPaddedName:(NSData *)encryptedPaddedName
{
const NSUInteger kEncodedNameLength = 108;
NSString *base64EncodedName = [encryptedPaddedName base64EncodedString];
NSString *urlEncodedName;
// name length must match exactly
if (base64EncodedName.length == kEncodedNameLength) {
urlEncodedName = base64EncodedName.encodeURIComponent;
} else {
// if name length doesn't match exactly, use a blank name.
// Since names are required, the server will reject this with HTTP405,
// which is desirable - we want this request to fail rather than upload
// a broken name.
OWSFailDebug(@"Couldn't encode name.");
OWSAssertDebug(encryptedPaddedName == nil);
urlEncodedName = @"";
}
NSString *urlString = [NSString stringWithFormat:@"v1/profile/name/%@", urlEncodedName];
NSURL *url = [NSURL URLWithString:urlString];
TSRequest *request = [[TSRequest alloc] initWithURL:url];
request.HTTPMethod = @"PUT";
return request;
}
#pragma mark - Versioned Profiles
+ (TSRequest *)versionedProfileSetRequestWithName:(nullable ProfileValue *)name
bio:(nullable ProfileValue *)bio
bioEmoji:(nullable ProfileValue *)bioEmoji
hasAvatar:(BOOL)hasAvatar
paymentAddress:(nullable ProfileValue *)paymentAddress
visibleBadgeIds:(NSArray<NSString *> *)visibleBadgeIds
version:(NSString *)version
commitment:(NSData *)commitment
{
OWSAssertDebug(version.length > 0);
OWSAssertDebug(commitment.length > 0);
NSString *base64EncodedCommitment = [commitment base64EncodedString];
NSMutableDictionary<NSString *, NSObject *> *parameters = [@{
@"version" : version,
@"avatar" : @(hasAvatar),
@"commitment" : base64EncodedCommitment,
} mutableCopy];
if (name != nil) {
OWSAssertDebug(name.hasValidBase64Length);
parameters[@"name"] = name.encryptedBase64;
}
if (bio != nil) {
OWSAssertDebug(bio.hasValidBase64Length);
parameters[@"about"] = bio.encryptedBase64;
}
if (bioEmoji != nil) {
OWSAssertDebug(bioEmoji.hasValidBase64Length);
parameters[@"aboutEmoji"] = bioEmoji.encryptedBase64;
}
if (paymentAddress != nil) {
OWSAssertDebug(paymentAddress.hasValidBase64Length);
parameters[@"paymentAddress"] = paymentAddress.encryptedBase64;
}
parameters[@"badgeIds"] = [visibleBadgeIds copy];
NSURL *url = [NSURL URLWithString:self.textSecureVersionedProfileAPI];
return [TSRequest requestWithUrl:url
method:@"PUT"
parameters:parameters];
}
#pragma mark - Remote Config
+ (TSRequest *)getRemoteConfigRequest
{
NSURL *url = [NSURL URLWithString:@"/v1/config/"];
return [TSRequest requestWithUrl:url method:@"GET" parameters:@{}];
}
#pragma mark - Groups v2
+ (TSRequest *)groupAuthenticationCredentialRequestWithFromRedemptionSeconds:(uint64_t)fromRedemptionSeconds
toRedemptionSeconds:(uint64_t)toRedemptionSeconds
{
OWSAssertDebug(fromRedemptionSeconds > 0);
OWSAssertDebug(toRedemptionSeconds > 0);
NSString *path =
[NSString stringWithFormat:@"/v1/certificate/auth/group?redemptionStartSeconds=%llu&redemptionEndSeconds=%llu",
(unsigned long long)fromRedemptionSeconds,
(unsigned long long)toRedemptionSeconds];
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
}
#pragma mark - Payments
+ (TSRequest *)paymentsAuthenticationCredentialRequest
{
NSString *path = @"/v1/payments/auth";
return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}];
}
#pragma mark - Spam
+ (TSRequest *)pushChallengeRequest
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"/v1/challenge/push"] method:@"POST" parameters:@{}];
}
+ (TSRequest *)pushChallengeResponseWithToken:(NSString *)challengeToken
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"/v1/challenge"]
method:@"PUT"
parameters:@{ @"type" : @"rateLimitPushChallenge", @"challenge" : challengeToken }];
}
+ (TSRequest *)recaptchChallengeResponseWithToken:(NSString *)serverToken captchaToken:(NSString *)captchaToken
{
return [TSRequest requestWithUrl:[NSURL URLWithString:@"/v1/challenge"]
method:@"PUT"
parameters:@{ @"type" : @"recaptcha", @"token" : serverToken, @"captcha" : captchaToken }];
}
#pragma mark - Subscriptions
+ (TSRequest *)subscriptionCreateStripePaymentMethodRequest:(NSString *)base64SubscriberID
{
TSRequest *request = [TSRequest
requestWithUrl:[NSURL URLWithString:[NSString stringWithFormat:@"/v1/subscription/%@/create_payment_method",
base64SubscriberID]]
method:@"POST"
parameters:@{}];
request.shouldHaveAuthorizationHeaders = NO;
request.shouldRedactUrlInLogs = YES;
return request;
}
+ (TSRequest *)subscriptionCreatePaypalPaymentMethodRequest:(NSString *)base64SubscriberID
returnUrl:(NSURL *)returnUrl
cancelUrl:(NSURL *)cancelUrl
{
TSRequest *request = [TSRequest
requestWithUrl:[NSURL
URLWithString:[NSString stringWithFormat:@"/v1/subscription/%@/create_payment_method/paypal",
base64SubscriberID]]
method:@"POST"
parameters:@{ @"returnUrl" : returnUrl.absoluteString, @"cancelUrl" : cancelUrl.absoluteString }];
request.shouldHaveAuthorizationHeaders = NO;
request.shouldRedactUrlInLogs = YES;
return request;
}
+ (TSRequest *)subscriptionSetDefaultPaymentMethodRequest:(NSString *)base64SubscriberID
processor:(NSString *)processor
paymentID:(NSString *)paymentID
{
TSRequest *request = [TSRequest
requestWithUrl:[NSURL
URLWithString:[NSString stringWithFormat:@"/v1/subscription/%@/default_payment_method/%@/%@",
base64SubscriberID,
processor,
paymentID]]
method:@"POST"
parameters:@{}];
request.shouldHaveAuthorizationHeaders = NO;
request.shouldRedactUrlInLogs = YES;
return request;
}
+ (TSRequest *)subscriptionSetSubscriptionLevelRequest:(NSString *)base64SubscriberID level:(NSString *)level currency:(NSString *)currency idempotencyKey:(NSString *)idempotencyKey {
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:[NSString stringWithFormat:@"/v1/subscription/%@/level/%@/%@/%@", base64SubscriberID, level, currency, idempotencyKey]]
method:@"PUT"
parameters:@{}];
request.shouldHaveAuthorizationHeaders = NO;
request.shouldRedactUrlInLogs = YES;
return request;
}
+ (TSRequest *)subscriptionReceiptCredentialsRequest:(NSString *)base64SubscriberID
request:(NSString *)base64ReceiptCredentialRequest
{
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:[NSString stringWithFormat:@"/v1/subscription/%@/receipt_credentials", base64SubscriberID]]
method:@"POST"
parameters:@{@"receiptCredentialRequest" : base64ReceiptCredentialRequest}];
request.shouldHaveAuthorizationHeaders = NO;
request.shouldRedactUrlInLogs = YES;
return request;
}
+ (TSRequest *)subscriptionRedeemReceiptCredential:(NSString *)base64ReceiptCredentialPresentation
{
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:@"/v1/donation/redeem-receipt"]
method:@"POST"
parameters:@{
@"receiptCredentialPresentation" : base64ReceiptCredentialPresentation,
@"visible" : @(self.subscriptionManager.displayBadgesOnProfile),
@"primary" : @(NO)
}];
return request;
}
+ (TSRequest *)subscriptionGetCurrentSubscriptionLevelRequest:(NSString *)base64SubscriberID
{
TSRequest *request = [TSRequest
requestWithUrl:[NSURL URLWithString:[NSString stringWithFormat:@"/v1/subscription/%@", base64SubscriberID]]
method:@"GET"
parameters:@{}];
request.shouldRedactUrlInLogs = YES;
request.shouldHaveAuthorizationHeaders = NO;
return request;
}
+ (TSRequest *)boostReceiptCredentialsWithPaymentIntentId:(NSString *)paymentIntentId
andRequest:(NSString *)base64ReceiptCredentialRequest
forPaymentProcessor:(NSString *)processor
{
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:@"/v1/subscription/boost/receipt_credentials"]
method:@"POST"
parameters:@{
@"paymentIntentId" : paymentIntentId,
@"receiptCredentialRequest" : base64ReceiptCredentialRequest,
@"processor" : processor
}];
request.shouldHaveAuthorizationHeaders = NO;
return request;
}
@end
NS_ASSUME_NONNULL_END