Separate out remaining bits of Session logic

pull/162/head
nielsandriesse 5 years ago
parent 6c834b6a6e
commit ceb33e36e4

@ -46,9 +46,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Durable Message Enqueue
+ (TSOutgoingMessage *)enqueueFriendRequestAcceptanceMessageInThread:(TSThread *)thread;
+ (void)enqueueDeviceLinkMessage:(LKDeviceLinkMessage *)message;
+ (void)enqueueUnlinkDeviceMessage:(LKUnlinkDeviceMessage *)message;
+ (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)fullMessageText
inThread:(TSThread *)thread

@ -85,15 +85,6 @@ typedef void (^BuildOutgoingMessageCompletionBlock)(TSOutgoingMessage *savedMess
#pragma mark - Durable Message Enqueue
+ (LKEphemeralMessage *)enqueueFriendRequestAcceptanceMessageInThread:(TSThread *)thread
{
LKEphemeralMessage *message = [[LKEphemeralMessage alloc] initInThread:thread];
[self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self.messageSenderJobQueue addMessage:message transaction:transaction];
}];
return message;
}
+ (void)enqueueDeviceLinkMessage:(LKDeviceLinkMessage *)message
{
[self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@ -101,13 +92,6 @@ typedef void (^BuildOutgoingMessageCompletionBlock)(TSOutgoingMessage *savedMess
}];
}
+ (void)enqueueUnlinkDeviceMessage:(LKUnlinkDeviceMessage *)message
{
[self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self.messageSenderJobQueue addMessage:message transaction:transaction];
}];
}
+ (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)fullMessageText
inThread:(TSThread *)thread
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel

@ -38,20 +38,7 @@ public class RefreshPreKeysOperation: OWSOperation {
// Loki: Doing this on the global queue to match Signal
DispatchQueue.global().async {
guard self.primaryStorage.currentSignedPrekeyId() == nil else {
print("[Loki] Skipping pre key refresh; using existing signed pre key.")
return self.reportSuccess()
}
let signedPreKeyRecord = self.primaryStorage.generateRandomSignedRecord()
signedPreKeyRecord.markAsAcceptedByService()
self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
self.primaryStorage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
TSPreKeyManager.clearPreKeyUpdateFailureCount()
TSPreKeyManager.clearSignedPreKeyRecords()
print("[Loki] Pre keys refreshed successfully.")
SessionProtocol.refreshPreKeys()
self.reportSuccess()
}

@ -29,15 +29,7 @@ public class RotateSignedPreKeyOperation: OWSOperation {
// Loki: Doing this on the global queue to match Signal
DispatchQueue.global().async {
let signedPreKeyRecord = self.primaryStorage.generateRandomSignedRecord()
signedPreKeyRecord.markAsAcceptedByService()
self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
self.primaryStorage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
TSPreKeyManager.clearPreKeyUpdateFailureCount()
TSPreKeyManager.clearSignedPreKeyRecords()
print("[Loki] Pre keys rotated successfully.")
SessionProtocol.rotatePreKeys()
self.reportSuccess()
}

@ -216,10 +216,7 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
if (!IsNoteToSelfEnabled()) {
return NO;
}
NSString *localNumber = self.tsAccountManager.localNumber;
NSString *masterDeviceHexEncodedPublicKey = [NSUserDefaults.standardUserDefaults stringForKey:@"masterDeviceHexEncodedPublicKey"];
bool isOurNumber = [self.contactIdentifier isEqualToString:localNumber] || (masterDeviceHexEncodedPublicKey != nil && [self.contactIdentifier isEqualToString:masterDeviceHexEncodedPublicKey]);
return (!self.isGroupThread && self.contactIdentifier != nil && isOurNumber);
return [SessionProtocol isMessageNoteToSelf:self];
}
#pragma mark - To be subclassed.

@ -27,7 +27,29 @@ public extension SessionProtocol {
/// Only ever modified from the message processing queue (`OWSBatchMessageProcessor.processingQueue`).
private static var syncMessageTimestamps: [String:Set<UInt64>] = [:]
// MARK: - General
@objc(shouldSkipMessageDecryptResult:)
public static func shouldSkipMessageDecryptResult(_ result: OWSMessageDecryptResult) -> Bool {
// Called from OWSMessageReceiver to prevent messages from even being added to the processing queue for some reason
return result.source == getUserHexEncodedPublicKey() // NOTE: This doesn't take into account multi device
}
// MARK: - Session Handling
@objc(handleDecryptionError:forHexEncodedPublicKey:using:)
public static func handleDecryptionError(_ rawValue: Int32, for hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
let type = TSErrorMessageType(rawValue: rawValue)
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
let thread = TSContactThread.getOrCreateThread(withContactId: masterHexEncodedPublicKey, transaction: transaction)
// Show the session reset prompt upon certain errors
switch type {
case .noSession, .invalidMessage, .invalidKeyException:
// Store the source device's public key in case it was a secondary device
thread.addSessionRestoreDevice(hexEncodedPublicKey, transaction: transaction)
default: break
}
}
@objc(isSessionRestoreMessage:)
public static func isSessionRestoreMessage(_ dataMessage: SSKProtoDataMessage) -> Bool {
let sessionRestoreFlag = SSKProtoDataMessage.SSKProtoDataMessageFlags.sessionRestore

@ -54,6 +54,16 @@ public extension SessionProtocol {
// TODO: Check that the behaviors described above make sense
@objc(isMessageNoteToSelf:)
public static func isMessageNoteToSelf(_ thread: TSThread) -> Bool {
guard let thread = thread as? TSContactThread else { return false }
var isNoteToSelf = false
storage.dbReadConnection.read { transaction in
isNoteToSelf = LokiDatabaseUtilities.isUserLinkedDevice(thread.contactIdentifier(), transaction: transaction)
}
return isNoteToSelf
}
@objc(isMessageNoteToSelf:inThread:)
public static func isMessageNoteToSelf(_ message: TSOutgoingMessage, in thread: TSThread) -> Bool {
guard let thread = thread as? TSContactThread, !(message is OWSOutgoingSyncMessage) && !(message is DeviceLinkMessage) else { return false }
@ -314,6 +324,30 @@ public extension SessionProtocol {
// MARK: - Typing Indicators
public static func shouldSendTypingIndicator(for thread: TSThread) -> Bool {
return !thread.isGroupThread() && !isMessageNoteToSelf(thread)
}
// MARK: - Receipts
// Used from OWSReadReceiptManager
@objc(shouldSendReadReceiptForThread:)
public static func shouldSendReadReceipt(for thread: TSThread) -> Bool {
return !isMessageNoteToSelf(thread) && !thread.isGroupThread()
}
// TODO: Not sure how these two relate
// Used from OWSOutgoingReceiptManager
@objc(shouldSendReceiptForThread:)
public static func shouldSendReceipt(for thread: TSThread) -> Bool {
return thread.friendRequestStatus == .friends && !thread.isGroupThread()
}
// MARK: - Sessions
// BEHAVIOR NOTE: OWSMessageSender.throws_encryptedMessageForMessageSend:recipientId:plaintext:transaction: sets
// isFriendRequest to true if the message in question is a friend request or a device linking request, but NOT if
@ -321,6 +355,42 @@ public extension SessionProtocol {
// TODO: Does the above make sense?
public static func createPreKeys() {
// We don't generate PreKeyRecords here.
// This is because we need the records to be linked to a contact since we don't have a central server.
// It's done automatically when we generate a pre key bundle to send to a contact (`generatePreKeyBundleForContact:`).
// You can use `getOrCreatePreKeyForContact:` to generate one if needed.
let signedPreKeyRecord = storage.generateRandomSignedRecord()
signedPreKeyRecord.markAsAcceptedByService()
storage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
storage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
print("[Loki] Pre keys created successfully.")
}
public static func refreshPreKeys() {
guard storage.currentSignedPrekeyId() == nil else {
print("[Loki] Skipping pre key refresh; using existing signed pre key.")
return
}
let signedPreKeyRecord = storage.generateRandomSignedRecord()
signedPreKeyRecord.markAsAcceptedByService()
storage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
storage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
TSPreKeyManager.clearPreKeyUpdateFailureCount()
TSPreKeyManager.clearSignedPreKeyRecords()
print("[Loki] Pre keys refreshed successfully.")
}
public static func rotatePreKeys() {
let signedPreKeyRecord = storage.generateRandomSignedRecord()
signedPreKeyRecord.markAsAcceptedByService()
storage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
storage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
TSPreKeyManager.clearPreKeyUpdateFailureCount()
TSPreKeyManager.clearSignedPreKeyRecords()
print("[Loki] Pre keys rotated successfully.")
}
public static func shouldUseFallbackEncryption(_ message: TSOutgoingMessage) -> Bool {
return !isSessionRequired(for: message)
}

@ -696,65 +696,6 @@ public class OWSLinkPreview: MTLModel {
})
return promise
}
public class func getImagePreview(from url: String, in transaction: YapDatabaseReadWriteTransaction) -> Promise<OWSLinkPreview> {
// Get the MIME type
guard let imageFileExtension = fileExtension(forImageUrl: url), let imageMIMEType = mimetype(forImageFileExtension: imageFileExtension) else {
return Promise(error: LinkPreviewError.invalidInput)
}
return downloadImage(url: url).map { data in
// Make sure the downloaded image has the correct MIME type
guard let newImageMIMEType = NSData(data: data).ows_guessMimeType() else {
throw LinkPreviewError.invalidContent
}
// Save the attachment
guard let attachmentId = saveAttachmentIfPossible(imageData: data, mimeType: newImageMIMEType, transaction: transaction) else {
Logger.verbose("Failed to save attachment for: \(url).")
throw LinkPreviewError.attachmentFailedToSave
}
// If it's a GIF and the data we have is not a GIF then we need to render a link preview without attachments
if (imageMIMEType == OWSMimeTypeImageGif && newImageMIMEType != OWSMimeTypeImageGif) {
return OWSLinkPreview(urlString: url, title: nil, imageAttachmentId: attachmentId)
}
return OWSLinkPreview(urlString: url, title: nil, imageAttachmentId: attachmentId, isDirectAttachment: true)
}
}
@objc(getImagePreviewWithURL:transaction:)
public class func objc_getImagePreview(url: String, in transaction: YapDatabaseReadWriteTransaction) -> AnyPromise {
return AnyPromise.from(getImagePreview(from: url, in: transaction))
}
public class func downloadImage(url imageUrl: String) -> Promise<Data> {
guard OWSLinkPreview.featureEnabled else {
return Promise(error: LinkPreviewError.featureDisabled)
}
guard SSKPreferences.areLinkPreviewsEnabled else {
return Promise(error: LinkPreviewError.featureDisabled)
}
guard isValidMediaUrl(imageUrl) else {
Logger.error("Invalid image URL.")
return Promise.init(error: LinkPreviewError.invalidInput)
}
guard let imageFileExtension = fileExtension(forImageUrl: imageUrl) else {
Logger.error("Image URL has unknown or invalid file extension: \(imageUrl).")
return Promise.init(error: LinkPreviewError.invalidInput)
}
guard let imageMimeType = mimetype(forImageFileExtension: imageFileExtension) else {
Logger.error("Image URL has unknown or invalid content type: \(imageUrl).")
return Promise.init(error: LinkPreviewError.invalidInput)
}
return downloadImage(url: imageUrl, imageMimeType: imageMimeType)
}
private class func downloadImage(url urlString: String, imageMimeType: String) -> Promise<Data> {

@ -100,10 +100,6 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) {
- (void)saveOpenGroupServerMessageID:(uint64_t)serverMessageID in:(YapDatabaseReadWriteTransaction *_Nullable)transaction;
#pragma mark - Link Preview
- (void)generateLinkPreviewIfNeededFromURL:(NSString *)url;
@end
NS_ASSUME_NONNULL_END

@ -511,44 +511,6 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
}
}
#pragma mark - Link Previews
- (void)generateLinkPreviewIfNeededFromURL:(NSString *)url {
[OWSLinkPreview tryToBuildPreviewInfoObjcWithPreviewUrl:url]
.thenOn(dispatch_get_main_queue(), ^(OWSLinkPreviewDraft *linkPreviewDraft) {
[self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
OWSLinkPreview *linkPreview = [OWSLinkPreview buildValidatedLinkPreviewFromInfo:linkPreviewDraft transaction:transaction error:nil];
self.linkPreview = linkPreview;
[self saveWithTransaction:transaction];
}];
})
.catchOn(dispatch_get_main_queue(), ^(NSError *error) {
// If we failed to get a link preview due to an invalid content type error then this could be a direct image link
if ([OWSLinkPreview isInvalidContentError:error]) {
__block AnyPromise *promise;
[self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
promise = [OWSLinkPreview getImagePreviewWithURL:url transaction:transaction]
.thenOn(dispatch_get_main_queue(), ^(OWSLinkPreview *linkPreview) {
// If we managed to get a direct image preview then render it
[self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
if (linkPreview.isDirectAttachment) {
[self addAttachmentWithID:linkPreview.imageAttachmentId in:transaction];
TSAttachmentStream *attachment = [TSAttachmentStream fetchObjectWithUniqueID:linkPreview.imageAttachmentId transaction:transaction];
attachment.albumMessageId = self.uniqueId;
attachment.isUploaded = true;
[attachment saveWithTransaction:transaction];
} else {
// Do nothing
}
}];
});
}];
return promise;
}
@throw error;
});
}
@end
NS_ASSUME_NONNULL_END

@ -673,28 +673,12 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
OWSAssertDebug(errorMessage);
if (errorMessage != nil) {
[errorMessage saveWithTransaction:transaction];
[self handleSessionRestoreForErrorMessage:errorMessage envelope:envelope transaction:transaction];
[SessionProtocol handleDecryptionError:errorMessage.errorType forHexEncodedPublicKey:envelope.source using:transaction];
[self notifyUserForErrorMessage:errorMessage envelope:envelope transaction:transaction];
}
}];
}
- (void)handleSessionRestoreForErrorMessage:(TSErrorMessage *)errorMessage
envelope:(SSKProtoEnvelope *)envelope
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
NSString *hexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source;
TSThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction];
// Trigger a session restore prompt if we get specific errors
if (errorMessage.errorType == TSErrorMessageNoSession ||
errorMessage.errorType == TSErrorMessageInvalidMessage ||
errorMessage.errorType == TSErrorMessageInvalidKeyException) {
// We want to store the source device's public key into the session restore in case it's a secondary device message
[((TSContactThread *)contactThread) addSessionRestoreDevice:envelope.source transaction:transaction];
}
}
- (void)notifyUserForErrorMessage:(TSErrorMessage *)errorMessage
envelope:(SSKProtoEnvelope *)envelope
transaction:(YapDatabaseReadWriteTransaction *)transaction

@ -398,8 +398,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
OWSAssertDebug(transaction);
// Loki: Don't process any messages from ourself
ECKeyPair *_Nullable userKeyPair = OWSIdentityManager.sharedManager.identityKeyPair;
if (userKeyPair && [result.source isEqualToString:userKeyPair.hexEncodedPublicKey]) {
if ([SessionProtocol shouldSkipMessageDecryptResult:result]) {
dispatch_async(self.serialQueue, ^{
completion(YES);
});

@ -173,12 +173,10 @@ NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptMa
}
TSThread *thread = [TSContactThread getOrCreateThreadWithContactId:recipientId];
// If we aren't friends with the user then don't send out any receipts
if (thread.friendRequestStatus != LKThreadFriendRequestStatusFriends) { continue; }
// Don't send any receipts for groups
if (thread.isGroupThread) { continue; }
if (![SessionProtocol shouldSendReceiptForThread:thread]) {
continue;
}
OWSReceiptsForSenderMessage *message;
NSString *receiptName;

@ -286,19 +286,10 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
self.toLinkedDevicesReadReceiptMap[threadUniqueId] = newReadReceipt;
}
__block BOOL isNoteToSelf;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
isNoteToSelf = [LKDatabaseUtilities isUserLinkedDevice:message.authorId in:transaction];
}];
if (isNoteToSelf) {
OWSLogVerbose(@"Ignoring read receipt for self-sender.");
if (![SessionProtocol shouldSendReadReceiptForThread:message.thread]) {
return;
}
// Don't send any receipts for groups
if (message.thread.isGroupThread) { return; }
if ([self areReadReceiptsEnabled]) {
OWSLogVerbose(@"Enqueuing read receipt for sender.");
[self.outgoingReceiptManager enqueueReadReceiptForEnvelope:messageAuthorId timestamp:message.timestamp];

@ -153,6 +153,7 @@ const int32_t kGroupIdLength = 16;
stringByAppendingString:NSLocalizedString(@"YOU_WERE_REMOVED", @"")];
} else {
NSArray *removedMemberNames = [[newModel.removedMembers allObjects] map:^NSString*(NSString* item) {
// TODO: Shouldn't this use DisplayNameUtilities?
return [contactsManager displayNameForPhoneIdentifier:item];
}];
if ([removedMemberNames count] > 1) {

@ -425,9 +425,9 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager {
private func generateSenderCertificate() -> Promise<(certificateData: Data, certificate: SMKSenderCertificate)> {
return Promise<(certificateData: Data, certificate: SMKSenderCertificate)> { seal in
// Loki: Generate a sender certifate locally
let sender = OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey
let certificate = SMKSenderCertificate(senderDeviceId: OWSDevicePrimaryDeviceId, senderRecipientId: sender!)
// Loki: Generate a sender certificate locally
let sender = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
let certificate = SMKSenderCertificate(senderDeviceId: OWSDevicePrimaryDeviceId, senderRecipientId: sender)
let certificateAsData = try certificate.serialized()
guard isValidCertificate(certificate) else {
throw OWSUDError.invalidData(description: "Invalid sender certificate.")
@ -439,14 +439,14 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager {
@objc
public func getSenderCertificate() -> SMKSenderCertificate? {
do {
let sender = OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey
let certificate = SMKSenderCertificate(senderDeviceId: OWSDevicePrimaryDeviceId, senderRecipientId: sender!)
let sender = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
let certificate = SMKSenderCertificate(senderDeviceId: OWSDevicePrimaryDeviceId, senderRecipientId: sender)
guard self.isValidCertificate(certificate) else {
throw OWSUDError.invalidData(description: "Invalid sender certificate returned by server")
}
return certificate
} catch {
Logger.error("\(error)")
print("[Loki] Couldn't get UD sender certificate due to error: \(error).")
return nil
}
}

@ -324,15 +324,7 @@ public class TypingIndicatorsImpl: NSObject, TypingIndicators {
}
// Loki: Don't send typing indicators in group or note to self threads
if (thread.isGroupThread()) {
return
} else {
var isNoteToSelf = false
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
isNoteToSelf = LokiDatabaseUtilities.isUserLinkedDevice(thread.contactIdentifier()!, transaction: transaction)
}
if isNoteToSelf { return }
}
if !SessionProtocol.shouldSendTypingIndicator(for: thread) { return }
let message = TypingIndicatorMessage(thread: thread, action: action)
messageSender.sendPromise(message: message).retainUntilComplete()

Loading…
Cancel
Save