Merge branch 'dev'

pull/111/head 1.0.3
Niels Andriesse 5 years ago
commit 65ca8e30e1

@ -1 +1 @@
Subproject commit edac100a2bda084edbba3291b080615aa26e5948
Subproject commit eeaffe766824af6c6b7e7351da6bddb1e9d99611

@ -4230,7 +4230,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 45;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -4244,7 +4244,7 @@
INFOPLIST_FILE = SignalShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.3;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -4292,7 +4292,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 45;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -4311,7 +4311,7 @@
INFOPLIST_FILE = SignalShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.3;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -4346,7 +4346,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 45;
DEBUG_INFORMATION_FORMAT = dwarf;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
@ -4365,7 +4365,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.3;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@ -4415,7 +4415,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 45;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
@ -4439,7 +4439,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.3;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@ -4624,7 +4624,7 @@
CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 45;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -4659,7 +4659,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.3;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;
@ -4691,7 +4691,7 @@
CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 45;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -4726,7 +4726,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.3;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

@ -5,15 +5,11 @@
<key>BuildDetails</key>
<dict>
<key>CarthageVersion</key>
<string>0.33.0</string>
<key>DateTime</key>
<string>Tue Feb 11 02:41:14 UTC 2020</string>
<string>0.34.0</string>
<key>OSXVersion</key>
<string>10.15.3</string>
<key>WebRTCCommit</key>
<string>1445d719bf05280270e9f77576f80f973fd847f8 M73</string>
<key>XCodeVersion</key>
<string>1100.1130</string>
</dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>

@ -1614,7 +1614,7 @@ static NSTimeInterval launchStartedAt;
- (void)createRSSFeedsIfNeeded
{
NSArray *feeds = @[ self.lokiNewsFeed, self.lokiMessengerUpdatesFeed ];
NSArray *feeds = @[ /*self.lokiNewsFeed,*/ self.lokiMessengerUpdatesFeed ];
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
for (LKRSSFeed *feed in feeds) {
NSString *userDefaultsKey = [@"isRSSFeedSetUp." stringByAppendingString:feed.id];

@ -142,13 +142,13 @@ public class SessionResetOperation: OWSOperation, DurableOperation {
* ================
*/
if (self.contactThread.sessionResetState != .requestReceived) {
if (self.contactThread.sessionResetStatus != .requestReceived) {
let message = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: self.contactThread, messageType: .typeLokiSessionResetInProgress)
message.save(with: transaction)
// Loki: We have initiated a session reset
print("[Loki] Session reset initiated.")
self.contactThread.sessionResetState = .initiated
self.contactThread.sessionResetStatus = .initiated
self.contactThread.save(with: transaction)
}
}

@ -155,7 +155,7 @@ final class DisplayNameVC : UIViewController {
return showError(title: NSLocalizedString("Please pick a shorter display name", comment: ""))
}
TSAccountManager.sharedInstance().didRegister()
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: nil, success: { }, failure: { }) // Try to save the user name but ignore the result
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: nil, success: { }, failure: { _ in }, requiresSync: false) // Try to save the user name but ignore the result
let homeVC = HomeVC()
navigationController!.setViewControllers([ homeVC ], animated: true)
}

@ -152,6 +152,7 @@ final class HomeVC : UIViewController, UITableViewDataSource, UITableViewDelegat
// openGroupSuggestionSheet.modalTransitionStyle = .crossDissolve
// present(openGroupSuggestionSheet, animated: true, completion: nil)
// }
UserDefaults.standard.set(true, forKey: "hasLaunchedOnce")
}
override func viewWillDisappear(_ animated: Bool) {

@ -274,15 +274,21 @@ final class SettingsVC : UIViewController, AvatarViewHelperDelegate {
self.displayNameToBeUploaded = nil
}
}
}, failure: {
}, failure: { error in
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
let alert = UIAlertController(title: NSLocalizedString("Couldn't Update Profile", comment: ""), message: NSLocalizedString("Please check your internet connection and try again", comment: ""), preferredStyle: .alert)
var isMaxFileSizeExceeded = false
if let error = error as? LokiDotNetAPI.LokiDotNetAPIError {
isMaxFileSizeExceeded = (error == .maxFileSizeExceeded)
}
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : NSLocalizedString("Couldn't Update Profile", comment: "")
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : NSLocalizedString("Please check your internet connection and try again", comment: "")
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
}
})
}, requiresSync: true)
}
}

@ -131,6 +131,7 @@ class ConversationViewItemActions: NSObject {
var actions: [MenuAction] = []
let isGroup = conversationViewItem.isGroupThread;
let isRSSFeed = conversationViewItem.isRSSFeed;
if shouldAllowReply {
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
@ -147,6 +148,11 @@ class ConversationViewItemActions: NSObject {
actions.append(saveMediaAction)
}
}
if isGroup && !isRSSFeed && conversationViewItem.interaction is TSIncomingMessage {
let copyPublicKeyAction = MessageActionBuilder.copyPublicKey(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(copyPublicKeyAction)
}
if !isGroup || conversationViewItem.userCanDeleteGroupMessage {
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: conversationViewItem, delegate: delegate)

@ -88,6 +88,7 @@
#import <YapDatabase/YapDatabaseAutoView.h>
#import <YapDatabase/YapDatabaseViewChange.h>
#import <YapDatabase/YapDatabaseViewConnection.h>
#import <SignalMetaDataKit/SignalMetadataKit-Swift.h>
@import Photos;
@ -1227,7 +1228,7 @@ typedef enum : NSUInteger {
}
}
[[[TSInfoMessage alloc] initWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageType:TSInfoMessageTypeLokiSessionResetInProgress] save];
thread.sessionResetState = TSContactThreadSessionResetStateRequestReceived;
thread.sessionResetStatus = LKSessionResetStatusRequestReceived;
[thread save];
[thread removeAllSessionRestoreDevicesWithTransaction:nil];
}
@ -3919,8 +3920,6 @@ typedef enum : NSUInteger {
- (void)tryToSendAttachments:(NSArray<SignalAttachment *> *)attachments messageText:(NSString *_Nullable)messageText
{
OWSLogError(@"");
DispatchMainThreadSafe(^{
__weak ConversationViewController *weakSelf = self;
if ([self isBlockedConversation]) {

@ -419,7 +419,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
}];
});
}
failure:^{
failure:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[modalActivityIndicator dismissWithCompletion:^{
[OWSAlerts showErrorAlertWithMessage:NSLocalizedString(
@ -428,7 +428,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
@"profile update fails.")];
}];
});
}];
} requiresSync:NO];
}];
}

@ -170,14 +170,14 @@ public class OnboardingProfileViewController: OnboardingBaseViewController {
self.onboardingController.profileDidComplete(fromView: self)
})
}
}, failure: {
}, failure: { _ in
DispatchQueue.main.async {
modal.dismiss(completion: {
OWSAlerts.showErrorAlert(message: NSLocalizedString("PROFILE_VIEW_ERROR_UPDATE_FAILED",
comment: "Error message shown when a profile update fails."))
})
}
})
}, requiresSync: false)
}
}

@ -336,10 +336,11 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
success:^{
resolve(@(1));
}
failure:^{
failure:^(NSError *error) {
// Ignore errors related to local profile.
resolve(@(1));
}];
}
requiresSync:YES];
}];
}

@ -224,9 +224,13 @@ NSString *const kSyncManagerLastContactSyncKey = @"kTSStorageManagerOWSSyncManag
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
if (!self.tsAccountManager.isRegisteredAndReady) {
return;
}
NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
BOOL hasLaunchedOnce = [userDefaults boolForKey:@"hasLaunchedOnce"];
if (hasLaunchedOnce) { // FIXME: Quick and dirty workaround to not do this on initial launch
[self sendConfigurationSyncMessage_AppReady];
}
[self sendConfigurationSyncMessage_AppReady];
}];
}

@ -50,7 +50,8 @@ extern const NSUInteger kOWSProfileManager_MaxAvatarDiameter;
- (void)updateLocalProfileName:(nullable NSString *)profileName
avatarImage:(nullable UIImage *)avatarImage
success:(void (^)(void))successBlock
failure:(void (^)(void))failureBlock;
failure:(void (^)(NSError *))failureBlock
requiresSync:(BOOL)requiresSync;
- (BOOL)isProfileNameTooLong:(nullable NSString *)profileName;

@ -229,13 +229,14 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
- (void)updateLocalProfileName:(nullable NSString *)profileName
avatarImage:(nullable UIImage *)avatarImage
success:(void (^)(void))successBlockParameter
failure:(void (^)(void))failureBlockParameter
failure:(void (^)(NSError *))failureBlockParameter
requiresSync:(BOOL)requiresSync
{
OWSAssertDebug(successBlockParameter);
OWSAssertDebug(failureBlockParameter);
// Ensure that the success and failure blocks are called on the main thread.
void (^failureBlock)(void) = ^{
void (^failureBlock)(NSError *) = ^(NSError *error) {
OWSLogError(@"Updating service with profile failed.");
// We use a "self-only" contact sync to indicate to desktop
@ -244,10 +245,12 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
//
// NOTE: We also inform the desktop in the failure case,
// since that _may have_ affected service state.
[[self.syncManager syncLocalContact] retainUntilComplete];
if (requiresSync) {
[[self.syncManager syncLocalContact] retainUntilComplete];
}
dispatch_async(dispatch_get_main_queue(), ^{
failureBlockParameter();
failureBlockParameter(error);
});
};
void (^successBlock)(void) = ^{
@ -256,7 +259,9 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
// We use a "self-only" contact sync to indicate to desktop
// that we've changed our profile and that it should do a
// profile fetch for "self".
[[self.syncManager syncLocalContact] retainUntilComplete];
if (requiresSync) {
[[self.syncManager syncLocalContact] retainUntilComplete];
}
dispatch_async(dispatch_get_main_queue(), ^{
successBlockParameter();
@ -288,7 +293,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
}];
}
failure:^(NSError *error) {
failureBlock();
failureBlock(error);
}];
};
@ -319,11 +324,11 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
tryToUpdateService(avatarUrlPath, fileName);
}
failure:^(NSError *error) {
failureBlock();
failureBlock(error);
}];
}
failure:^(NSError *error) {
failureBlock();
failureBlock(error);
}];
}
} else if (userProfile.avatarUrlPath) {
@ -333,7 +338,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
tryToUpdateService(nil, nil);
}
failure:^(NSError *error) {
failureBlock();
failureBlock(error);
}];
} else {
OWSLogVerbose(@"Updating local profile on service with no avatar.");

@ -243,7 +243,10 @@ NSString *const kLocalProfileUniqueId = @"kLocalProfileUniqueId";
// We populate an initial (empty) profile on launch of a new install, but until
// we have a registered account, syncing will fail (and there could not be any
// linked device to sync to at this point anyway).
if ([self.tsAccountManager isRegistered] && CurrentAppContext().isMainApp) {
NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
BOOL hasLaunchedOnce = [userDefaults boolForKey:@"hasLaunchedOnce"];
if ([self.tsAccountManager isRegistered] && CurrentAppContext().isMainApp && hasLaunchedOnce) {
[[self.syncManager syncLocalContact] retainUntilComplete];
}

@ -180,7 +180,7 @@ typedef void (^BuildOutgoingMessageCompletionBlock)(TSOutgoingMessage *savedMess
[SignalAttachment attachmentWithDataSource:dataSource dataUTI:kOversizeTextAttachmentUTI];
attachments = [mediaAttachments arrayByAddingObject:oversizeTextAttachment];
} else {
OWSFailDebug(@"dataSource was unexpectedly nil");
OWSFailDebug(@"dataSource was unexpectedly nil.");
}
}
@ -232,8 +232,7 @@ typedef void (^BuildOutgoingMessageCompletionBlock)(TSOutgoingMessage *savedMess
NSMutableArray<OWSOutgoingAttachmentInfo *> *attachmentInfos = [NSMutableArray new];
for (SignalAttachment *attachment in attachments) {
OWSOutgoingAttachmentInfo *attachmentInfo =
[attachment buildOutgoingAttachmentInfoWithMessage:message];
OWSOutgoingAttachmentInfo *attachmentInfo = [attachment buildOutgoingAttachmentInfoWithMessage:message];
[attachmentInfos addObject:attachmentInfo];
}
completionBlock(message, attachmentInfos, writeTransaction);

@ -216,6 +216,8 @@ typedef NS_ENUM(NSInteger, LKThreadFriendRequestStatus) {
*/
- (void)removeOldIncomingFriendRequestMessagesIfNeededWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
- (TSInteraction *)getLastInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction;
@end
NS_ASSUME_NONNULL_END

@ -292,12 +292,17 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
{
__block TSInteraction *interaction;
[self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
YapDatabaseViewTransaction *interactions = [transaction ext:TSMessageDatabaseViewExtensionName];
interaction = [interactions lastObjectInGroup:self.uniqueId];
interaction = [self getLastInteractionWithTransaction:transaction];
}];
return interaction;
}
- (TSInteraction *)getLastInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction
{
YapDatabaseViewTransaction *interactions = [transaction ext:TSMessageDatabaseViewExtensionName];
return [interactions lastObjectInGroup:self.uniqueId];
}
/**
* Useful for tests and debugging. In production use an enumeration method.
*/

@ -6,22 +6,14 @@
NS_ASSUME_NONNULL_BEGIN
// Loki: Session reset state
typedef NS_ENUM(NSInteger, TSContactThreadSessionResetState) {
// No ongoing session reset
TSContactThreadSessionResetStateNone,
// We initiated a session reset
TSContactThreadSessionResetStateInitiated,
// We received a session reset
TSContactThreadSessionResetStateRequestReceived,
};
extern NSString *const TSContactThreadPrefix;
typedef NS_ENUM(NSInteger, LKSessionResetStatus);
@interface TSContactThread : TSThread
// Loki: The current session reset state for this thread
@property (atomic) TSContactThreadSessionResetState sessionResetState;
// Loki: The current session reset status for this thread
@property (atomic) LKSessionResetStatus sessionResetStatus;
@property (atomic, readonly) NSArray<NSString *> *sessionRestoreDevices;
@property (nonatomic) BOOL hasDismissedOffers;

@ -11,6 +11,7 @@
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <YapDatabase/YapDatabaseConnection.h>
#import <YapDatabase/YapDatabaseTransaction.h>
#import <SignalMetadataKit/SignalMetadataKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@ -26,7 +27,7 @@ NSString *const TSContactThreadPrefix = @"c";
self = [super initWithUniqueId:uniqueIdentifier];
// No session reset ongoing
_sessionResetState = TSContactThreadSessionResetStateNone;
_sessionResetStatus = LKSessionResetStatusNone;
_sessionRestoreDevices = @[];
return self;

@ -1,7 +1,5 @@
import PromiseKit
extension String : Error { }
public extension LokiAPI {
fileprivate static var failureCount: [LokiAPITarget:UInt] = [:]
@ -12,24 +10,10 @@ public extension LokiAPI {
fileprivate static let failureThreshold = 2
// MARK: Caching
internal static var swarmCache: [String:[LokiAPITarget]] = [:]
private static let swarmCacheKey = "swarmCacheKey"
private static let swarmCacheCollection = "swarmCacheCollection"
internal static var swarmCache: [String:[LokiAPITarget]] {
get {
var result: [String:[LokiAPITarget]]? = nil
storage.dbReadConnection.read { transaction in
result = transaction.object(forKey: swarmCacheKey, inCollection: swarmCacheCollection) as! [String:[LokiAPITarget]]?
}
return result ?? [:]
}
set {
storage.dbReadWriteConnection.readWrite { transaction in
transaction.setObject(newValue, forKey: swarmCacheKey, inCollection: swarmCacheCollection)
}
}
}
internal static func dropIfNeeded(_ target: LokiAPITarget, hexEncodedPublicKey: String) {
let swarm = LokiAPI.swarmCache[hexEncodedPublicKey]
if var swarm = swarm, let index = swarm.firstIndex(of: target) {
@ -67,7 +51,7 @@ public extension LokiAPI {
print("[Loki] Invoking get_n_service_nodes on \(target).")
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { intermediate in
let rawResponse = intermediate.responseObject
guard let json = rawResponse as? JSON, let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw "Failed to update random snode pool from: \(rawResponse)." }
guard let json = rawResponse as? JSON, let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw LokiAPIError.randomSnodePoolUpdatingFailed }
randomSnodePool = try Set(rawTargets.flatMap { rawTarget in
guard let address = rawTarget["public_ip"] as? String, let port = rawTarget["storage_port"] as? Int, let idKey = rawTarget["pubkey_ed25519"] as? String, let encryptionKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else {
print("[Loki] Failed to parse target from: \(rawTarget).")
@ -137,6 +121,9 @@ internal extension Promise {
LokiAPI.randomSnodePool.remove(target) // Remove it from the random snode pool
LokiAPI.failureCount[target] = 0
}
case 406:
print("[Loki] The user's clock is out of sync with the service node network.")
throw LokiAPI.LokiAPIError.clockOutOfSync
case 421:
// The snode isn't associated with the given public key anymore
print("[Loki] Invalidating swarm for: \(hexEncodedPublicKey).")

@ -21,22 +21,17 @@ public final class LokiAPI : NSObject {
private static var userIDScanLimit: UInt = 4096
internal static var powDifficulty: UInt = 4
public static let defaultMessageTTL: UInt64 = 24 * 60 * 60 * 1000
public static let deviceLinkUpdateInterval: TimeInterval = 60
public static let deviceLinkUpdateInterval: TimeInterval = 20
// MARK: Types
public typealias RawResponse = Any
public enum Error : LocalizedError {
/// Only applicable to snode targets as proof of work isn't required for P2P messaging.
case proofOfWorkCalculationFailed
case messageConversionFailed
@objc public class LokiAPIError : NSError { // Not called `Error` for Obj-C interoperablity
public var errorDescription: String? {
switch self {
case .proofOfWorkCalculationFailed: return NSLocalizedString("Failed to calculate proof of work.", comment: "")
case .messageConversionFailed: return "Failed to convert Signal message to Loki message."
}
}
@objc public static let proofOfWorkCalculationFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Failed to calculate proof of work." ])
@objc public static let messageConversionFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Failed to construct message." ])
@objc public static let clockOutOfSync = LokiAPIError(domain: "LokiAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Your clock is out of sync with the service node network." ])
@objc public static let randomSnodePoolUpdatingFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Failed to update random service node pool." ])
}
@objc(LKDestination)
@ -110,9 +105,17 @@ public final class LokiAPI : NSObject {
}
public static func getDestinations(for hexEncodedPublicKey: String) -> Promise<[Destination]> {
var result: Promise<[Destination]>!
storage.dbReadConnection.readWrite { transaction in
result = getDestinations(for: hexEncodedPublicKey, in: transaction)
}
return result
}
public static func getDestinations(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> Promise<[Destination]> {
let (promise, seal) = Promise<[Destination]>.pending()
func getDestinations() {
storage.dbReadConnection.read { transaction in
func getDestinations(in transaction: YapDatabaseReadTransaction? = nil) {
func getDestinationsInternal(in transaction: YapDatabaseReadTransaction) {
var destinations: [Destination] = []
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
let masterDestination = Destination(hexEncodedPublicKey: masterHexEncodedPublicKey, kind: .master)
@ -122,6 +125,13 @@ public final class LokiAPI : NSObject {
destinations.append(contentsOf: slaveDestinations)
seal.fulfill(destinations)
}
if let transaction = transaction {
getDestinationsInternal(in: transaction)
} else {
storage.dbReadConnection.read { transaction in
getDestinationsInternal(in: transaction)
}
}
}
let timeSinceLastUpdate: TimeInterval
if let lastDeviceLinkUpdate = lastDeviceLinkUpdate[hexEncodedPublicKey] {
@ -130,29 +140,27 @@ public final class LokiAPI : NSObject {
timeSinceLastUpdate = .infinity
}
if timeSinceLastUpdate > deviceLinkUpdateInterval {
storage.dbReadConnection.read { transaction in
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
LokiFileServerAPI.getDeviceLinks(associatedWith: masterHexEncodedPublicKey).done(on: DispatchQueue.global()) { _ in
getDestinations()
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
LokiFileServerAPI.getDeviceLinks(associatedWith: masterHexEncodedPublicKey, in: transaction).done(on: DispatchQueue.global()) { _ in
getDestinations()
lastDeviceLinkUpdate[hexEncodedPublicKey] = Date()
}.catch(on: DispatchQueue.global()) { error in
if (error as? LokiDotNetAPI.LokiDotNetAPIError) == LokiDotNetAPI.LokiDotNetAPIError.parsingFailed {
// Don't immediately re-fetch in case of failure due to a parsing error
lastDeviceLinkUpdate[hexEncodedPublicKey] = Date()
}.catch(on: DispatchQueue.global()) { error in
if (error as? LokiDotNetAPI.Error) == LokiDotNetAPI.Error.parsingFailed {
// Don't immediately re-fetch in case of failure due to a parsing error
lastDeviceLinkUpdate[hexEncodedPublicKey] = Date()
getDestinations()
} else {
seal.reject(error)
}
getDestinations()
} else {
seal.reject(error)
}
}
} else {
getDestinations()
getDestinations(in: transaction)
}
return promise
}
public static func sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> Promise<Set<RawResponsePromise>> {
guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: Error.messageConversionFailed) }
guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: LokiAPIError.messageConversionFailed) }
let notificationCenter = NotificationCenter.default
let destination = lokiMessage.destination
func sendLokiMessage(_ lokiMessage: LokiMessage, to target: LokiAPITarget) -> RawResponsePromise {
@ -210,6 +218,12 @@ public final class LokiAPI : NSObject {
return AnyPromise.from(promise)
}
@objc(getDestinationsFor:inTransaction:)
public static func objc_getDestinations(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> AnyPromise {
let promise = getDestinations(for: hexEncodedPublicKey, in: transaction)
return AnyPromise.from(promise)
}
@objc(sendSignalMessage:onP2PSuccess:)
public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> AnyPromise {
let promise = sendSignalMessage(signalMessage, onP2PSuccess: onP2PSuccess).mapValues { AnyPromise.from($0) }.map { Set($0) }

@ -1,8 +1,9 @@
import PromiseKit
import SignalMetadataKit
/// Base class for `LokiFileServerAPI` and `LokiPublicChatAPI`.
public class LokiDotNetAPI : NSObject {
// MARK: Convenience
internal static let storage = OWSPrimaryStorage.shared()
internal static let userKeyPair = OWSIdentityManager.shared().identityKeyPair()!
@ -12,32 +13,57 @@ public class LokiDotNetAPI : NSObject {
private static let attachmentType = "network.loki"
// MARK: Error
@objc public class Error : NSError {
@objc public class LokiDotNetAPIError : NSError { // Not called `Error` for Obj-C interoperablity
@objc public static let generic = Error(domain: "com.loki-project.loki-messenger", code: 1, userInfo: [ NSLocalizedDescriptionKey : "An error occurred." ])
@objc public static let parsingFailed = Error(domain: "com.loki-project.loki-messenger", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Invalid file server response." ])
@objc public static let signingFailed = Error(domain: "com.loki-project.loki-messenger", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Couldn't sign message." ])
@objc public static let encryptionFailed = Error(domain: "com.loki-project.loki-messenger", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Couldn't encrypt file." ])
@objc public static let decryptionFailed = Error(domain: "com.loki-project.loki-messenger", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt file." ])
@objc public static let maxFileSizeExceeded = Error(domain: "com.loki-project.loki-messenger", code: 6, userInfo: [ NSLocalizedDescriptionKey : "Maximum file size exceeded." ])
@objc public static let generic = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "An error occurred." ])
@objc public static let parsingFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Invalid file server response." ])
@objc public static let signingFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Couldn't sign message." ])
@objc public static let encryptionFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Couldn't encrypt file." ])
@objc public static let decryptionFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt file." ])
@objc public static let maxFileSizeExceeded = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 6, userInfo: [ NSLocalizedDescriptionKey : "Maximum file size exceeded." ])
}
// MARK: Database
/// To be overridden by subclasses.
internal class var authTokenCollection: String { preconditionFailure("authTokenCollection is abstract and must be overridden.") }
private static func getAuthTokenFromDatabase(for server: String) -> String? {
var result: String? = nil
storage.dbReadConnection.read { transaction in
result = transaction.object(forKey: server, inCollection: authTokenCollection) as! String?
private static func getAuthTokenFromDatabase(for server: String, in transaction: YapDatabaseReadTransaction? = nil) -> String? {
func getAuthTokenInternal(in transaction: YapDatabaseReadTransaction) -> String? {
return transaction.object(forKey: server, inCollection: authTokenCollection) as! String?
}
if let transaction = transaction {
return getAuthTokenInternal(in: transaction)
} else {
var result: String? = nil
storage.dbReadConnection.read { transaction in
result = getAuthTokenInternal(in: transaction)
}
return result
}
}
internal static func getAuthToken(for server: String, in transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise<String> {
if let token = getAuthTokenFromDatabase(for: server, in: transaction) {
return Promise.value(token)
} else {
return requestNewAuthToken(for: server).then(on: DispatchQueue.global()) { submitAuthToken($0, for: server) }.map { token -> String in
setAuthToken(for: server, to: token, in: transaction)
return token
}
}
return result
}
private static func setAuthToken(for server: String, to newValue: String) {
storage.dbReadWriteConnection.readWrite { transaction in
private static func setAuthToken(for server: String, to newValue: String, in transaction: YapDatabaseReadWriteTransaction? = nil) {
func setAuthTokenInternal(in transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(newValue, forKey: server, inCollection: authTokenCollection)
}
if let transaction = transaction {
setAuthTokenInternal(in: transaction)
} else {
storage.dbReadWriteConnection.readWrite { transaction in
setAuthTokenInternal(in: transaction)
}
}
}
// MARK: Lifecycle
@ -52,7 +78,7 @@ public class LokiDotNetAPI : NSObject {
let data: Data
guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else {
print("[Loki] Couldn't read attachment from disk.")
return seal.reject(Error.generic)
return seal.reject(LokiDotNetAPIError.generic)
}
// Encrypt the attachment if needed
if isEncryptionRequired {
@ -60,7 +86,7 @@ public class LokiDotNetAPI : NSObject {
var digest = NSData()
guard let encryptedAttachmentData = Cryptography.encryptAttachmentData(unencryptedAttachmentData, outKey: &encryptionKey, outDigest: &digest) else {
print("[Loki] Couldn't encrypt attachment.")
return seal.reject(Error.encryptionFailed)
return seal.reject(LokiDotNetAPIError.encryptionFailed)
}
attachment.encryptionKey = encryptionKey as Data
attachment.digest = digest as Data
@ -71,7 +97,7 @@ public class LokiDotNetAPI : NSObject {
// Check the file size if needed
let isLokiFileServer = (server == LokiFileServerAPI.server)
if isLokiFileServer && data.count > LokiFileServerAPI.maxFileSize {
return seal.reject(Error.maxFileSizeExceeded)
return seal.reject(LokiDotNetAPIError.maxFileSizeExceeded)
}
// Create the request
let url = "\(server)/files"
@ -86,11 +112,11 @@ public class LokiDotNetAPI : NSObject {
return seal.reject(error)
}
// Send the request
func parseResponse(_ response: Any) {
func parseResponse(_ responseObject: Any) {
// Parse the server ID & download URL
guard let json = response as? JSON, let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse attachment from: \(response).")
return seal.reject(Error.parsingFailed)
guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse attachment from: \(responseObject).")
return seal.reject(LokiDotNetAPIError.parsingFailed)
}
// Update the attachment
attachment.serverId = serverID
@ -125,7 +151,7 @@ public class LokiDotNetAPI : NSObject {
let isSuccessful = (200...299) ~= statusCode
guard isSuccessful else {
print("[Loki] Couldn't upload attachment.")
return seal.reject(Error.generic)
return seal.reject(LokiDotNetAPIError.generic)
}
parseResponse(responseObject)
})
@ -144,18 +170,6 @@ public class LokiDotNetAPI : NSObject {
}
}
}
// MARK: Internal API
internal static func getAuthToken(for server: String) -> Promise<String> {
if let token = getAuthTokenFromDatabase(for: server) {
return Promise.value(token)
} else {
return requestNewAuthToken(for: server).then(on: DispatchQueue.global()) { submitAuthToken($0, for: server) }.map { token -> String in
setAuthToken(for: server, to: token)
return token
}
}
}
// MARK: Private API
private static func requestNewAuthToken(for server: String) -> Promise<String> {
@ -166,7 +180,7 @@ public class LokiDotNetAPI : NSObject {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String,
let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else {
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
// Discard the "05" prefix if needed
if serverPublicKey.count == 33 {
@ -176,7 +190,7 @@ public class LokiDotNetAPI : NSObject {
// The challenge is prefixed by the 16 bit IV
guard let tokenAsData = try? DiffieHellman.decrypt(challenge, publicKey: serverPublicKey, privateKey: userKeyPair.privateKey),
let token = String(bytes: tokenAsData, encoding: .utf8) else {
throw Error.decryptionFailed
throw LokiDotNetAPIError.decryptionFailed
}
return token
}

@ -19,23 +19,23 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
// MARK: Device Links (Public API)
/// Gets the device links associated with the given hex encoded public key from the
/// server and stores and returns the valid ones.
public static func getDeviceLinks(associatedWith hexEncodedPublicKey: String) -> Promise<Set<DeviceLink>> {
return getDeviceLinks(associatedWith: [ hexEncodedPublicKey ])
public static func getDeviceLinks(associatedWith hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise<Set<DeviceLink>> {
return getDeviceLinks(associatedWith: [ hexEncodedPublicKey ], in: transaction)
}
/// Gets the device links associated with the given hex encoded public keys from the
/// server and stores and returns the valid ones.
public static func getDeviceLinks(associatedWith hexEncodedPublicKeys: Set<String>) -> Promise<Set<DeviceLink>> {
public static func getDeviceLinks(associatedWith hexEncodedPublicKeys: Set<String>, in transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise<Set<DeviceLink>> {
let hexEncodedPublicKeysDescription = "[ \(hexEncodedPublicKeys.joined(separator: ", ")) ]"
print("[Loki] Getting device links for: \(hexEncodedPublicKeysDescription).")
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Set<DeviceLink>> in
return getAuthToken(for: server, in: transaction).then(on: DispatchQueue.global()) { token -> Promise<Set<DeviceLink>> in
let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
let url = URL(string: "\(server)/users?\(queryParameters)")!
let request = TSRequest(url: url)
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject }.map { rawResponse -> Set<DeviceLink> in
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map(on: DispatchQueue.global()) { $0.responseObject }.map(on: DispatchQueue.global()) { rawResponse -> Set<DeviceLink> in
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
return Set(data.flatMap { data -> [DeviceLink] in
guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] }
@ -74,7 +74,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
return deviceLink
}
})
}.map { deviceLinks -> Set<DeviceLink> in
}.map(on: DispatchQueue.global()) { deviceLinks -> Set<DeviceLink> in
storage.dbReadWriteConnection.readWrite { transaction in
storage.setDeviceLinks(deviceLinks, in: transaction)
}
@ -138,6 +138,9 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
// MARK: Profile Pictures (Public API)
public static func setProfilePicture(_ profilePicture: Data) -> Promise<String> {
return Promise<String>() { seal in
guard profilePicture.count < maxFileSize else {
return seal.reject(LokiDotNetAPIError.maxFileSizeExceeded)
}
getAuthToken(for: server).done { token in
let url = "\(server)/users/me/avatar"
let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
@ -150,24 +153,15 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
print("[Loki] Couldn't upload profile picture due to error: \(error).")
throw error
}
let task = AFURLSessionManager(sessionConfiguration: .default).uploadTask(withStreamedRequest: request as URLRequest, progress: nil, completionHandler: { response, responseObject, error in
if let error = error {
print("[Loki] Couldn't upload profile picture due to error: \(error).")
return seal.reject(error)
}
let statusCode = (response as! HTTPURLResponse).statusCode
let isSuccessful = (200...299) ~= statusCode
guard isSuccessful else {
print("[Loki] Couldn't upload profile picture.")
return seal.reject(Error.generic)
}
let _ = LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).done { responseObject in
guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let profilePicture = data["avatar_image"] as? JSON, let downloadURL = profilePicture["url"] as? String else {
print("[Loki] Couldn't parse profile picture from: \(responseObject).")
return seal.reject(Error.parsingFailed)
return seal.reject(LokiDotNetAPIError.parsingFailed)
}
return seal.fulfill(downloadURL)
})
task.resume()
}.catch { error in
seal.reject(error)
}
}.catch { error in
print("[Loki] Couldn't upload profile picture due to error: \(error).")
seal.reject(error)

@ -1,4 +1,5 @@
import PromiseKit
import SignalMetadataKit
internal class LokiFileServerProxy : LokiHTTPClient {
private let server: String
@ -48,11 +49,11 @@ internal class LokiFileServerProxy : LokiHTTPClient {
var headers = getCanonicalHeaders(for: request)
return LokiAPI.getRandomSnode().then { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] proxy -> Promise<Any> in
let url = "\(proxy.address):\(proxy.port)/file_proxy"
print("[Loki] Proxying file server request through \(proxy).")
guard let urlAsString = request.url?.absoluteString, let serverURLEndIndex = urlAsString.range(of: server)?.upperBound,
serverURLEndIndex < urlAsString.endIndex else { throw Error.endpointParsingFailed }
let endpointStartIndex = urlAsString.index(after: serverURLEndIndex)
let endpoint = String(urlAsString[endpointStartIndex..<urlAsString.endIndex])
print("[Loki] Proxying file server request (\(endpoint)) through \(proxy).")
let parametersAsString: String
if let tsRequest = request as? TSRequest {
headers["Content-Type"] = "application/json"
@ -106,7 +107,7 @@ internal class LokiFileServerProxy : LokiHTTPClient {
print("[Loki] Received an invalid response.")
throw Error.proxyResponseParsingFailed
}
let isSuccess = (200..<300).contains(statusCode)
let isSuccess = (200...299) ~= statusCode
guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: nil, underlyingError: Error.fileServerHTTPError(code: statusCode, message: nil)) }
let uncheckedJSONAsData = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
if uncheckedJSONAsData.isEmpty { return () }

@ -83,13 +83,8 @@ public final class LokiLongPoller : NSObject {
return LokiAPI.getRawMessages(from: target, usingLongPolling: true).then(on: DispatchQueue.global()) { [weak self] rawResponse -> Promise<Void> in
guard let strongSelf = self, !strongSelf.hasStopped else { return Promise.value(()) }
let messages = LokiAPI.parseRawMessagesResponse(rawResponse, from: target)
let hexEncodedPublicKeys = Set(messages.compactMap { $0.source })
let promises = hexEncodedPublicKeys.map { LokiAPI.getDestinations(for: $0) }
return when(resolved: promises).then(on: DispatchQueue.global()) { _ -> Promise<Void> in
guard let strongSelf = self, !strongSelf.hasStopped else { return Promise.value(()) }
strongSelf.onMessagesReceived(messages)
return strongSelf.longPoll(target, seal: seal)
}
strongSelf.onMessagesReceived(messages)
return strongSelf.longPoll(target, seal: seal)
}
}
}

@ -58,7 +58,7 @@ public struct LokiMessage {
result.nonce = nonce
seal.fulfill(result)
} else {
seal.reject(LokiAPI.Error.proofOfWorkCalculationFailed)
seal.reject(LokiAPI.LokiAPIError.proofOfWorkCalculationFailed)
}
}
}

@ -1,4 +1,5 @@
import PromiseKit
import SignalMetadataKit
internal class LokiSnodeProxy : LokiHTTPClient {
private let target: LokiAPITarget

@ -85,7 +85,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
return rawMessages.flatMap { message in
let isDeleted = (message["is_deleted"] as? Int == 1)
@ -97,11 +97,11 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).")
return nil
}
var avatar: LokiPublicChatMessage.Avatar? = nil
var profilePicture: LokiPublicChatMessage.ProfilePicture? = nil
let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "")
if let userAnnotations = user["annotations"] as? [JSON], let avatarAnnotation = userAnnotations.first(where: { $0["type"] as? String == profilePictureType }),
let avatarValue = avatarAnnotation["value"] as? JSON, let profileKeyString = avatarValue["profileKey"] as? String, let profileKey = Data(base64Encoded: profileKeyString), let url = avatarValue["url"] as? String {
avatar = LokiPublicChatMessage.Avatar(profileKey: profileKey, url: url)
if let userAnnotations = user["annotations"] as? [JSON], let profilePictureAnnotation = userAnnotations.first(where: { $0["type"] as? String == profilePictureType }),
let profilePictureValue = profilePictureAnnotation["value"] as? JSON, let profileKeyString = profilePictureValue["profileKey"] as? String, let profileKey = Data(base64Encoded: profileKeyString), let url = profilePictureValue["url"] as? String {
profilePicture = LokiPublicChatMessage.ProfilePicture(profileKey: profileKey, url: url)
}
let lastMessageServerID = getLastMessageServerID(for: channel, on: server)
if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: channel, on: server, to: serverID) }
@ -135,7 +135,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiPublicChatMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags,
width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle)
}
let result = LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, avatar: avatar,
let result = LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: profilePicture,
body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature)
guard result.hasValidSignature() else {
print("[Loki] Ignoring public chat message with invalid signature.")
@ -155,7 +155,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
}
public static func sendMessage(_ message: LokiPublicChatMessage, to channel: UInt64, on server: String) -> Promise<LokiPublicChatMessage> {
guard let signedMessage = message.sign(with: userKeyPair.privateKey) else { return Promise(error: Error.signingFailed) }
guard let signedMessage = message.sign(with: userKeyPair.privateKey) else { return Promise(error: LokiDotNetAPIError.signingFailed) }
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<LokiPublicChatMessage> in
print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).")
let url = URL(string: "\(server)/channels/\(channel)/messages")!
@ -170,10 +170,10 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String,
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, avatar: signedMessage.avatar, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
}
}.recover(on: DispatchQueue.global()) { error -> Promise<LokiPublicChatMessage> in
if let error = error as? NetworkManagerError, error.statusCode == 401 {
@ -197,7 +197,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
return deletions.flatMap { deletion in
guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else {
@ -231,7 +231,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let moderators = json["moderators"] as? [String] else {
print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
let moderatorAsSet = Set(moderators);
if self.moderators.keys.contains(server) {
@ -274,7 +274,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let users = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse user count for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
let userCount = users.count
let storage = OWSPrimaryStorage.shared()
@ -298,7 +298,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse display names for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
storage.dbReadWriteConnection.readWrite { transaction in
data.forEach { data in
@ -361,7 +361,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
let info = annotation["value"] as? JSON,
let displayName = info["name"] as? String else {
print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
return LokiPublicChatInfo(displayName: displayName)
}

@ -5,7 +5,7 @@ public final class LokiPublicChatMessage : NSObject {
public let serverID: UInt64?
public let hexEncodedPublicKey: String
public let displayName: String
public let avatar: Avatar?
public let profilePicture: ProfilePicture?
public let body: String
/// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
public let timestamp: UInt64
@ -22,7 +22,7 @@ public final class LokiPublicChatMessage : NSObject {
private let attachmentType = "net.app.core.oembed"
// MARK: Types
public struct Avatar {
public struct ProfilePicture {
public let profileKey: Data
public let url: String
}
@ -72,11 +72,11 @@ public final class LokiPublicChatMessage : NSObject {
}
// MARK: Initialization
public init(serverID: UInt64?, hexEncodedPublicKey: String, displayName: String, avatar: Avatar?, body: String, type: String, timestamp: UInt64, quote: Quote?, attachments: [Attachment], signature: Signature?) {
public init(serverID: UInt64?, hexEncodedPublicKey: String, displayName: String, profilePicture: ProfilePicture?, body: String, type: String, timestamp: UInt64, quote: Quote?, attachments: [Attachment], signature: Signature?) {
self.serverID = serverID
self.hexEncodedPublicKey = hexEncodedPublicKey
self.displayName = displayName
self.avatar = avatar
self.profilePicture = profilePicture
self.body = body
self.type = type
self.timestamp = timestamp
@ -100,7 +100,7 @@ public final class LokiPublicChatMessage : NSObject {
} else {
signature = nil
}
self.init(serverID: nil, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, avatar: nil, body: body, type: type, timestamp: timestamp, quote: quote, attachments: [], signature: signature)
self.init(serverID: nil, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: nil, body: body, type: type, timestamp: timestamp, quote: quote, attachments: [], signature: signature)
}
// MARK: Crypto
@ -115,7 +115,7 @@ public final class LokiPublicChatMessage : NSObject {
return nil
}
let signature = Signature(data: signatureData, version: signatureVersion)
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, avatar: avatar, body: body, type: type, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature)
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: profilePicture, body: body, type: type, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature)
}
internal func hasValidSignature() -> Bool {
@ -135,8 +135,8 @@ public final class LokiPublicChatMessage : NSObject {
value["sig"] = signature.data.toHexString()
value["sigver"] = signature.version
}
if let avatar = avatar {
value["avatar"] = avatar;
if let profilePicture = profilePicture {
value["avatar"] = profilePicture;
}
let annotation: JSON = [ "type" : type, "value" : value ]
let attachmentAnnotations: [JSON] = attachments.map { attachment in

@ -1,7 +1,5 @@
import PromiseKit
// TODO: Move away from polling
@objc(LKPublicChatPoller)
public final class LokiPublicChatPoller : NSObject {
private let publicChat: LokiPublicChat
@ -129,9 +127,9 @@ public final class LokiPublicChatPoller : NSObject {
}
let profile = SSKProtoDataMessageLokiProfile.builder()
profile.setDisplayName(message.displayName)
if let avatar = message.avatar {
profile.setProfilePicture(avatar.url)
dataMessage.setProfileKey(avatar.profileKey)
if let profilePicture = message.profilePicture {
profile.setProfilePicture(profilePicture.url)
dataMessage.setProfileKey(profilePicture.profileKey)
}
dataMessage.setProfile(try! profile.build())
dataMessage.setTimestamp(message.timestamp)
@ -170,12 +168,12 @@ public final class LokiPublicChatPoller : NSObject {
let messageServerID = message.serverID
SSKEnvironment.shared.messageManager.throws_processEnvelope(try! envelope.build(), plaintextData: try! content.build().serializedData(), wasReceivedByUD: false, transaction: transaction, serverID: messageServerID ?? 0)
// If we got a message from our master device then we should use its profile picture
if let avatar = message.avatar, masterHexEncodedPublicKey == message.hexEncodedPublicKey {
if let profilePicture = message.profilePicture, masterHexEncodedPublicKey == message.hexEncodedPublicKey {
if (message.displayName.count > 0) {
SSKEnvironment.shared.profileManager.updateProfileForContact(withID: masterHexEncodedPublicKey!, displayName: message.displayName, with: transaction)
}
SSKEnvironment.shared.profileManager.updateService(withProfileName: message.displayName, avatarUrl: avatar.url)
SSKEnvironment.shared.profileManager.setProfileKeyData(avatar.profileKey, forRecipientId: masterHexEncodedPublicKey!, avatarURL: avatar.url)
SSKEnvironment.shared.profileManager.updateService(withProfileName: message.displayName, avatarUrl: profilePicture.url)
SSKEnvironment.shared.profileManager.setProfileKeyData(profilePicture.profileKey, forRecipientId: masterHexEncodedPublicKey!, avatarURL: profilePicture.url)
}
}
}
@ -198,7 +196,7 @@ public final class LokiPublicChatPoller : NSObject {
LokiAPI.lastDeviceLinkUpdate[$0] = Date()
}
}.catch(on: DispatchQueue.global()) { error in
if (error as? LokiDotNetAPI.Error) == LokiDotNetAPI.Error.parsingFailed {
if (error as? LokiDotNetAPI.LokiDotNetAPIError) == LokiDotNetAPI.LokiDotNetAPIError.parsingFailed {
// Don't immediately re-fetch in case of failure due to a parsing error
hexEncodedPublicKeysToUpdate.forEach {
LokiAPI.lastDeviceLinkUpdate[$0] = Date()

@ -1,40 +0,0 @@
import CryptoSwift
import Curve25519Kit
public enum DiffieHellman {
public static let ivLength: Int32 = 16;
public static func encrypt(_ plainTextData: Data, using symmetricKey: Data) throws -> Data {
let iv = Randomness.generateRandomBytes(ivLength)!
let ivBytes = [UInt8](iv)
let symmetricKeyBytes = [UInt8](symmetricKey)
let messageBytes = [UInt8](plainTextData)
let blockMode = CBC(iv: ivBytes)
let aes = try AES(key: symmetricKeyBytes, blockMode: blockMode)
let cipherText = try aes.encrypt(messageBytes)
let ivAndCipher = ivBytes + cipherText
return Data(bytes: ivAndCipher, count: ivAndCipher.count)
}
public static func encrypt(_ plainTextData: Data, publicKey: Data, privateKey: Data) throws -> Data {
let symmetricKey = try Curve25519.generateSharedSecret(fromPublicKey: publicKey, privateKey: privateKey)
return try encrypt(plainTextData, using: symmetricKey)
}
public static func decrypt(_ encryptedData: Data, using symmetricKey: Data) throws -> Data {
let symmetricKeyBytes = [UInt8](symmetricKey)
guard encryptedData.count >= ivLength else { throw "Couldn't decrypt data." }
let ivBytes = [UInt8](encryptedData[..<ivLength])
let cipherBytes = [UInt8](encryptedData[ivLength...])
let blockMode = CBC(iv: ivBytes)
let aes = try AES(key: symmetricKeyBytes, blockMode: blockMode)
let decrypted = try aes.decrypt(cipherBytes)
return Data(bytes: decrypted, count: decrypted.count)
}
public static func decrypt(_ encryptedData: Data, publicKey: Data, privateKey: Data) throws -> Data {
let symmetricKey = try Curve25519.generateSharedSecret(fromPublicKey: publicKey, privateKey: privateKey)
return try decrypt(encryptedData, using: symmetricKey)
}
}

@ -1,95 +0,0 @@
import CryptoSwift
import Curve25519Kit
private extension String {
// Convert hex string to Data
fileprivate var hexData: Data {
var hex = self
var data = Data()
while(hex.count > 0) {
let subIndex = hex.index(hex.startIndex, offsetBy: 2)
let c = String(hex[..<subIndex])
hex = String(hex[subIndex...])
var ch: UInt32 = 0
Scanner(string: c).scanHexInt32(&ch)
var char = UInt8(ch)
data.append(&char, count: 1)
}
return data
}
}
/// A fallback session cipher which uses the the recipients public key to encrypt data
@objc public final class FallBackSessionCipher : NSObject {
// The pubkey hex string of the recipient
private let recipientId: String
// The identity manager
private let identityKeyStore: OWSIdentityManager
// The length of the iv
private let ivLength: Int32 = 16;
// The pubkey representation of the hex id
private lazy var recipientPubKey: Data = {
var recipientId = self.recipientId
// We need to check here if the id is prefix with '05'
// We only need to do this if the length is 66
if (recipientId.count == 66 && recipientId.hasPrefix("05")) {
recipientId = recipientId.substring(from: 2)
}
return recipientId.hexData
}()
// Our identity key
private lazy var userIdentityKeyPair: ECKeyPair? = identityKeyStore.identityKeyPair()
// A symmetric key used for encryption and decryption
private lazy var symmetricKey: Data? = {
guard let userIdentityKeyPair = userIdentityKeyPair else { return nil }
return try? Curve25519.generateSharedSecret(fromPublicKey: recipientPubKey, privateKey: userIdentityKeyPair.privateKey)
}()
/// Create a FallBackSessionCipher.
/// This is a very basic cipher and should only be used in special cases such as Friend Requests.
///
/// - Parameters:
/// - recipientId: The pubkey string of the recipient
/// - identityKeyStore: The identity manager
@objc public init(recipientId: String, identityKeyStore: OWSIdentityManager) {
self.recipientId = recipientId
self.identityKeyStore = identityKeyStore
super.init()
}
/// Encrypt a message
///
/// - Parameter message: The message to encrypt
/// - Returns: The encypted message or `nil` if it failed
@objc public func encrypt(message: Data) -> Data? {
guard let symmetricKey = symmetricKey else { return nil }
do {
return try DiffieHellman.encrypt(message, using: symmetricKey)
} catch {
Logger.warn("FallBackSessionCipher: Failed to encrypt message")
return nil
}
}
/// Decrypt a message
///
/// - Parameter message: The message to decrypt
/// - Returns: The decrypted message or `nil` if it failed
@objc public func decrypt(message: Data) -> Data? {
guard let symmetricKey = symmetricKey else { return nil }
do {
return try DiffieHellman.decrypt(message, using: symmetricKey)
} catch {
Logger.warn("FallBackSessionCipher: Failed to decrypt message")
return nil
}
}
}

@ -1,25 +0,0 @@
// Loki: Refer to Docs/SessionReset.md for explanations
#import <AxolotlKit/SessionCipher.h>
NS_ASSUME_NONNULL_BEGIN
extern NSString *const kNSNotificationName_SessionAdopted;
extern NSString *const kNSNotificationKey_ContactPubKey;
@interface SessionCipher (Loki)
/**
Decrypt the given `CipherMessage`.
This function is a wrapper around `throws_decrypt:protocolContext:` and adds on the custom Loki session handling.
Refer to SignalServiceKit/Loki/Docs/SessionReset.md for an overview of how it works.
@param whisperMessage The cipher message.
@param protocolContext The protocol context (a `YapDatabaseReadWriteTransaction`).
@return The decrypted data.
*/
- (NSData *)throws_lokiDecrypt:(id<CipherMessage>)whisperMessage protocolContext:(nullable id)protocolContext NS_SWIFT_UNAVAILABLE("throws Obj-C exceptions");
@end
NS_ASSUME_NONNULL_END

@ -27,9 +27,9 @@ NSString *const kNSNotificationKey_ContactPubKey = @"kNSNotificationKey_ContactP
// Our state before we decrypt the message
SessionState *_Nullable state = [self getCurrentState:protocolContext];
// Loki: Verify incoming friend request messages
// Verify incoming friend request messages
if (!state) {
[self throws_verifyFriendRequestAcceptPreKeyForMessage:whisperMessage protocolContext:protocolContext];
[self throws_validatePreKeysForFriendRequestAcceptance:whisperMessage protocolContext:protocolContext];
}
// While decrypting our state may change internally
@ -132,16 +132,16 @@ NSString *const kNSNotificationKey_ContactPubKey = @"kNSNotificationKey_ContactP
[self.sessionStore storeSession:self.recipientId deviceId:self.deviceId session:record protocolContext:protocolContext];
}
/// Check that we have matching prekeys in the case of a `PreKeyWhisperMessage`
/// This is so that we don't trigger a false friend request accept on unknown contacts
- (void)throws_verifyFriendRequestAcceptPreKeyForMessage:(id<CipherMessage>)whisperMessage protocolContext:(nullable id)protocolContext {
/// Check that we have matching pre keys in the case of a `PreKeyWhisperMessage`.
/// This is so that we don't trigger a false friend request accept on unknown contacts.
- (void)throws_validatePreKeysForFriendRequestAcceptance:(id<CipherMessage>)whisperMessage protocolContext:(nullable id)protocolContext {
OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadTransaction class]]);
YapDatabaseReadTransaction *transaction = protocolContext;
// We only want to look at `PreKeyWhisperMessage`
// Ignore anything that isn't a `PreKeyWhisperMessage`
if (![whisperMessage isKindOfClass:[PreKeyWhisperMessage class]]) { return; }
// We need the primary storage to access contact prekeys
// Check the pre key store
if (![self.prekeyStore isKindOfClass:[OWSPrimaryStorage class]]) { return; }
PreKeyWhisperMessage *preKeyMessage = whisperMessage;
@ -149,11 +149,11 @@ NSString *const kNSNotificationKey_ContactPubKey = @"kNSNotificationKey_ContactP
PreKeyRecord *_Nullable storedPreKey = [primaryStorage getPreKeyForContact:self.recipientId transaction:transaction];
if (!storedPreKey) {
OWSRaiseException(@"LokiInvalidPreKey", @"Received a friend request from a public key for which no prekey bundle was created.");
OWSRaiseException(@"Loki", @"Received a friend request from a public key for which no pre key bundle was created.");
}
if (storedPreKey.Id != preKeyMessage.prekeyID) {
OWSRaiseException(@"LokiPreKeyIdsDontMatch", @"Received a PreKeyWhisperMessage (friend request accept) from an unknown source.");
OWSRaiseException(@"Loki", @"Received a PreKeyWhisperMessage (friend request accept) from an unknown source.");
}
}

@ -0,0 +1,63 @@
import Foundation
import SignalMetadataKit
@objc(LKSessionResetImplementation)
public class LokiSessionResetImplementation : NSObject, SessionResetProtocol {
private let storage: OWSPrimaryStorage
@objc public init(storage: OWSPrimaryStorage) {
self.storage = storage
}
enum Errors : Error {
case invalidPreKey
case preKeyIDsDontMatch
}
public func validatePreKeyForFriendRequestAcceptance(for recipientID: String, whisperMessage: CipherMessage, protocolContext: Any?) throws {
guard let transaction = protocolContext as? YapDatabaseReadWriteTransaction else {
print("[Loki] Could not verify friend request acceptance pre key because an invalid transaction was provided.")
return
}
guard let preKeyMessage = whisperMessage as? PreKeyWhisperMessage else { return }
guard let storedPreKey = storage.getPreKey(forContact: recipientID, transaction: transaction) else {
print("[Loki] Received a friend request from a public key for which no pre key bundle was created.")
throw Errors.invalidPreKey
}
guard storedPreKey.id == preKeyMessage.prekeyID else {
print("[Loki] Received a `PreKeyWhisperMessage` (friend request acceptance) from an unknown source.")
throw Errors.preKeyIDsDontMatch
}
}
public func getSessionResetStatus(for recipientID: String, protocolContext: Any?) -> SessionResetStatus {
guard let transaction = protocolContext as? YapDatabaseReadWriteTransaction else {
print("[Loki] Could not get session reset status for \(recipientID) because an invalid transaction was provided.")
return .none
}
guard let thread = TSContactThread.getWithContactId(recipientID, transaction: transaction) else { return .none }
return thread.sessionResetStatus
}
public func onNewSessionAdopted(for recipientID: String, protocolContext: Any?) {
guard let transaction = protocolContext as? YapDatabaseReadWriteTransaction else {
Logger.warn("[Loki] Cannot handle new session adoption because an invalid transaction was provided.")
return
}
guard !recipientID.isEmpty else { return }
guard let thread = TSContactThread.getWithContactId(recipientID, transaction: transaction) else {
Logger.debug("[Loki] A new session was adopted but the thread couldn't be found for: \(recipientID).")
return
}
// If the current user initiated the reset then send back an empty message to acknowledge the completion of the session reset
if thread.sessionResetStatus == .initiated {
let emptyMessage = EphemeralMessage(in: thread)
SSKEnvironment.shared.messageSender.sendPromise(message: emptyMessage).retainUntilComplete()
}
// Show session reset done message
TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeLokiSessionResetDone).save(with: transaction)
// Clear the session reset status
thread.sessionResetStatus = .none
thread.save(with: transaction)
}
}

@ -10,9 +10,7 @@
expiresInSeconds:0 expireStartedAt:0 isVoiceMessage:NO groupMetaMessage:TSGroupMetaMessageUnspecified quotedMessage:nil contactShare:nil linkPreview:nil];
}
- (BOOL)shouldBeSaved {
return NO;
}
- (BOOL)shouldBeSaved { return NO; }
#pragma mark Building
- (nullable SSKProtoDataMessageBuilder *)dataMessageBuilder

@ -759,14 +759,10 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
TSOutgoingMessageRecipientState *_Nullable recipientState
= message.recipientStateMap[recipientId];
if (!recipientState) {
// OWSFailDebug(@"Missing recipient state for delivered recipient: %@", recipientId);
return;
}
TSOutgoingMessageRecipientState *_Nullable recipientState = message.recipientStateMap[recipientId];
if (!recipientState) { return; }
if (recipientState.state != OWSOutgoingMessageRecipientStateSent) {
OWSLogWarn(@"marking unsent message as delivered.");
OWSLogWarn(@"Marking unsent message as delivered.");
}
recipientState.state = OWSOutgoingMessageRecipientStateSent;
recipientState.readTimestamp = @(readTimestamp);

@ -9,11 +9,19 @@ NS_ASSUME_NONNULL_BEGIN
@class SSKProtoEnvelope;
@class YapDatabaseReadWriteTransaction;
@interface OWSMessageContentQueue : NSObject
- (dispatch_queue_t)serialQueue;
@end
// This class is used to write incoming (decrypted, unprocessed)
// messages to a durable queue and then process them in batches,
// in the order in which they were received.
@interface OWSBatchMessageProcessor : NSObject
@property (nonatomic, readonly) OWSMessageContentQueue *processingQueue;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER;

@ -237,7 +237,7 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
#pragma mark - Queue Processing
@interface OWSMessageContentQueue : NSObject
@interface OWSMessageContentQueue ()
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@property (nonatomic, readonly) OWSMessageContentJobFinder *finder;
@ -365,20 +365,12 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
{
OWSAssertDebug(AppReadiness.isAppReady);
// Don't process incoming messages in app extensions.
if (!CurrentAppContext().isMainApp) {
return;
}
if (!self.tsAccountManager.isRegisteredAndReady) {
return;
}
if (!CurrentAppContext().isMainApp) { return; }
if (!self.tsAccountManager.isRegisteredAndReady) { return; }
dispatch_async(self.serialQueue, ^{
if (self.isDrainingQueue) {
return;
}
if (self.isDrainingQueue) { return; }
self.isDrainingQueue = YES;
[self drainQueueWorkStep];
});
}
@ -387,7 +379,7 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
{
AssertOnDispatchQueue(self.serialQueue);
// We want a value that is just high enough to yield perf benefits.
// We want a value that is just high enough to yield performance benefits
const NSUInteger kIncomingMessageBatchSize = 32;
NSArray<OWSMessageContentJob *> *batchJobs = [self.finder nextJobsForBatchSize:kIncomingMessageBatchSize];
@ -431,10 +423,8 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
void (^reportFailure)(YapDatabaseReadWriteTransaction *transaction) = ^(
YapDatabaseReadWriteTransaction *transaction) {
// TODO: Add analytics.
TSErrorMessage *errorMessage = [TSErrorMessage corruptedMessageInUnknownThread];
[SSKEnvironment.shared.notificationsManager notifyUserForThreadlessErrorMessage:errorMessage
transaction:transaction];
[SSKEnvironment.shared.notificationsManager notifyUserForThreadlessErrorMessage:errorMessage transaction:transaction];
};
@try {
@ -449,9 +439,9 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
serverID:0];
}
} @catch (NSException *exception) {
// OWSFailDebug(@"Received an invalid envelope: %@", exception.debugDescription);
reportFailure(transaction);
}
[processedJobs addObject:job];
if (self.isAppInBackground) {
@ -473,7 +463,6 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
@interface OWSBatchMessageProcessor ()
@property (nonatomic, readonly) OWSMessageContentQueue *processingQueue;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@end
@ -528,7 +517,7 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
if (envelopeData.length < 1) {
OWSFailDebug(@"Empty envelope.");
OWSFailDebug(@"Received an empty envelope.");
return;
}
OWSAssert(transaction);

@ -15,7 +15,6 @@
#import "OWSPrimaryStorage+SessionStore.h"
#import "OWSPrimaryStorage+SignedPreKeyStore.h"
#import "OWSPrimaryStorage.h"
#import "SessionCipher+Loki.h"
#import "SSKEnvironment.h"
#import "SignalRecipient.h"
#import "TSAccountManager.h"
@ -84,6 +83,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
@interface OWSMessageDecrypter ()
@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage;
@property (nonatomic, readonly) LKSessionResetImplementation *sessionResetImplementation;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@end
@ -101,7 +101,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
}
_primaryStorage = primaryStorage;
_sessionResetImplementation = [[LKSessionResetImplementation alloc] initWithStorage:primaryStorage];
_dbConnection = primaryStorage.newDatabaseConnection;
OWSSingletonAssert();
@ -177,7 +177,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
// Ensure all blocked messages are discarded.
if ([self isEnvelopeSenderBlocked:envelope]) {
OWSLogInfo(@"Ignoring blocked envelope: %@", envelope.source);
OWSLogInfo(@"Ignoring blocked envelope from: %@.", envelope.source);
return failureBlock();
}
@ -195,21 +195,21 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
};
@try {
OWSLogInfo(@"decrypting envelope: %@", [self descriptionForEnvelope:envelope]);
OWSLogInfo(@"Decrypting envelope: %@.", [self descriptionForEnvelope:envelope]);
if (envelope.type != SSKProtoEnvelopeTypeUnidentifiedSender) {
if (!envelope.hasSource || envelope.source.length < 1 || ![ECKeyPair isValidHexEncodedPublicKeyWithCandidate:envelope.source]) {
OWSFailDebug(@"incoming envelope has invalid source");
OWSFailDebug(@"Incoming envelope with invalid source.");
return failureBlock();
}
if (!envelope.hasSourceDevice || envelope.sourceDevice < 1) {
OWSFailDebug(@"incoming envelope has invalid source device");
OWSFailDebug(@"Incoming envelope with invalid source device.");
return failureBlock();
}
// We block UD messages later, after they are decrypted.
if ([self isEnvelopeSenderBlocked:envelope]) {
OWSLogInfo(@"ignoring blocked envelope: %@", envelope.source);
OWSLogInfo(@"Ignoring blocked envelope from: %@.", envelope.source);
return failureBlock();
}
}
@ -224,7 +224,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
successBlock(result, transaction);
}
failureBlock:^(NSError * _Nullable error) {
OWSLogError(@"Decrypting friend request message from address: %@ failed with error: %@.",
OWSLogError(@"Decrypting friend request message from: %@ failed with error: %@.",
envelopeAddress(envelope),
error);
failureBlock();
@ -237,11 +237,11 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
[self throws_decryptSecureMessage:envelope
envelopeData:envelopeData
successBlock:^(OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
OWSLogDebug(@"decrypted secure message.");
OWSLogDebug(@"Decrypted secure message.");
successBlock(result, transaction);
}
failureBlock:^(NSError *_Nullable error) {
OWSLogError(@"decrypting secure message from address: %@ failed with error: %@",
OWSLogError(@"Decrypting secure message from: %@ failed with error: %@.",
envelopeAddress(envelope),
error);
OWSProdError([OWSAnalyticsEvents messageManagerErrorCouldNotHandleSecureMessage]);
@ -254,12 +254,11 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
[self throws_decryptPreKeyBundle:envelope
envelopeData:envelopeData
successBlock:^(OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
OWSLogDebug(@"decrypted pre-key whisper message");
OWSLogDebug(@"Decrypted pre key bundle message.");
successBlock(result, transaction);
}
failureBlock:^(NSError *_Nullable error) {
OWSLogError(@"decrypting pre-key whisper message from address: %@ failed "
@"with error: %@",
OWSLogError(@"Decrypting pre key bundle message from: %@ failed with error: %@.",
envelopeAddress(envelope),
error);
OWSProdError([OWSAnalyticsEvents messageManagerErrorCouldNotHandlePrekeyBundle]);
@ -287,12 +286,11 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
case SSKProtoEnvelopeTypeUnidentifiedSender: {
[self decryptUnidentifiedSender:envelope
successBlock:^(OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
OWSLogDebug(@"decrypted unidentified sender message");
OWSLogDebug(@"Decrypted unidentified sender message.");
successBlock(result, transaction);
}
failureBlock:^(NSError *_Nullable error) {
OWSLogError(@"decrypting unidentified sender message from address: %@ failed "
@"with error: %@",
OWSLogError(@"Decrypting unidentified sender message from: %@ failed with error: %@.",
envelopeAddress(envelope),
error);
OWSProdError([OWSAnalyticsEvents messageManagerErrorCouldNotHandleUnidentifiedSenderMessage]);
@ -302,11 +300,11 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
return;
}
default:
OWSLogWarn(@"Received unhandled envelope type: %d", (int)envelope.type);
OWSLogWarn(@"Received unhandled envelope type: %d.", (int)envelope.type);
break;
}
} @catch (NSException *exception) {
OWSFailDebug(@"Received an invalid envelope: %@", exception.debugDescription);
OWSFailDebug(@"Received an invalid envelope: %@.", exception.debugDescription);
OWSProdFail([OWSAnalyticsEvents messageManagerErrorInvalidProtocolMessage]);
[[self.primaryStorage newDatabaseConnection]
@ -338,11 +336,12 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
}
NSString *recipientId = envelope.source;
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientId:recipientId identityKeyStore:self.identityManager];
ECKeyPair *identityKeyPair = self.identityManager.identityKeyPair;
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientId:recipientId privateKey:identityKeyPair.privateKey];
NSData *_Nullable plaintextData = [[cipher decryptWithMessage:encryptedData] removePadding];
if (!plaintextData) {
NSString *errorString = [NSString stringWithFormat:@"Failed to decrypt friend request message for: %@.", recipientId];
NSString *errorString = [NSString stringWithFormat:@"Failed to decrypt friend request message from: %@.", recipientId];
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, errorString);
return failureBlock(error);
}
@ -419,7 +418,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
NSData *encryptedData = envelope.content ?: envelope.legacyMessage;
if (!encryptedData) {
OWSProdFail([OWSAnalyticsEvents messageManagerErrorMessageEnvelopeHasNoContent]);
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, @"Envelope has no content");
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, @"Envelope has no content.");
return failureBlock(error);
}
@ -427,23 +426,22 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@try {
id<CipherMessage> cipherMessage = cipherMessageBlock(encryptedData);
SessionCipher *cipher = [[SessionCipher alloc] initWithSessionStore:self.primaryStorage
preKeyStore:self.primaryStorage
signedPreKeyStore:self.primaryStorage
identityKeyStore:self.identityManager
recipientId:recipientId
deviceId:deviceId];
LKSessionCipher *cipher = [[LKSessionCipher alloc]
initWithSessionResetImplementation:self.sessionResetImplementation
sessionStore:self.primaryStorage
preKeyStore:self.primaryStorage
signedPreKeyStore:self.primaryStorage
identityKeyStore:self.primaryStorage
recipientID:recipientId
deviceID:deviceId];
// plaintextData may be nil for some envelope types.
NSData *_Nullable plaintextData =
[[cipher throws_lokiDecrypt:cipherMessage protocolContext:transaction] removePadding];
/* Loki: Original code
* ================
NSData *_Nullable plaintextData =
[[cipher throws_decrypt:cipherMessage protocolContext:transaction] removePadding];
* ================
*/
NSError *error = nil;
NSData *_Nullable decryptedData = [cipher decrypt:cipherMessage protocolContext:transaction error:&error];
// Throw if we got an error
SCKRaiseIfExceptionWrapperError(error);
NSData *_Nullable plaintextData = decryptedData != nil ? [decryptedData removePadding] : nil;
OWSMessageDecryptResult *result = [OWSMessageDecryptResult resultWithEnvelopeData:envelopeData
plaintextData:plaintextData
source:envelope.source
@ -454,7 +452,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self processException:exception envelope:envelope];
NSString *errorDescription = [NSString
stringWithFormat:@"Exception while decrypting %@: %@", cipherTypeName, exception.description];
stringWithFormat:@"Exception while decrypting %@: %@.", cipherTypeName, exception.description];
OWSLogError(@"%@", errorDescription);
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, errorDescription);
failureBlock(error);
@ -491,14 +489,16 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
[self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSError *cipherError;
SMKSecretSessionCipher *_Nullable cipher =
[[SMKSecretSessionCipher alloc] initWithSessionStore:self.primaryStorage
[[SMKSecretSessionCipher alloc] initWithSessionResetImplementation:self.sessionResetImplementation
sessionStore:self.primaryStorage
preKeyStore:self.primaryStorage
signedPreKeyStore:self.primaryStorage
identityStore:self.identityManager
error:&cipherError];
if (cipherError || !cipher) {
OWSFailDebug(@"Could not create secret session cipher: %@", cipherError);
cipherError = EnsureDecryptError(cipherError, @"Could not create secret session cipher");
OWSFailDebug(@"Could not create secret session cipher: %@.", cipherError);
cipherError = EnsureDecryptError(cipherError, @"Could not create secret session cipher.");
return failureBlock(cipherError);
}
@ -514,9 +514,9 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
if (!decryptResult) {
if (!decryptError) {
OWSFailDebug(@"Caller should provide specific error");
OWSFailDebug(@"Caller should provide specific error.");
NSError *error = OWSErrorWithCodeDescription(
OWSErrorCodeFailedToDecryptUDMessage, @"Could not decrypt UD message");
OWSErrorCodeFailedToDecryptUDMessage, @"Could not decrypt UD message.");
return failureBlock(error);
}
@ -565,7 +565,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self processException:underlyingException envelope:identifiedEnvelope];
NSString *errorDescription = [NSString
stringWithFormat:@"Exception while decrypting ud message: %@", underlyingException.description];
stringWithFormat:@"Exception while decrypting UD message: %@.", underlyingException.description];
OWSLogError(@"%@", errorDescription);
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, errorDescription);
failureBlock(error);
@ -580,7 +580,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
return;
}
OWSFailDebug(@"Could not decrypt UD message: %@", underlyingError);
OWSFailDebug(@"Could not decrypt UD message: %@.", underlyingError);
failureBlock(underlyingError);
return;
}

@ -36,7 +36,6 @@
#import "OWSSyncGroupsMessage.h"
#import "OWSSyncGroupsRequestMessage.h"
#import "ProfileManagerProtocol.h"
#import "SessionCipher+Loki.h"
#import "SSKEnvironment.h"
#import "TSAccountManager.h"
#import "TSAttachment.h"
@ -53,12 +52,14 @@
#import "TSQuotedMessage.h"
#import <SignalCoreKit/Cryptography.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalMetadataKit/SignalMetadataKit-Swift.h>
#import <SignalServiceKit/NSObject+Casting.h>
#import <SignalServiceKit/SignalRecipient.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <YapDatabase/YapDatabase.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import "OWSDispatch.h"
#import "OWSBatchMessageProcessor.h"
#import "OWSQueues.h"
NS_ASSUME_NONNULL_BEGIN
@ -92,9 +93,6 @@ NS_ASSUME_NONNULL_BEGIN
_primaryStorage = primaryStorage;
_dbConnection = primaryStorage.newDatabaseConnection;
_incomingMessageFinder = [[OWSIncomingMessageFinder alloc] initWithPrimaryStorage:primaryStorage];
// Loki: Observe session changes
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleNewSessionAdopted:) name:kNSNotificationName_SessionAdopted object:nil];
OWSSingletonAssert();
@ -258,25 +256,33 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
if (!CurrentAppContext().isMainApp) {
OWSFail(@"Not main app.");
OWSFail(@"Not the main app.");
return;
}
OWSLogInfo(@"handling decrypted envelope: %@", [self descriptionForEnvelope:envelope]);
OWSLogInfo(@"Handling decrypted envelope: %@.", [self descriptionForEnvelope:envelope]);
if (!wasReceivedByUD) {
if (!envelope.hasSource || envelope.source.length < 1) {
OWSFailDebug(@"incoming envelope has invalid source");
OWSFailDebug(@"Incoming envelope with invalid source.");
return;
}
if (!envelope.hasSourceDevice || envelope.sourceDevice < 1) {
OWSFailDebug(@"incoming envelope has invalid source device");
OWSFailDebug(@"Incoming envelope with invalid source device.");
return;
}
}
OWSAssertDebug(![self isEnvelopeSenderBlocked:envelope]);
// Loki: Ignore any friend requests from before restoration
// The envelope type is set during UD decryption.
uint64_t restorationTime = [NSNumber numberWithDouble:[OWSPrimaryStorage.sharedManager getRestorationTime]].unsignedLongLongValue;
if (envelope.type == SSKProtoEnvelopeTypeFriendRequest && envelope.timestamp < restorationTime * 1000) {
[LKLogger print:@"[Loki] Ignoring friend request received before restoration."];
return;
}
[self checkForUnknownLinkedDevice:envelope transaction:transaction];
switch (envelope.type) {
@ -408,7 +414,7 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
if (envelope.sourceDevice < 1) {
OWSFailDebug(@"Invaid source device.");
OWSFailDebug(@"Invalid source device.");
return;
}
@ -417,23 +423,24 @@ NS_ASSUME_NONNULL_BEGIN
sourceDeviceId:envelope.sourceDevice
transaction:transaction];
if (duplicateEnvelope) {
OWSLogInfo(@"Ignoring previously received envelope from %@ with timestamp: %llu",
OWSLogInfo(@"Ignoring previously received envelope from: %@ with timestamp: %llu.",
envelopeAddress(envelope),
envelope.timestamp);
return;
}
// Loki: Handle friend request acceptance if needed
// The envelope type is set during UD decryption.
[self handleFriendRequestAcceptanceIfNeededWithEnvelope:envelope transaction:transaction];
if (envelope.content != nil) {
NSError *error;
SSKProtoContent *_Nullable contentProto = [SSKProtoContent parseData:plaintextData error:&error];
if (error || !contentProto) {
OWSFailDebug(@"could not parse proto: %@", error);
OWSFailDebug(@"Could not parse proto due to error: %@.", error);
return;
}
OWSLogInfo(@"handling content: <Content: %@>", [self descriptionForContent:contentProto]);
OWSLogInfo(@"Handling content: <Content: %@>.", [self descriptionForContent:contentProto]);
// Loki: Workaround for duplicate sync transcript issue
if (contentProto.syncMessage != nil && contentProto.syncMessage.sent != nil) {
@ -451,7 +458,8 @@ NS_ASSUME_NONNULL_BEGIN
}
[self.primaryStorage setPreKeyBundle:bundle forContact:envelope.source transaction:transaction];
// Loki: If we received a friend request, but we were already friends with this user, then reset the session
// Loki: If we received a friend request, but we were already friends with this user, reset the session
// The envelope type is set during UD decryption.
if (envelope.type == SSKProtoEnvelopeTypeFriendRequest) {
TSContactThread *thread = [TSContactThread getThreadWithContactId:envelope.source transaction:transaction];
if (thread && thread.isContactFriend) {
@ -549,13 +557,13 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
// Loki: Don't process session request messages
// Loki: Don't process session request messages any further
if ((dataMessage.flags & SSKProtoDataMessageFlagsSessionRequest) != 0) { return; }
// Loki: Don't process session restore messages
// Loki: Don't process session restore messages any further
if ((dataMessage.flags & SSKProtoDataMessageFlagsSessionRestore) != 0) { return; }
if ([self isDataMessageBlocked:dataMessage envelope:envelope]) {
NSString *logMessage = [NSString stringWithFormat:@"Ignoring blocked message from sender: %@", envelope.source];
NSString *logMessage = [NSString stringWithFormat:@"Ignoring blocked message from sender: %@.", envelope.source];
if (dataMessage.group) {
logMessage = [logMessage stringByAppendingFormat:@" in group: %@", dataMessage.group.id];
}
@ -565,14 +573,12 @@ NS_ASSUME_NONNULL_BEGIN
if (dataMessage.hasTimestamp) {
if (dataMessage.timestamp <= 0) {
OWSFailDebug(@"Ignoring message with invalid data message timestamp: %@", envelope.source);
// TODO: Add analytics.
OWSFailDebug(@"Ignoring data message with invalid timestamp: %@.", envelope.source);
return;
}
// This prevents replay attacks by the service.
if (dataMessage.timestamp != envelope.timestamp) {
OWSFailDebug(@"Ignoring message with non-matching data message timestamp: %@", envelope.source);
// TODO: Add analytics.
OWSFailDebug(@"Ignoring data message with non-matching timestamp: %@.", envelope.source);
return;
}
}
@ -598,7 +604,7 @@ NS_ASSUME_NONNULL_BEGIN
[self sendGroupInfoRequest:dataMessage.group.id envelope:envelope transaction:transaction];
return;
} else {
OWSLogInfo(@"Ignoring group message for unknown group from: %@", envelope.source);
OWSLogInfo(@"Ignoring group message for unknown group from: %@.", envelope.source);
return;
}
}
@ -881,7 +887,7 @@ NS_ASSUME_NONNULL_BEGIN
TSThread *_Nullable thread = [self threadForEnvelope:envelope dataMessage:dataMessage transaction:transaction];
if (!thread) {
OWSFailDebug(@"ignoring media message for unknown group.");
OWSFailDebug(@"Ignoring media message for unknown group.");
return;
}
@ -896,17 +902,17 @@ NS_ASSUME_NONNULL_BEGIN
[message saveWithTransaction:transaction];
OWSLogDebug(@"incoming attachment message: %@", message.debugDescription);
OWSLogDebug(@"Incoming attachment message: %@.", message.debugDescription);
[self.attachmentDownloads downloadAttachmentsForMessage:message
transaction:transaction
success:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
OWSLogDebug(@"successfully fetched attachments: %lu for message: %@",
OWSLogDebug(@"Successfully fetched attachments: %lu for message: %@.",
(unsigned long)attachmentStreams.count,
message);
}
failure:^(NSError *error) {
OWSLogError(@"failed to fetch attachments for message: %@ with error: %@", message, error);
OWSLogError(@"Failed to fetch attachments for message: %@ with error: %@.", message, error);
}];
}
@ -1141,7 +1147,7 @@ NS_ASSUME_NONNULL_BEGIN
[self.primaryStorage archiveAllSessionsForContact:hexEncodedPublicKey protocolContext:transaction];
// Loki: Set our session reset state
thread.sessionResetState = TSContactThreadSessionResetStateRequestReceived;
thread.sessionResetStatus = LKSessionResetStatusRequestReceived;
[thread saveWithTransaction:transaction];
// Loki: Send an empty message to trigger the session reset code for both parties
@ -1391,6 +1397,18 @@ NS_ASSUME_NONNULL_BEGIN
return nil;
}
// The envelope source is set during UD decryption.
if ([ECKeyPair isValidHexEncodedPublicKeyWithCandidate:envelope.source]) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[[LKAPI getDestinationsFor:envelope.source inTransaction:transaction].ensureOn(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^() {
dispatch_semaphore_signal(semaphore);
}).catchOn(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(NSError *error) {
dispatch_semaphore_signal(semaphore);
}) retainUntilComplete];
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC));
}
if (groupId.length > 0) {
NSMutableSet *newMemberIds = [NSMutableSet setWithArray:dataMessage.group.members];
NSMutableSet *removedMemberIds = [NSMutableSet new];
@ -1410,7 +1428,7 @@ NS_ASSUME_NONNULL_BEGIN
// We distinguish between the old group state (if any) and the new group state.
TSGroupThread *_Nullable oldGroupThread = [TSGroupThread threadWithGroupId:groupId transaction:transaction];
if (oldGroupThread) {
// Loki: Try to figure out removed members
// Loki: Determine removed members
removedMemberIds = [NSMutableSet setWithArray:oldGroupThread.groupModel.groupMemberIds];
[removedMemberIds minusSet:newMemberIds];
[removedMemberIds removeObject:hexEncodedPublicKey];
@ -1540,7 +1558,7 @@ NS_ASSUME_NONNULL_BEGIN
// Loki: Don't process friend requests in group chats
if (body.length == 0 && attachmentPointers.count < 1 && !contact) {
OWSLogWarn(@"ignoring empty incoming message from: %@ for group: %@ with timestamp: %lu",
OWSLogWarn(@"Ignoring empty incoming message from: %@ for group: %@ with timestamp: %lu.",
hexEncodedPublicKey,
groupId,
(unsigned long)timestamp);
@ -1569,14 +1587,14 @@ NS_ASSUME_NONNULL_BEGIN
return incomingMessage;
}
default: {
OWSLogWarn(@"Ignoring unknown group message type: %d", (int)dataMessage.group.type);
OWSLogWarn(@"Ignoring unknown group message type: %d.", (int)dataMessage.group.type);
return nil;
}
}
} else {
// Loki: A message from a secondary device should appear as if it came from the primary device; the underlying
// friend request logic, however, should still be specific to the secondary device.
// Loki: A message from a slave device should appear as if it came from the master device; the underlying
// friend request logic, however, should still be specific to the slave device.
// Loki: Get the master hex encoded public key and thread
NSString *hexEncodedPublicKey = envelope.source;
@ -1584,7 +1602,7 @@ NS_ASSUME_NONNULL_BEGIN
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction];
TSContactThread *masterThread = [TSContactThread getOrCreateThreadWithContactId:masterHexEncodedPublicKey transaction:transaction];
OWSLogDebug(@"incoming message from: %@ with timestamp: %lu", hexEncodedPublicKey, (unsigned long)timestamp);
OWSLogDebug(@"Incoming message from: %@ with timestamp: %lu.", hexEncodedPublicKey, (unsigned long)timestamp);
[[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer
thread:masterThread
@ -1643,7 +1661,7 @@ NS_ASSUME_NONNULL_BEGIN
[self handleFriendRequestMessageIfNeededWithEnvelope:envelope data:dataMessage message:incomingMessage thread:thread transaction:transaction];
if (body.length == 0 && attachmentPointers.count < 1 && !contact) {
OWSLogWarn(@"ignoring empty incoming message from: %@ with timestamp: %lu",
OWSLogWarn(@"Ignoring empty incoming message from: %@ with timestamp: %lu.",
hexEncodedPublicKey,
(unsigned long)timestamp);
return nil;
@ -1681,13 +1699,11 @@ NS_ASSUME_NONNULL_BEGIN
if (profileKey.length == kAES256_KeyByteLength) {
[self.profileManager setProfileKeyData:profileKey forRecipientId:recipientId avatarURL:url];
} else {
OWSFailDebug(
@"Unexpected profile key length:%lu on message from:%@", (unsigned long)profileKey.length, recipientId);
OWSFailDebug(@"Unexpected profile key length:%lu on message from:%@", (unsigned long)profileKey.length, recipientId);
}
}
}
// Loki: Establish a session if there is no session between the memebers of a group
- (void)establishSessionsWithMembersIfNeeded:(NSArray *)members forThread:(TSGroupThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction
{
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
@ -1737,8 +1753,9 @@ NS_ASSUME_NONNULL_BEGIN
if (envelope.isGroupChatMessage) {
return NSLog(@"[Loki] Ignoring friend request in group chat.", @"");
}
// The envelope type is set during UD decryption.
if (envelope.type != SSKProtoEnvelopeTypeFriendRequest) {
return NSLog(@"[Loki] handleFriendRequestMessageIfNeededWithEnvelope:data:message:thread:transaction was called with an envelope that isn't of type SSKProtoEnvelopeTypeFriendRequest.");
return NSLog(@"[Loki] Ignoring friend request logic for non friend request type envelope.");
}
if ([self canFriendRequestBeAutoAcceptedForThread:thread transaction:transaction]) {
[thread saveFriendRequestStatus:LKThreadFriendRequestStatusFriends withTransaction:transaction];
@ -1782,6 +1799,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)handleFriendRequestAcceptanceIfNeededWithEnvelope:(SSKProtoEnvelope *)envelope transaction:(YapDatabaseReadWriteTransaction *)transaction {
// If we get an envelope that isn't a friend request, then we can infer that we had to use
// Signal cipher decryption and thus that we have a session with the other person.
// The envelope type is set during UD decryption.
if (envelope.isGroupChatMessage || envelope.type == SSKProtoEnvelopeTypeFriendRequest) return;
// Currently this uses `envelope.source` but with sync messages we'll need to use the message sender ID
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:envelope.source transaction:transaction];
@ -1789,7 +1807,7 @@ NS_ASSUME_NONNULL_BEGIN
if (thread.friendRequestStatus == LKThreadFriendRequestStatusNone) { return; }
// Become happy friends and go on great adventures
[thread saveFriendRequestStatus:LKThreadFriendRequestStatusFriends withTransaction:transaction];
TSOutgoingMessage *existingFriendRequestMessage = [thread.lastInteraction as:TSOutgoingMessage.class];
TSOutgoingMessage *existingFriendRequestMessage = [[thread getLastInteractionWithTransaction:transaction] as:TSOutgoingMessage.class];
if (existingFriendRequestMessage != nil && existingFriendRequestMessage.isFriendRequest) {
[existingFriendRequestMessage saveFriendRequestStatus:LKMessageFriendRequestStatusAccepted withTransaction:transaction];
}
@ -1874,7 +1892,7 @@ NS_ASSUME_NONNULL_BEGIN
}];
}
failure:^(NSError *error) {
OWSLogWarn(@"failed to download attachment for message: %lu with error: %@",
OWSLogWarn(@"Failed to download attachment for message: %lu with error: %@.",
(unsigned long)incomingMessage.timestamp,
error);
}];
@ -1995,36 +2013,6 @@ NS_ASSUME_NONNULL_BEGIN
}
}
# pragma mark - Loki Session
- (void)handleNewSessionAdopted:(NSNotification *)notification {
NSString *hexEncodedPublicKey = notification.userInfo[kNSNotificationKey_ContactPubKey];
if (hexEncodedPublicKey.length == 0) { return; }
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
TSContactThread *thread = [TSContactThread getThreadWithContactId:hexEncodedPublicKey transaction:transaction];
if (thread == nil) {
NSLog(@"[Loki] A new session was adopted but the thread couldn't be found for: %@.", hexEncodedPublicKey);
return;
}
// If the current user initiated the reset then send back an empty message to acknowledge the completion of the session reset
if (thread.sessionResetState == TSContactThreadSessionResetStateInitiated) {
LKEphemeralMessage *emptyMessage = [[LKEphemeralMessage alloc] initInThread:thread];
[self.messageSenderJobQueue addMessage:emptyMessage transaction:transaction];
}
// Show session reset done message
[[[TSInfoMessage alloc] initWithTimestamp:NSDate.ows_millisecondTimeStamp
inThread:thread
messageType:TSInfoMessageTypeLokiSessionResetDone] saveWithTransaction:transaction];
// Clear the session reset state
thread.sessionResetState = TSContactThreadSessionResetStateNone;
[thread saveWithTransaction:transaction];
}];
}
@end
NS_ASSUME_NONNULL_END

@ -323,20 +323,12 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
{
OWSAssertDebug(AppReadiness.isAppReady);
// Don't decrypt messages in app extensions.
if (!CurrentAppContext().isMainApp) {
return;
}
if (!self.tsAccountManager.isRegisteredAndReady) {
return;
}
if (!CurrentAppContext().isMainApp) { return; }
if (!self.tsAccountManager.isRegisteredAndReady) { return; }
dispatch_async(self.serialQueue, ^{
if (self.isDrainingQueue) {
return;
}
if (self.isDrainingQueue) { return; }
self.isDrainingQueue = YES;
[self drainQueueWorkStep];
});
}
@ -346,6 +338,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
AssertOnDispatchQueue(self.serialQueue);
OWSMessageDecryptJob *_Nullable job = [self.finder nextJob];
if (!job) {
self.isDrainingQueue = NO;
OWSLogVerbose(@"Queue is drained.");
@ -369,8 +362,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
- (BOOL)wasReceivedByUD:(SSKProtoEnvelope *)envelope
{
return (
envelope.type == SSKProtoEnvelopeTypeUnidentifiedSender && (!envelope.hasSource || envelope.source.length < 1));
return (envelope.type == SSKProtoEnvelopeTypeUnidentifiedSender && (!envelope.hasSource || envelope.source.length < 1));
}
- (void)processJob:(OWSMessageDecryptJob *)job completion:(void (^)(BOOL))completion
@ -379,9 +371,9 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
OWSAssertDebug(job);
SSKProtoEnvelope *_Nullable envelope = job.envelopeProto;
if (!envelope) {
OWSFailDebug(@"Could not parse proto.");
// TODO: Add analytics.
OWSFailDebug(@"Couldn't parse proto.");
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
TSErrorMessage *errorMessage = [TSErrorMessage corruptedMessageInUnknownThread];
@ -392,25 +384,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
dispatch_async(self.serialQueue, ^{
completion(NO);
});
return;
}
// Loki: Don't process any messages from ourself
ECKeyPair *_Nullable keyPair = OWSIdentityManager.sharedManager.identityKeyPair;
if (keyPair && [envelope.source isEqualToString:keyPair.hexEncodedPublicKey]) {
dispatch_async(self.serialQueue, ^{
completion(YES);
});
return;
}
// Loki: Ignore any friend requests that we got before restoration
uint64_t restorationTime = [NSNumber numberWithDouble:[OWSPrimaryStorage.sharedManager getRestorationTime]].unsignedLongLongValue;
if (envelope.type == SSKProtoEnvelopeTypeFriendRequest && envelope.timestamp < restorationTime * 1000) {
[LKLogger print:@"[Loki] Ignoring friend request received before restoration."];
dispatch_async(self.serialQueue, ^{
completion(YES);
});
return;
}
@ -423,6 +397,15 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
successBlock:^(OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
OWSAssertDebug(transaction);
// Loki: Don't process any messages from ourself
ECKeyPair *_Nullable keyPair = OWSIdentityManager.sharedManager.identityKeyPair;
if (keyPair && [result.source isEqualToString:keyPair.hexEncodedPublicKey]) {
dispatch_async(self.serialQueue, ^{
completion(YES);
});
return;
}
// We persist the decrypted envelope data in the same transaction within which
// it was decrypted to prevent data loss. If the new job isn't persisted,
// the session state side effects of its decryption are also rolled back.
@ -472,8 +455,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
// For coherency we use the same dbConnection to persist and read the unprocessed envelopes
YapDatabaseConnection *dbConnection = [primaryStorage newDatabaseConnection];
OWSMessageDecryptJobFinder *finder = [[OWSMessageDecryptJobFinder alloc] initWithDBConnection:dbConnection];
OWSMessageDecryptQueue *processingQueue =
[[OWSMessageDecryptQueue alloc] initWithDBConnection:dbConnection finder:finder];
OWSMessageDecryptQueue *processingQueue = [[OWSMessageDecryptQueue alloc] initWithDBConnection:dbConnection finder:finder];
_processingQueue = processingQueue;
@ -503,7 +485,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
- (void)handleReceivedEnvelopeData:(NSData *)envelopeData
{
if (envelopeData.length < 1) {
OWSFailDebug(@"Empty envelope.");
OWSFailDebug(@"Received an empty envelope.");
return;
}
@ -511,7 +493,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
NSUInteger kMaxEnvelopeByteCount = 250 * 1024;
if (envelopeData.length > kMaxEnvelopeByteCount) {
OWSProdError([OWSAnalyticsEvents messageReceiverErrorOversizeMessage]);
OWSFailDebug(@"Oversize message.");
OWSFailDebug(@"Received an oversized message.");
return;
}
@ -520,7 +502,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
NSUInteger kLargeEnvelopeWarningByteCount = 25 * 1024;
if (envelopeData.length > kLargeEnvelopeWarningByteCount) {
OWSProdError([OWSAnalyticsEvents messageReceiverErrorLargeMessage]);
OWSFailDebug(@"Unexpectedly large message.");
OWSFailDebug(@"Received an unexpectedly large message.");
}
[self.processingQueue enqueueEnvelopeData:envelopeData];

@ -186,9 +186,7 @@ void AssertIsOnSendingQueue()
- (nullable NSError *)checkForPreconditionError
{
__block NSError *_Nullable error = [super checkForPreconditionError];
if (error) {
return error;
}
if (error) { return error; }
// Sanity check preconditions
if (self.message.hasAttachments) {
@ -214,9 +212,8 @@ void AssertIsOnSendingQueue()
{
// If the message has been deleted, abort send.
if (self.message.shouldBeSaved && ![TSOutgoingMessage fetchObjectWithUniqueID:self.message.uniqueId]) {
OWSLogInfo(@"aborting message send; message deleted.");
NSError *error = OWSErrorWithCodeDescription(
OWSErrorCodeMessageDeletedBeforeSent, @"Message was deleted before it could be sent.");
OWSLogInfo(@"Aborting message send; message deleted.");
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageDeletedBeforeSent, @"Message was deleted before it could be sent.");
error.isFatal = YES;
[self reportError:error];
return;
@ -233,16 +230,12 @@ void AssertIsOnSendingQueue()
- (void)didSucceed
{
if (self.message.messageState != TSOutgoingMessageStateSent) {
// OWSFailDebug(@"unexpected message status: %@", self.message.statusDescription);
}
self.successHandler();
}
- (void)didFailWithError:(NSError *)error
{
OWSLogError(@"failed with error: %@", error);
OWSLogError(@"Message failed to send due to error: %@.", error);
self.failureHandler(error);
}
@ -370,10 +363,10 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
//
// That's key - we don't want to send any messages in response
// to an incoming message until processing of that batch of messages
// is complete. For example, we wouldn't want to auto-reply to a
// is complete. For example, we wouldn't want to auto-reply to a
// group info request before that group info request's batch was
// finished processing. Otherwise, we might receive a delivery
// notice for a group update we hadn't yet saved to the db.
// finished processing. Otherwise, we might receive a delivery
// notice for a group update we hadn't yet saved to the database.
//
// So we're using YDB behavior to ensure this invariant, which is a bit
// unorthodox.
@ -634,7 +627,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
successHandlerParam();
}
failure:^(NSError *error) {
OWSLogError(@"Error sending sync message for message: %@ timestamp: %llu",
OWSLogError(@"Error sending sync message for message: %@ timestamp: %llu.",
message.class,
message.timestamp);
@ -650,13 +643,12 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
failureHandlerParam(error);
}
failure:^(NSError *syncError) {
OWSLogError(@"Error sending sync message for message: %@ timestamp: %llu, %@",
OWSLogError(@"Error sending sync message for message: %@ timestamp: %llu, %@.",
message.class,
message.timestamp,
syncError);
// Discard the "sync message" error in favor of the
// original error.
// Discard the sync message error in favor of the original error
failureHandlerParam(error);
}];
});
@ -1046,7 +1038,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
return messageSend.success();
}
OWSLogInfo(@"attempting to send message: %@, timestamp: %llu, recipient: %@",
OWSLogInfo(@"Attempting to send message: %@, timestamp: %llu, recipient: %@.",
message.class,
message.timestamp,
recipient.uniqueId);
@ -1062,13 +1054,13 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// re-enable message sending.
[TSPreKeyManager
rotateSignedPreKeyWithSuccess:^{
OWSLogInfo(@"New prekeys registered with server.");
OWSLogInfo(@"New pre keys registered with server.");
NSError *error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError();
[error setIsRetryable:YES];
return messageSend.failure(error);
}
failure:^(NSError *error) {
OWSLogWarn(@"Failed to update prekeys with the server: %@", error);
OWSLogWarn(@"Failed to update pre keys with the server due to error: %@.", error);
return messageSend.failure(error);
}];
}
@ -1191,16 +1183,10 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// Loki: TSFriendRequestMessageType represents a Loki friend request
NSArray *validMessageTypes = @[ @(TSEncryptedWhisperMessageType), @(TSPreKeyWhisperMessageType), @(TSFriendRequestMessageType) ];
hasValidMessageType = [validMessageTypes containsObject:messageType];
/* Loki: Original code
* ========
hasValidMessageType = ([messageType isEqualToNumber:@(TSEncryptedWhisperMessageType)] || [messageType isEqualToNumber:@(TSPreKeyWhisperMessageType)]);
* ========
*/
}
if (!hasValidMessageType) {
OWSFailDebug(@"Invalid message type: %@", messageType);
OWSFailDebug(@"Invalid message type: %@.", messageType);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:NO];
return messageSend.failure(error);
@ -1229,7 +1215,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}
void (^failedMessageSend)(NSError *error) = ^(NSError *error) {
// Handle the error
NSUInteger statusCode = 0;
NSData *_Nullable responseData = nil;
if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) {
@ -1240,9 +1225,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
} else {
OWSFailDebug(@"Missing underlying error: %@.", error);
}
} else {
// TODO: Re-enable?
// OWSFailDebug(@"Unexpected error: %@.", error);
}
[self messageSendDidFail:messageSend deviceMessages:deviceMessages statusCode:statusCode error:error responseData:responseData];
};
@ -1290,7 +1272,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}];
[self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:messageSend.isUDSend wasSentByWebsocket:false];
})
.catchOn(OWSDispatch.sendingQueue, ^(NSError *error) { // The snode is unreachable
.catchOn(OWSDispatch.sendingQueue, ^(NSError *error) {
failedMessageSend(error);
}) retainUntilComplete];
} else {
@ -1346,7 +1328,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// Handle the error
failedMessageSend(error);
};
// Send the message using the Loki API
// Send the message
[[LKAPI sendSignalMessage:signalMessage onP2PSuccess:onP2PSuccess]
.thenOn(OWSDispatch.sendingQueue, ^(id result) {
NSSet<AnyPromise *> *promises = (NSSet<AnyPromise *> *)result;
@ -1486,7 +1468,12 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
switch (statusCode) {
case 0: { // Loki
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
NSError *error;
if ([responseError isKindOfClass:LokiAPIError.class] || [responseError isKindOfClass:LokiDotNetAPIError.class] || [responseError isKindOfClass:DiffieHellmanError.class]) {
error = responseError;
} else {
error = OWSErrorMakeFailedToSendOutgoingMessageError();
}
[error setIsRetryable:NO];
return messageSend.failure(error);
}
@ -1717,12 +1704,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
NSData *_Nullable plainText = [messageSend.message buildPlainTextData:recipient];
if (!plainText) {
OWSRaiseException(InvalidMessageException, @"Failed to build message proto");
OWSRaiseException(InvalidMessageException, @"Failed to build message proto.");
}
OWSLogDebug(
@"built message: %@ plainTextData.length: %lu", [messageSend.message class], (unsigned long)plainText.length);
OWSLogDebug(@"Built message: %@ plainTextData.length: %lu", [messageSend.message class], (unsigned long)plainText.length);
OWSLogVerbose(@"building device messages for: %@ %@ (isLocalNumber: %d, isUDSend: %d)",
OWSLogVerbose(@"Building device messages for: %@ %@ (isLocalNumber: %d, isUDSend: %d).",
recipient.recipientId,
recipient.devices,
messageSend.isLocalNumber,
@ -1736,7 +1722,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// This may involve blocking network requests, so we do it _before_
// we open a transaction.
// Loki: Both for friend request messages and device link messages we don't require a session
// Loki: We don't require a session for friend requests, session requests and device link requests
BOOL isFriendRequest = [messageSend.message isKindOfClass:LKFriendRequestMessage.class];
BOOL isSessionRequest = [messageSend.message isKindOfClass:LKSessionRequestMessage.class];
BOOL isDeviceLinkMessage = [messageSend.message isKindOfClass:LKDeviceLinkMessage.class];
@ -1759,14 +1745,14 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}];
if (encryptionException) {
OWSLogInfo(@"Exception during encryption: %@", encryptionException);
OWSLogInfo(@"Exception during encryption: %@.", encryptionException);
@throw encryptionException;
}
if (messageDict) {
[messagesArray addObject:messageDict];
} else {
OWSRaiseException(InvalidMessageException, @"Failed to encrypt message");
OWSRaiseException(InvalidMessageException, @"Failed to encrypt message.");
}
} @catch (NSException *exception) {
if ([exception.name isEqualToString:OWSMessageSenderInvalidDeviceException]) {
@ -1938,7 +1924,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
NSString *recipientId = recipient.recipientId;
TSOutgoingMessage *message = messageSend.message;
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientId:recipientId identityKeyStore:self.identityManager];
ECKeyPair *identityKeyPair = self.identityManager.identityKeyPair;
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientId:recipientId privateKey:identityKeyPair.privateKey];
// This will return nil if encryption failed
NSData *_Nullable serializedMessage = [cipher encryptWithMessage:[plainText paddedMessageBody]];
@ -1983,16 +1970,16 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
OWSPrimaryStorage *storage = self.primaryStorage;
TSOutgoingMessage *message = messageSend.message;
// Loki: Both for friend request messages and device link messages we use fallback encryption as we don't necessarily have a session yet
// Loki: Use fallback encryption for friend requests, session requests and device link requests
BOOL isFriendRequest = [messageSend.message isKindOfClass:LKFriendRequestMessage.class];
BOOL isSessionRequest = [messageSend.message isKindOfClass:LKSessionRequestMessage.class];
BOOL isDeviceLinkMessage = [messageSend.message isKindOfClass:LKDeviceLinkMessage.class] && ((LKDeviceLinkMessage *)messageSend.message).kind == LKDeviceLinkMessageKindRequest;
// This may throw an exception.
// This may throw an exception
if (!isFriendRequest && !isSessionRequest && !isDeviceLinkMessage && ![storage containsSession:recipientID deviceId:@(OWSDevicePrimaryDeviceId).intValue protocolContext:transaction]) {
NSString *missingSessionException = @"missingSessionException";
OWSRaiseException(missingSessionException,
@"Unexpectedly missing session for recipient: %@, device: %@",
@"Unexpectedly missing session for recipient: %@, device: %@.",
recipientID,
@(OWSDevicePrimaryDeviceId));
}
@ -2025,14 +2012,15 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
protocolContext:transaction
isFriendRequest:isFriendRequest || isDeviceLinkMessage
error:&error];
SCKRaiseIfExceptionWrapperError(error);
if (!serializedMessage || error) {
OWSFailDebug(@"error while UD encrypting message: %@", error);
OWSFailDebug(@"Error while UD encrypting message: %@.", error);
return nil;
}
messageType = TSUnidentifiedSenderMessageType;
} else {
// This may throw an exception.
// This may throw an exception
id<CipherMessage> encryptedMessage =
[cipher throws_encryptMessage:[plainText paddedMessageBody] protocolContext:transaction];
serializedMessage = encryptedMessage.serialized;
@ -2167,7 +2155,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
[attachmentIds addObject:attachment.uniqueId];
} else {
OWSFailDebug(@"unexpected avatarAttachment: %@", attachment);
OWSFailDebug(@"Unexpected avatarAttachment: %@.", attachment);
}
}
@ -2177,7 +2165,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
[attachmentIds addObject:attachment.uniqueId];
} else {
OWSFailDebug(@"unexpected attachment: %@", attachment);
OWSFailDebug(@"Unexpected attachment: %@.", attachment);
}
}
@ -2205,6 +2193,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
sourceFilename:attachmentInfo.sourceFilename
caption:attachmentInfo.caption
albumMessageId:attachmentInfo.albumMessageId];
if (outgoingMessage.isVoiceMessage) {
attachmentStream.attachmentType = TSAttachmentTypeVoiceMessage;
}

@ -425,14 +425,14 @@ 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
// Loki: Generate a sender certifate locally
let sender = OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey
let certificate = SMKSenderCertificate(senderDeviceId: OWSDevicePrimaryDeviceId, senderRecipientId: sender!)
let certificateData = try certificate.serialized()
guard self.isValidCertificate(certificate) else {
throw OWSUDError.invalidData(description: "Invalid sender certificate returned by server")
let certificateAsData = try certificate.serialized()
guard isValidCertificate(certificate) else {
throw OWSUDError.invalidData(description: "Invalid sender certificate.")
}
seal.fulfill((certificateData: certificateData, certificate: certificate))
seal.fulfill((certificateData: certificateAsData, certificate: certificate))
}
}

@ -34,8 +34,6 @@ public class MessageSenderJobQueue: NSObject, JobQueue {
}
}
// MARK:
@objc(addMessage:transaction:)
public func add(message: TSOutgoingMessage, transaction: YapDatabaseReadWriteTransaction) {
self.add(message: message, removeMessageAfterSending: false, transaction: transaction)
@ -71,7 +69,7 @@ public class MessageSenderJobQueue: NSObject, JobQueue {
do {
jobRecord = try SSKMessageSenderJobRecord(message: message, removeMessageAfterSending: false, label: self.jobRecordLabel)
} catch {
owsFailDebug("failed to build job: \(error)")
owsFailDebug("Failed to build job due to error: \(error).")
return
}
self.add(jobRecord: jobRecord, transaction: transaction)
@ -121,7 +119,7 @@ public class MessageSenderJobQueue: NSObject, JobQueue {
message = fetchedMessage
} else {
assert(jobRecord.messageId != nil)
throw JobError.obsolete(description: "message no longer exists")
throw JobError.obsolete(description: "Message no longer exists.")
}
return MessageSenderOperation(message: message, jobRecord: jobRecord)
@ -205,8 +203,6 @@ public class MessageSenderOperation: OWSOperation, DurableOperation {
}
override public func didReportError(_ error: Error) {
Logger.debug("remainingRetries: \(self.remainingRetries)")
self.dbConnection.readWrite { transaction in
self.durableOperationDelegate?.durableOperation(self, didReportError: error, transaction: transaction)
}

@ -38,7 +38,7 @@ class SAEFailedViewController: UIViewController {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
target: self,
action: #selector(cancelPressed))
self.navigationItem.title = "Signal"
self.navigationItem.title = "Session"
self.view.backgroundColor = UIColor.ows_signalBrandBlue

@ -59,7 +59,7 @@ class SAELoadViewController: UIViewController {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
target: self,
action: #selector(cancelPressed))
self.navigationItem.title = "Signal"
self.navigationItem.title = "Session"
self.view.backgroundColor = UIColor.ows_signalBrandBlue

Loading…
Cancel
Save