UD send via REST.

pull/1/head
Matthew Chen 7 years ago
parent c856859fbd
commit d08479980d

@ -0,0 +1,66 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import SignalMetadataKit
@objc
public class OWSMessageSend: NSObject {
@objc
public let message: TSOutgoingMessage
// thread may be nil if message is an OWSOutgoingSyncMessage.
@objc
public let thread: TSThread?
@objc
public let recipient: SignalRecipient
// TODO: Should this be per-recipient or per-message?
private static let kMaxRetriesPerRecipient: Int = 3
@objc
public var remainingAttempts = OWSMessageSend.kMaxRetriesPerRecipient
// We "fail over" to REST sends after _any_ error sending
// via the web socket.
@objc
public var useWebsocketIfAvailable = true
// We "fail over" to non-UD sends after certain errors sending
// via UD.
@objc
public var canUseUD = true
@objc
public let udAccessKey: SMKUDAccessKey?
@objc
public let localNumber: String
@objc
public let isLocalNumber: Bool
@objc
public init(message: TSOutgoingMessage,
thread: TSThread?,
recipient: SignalRecipient, udManager: OWSUDManager,
localNumber: String) {
self.message = message
self.thread = thread
self.recipient = recipient
var udAccessKey: SMKUDAccessKey?
var isLocalNumber: Bool = false
if let recipientId = recipient.uniqueId {
udAccessKey = udManager.udAccessKeyForRecipient(recipientId)
isLocalNumber = localNumber == recipientId
} else {
owsFailDebug("SignalRecipient missing recipientId")
}
self.udAccessKey = udAccessKey
self.localNumber = localNumber
self.isLocalNumber = isLocalNumber
}
}

@ -39,6 +39,7 @@
#import "TSOutgoingMessage.h" #import "TSOutgoingMessage.h"
#import "TSPreKeyManager.h" #import "TSPreKeyManager.h"
#import "TSQuotedMessage.h" #import "TSQuotedMessage.h"
#import "TSRequest.h"
#import "TSSocketManager.h" #import "TSSocketManager.h"
#import "TSThread.h" #import "TSThread.h"
#import <AxolotlKit/AxolotlExceptions.h> #import <AxolotlKit/AxolotlExceptions.h>
@ -47,8 +48,10 @@
#import <AxolotlKit/SessionBuilder.h> #import <AxolotlKit/SessionBuilder.h>
#import <AxolotlKit/SessionCipher.h> #import <AxolotlKit/SessionCipher.h>
#import <PromiseKit/AnyPromise.h> #import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/NSData+OWS.h>
#import <SignalCoreKit/NSDate+OWS.h> #import <SignalCoreKit/NSDate+OWS.h>
#import <SignalCoreKit/Threading.h> #import <SignalCoreKit/Threading.h>
#import <SignalMetadataKit/SignalMetadataKit-Swift.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h> #import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -195,7 +198,8 @@ void AssertIsOnSendingQueue()
@end @end
int const OWSMessageSenderRetryAttempts = 3; #pragma mark -
NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException"; NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException";
NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
@ -207,6 +211,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
@end @end
#pragma mark -
@implementation OWSMessageSender @implementation OWSMessageSender
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage - (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
@ -225,6 +231,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
return self; return self;
} }
#pragma mark -Dependencies
- (id<ContactsManagerProtocol>)contactsManager - (id<ContactsManagerProtocol>)contactsManager
{ {
OWSAssertDebug(SSKEnvironment.shared.contactsManager); OWSAssertDebug(SSKEnvironment.shared.contactsManager);
@ -246,6 +254,20 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
return SSKEnvironment.shared.networkManager; return SSKEnvironment.shared.networkManager;
} }
- (id<OWSUDManager>)udManager
{
OWSAssertDebug(SSKEnvironment.shared.udManager);
return SSKEnvironment.shared.udManager;
}
- (TSAccountManager *)tsAccountManager
{
return TSAccountManager.sharedInstance;
}
#pragma mark -
- (NSOperationQueue *)sendingQueueForMessage:(TSOutgoingMessage *)message - (NSOperationQueue *)sendingQueueForMessage:(TSOutgoingMessage *)message
{ {
OWSAssertDebug(message); OWSAssertDebug(message);
@ -435,23 +457,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}); });
} }
- (NSArray<SignalRecipient *> *)signalRecipientsForRecipientIds:(NSArray<NSString *> *)recipientIds
message:(TSOutgoingMessage *)message
{
OWSAssertDebug(recipientIds);
OWSAssertDebug(message);
NSMutableArray<SignalRecipient *> *recipients = [NSMutableArray new];
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
for (NSString *recipientId in recipientIds) {
SignalRecipient *recipient =
[SignalRecipient getOrBuildUnsavedRecipientForRecipientId:recipientId transaction:transaction];
[recipients addObject:recipient];
}
}];
return recipients;
}
- (void)sendMessageToService:(TSOutgoingMessage *)message - (void)sendMessageToService:(TSOutgoingMessage *)message
success:(void (^)(void))successHandler success:(void (^)(void))successHandler
failure:(RetryableFailureHandler)failureHandler failure:(RetryableFailureHandler)failureHandler
@ -459,7 +464,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
dispatch_async([OWSDispatch sendingQueue], ^{ dispatch_async([OWSDispatch sendingQueue], ^{
TSThread *_Nullable thread = message.thread; TSThread *_Nullable thread = message.thread;
// TODO: It would be nice to combine the "contact" and "group" send logic here. // In the "self-send" special case, we ony need to send a sync message with a delivery receipt.
if ([thread isKindOfClass:[TSContactThread class]] && if ([thread isKindOfClass:[TSContactThread class]] &&
[((TSContactThread *)thread).contactIdentifier isEqualToString:[TSAccountManager localNumber]]) { [((TSContactThread *)thread).contactIdentifier isEqualToString:[TSAccountManager localNumber]]) {
// Send to self. // Send to self.
@ -476,8 +481,10 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
successHandler(); successHandler();
return; return;
} else if ([thread isKindOfClass:[TSGroupThread class]]) { }
NSMutableSet<NSString *> *recipientIds = [NSMutableSet new];
if (thread.isGroupThread) {
TSGroupThread *gThread = (TSGroupThread *)thread; TSGroupThread *gThread = (TSGroupThread *)thread;
// Send to the intersection of: // Send to the intersection of:
@ -491,9 +498,45 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// * The recipient is still in the group. // * The recipient is still in the group.
// * The recipient is in the "sending" state. // * The recipient is in the "sending" state.
NSMutableSet<NSString *> *sendingRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds]; [recipientIds addObjectsFromArray:message.sendingRecipientIds];
[sendingRecipientIds intersectSet:[NSSet setWithArray:gThread.groupModel.groupMemberIds]]; // Only send to members in the latest known group member list.
[sendingRecipientIds minusSet:[NSSet setWithArray:self.blockingManager.blockedPhoneNumbers]]; [recipientIds intersectSet:[NSSet setWithArray:gThread.groupModel.groupMemberIds]];
} else if ([message isKindOfClass:[OWSOutgoingSyncMessage class]]) {
[recipientIds addObject:[TSAccountManager localNumber]];
} else if ([thread isKindOfClass:[TSContactThread class]]) {
NSString *recipientContactId = ((TSContactThread *)thread).contactIdentifier;
// Treat 1:1 sends to blocked contacts as failures.
// If we block a user, don't send 1:1 messages to them. The UI
// should prevent this from occurring, but in some edge cases
// you might, for example, have a pending outgoing message when
// you block them.
OWSAssertDebug(recipientContactId.length > 0);
if ([self.blockingManager isRecipientIdBlocked:recipientContactId]) {
OWSLogInfo(@"skipping 1:1 send to blocked contact: %@", recipientContactId);
NSError *error = OWSErrorMakeMessageSendFailedToBlockListError();
// No need to retry - the user will continue to be blocked.
[error setIsRetryable:NO];
failureHandler(error);
return;
}
[recipientIds addObject:recipientContactId];
} else {
// Neither a group nor contact thread? This should never happen.
OWSFailDebug(@"Unknown message type: %@", [message class]);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:NO];
failureHandler(error);
}
[recipientIds minusSet:[NSSet setWithArray:self.blockingManager.blockedPhoneNumbers]];
if ([recipientIds containsObject:TSAccountManager.localNumber]) {
OWSFailDebug(@"Message send recipients should not include self.");
}
[recipientIds removeObject:TSAccountManager.localNumber];
// Mark skipped recipients as such. We skip because: // Mark skipped recipients as such. We skip because:
// //
@ -502,7 +545,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// //
// Elsewhere, we skip recipient if their Signal account has been deactivated. // Elsewhere, we skip recipient if their Signal account has been deactivated.
NSMutableSet<NSString *> *obsoleteRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds]; NSMutableSet<NSString *> *obsoleteRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds];
[obsoleteRecipientIds minusSet:sendingRecipientIds]; [obsoleteRecipientIds minusSet:recipientIds];
if (obsoleteRecipientIds.count > 0) { if (obsoleteRecipientIds.count > 0) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (NSString *recipientId in obsoleteRecipientIds) { for (NSString *recipientId in obsoleteRecipientIds) {
@ -512,102 +555,51 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}]; }];
} }
if (sendingRecipientIds.count < 1) { if (recipientIds.count < 1) {
// All recipients are already sent or can be skipped. // All recipients are already sent or can be skipped.
successHandler(); successHandler();
return; return;
} }
NSArray<SignalRecipient *> *recipients = NSMutableArray<OWSMessageSend *> *messageSends = [NSMutableArray new];
[self signalRecipientsForRecipientIds:sendingRecipientIds.allObjects message:message]; [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
OWSAssertDebug(recipients.count == sendingRecipientIds.count); for (NSString *recipientId in recipientIds) {
SignalRecipient *recipient =
[self groupSend:recipients message:message thread:gThread success:successHandler failure:failureHandler]; [SignalRecipient getOrBuildUnsavedRecipientForRecipientId:recipientId transaction:transaction];
OWSMessageSend *messageSend =
} else if ([thread isKindOfClass:[TSContactThread class]] [[OWSMessageSend alloc] initWithMessage:message
|| [message isKindOfClass:[OWSOutgoingSyncMessage class]]) { thread:thread
recipient:recipient
TSContactThread *contactThread = (TSContactThread *)thread; udManager:self.udManager
localNumber:self.tsAccountManager.localNumber];
NSString *recipientContactId [messageSends addObject:messageSend];
= ([message isKindOfClass:[OWSOutgoingSyncMessage class]] ? [TSAccountManager localNumber]
: contactThread.contactIdentifier);
// If we block a user, don't send 1:1 messages to them. The UI
// should prevent this from occurring, but in some edge cases
// you might, for example, have a pending outgoing message when
// you block them.
OWSAssertDebug(recipientContactId.length > 0);
if ([self.blockingManager isRecipientIdBlocked:recipientContactId]) {
OWSLogInfo(@"skipping 1:1 send to blocked contact: %@", recipientContactId);
NSError *error = OWSErrorMakeMessageSendFailedToBlockListError();
// No need to retry - the user will continue to be blocked.
[error setIsRetryable:NO];
failureHandler(error);
return;
}
NSArray<SignalRecipient *> *recipients =
[self signalRecipientsForRecipientIds:@[recipientContactId] message:message];
OWSAssertDebug(recipients.count == 1);
SignalRecipient *recipient = recipients.firstObject;
if (!recipient) {
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
OWSLogWarn(@"recipient contact still not found after attempting lookup.");
// No need to repeat trying to find a failure. Apart from repeatedly failing, it would also cause us to
// print redundant error messages.
[error setIsRetryable:NO];
failureHandler(error);
return;
} }
}];
OWSAssertDebug(messageSends.count == recipientIds.count);
OWSAssertDebug(messageSends.count > 0);
[self sendMessageToService:message [self sendWithMessageSends:messageSends
recipient:recipient isGroupSend:thread.isGroupThread
thread:thread
attempts:OWSMessageSenderRetryAttempts
useWebsocketIfAvailable:YES
success:successHandler success:successHandler
failure:failureHandler]; failure:failureHandler];
} else {
// Neither a group nor contact thread? This should never happen.
OWSFailDebug(@"Unknown message type: %@", [message class]);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:NO];
failureHandler(error);
}
}); });
} }
- (void)groupSend:(NSArray<SignalRecipient *> *)recipients - (void)sendWithMessageSends:(NSArray<OWSMessageSend *> *)messageSends
message:(TSOutgoingMessage *)message isGroupSend:(BOOL)isGroupSend
thread:(TSThread *)thread
success:(void (^)(void))successHandler success:(void (^)(void))successHandler
failure:(RetryableFailureHandler)failureHandler failure:(RetryableFailureHandler)failureHandler
{ {
[self saveGroupMessage:message inThread:thread]; OWSAssertDebug(messageSends.count > 0);
AssertIsOnSendingQueue();
NSMutableArray<AnyPromise *> *sendPromises = [NSMutableArray array]; NSMutableArray<AnyPromise *> *sendPromises = [NSMutableArray array];
NSMutableArray<NSError *> *sendErrors = [NSMutableArray array]; NSMutableArray<NSError *> *sendErrors = [NSMutableArray array];
for (SignalRecipient *recipient in recipients) { for (OWSMessageSend *messageSend in messageSends) {
NSString *recipientId = recipient.recipientId;
// We don't need to send the message to ourselves...
if ([recipientId isEqualToString:[TSAccountManager localNumber]]) {
continue;
}
// ...otherwise we send.
// For group sends, we're using chained promises to make the code more readable. // For group sends, we're using chained promises to make the code more readable.
AnyPromise *sendPromise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { AnyPromise *sendPromise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[self sendMessageToService:message [self sendMessageToRecipient:messageSend
recipient:recipient
thread:thread
attempts:OWSMessageSenderRetryAttempts
useWebsocketIfAvailable:YES
success:^{ success:^{
// The value doesn't matter, we just need any non-NSError value. // The value doesn't matter, we just need any non-NSError value.
resolve(@(1)); resolve(@(1));
@ -643,7 +635,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// Some errors should be ignored when sending messages // Some errors should be ignored when sending messages
// to groups. See discussion on // to groups. See discussion on
// NSError (OWSMessageSender) category. // NSError (OWSMessageSender) category.
if ([error shouldBeIgnoredForGroups]) { if (isGroupSend && [error shouldBeIgnoredForGroups]) {
continue; continue;
} }
@ -664,7 +656,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
} }
} }
// If any of the group send errors are retryable, we want to retry. // If any of the send errors are retryable, we want to retry.
// Therefore, prefer to propagate a retryable error. // Therefore, prefer to propagate a retryable error.
if (firstRetryableError) { if (firstRetryableError) {
return failureHandler(firstRetryableError); return failureHandler(firstRetryableError);
@ -674,7 +666,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// If we only received errors that we should ignore, // If we only received errors that we should ignore,
// consider this send a success, unless the message could // consider this send a success, unless the message could
// not be sent to any recipient. // not be sent to any recipient.
if (message.sentRecipientsCount == 0) { if (messageSends.lastObject.message.sentRecipientsCount == 0) {
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageSendNoValidRecipients, NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageSendNoValidRecipients,
NSLocalizedString(@"ERROR_DESCRIPTION_NO_VALID_RECIPIENTS", NSLocalizedString(@"ERROR_DESCRIPTION_NO_VALID_RECIPIENTS",
@"Error indicating that an outgoing message had no valid recipients.")); @"Error indicating that an outgoing message had no valid recipients."));
@ -713,60 +705,19 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}]; }];
} }
- (void)sendMessageToService:(TSOutgoingMessage *)message - (nullable NSArray<NSDictionary *> *)deviceMessagesForMessageSendSafe:(OWSMessageSend *)messageSend
recipient:(SignalRecipient *)recipient error:(NSError **)errorHandle
thread:(nullable TSThread *)thread
attempts:(int)remainingAttemptsParam
useWebsocketIfAvailable:(BOOL)useWebsocketIfAvailable
success:(void (^)(void))successHandler
failure:(RetryableFailureHandler)failureHandler
{ {
OWSAssertDebug(message); OWSAssertDebug(messageSend);
OWSAssertDebug(recipient); OWSAssertDebug(errorHandle);
OWSAssertDebug(thread || [message isKindOfClass:[OWSOutgoingSyncMessage class]]);
OWSLogInfo(@"attempting to send message: %@, timestamp: %llu, recipient: %@",
message.class,
message.timestamp,
recipient.uniqueId);
AssertIsOnSendingQueue(); AssertIsOnSendingQueue();
if ([TSPreKeyManager isAppLockedDueToPreKeyUpdateFailures]) { SignalRecipient *recipient = messageSend.recipient;
OWSProdError([OWSAnalyticsEvents messageSendErrorFailedDueToPrekeyUpdateFailures]);
// Retry prekey update every time user tries to send a message while app
// is disabled due to prekey update failures.
//
// Only try to update the signed prekey; updating it is sufficient to
// re-enable message sending.
[TSPreKeyManager
rotateSignedPreKeyWithSuccess:^{
OWSLogInfo(@"New prekeys registered with server.");
NSError *error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError();
[error setIsRetryable:YES];
return failureHandler(error);
}
failure:^(NSError *error) {
OWSLogWarn(@"Failed to update prekeys with the server: %@", error);
return failureHandler(error);
}];
}
if (remainingAttemptsParam <= 0) {
// We should always fail with a specific error.
OWSProdFail([OWSAnalyticsEvents messageSenderErrorGenericSendFailure]);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:YES];
return failureHandler(error);
}
int remainingAttempts = remainingAttemptsParam - 1;
NSArray<NSDictionary *> *deviceMessages; NSArray<NSDictionary *> *deviceMessages;
@try { @try {
deviceMessages = [self deviceMessages:message recipient:recipient]; deviceMessages = [self deviceMessagesForMessageSendUnsafe:messageSend];
} @catch (NSException *exception) { } @catch (NSException *exception) {
deviceMessages = @[];
if ([exception.name isEqualToString:UntrustedIdentityKeyException]) { if ([exception.name isEqualToString:UntrustedIdentityKeyException]) {
// This *can* happen under normal usage, but it should happen relatively rarely. // This *can* happen under normal usage, but it should happen relatively rarely.
// We expect it to happen whenever Bob reinstalls, and Alice messages Bob before // We expect it to happen whenever Bob reinstalls, and Alice messages Bob before
@ -788,67 +739,127 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[error setIsRetryable:NO]; [error setIsRetryable:NO];
// Avoid the "Too many failures with this contact" error rate limiting. // Avoid the "Too many failures with this contact" error rate limiting.
[error setIsFatal:YES]; [error setIsFatal:YES];
*errorHandle = error;
PreKeyBundle *_Nullable newKeyBundle = exception.userInfo[TSInvalidPreKeyBundleKey]; PreKeyBundle *_Nullable newKeyBundle = exception.userInfo[TSInvalidPreKeyBundleKey];
if (newKeyBundle == nil) { if (newKeyBundle == nil) {
OWSProdFail([OWSAnalyticsEvents messageSenderErrorMissingNewPreKeyBundle]); OWSProdFail([OWSAnalyticsEvents messageSenderErrorMissingNewPreKeyBundle]);
failureHandler(error); return nil;
return;
} }
if (![newKeyBundle isKindOfClass:[PreKeyBundle class]]) { if (![newKeyBundle isKindOfClass:[PreKeyBundle class]]) {
OWSProdFail([OWSAnalyticsEvents messageSenderErrorUnexpectedKeyBundle]); OWSProdFail([OWSAnalyticsEvents messageSenderErrorUnexpectedKeyBundle]);
failureHandler(error); return nil;
return;
} }
NSData *newIdentityKeyWithVersion = newKeyBundle.identityKey; NSData *newIdentityKeyWithVersion = newKeyBundle.identityKey;
if (![newIdentityKeyWithVersion isKindOfClass:[NSData class]]) { if (![newIdentityKeyWithVersion isKindOfClass:[NSData class]]) {
OWSProdFail([OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyType]); OWSProdFail([OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyType]);
failureHandler(error); return nil;
return;
} }
// TODO migrate to storing the full 33 byte representation of the identity key. // TODO migrate to storing the full 33 byte representation of the identity key.
if (newIdentityKeyWithVersion.length != kIdentityKeyLength) { if (newIdentityKeyWithVersion.length != kIdentityKeyLength) {
OWSProdFail([OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyLength]); OWSProdFail([OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyLength]);
failureHandler(error); return nil;
return;
} }
NSData *newIdentityKey = [newIdentityKeyWithVersion removeKeyType]; NSData *newIdentityKey = [newIdentityKeyWithVersion removeKeyType];
[[OWSIdentityManager sharedManager] saveRemoteIdentity:newIdentityKey recipientId:recipient.recipientId]; [[OWSIdentityManager sharedManager] saveRemoteIdentity:newIdentityKey recipientId:recipient.recipientId];
failureHandler(error); return nil;
return;
} }
if ([exception.name isEqualToString:OWSMessageSenderRateLimitedException]) { if ([exception.name isEqualToString:OWSMessageSenderRateLimitedException]) {
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceRateLimited, NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceRateLimited,
NSLocalizedString(@"FAILED_SENDING_BECAUSE_RATE_LIMIT", NSLocalizedString(@"FAILED_SENDING_BECAUSE_RATE_LIMIT",
@"action sheet header when re-sending message which failed because of too many attempts")); @"action sheet header when re-sending message which failed because of too many attempts"));
// We're already rate-limited. No need to exacerbate the problem. // We're already rate-limited. No need to exacerbate the problem.
[error setIsRetryable:NO]; [error setIsRetryable:NO];
// Avoid exacerbating the rate limiting. // Avoid exacerbating the rate limiting.
[error setIsFatal:YES]; [error setIsFatal:YES];
return failureHandler(error); *errorHandle = error;
return nil;
} }
if (remainingAttempts == 0) { if (messageSend.remainingAttempts == 0) {
OWSLogWarn(@"Terminal failure to build any device messages. Giving up with exception:%@", exception); OWSLogWarn(@"Terminal failure to build any device messages. Giving up with exception: %@", exception);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
// Since we've already repeatedly failed to build messages, it's unlikely that repeating the whole process // Since we've already repeatedly failed to build messages, it's unlikely that repeating the whole process
// will succeed. // will succeed.
[error setIsRetryable:NO]; [error setIsRetryable:NO];
*errorHandle = error;
return nil;
}
OWSLogWarn(@"Could not build device messages: %@", exception);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:YES];
*errorHandle = error;
return nil;
}
return deviceMessages;
}
- (void)sendMessageToRecipient:(OWSMessageSend *)messageSend
success:(void (^)(void))successHandler
failure:(RetryableFailureHandler)failureHandler
{
OWSAssertDebug(messageSend);
OWSAssertDebug(messageSend.thread || [messageSend.message isKindOfClass:[OWSOutgoingSyncMessage class]]);
TSOutgoingMessage *message = messageSend.message;
SignalRecipient *recipient = messageSend.recipient;
OWSLogInfo(@"attempting to send message: %@, timestamp: %llu, recipient: %@",
message.class,
message.timestamp,
recipient.uniqueId);
AssertIsOnSendingQueue();
if ([TSPreKeyManager isAppLockedDueToPreKeyUpdateFailures]) {
OWSProdError([OWSAnalyticsEvents messageSendErrorFailedDueToPrekeyUpdateFailures]);
// Retry prekey update every time user tries to send a message while app
// is disabled due to prekey update failures.
//
// Only try to update the signed prekey; updating it is sufficient to
// re-enable message sending.
[TSPreKeyManager
rotateSignedPreKeyWithSuccess:^{
OWSLogInfo(@"New prekeys registered with server.");
NSError *error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError();
[error setIsRetryable:YES];
return failureHandler(error); return failureHandler(error);
} }
failure:^(NSError *error) {
OWSLogWarn(@"Failed to update prekeys with the server: %@", error);
return failureHandler(error);
}];
} }
NSString *localNumber = [TSAccountManager localNumber]; if (messageSend.remainingAttempts <= 0) {
BOOL isLocalNumber = [localNumber isEqualToString:recipient.uniqueId]; // We should always fail with a specific error.
if (isLocalNumber) { OWSProdFail([OWSAnalyticsEvents messageSenderErrorGenericSendFailure]);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:YES];
return failureHandler(error);
}
// Consume an attempt.
messageSend.remainingAttempts = messageSend.remainingAttempts - 1;
NSError *deviceMessagesError;
NSArray<NSDictionary *> *_Nullable deviceMessages =
[self deviceMessagesForMessageSendSafe:messageSend error:&deviceMessagesError];
if (deviceMessagesError || !deviceMessages) {
OWSAssertDebug(deviceMessagesError);
return failureHandler(deviceMessagesError);
}
if (messageSend.isLocalNumber) {
OWSAssertDebug([message isKindOfClass:[OWSOutgoingSyncMessage class]]); OWSAssertDebug([message isKindOfClass:[OWSOutgoingSyncMessage class]]);
// Messages sent to the "local number" should be sync messages. // Messages sent to the "local number" should be sync messages.
// //
@ -878,9 +889,9 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
OWSAssertDebug([message isKindOfClass:[OWSOutgoingSyncMessage class]]); OWSAssertDebug([message isKindOfClass:[OWSOutgoingSyncMessage class]]);
dispatch_async([OWSDispatch sendingQueue], ^{ dispatch_async([OWSDispatch sendingQueue], ^{
// This emulates the completion logic of an actual successful save (see below). // This emulates the completion logic of an actual successful send (see below).
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[message updateWithSkippedRecipient:localNumber transaction:transaction]; [message updateWithSkippedRecipient:messageSend.localNumber transaction:transaction];
}]; }];
successHandler(); successHandler();
}); });
@ -918,51 +929,55 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
TSRequest *request = [OWSRequestFactory submitMessageRequestWithRecipient:recipient.uniqueId TSRequest *request = [OWSRequestFactory submitMessageRequestWithRecipient:recipient.uniqueId
messages:deviceMessages messages:deviceMessages
timeStamp:message.timestamp]; timeStamp:message.timestamp];
if (useWebsocketIfAvailable && TSSocketManager.canMakeRequests) {
BOOL isUDSend = messageSend.canUseUD && messageSend.udAccessKey != nil;
if (isUDSend) {
DDLogVerbose(@"UD send.");
request.shouldHaveAuthorizationHeaders = YES;
[request setValue:[messageSend.udAccessKey.keyData base64EncodedString] forKey:@"Unidentified-Access-Key"];
}
// TODO: UD sends over websocket.
if (messageSend.useWebsocketIfAvailable && TSSocketManager.canMakeRequests && !isUDSend) {
[TSSocketManager.sharedManager makeRequest:request [TSSocketManager.sharedManager makeRequest:request
success:^(id _Nullable responseObject) { success:^(id _Nullable responseObject) {
[self messageSendDidSucceed:message [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages success:successHandler];
recipient:recipient
isLocalNumber:isLocalNumber
deviceMessages:deviceMessages
success:successHandler];
} }
failure:^(NSInteger statusCode, NSData *_Nullable responseData, NSError *error) { failure:^(NSInteger statusCode, NSData *_Nullable responseData, NSError *error) {
dispatch_async([OWSDispatch sendingQueue], ^{ dispatch_async([OWSDispatch sendingQueue], ^{
OWSLogDebug(@"falling back to REST since first attempt failed."); OWSLogDebug(@"Web socket send failed; failing over to REST.");
// Websockets can fail in different ways, so we don't decrement remainingAttempts for websocket // Websockets can fail in different ways, so we don't decrement remainingAttempts for websocket
// failure. Instead we fall back to REST, which will decrement retries. e.g. after linking a new // failure. Instead we fall back to REST, which will decrement retries. e.g. after linking a new
// device, sync messages will fail until the websocket re-opens. // device, sync messages will fail until the websocket re-opens.
[self sendMessageToService:message messageSend.useWebsocketIfAvailable = NO;
recipient:recipient [self sendMessageToRecipient:messageSend success:successHandler failure:failureHandler];
thread:thread
attempts:remainingAttemptsParam
useWebsocketIfAvailable:NO
success:successHandler
failure:failureHandler];
}); });
}]; }];
} else { } else {
[self.networkManager makeRequest:request [self.networkManager makeRequest:request
success:^(NSURLSessionDataTask *task, id responseObject) { success:^(NSURLSessionDataTask *task, id responseObject) {
[self messageSendDidSucceed:message [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages success:successHandler];
recipient:recipient
isLocalNumber:isLocalNumber
deviceMessages:deviceMessages
success:successHandler];
} }
failure:^(NSURLSessionDataTask *task, NSError *error) { failure:^(NSURLSessionDataTask *task, NSError *error) {
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response; NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
NSInteger statusCode = response.statusCode; NSInteger statusCode = response.statusCode;
NSData *_Nullable responseData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; NSData *_Nullable responseData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
[self messageSendDidFail:message if (isUDSend && statusCode > 0) {
recipient:recipient // If a UD send fails due to service response (as opposed to network
thread:thread // failure), mark recipient as _not_ in UD mode, then retry.
isLocalNumber:isLocalNumber //
// TODO: Do we want to discriminate based on exact error?
OWSLogDebug(@"UD send failed; failing over to non-UD send.");
[self.udManager removeUDRecipientId:recipient.uniqueId];
messageSend.canUseUD = NO;
[self sendMessageToRecipient:messageSend success:successHandler failure:failureHandler];
return;
}
[self messageSendDidFail:messageSend
deviceMessages:deviceMessages deviceMessages:deviceMessages
remainingAttempts:remainingAttempts
statusCode:statusCode statusCode:statusCode
error:error error:error
responseData:responseData responseData:responseData
@ -972,20 +987,19 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
} }
} }
- (void)messageSendDidSucceed:(TSOutgoingMessage *)message - (void)messageSendDidSucceed:(OWSMessageSend *)messageSend
recipient:(SignalRecipient *)recipient
isLocalNumber:(BOOL)isLocalNumber
deviceMessages:(NSArray<NSDictionary *> *)deviceMessages deviceMessages:(NSArray<NSDictionary *> *)deviceMessages
success:(void (^)(void))successHandler success:(void (^)(void))successHandler
{ {
OWSAssertDebug(message); OWSAssertDebug(messageSend);
OWSAssertDebug(recipient);
OWSAssertDebug(deviceMessages); OWSAssertDebug(deviceMessages);
OWSAssertDebug(successHandler); OWSAssertDebug(successHandler);
SignalRecipient *recipient = messageSend.recipient;
OWSLogInfo(@"Message send succeeded."); OWSLogInfo(@"Message send succeeded.");
if (isLocalNumber && deviceMessages.count == 0) { if (messageSend.isLocalNumber && deviceMessages.count == 0) {
OWSLogInfo(@"Sent a message with no device messages; clearing 'mayHaveLinkedDevices'."); OWSLogInfo(@"Sent a message with no device messages; clearing 'mayHaveLinkedDevices'.");
// In order to avoid skipping necessary sync messages, the default value // In order to avoid skipping necessary sync messages, the default value
// for mayHaveLinkedDevices is YES. Once we've successfully sent a // for mayHaveLinkedDevices is YES. Once we've successfully sent a
@ -999,42 +1013,40 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
dispatch_async([OWSDispatch sendingQueue], ^{ dispatch_async([OWSDispatch sendingQueue], ^{
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[message updateWithSentRecipient:recipient.uniqueId transaction:transaction]; [messageSend.message updateWithSentRecipient:messageSend.recipient.uniqueId transaction:transaction];
// If we've just delivered a message to a user, we know they // If we've just delivered a message to a user, we know they
// have a valid Signal account. // have a valid Signal account.
[SignalRecipient markRecipientAsRegisteredAndGet:recipient.recipientId transaction:transaction]; [SignalRecipient markRecipientAsRegisteredAndGet:recipient.recipientId transaction:transaction];
}]; }];
[self handleMessageSentLocally:message]; [self handleMessageSentLocally:messageSend.message];
successHandler(); successHandler();
}); });
} }
- (void)messageSendDidFail:(TSOutgoingMessage *)message - (void)messageSendDidFail:(OWSMessageSend *)messageSend
recipient:(SignalRecipient *)recipient
thread:(nullable TSThread *)thread
isLocalNumber:(BOOL)isLocalNumber
deviceMessages:(NSArray<NSDictionary *> *)deviceMessages deviceMessages:(NSArray<NSDictionary *> *)deviceMessages
remainingAttempts:(int)remainingAttempts
statusCode:(NSInteger)statusCode statusCode:(NSInteger)statusCode
error:(NSError *)responseError error:(NSError *)responseError
responseData:(nullable NSData *)responseData responseData:(nullable NSData *)responseData
success:(void (^)(void))successHandler success:(void (^)(void))successHandler
failure:(RetryableFailureHandler)failureHandler failure:(RetryableFailureHandler)failureHandler
{ {
OWSAssertDebug(message); OWSAssertDebug(messageSend);
OWSAssertDebug(recipient); OWSAssertDebug(messageSend.thread || [messageSend.message isKindOfClass:[OWSOutgoingSyncMessage class]]);
OWSAssertDebug(thread || [message isKindOfClass:[OWSOutgoingSyncMessage class]]);
OWSAssertDebug(deviceMessages); OWSAssertDebug(deviceMessages);
OWSAssertDebug(responseError); OWSAssertDebug(responseError);
OWSAssertDebug(successHandler); OWSAssertDebug(successHandler);
OWSAssertDebug(failureHandler); OWSAssertDebug(failureHandler);
TSOutgoingMessage *message = messageSend.message;
SignalRecipient *recipient = messageSend.recipient;
OWSLogInfo(@"sending to recipient: %@, failed with error.", recipient.uniqueId); OWSLogInfo(@"sending to recipient: %@, failed with error.", recipient.uniqueId);
void (^retrySend)(void) = ^void() { void (^retrySend)(void) = ^void() {
if (remainingAttempts <= 0) { if (messageSend.remainingAttempts <= 0) {
// Since we've already repeatedly failed to send to the messaging API, // Since we've already repeatedly failed to send to the messaging API,
// it's unlikely that repeating the whole process will succeed. // it's unlikely that repeating the whole process will succeed.
[responseError setIsRetryable:NO]; [responseError setIsRetryable:NO];
@ -1043,19 +1055,15 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
dispatch_async([OWSDispatch sendingQueue], ^{ dispatch_async([OWSDispatch sendingQueue], ^{
OWSLogDebug(@"Retrying: %@", message.debugDescription); OWSLogDebug(@"Retrying: %@", message.debugDescription);
[self sendMessageToService:message // TODO: Should this use sendMessageToRecipient or sendMessageToService?
recipient:recipient [self sendMessageToRecipient:messageSend success:successHandler failure:failureHandler];
thread:thread
attempts:remainingAttempts
useWebsocketIfAvailable:NO
success:successHandler
failure:failureHandler];
}); });
}; };
void (^handle404)(void) = ^{ void (^handle404)(void) = ^{
OWSLogWarn(@"Unregistered recipient: %@", recipient.uniqueId); OWSLogWarn(@"Unregistered recipient: %@", recipient.uniqueId);
TSThread *_Nullable thread = messageSend.thread;
OWSAssertDebug(thread); OWSAssertDebug(thread);
dispatch_async([OWSDispatch sendingQueue], ^{ dispatch_async([OWSDispatch sendingQueue], ^{
@ -1207,17 +1215,19 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
OWSOutgoingSentMessageTranscript *sentMessageTranscript = OWSOutgoingSentMessageTranscript *sentMessageTranscript =
[[OWSOutgoingSentMessageTranscript alloc] initWithOutgoingMessage:message]; [[OWSOutgoingSentMessageTranscript alloc] initWithOutgoingMessage:message];
NSString *recipientId = [TSAccountManager localNumber]; NSString *recipientId = self.tsAccountManager.localNumber;
__block SignalRecipient *recipient; __block SignalRecipient *recipient;
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
recipient = [SignalRecipient markRecipientAsRegisteredAndGet:recipientId transaction:transaction]; recipient = [SignalRecipient markRecipientAsRegisteredAndGet:recipientId transaction:transaction];
}]; }];
[self sendMessageToService:sentMessageTranscript OWSMessageSend *messageSend = [[OWSMessageSend alloc] initWithMessage:sentMessageTranscript
recipient:recipient
thread:message.thread thread:message.thread
attempts:OWSMessageSenderRetryAttempts recipient:recipient
useWebsocketIfAvailable:YES udManager:self.udManager
localNumber:self.tsAccountManager.localNumber];
[self sendMessageToRecipient:messageSend
success:^{ success:^{
OWSLogInfo(@"Successfully sent sync transcript."); OWSLogInfo(@"Successfully sent sync transcript.");
} }
@ -1231,20 +1241,25 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}]; }];
} }
- (NSArray<NSDictionary *> *)deviceMessages:(TSOutgoingMessage *)message recipient:(SignalRecipient *)recipient // NOTE: This method uses exceptions for control flow.
- (NSArray<NSDictionary *> *)deviceMessagesForMessageSendUnsafe:(OWSMessageSend *)messageSend
{ {
OWSAssertDebug(message); OWSAssertDebug(messageSend.message);
OWSAssertDebug(recipient); OWSAssertDebug(messageSend.recipient);
TSOutgoingMessage *message = messageSend.message;
SignalRecipient *recipient = messageSend.recipient;
NSMutableArray *messagesArray = [NSMutableArray arrayWithCapacity:recipient.devices.count]; NSMutableArray *messagesArray = [NSMutableArray arrayWithCapacity:recipient.devices.count];
NSData *_Nullable plainText = [message buildPlainTextData:recipient]; NSData *_Nullable plainText = [messageSend.message buildPlainTextData:messageSend.recipient];
if (!plainText) { if (!plainText) {
OWSRaiseException(InvalidMessageException, @"Failed to build message proto"); OWSRaiseException(InvalidMessageException, @"Failed to build message proto");
} }
OWSLogDebug(@"built message: %@ plainTextData.length: %lu", [message class], (unsigned long)plainText.length); OWSLogDebug(
@"built message: %@ plainTextData.length: %lu", [messageSend.message class], (unsigned long)plainText.length);
for (NSNumber *deviceNumber in recipient.devices) { for (NSNumber *deviceNumber in messageSend.recipient.devices) {
@try { @try {
__block NSDictionary *messageDict; __block NSDictionary *messageDict;
__block NSException *encryptionException; __block NSException *encryptionException;
@ -1252,7 +1267,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@try { @try {
messageDict = [self encryptedMessageWithPlaintext:plainText messageDict = [self encryptedMessageWithPlaintext:plainText
recipient:recipient recipient:messageSend.recipient
deviceId:deviceNumber deviceId:deviceNumber
keyingStorage:self.primaryStorage keyingStorage:self.primaryStorage
isSilent:message.isSilent isSilent:message.isSilent
@ -1412,23 +1427,24 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
} }
} }
- (void)saveGroupMessage:(TSOutgoingMessage *)message inThread:(TSThread *)thread // TODO: Huh?
{ //- (void)saveGroupMessage:(TSOutgoingMessage *)message inThread:(TSThread *)thread
if (message.groupMetaMessage == TSGroupMetaMessageDeliver) { //{
// TODO: Why is this necessary? // if (message.groupMetaMessage == TSGroupMetaMessageDeliver) {
[message save]; // // TODO: Why is this necessary?
} else if (message.groupMetaMessage == TSGroupMetaMessageQuit) { // [message save];
[[[TSInfoMessage alloc] initWithTimestamp:message.timestamp // } else if (message.groupMetaMessage == TSGroupMetaMessageQuit) {
inThread:thread // [[[TSInfoMessage alloc] initWithTimestamp:message.timestamp
messageType:TSInfoMessageTypeGroupQuit // inThread:thread
customMessage:message.customMessage] save]; // messageType:TSInfoMessageTypeGroupQuit
} else { // customMessage:message.customMessage] save];
[[[TSInfoMessage alloc] initWithTimestamp:message.timestamp // } else {
inThread:thread // [[[TSInfoMessage alloc] initWithTimestamp:message.timestamp
messageType:TSInfoMessageTypeGroupUpdate // inThread:thread
customMessage:message.customMessage] save]; // messageType:TSInfoMessageTypeGroupUpdate
} // customMessage:message.customMessage] save];
} // }
//}
// Called when the server indicates that the devices no longer exist - e.g. when the remote recipient has reinstalled. // Called when the server indicates that the devices no longer exist - e.g. when the remote recipient has reinstalled.
- (void)handleStaleDevicesWithResponseJson:(NSDictionary *)responseJson - (void)handleStaleDevicesWithResponseJson:(NSDictionary *)responseJson

@ -26,6 +26,10 @@ public enum OWSUDError: Error {
// No-op if this recipient id is already marked as _NOT_ a "UD recipient". // No-op if this recipient id is already marked as _NOT_ a "UD recipient".
@objc func removeUDRecipientId(_ recipientId: String) @objc func removeUDRecipientId(_ recipientId: String)
// Returns the UD access key for a given recipient if they are
// a UD recipient and we have a valid profile key for them.
@objc func udAccessKeyForRecipient(_ recipientId: String) -> SMKUDAccessKey?
// MARK: - Sender Certificate // MARK: - Sender Certificate
// We use completion handlers instead of a promise so that message sending // We use completion handlers instead of a promise so that message sending
@ -81,6 +85,12 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager {
ensureSenderCertificate().retainUntilComplete() ensureSenderCertificate().retainUntilComplete()
} }
// MARK: - Dependencies
private var profileManager: ProfileManagerProtocol {
return SSKEnvironment.shared.profileManager
}
// MARK: - Recipient state // MARK: - Recipient state
@objc @objc
@ -98,6 +108,28 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager {
dbConnection.removeObject(forKey: recipientId, inCollection: kUDRecipientModeCollection) dbConnection.removeObject(forKey: recipientId, inCollection: kUDRecipientModeCollection)
} }
// Returns the UD access key for a given recipient if they are
// a UD recipient and we have a valid profile key for them.
@objc
public func udAccessKeyForRecipient(_ recipientId: String) -> SMKUDAccessKey? {
guard isUDRecipientId(recipientId) else {
return nil
}
guard let profileKey = profileManager.profileKeyData(forRecipientId: recipientId) else {
// Mark as "not a UD recipient".
removeUDRecipientId(recipientId)
return nil
}
do {
let udAccessKey = try SMKUDAccessKey(profileKey: profileKey)
return udAccessKey
} catch {
Logger.error("Could not determine udAccessKey: \(error)")
removeUDRecipientId(recipientId)
return nil
}
}
// MARK: - Sender Certificate // MARK: - Sender Certificate
#if DEBUG #if DEBUG

@ -8,7 +8,7 @@
@property (atomic, nullable) NSString *authUsername; @property (atomic, nullable) NSString *authUsername;
@property (atomic, nullable) NSString *authPassword; @property (atomic, nullable) NSString *authPassword;
@property (nonatomic, readonly) NSDictionary *parameters; @property (nonatomic, readonly) NSDictionary<NSString *, id> *parameters;
- (instancetype)init NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE;
@ -26,4 +26,6 @@
method:(NSString *)method method:(NSString *)method
parameters:(nullable NSDictionary<NSString *, id> *)parameters; parameters:(nullable NSDictionary<NSString *, id> *)parameters;
- (void)setParameterWithValue:(id)value forKey:(NSString *)key;
@end @end

@ -110,4 +110,15 @@
} }
} }
- (void)setParameterWithValue:(id)value forKey:(NSString *)key
{
OWSAssertDebug(value);
OWSAssertDebug(key.length > 0);
NSMutableDictionary<NSString *, id> *parameters
= (self.parameters ? [self.parameters mutableCopy] : [NSMutableDictionary new]);
parameters[key] = value;
_parameters = [parameters copy];
}
@end @end

@ -3,6 +3,7 @@
// //
import Foundation import Foundation
import SignalMetadataKit
#if DEBUG #if DEBUG
@ -30,6 +31,28 @@ public class OWSFakeUDManager: NSObject, OWSUDManager {
udRecipientSet.remove(recipientId) udRecipientSet.remove(recipientId)
} }
// Returns the UD access key for a given recipient if they are
// a UD recipient and we have a valid profile key for them.
@objc
public func udAccessKeyForRecipient(_ recipientId: String) -> SMKUDAccessKey? {
guard isUDRecipientId(recipientId) else {
return nil
}
guard let profileKey = Randomness.generateRandomBytes(Int32(kAES256_KeyByteLength)) else {
// Mark as "not a UD recipient".
removeUDRecipientId(recipientId)
return nil
}
do {
let udAccessKey = try SMKUDAccessKey(profileKey: profileKey)
return udAccessKey
} catch {
Logger.error("Could not determine udAccessKey: \(error)")
removeUDRecipientId(recipientId)
return nil
}
}
// MARK: - Server Certificate // MARK: - Server Certificate
// Tests can control the behavior of this mock by setting this property. // Tests can control the behavior of this mock by setting this property.

Loading…
Cancel
Save