Merge branch 'dev' into light-mode

pull/107/head
Niels Andriesse 5 years ago
commit b931ec9394

@ -1 +1 @@
Subproject commit 6fae72d48c06c35c8219ebfc58116450c473b8f1
Subproject commit 67dbced37481e0011a3df1397ed57711384a4957

@ -1,18 +1,28 @@
# Session iOS
Session is a fully end-to-end encrypted messenger that aims to remove any chance of metadata collection by routing all messages through an onion routing network. Storage of offline messages is handled by Service Nodes, which are a distributed set of nodes run by the community.
[Download Session on the App Store](https://getsession.org/iphone)
## Contributing Code
Code should be contributed via pull request to the master branch, all submitted code will be reviewed.
## Summary
## Contributing Ideas
Have something you want to say about Session? Create an issue with the discussion tag and the community can weigh in.
Session integrates directly with [Loki Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users' IP addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper).
## Cryptography Notice
![iOSSession](https://i.imgur.com/vM62EJm.png)
This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software.
BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted.
See <http://www.wassenaar.org/> for more information.
The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms.
The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code.
## Want to Contribute? Found a Bug or Have a feature request?
Please search for any [existing issues](https://github.com/loki-project/session-ios/issues) that describe your bugs in order to avoid duplicate submissions. Submissions can be made by making a pull request to our development branch. If you don't know where to start contributing , try reading the Github issues page for ideas.
## Build instruction
Build instructions can be found in [BUILDING.md](BUILDING.md).
## License
Copyright 2011 Whisper Systems
Copyright 2013-2017 Open Whisper Systems
Copyright 2019-2020 The Loki Project
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html

@ -4258,7 +4258,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 46;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -4272,7 +4272,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.4;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -4320,7 +4320,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 46;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -4339,7 +4339,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.4;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -4374,7 +4374,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 46;
DEBUG_INFORMATION_FORMAT = dwarf;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
@ -4393,7 +4393,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.4;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@ -4443,7 +4443,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 46;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
@ -4467,7 +4467,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.4;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@ -4652,7 +4652,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 = 46;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -4687,7 +4687,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.4;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;
@ -4719,7 +4719,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 = 46;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -4754,7 +4754,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.4;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

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

@ -12,8 +12,9 @@ final class LokiPushNotificationManager : NSObject {
@objc(registerWithToken:)
func register(with token: Data) {
let hexEncodedToken = token.map { String(format: "%02.2hhx", $0) }.joined()
let oldToken = UserDefaults.standard.string(forKey: "deviceToken")
let lastUploadTime = UserDefaults.standard.double(forKey: "lastDeviceTokenUploadTime")
let userDefaults = UserDefaults.standard
let oldToken = userDefaults[.deviceToken]
let lastUploadTime = userDefaults[.lastDeviceTokenUpload]
let now = Date().timeIntervalSince1970
if hexEncodedToken == oldToken && now - lastUploadTime < 2 * 24 * 60 * 60 {
print("[Loki] Device token hasn't changed; no need to upload.")
@ -29,8 +30,8 @@ final class LokiPushNotificationManager : NSObject {
guard json["code"] as? Int != 0 else {
return print("[Loki] An error occured during device token registration: \(json["message"] as? String ?? "nil").")
}
UserDefaults.standard.set(hexEncodedToken, forKey: "deviceToken")
UserDefaults.standard.set(now, forKey: "lastDeviceTokenUploadTime")
userDefaults[.deviceToken] = hexEncodedToken
userDefaults[.lastDeviceTokenUpload] = now
}, failure: { _, error in
print("[Loki] Couldn't register device token.")
})

@ -92,7 +92,7 @@ final class DeviceNameModal : Modal {
@objc private func changeName() {
let name = nameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if !name.isEmpty {
UserDefaults.standard.set(name, forKey: "\(device.hexEncodedPublicKey)_display_name")
UserDefaults.standard[.slaveDeviceName(device.hexEncodedPublicKey)] = name
delegate?.handleDeviceNameChanged(to: name, for: device)
} else {
let alert = UIAlertController(title: NSLocalizedString("Error", comment: ""), message: NSLocalizedString("Please pick a name", comment: ""), preferredStyle: .alert)

@ -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: { _ in }) // 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)
}

@ -89,8 +89,9 @@ final class HomeVC : UIViewController, UITableViewDataSource, UITableViewDelegat
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
navigationItem.titleView = titleLabel
// Set up seed reminder view if needed
let hasViewedSeed = UserDefaults.standard.bool(forKey: "hasViewedSeed")
let isMasterDevice = (UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") == nil)
let userDefaults = UserDefaults.standard
let hasViewedSeed = userDefaults[.hasViewedSeed]
let isMasterDevice = userDefaults.isMasterDevice
if !hasViewedSeed && isMasterDevice {
view.addSubview(seedReminderView)
seedReminderView.pin(.leading, to: .leading, of: view)
@ -146,13 +147,14 @@ final class HomeVC : UIViewController, UITableViewDataSource, UITableViewDelegat
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
isViewVisible = true
// let hasSeenOpenGroupSuggestionSheet = UserDefaults.standard.bool(forKey: "hasSeenOpenGroupSuggestionSheet")
// let hasSeenOpenGroupSuggestionSheet = UserDefaults.standard[.hasSeenOpenGroupSuggestionSheet]
// if !hasSeenOpenGroupSuggestionSheet {
// let openGroupSuggestionSheet = OpenGroupSuggestionSheet()
// openGroupSuggestionSheet.modalPresentationStyle = .overFullScreen
// openGroupSuggestionSheet.modalTransitionStyle = .crossDissolve
// present(openGroupSuggestionSheet, animated: true, completion: nil)
// }
UserDefaults.standard[.hasLaunchedOnce] = true
}
override func viewWillDisappear(_ animated: Bool) {

@ -100,7 +100,7 @@ final class LandingVC : UIViewController, LinkDeviceVCDelegate, DeviceLinkingMod
mainStackView.pin(to: view)
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
// Show device unlinked alert if needed
if UserDefaults.standard.bool(forKey: "wasUnlinked") {
if UserDefaults.standard[.wasUnlinked] {
let alert = UIAlertController(title: NSLocalizedString("Device Unlinked", comment: ""), message: NSLocalizedString("Your device was unlinked successfully", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
present(alert, animated: true, completion: nil)
@ -190,8 +190,7 @@ final class LandingVC : UIViewController, LinkDeviceVCDelegate, DeviceLinkingMod
}
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) {
let userDefaults = UserDefaults.standard
userDefaults.set(deviceLink.master.hexEncodedPublicKey, forKey: "masterDeviceHexEncodedPublicKey")
UserDefaults.standard[.masterHexEncodedPublicKey] = deviceLink.master.hexEncodedPublicKey
fakeChatViewContentOffset = fakeChatView.contentOffset
DispatchQueue.main.async {
self.fakeChatView.contentOffset = self.fakeChatViewContentOffset

@ -180,15 +180,15 @@ final class NewClosedGroupVC : UIViewController, UITableViewDataSource, UITableV
DispatchQueue.main.async {
SSKEnvironment.shared.messageSender.send(message, success: {
DispatchQueue.main.async {
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
self?.presentingViewController?.dismiss(animated: true, completion: nil)
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
}
}, failure: { error in
let message = TSErrorMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, failedMessageType: .groupCreationFailed)
message.save()
DispatchQueue.main.async {
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
self?.presentingViewController?.dismiss(animated: true, completion: nil)
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
}
})
}

@ -155,7 +155,7 @@ private final class EnterPublicKeyVC : UIViewController {
weak var newPrivateChatVC: NewPrivateChatVC!
private lazy var userHexEncodedPublicKey: String = {
if let masterHexEncodedPublicKey = UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") {
if let masterHexEncodedPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] {
return masterHexEncodedPublicKey
} else {
return getUserHexEncodedPublicKey()

@ -71,7 +71,7 @@ final class OpenGroupSuggestionSheet : Sheet {
}
override func close() {
UserDefaults.standard.set(true, forKey: "hasSeenOpenGroupSuggestionSheet")
UserDefaults.standard[.hasSeenOpenGroupSuggestionSheet] = true
super.close()
}
}

@ -152,7 +152,7 @@ private final class ViewMyQRCodeVC : UIViewController {
private var bottomConstraint: NSLayoutConstraint!
private lazy var userHexEncodedPublicKey: String = {
if let masterHexEncodedPublicKey = UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") {
if let masterHexEncodedPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] {
return masterHexEncodedPublicKey
} else {
return getUserHexEncodedPublicKey()

@ -187,7 +187,7 @@ final class RegisterVC : UIViewController {
databaseConnection.setObject(keyPair!, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair!.hexEncodedPublicKey
OWSPrimaryStorage.shared().setRestorationTime(0)
UserDefaults.standard.set(false, forKey: "hasViewedSeed")
UserDefaults.standard[.hasViewedSeed] = false
let displayNameVC = DisplayNameVC()
navigationController!.pushViewController(displayNameVC, animated: true)
}

@ -178,7 +178,7 @@ final class RestoreVC : UIViewController {
databaseConnection.setObject(keyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey
OWSPrimaryStorage.shared().setRestorationTime(Date().timeIntervalSince1970)
UserDefaults.standard.set(true, forKey: "hasViewedSeed")
UserDefaults.standard[.hasViewedSeed] = true
mnemonicTextField.resignFirstResponder()
Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in
let displayNameVC = DisplayNameVC()

@ -62,7 +62,7 @@ final class SeedModal : Modal {
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.largeSpacing)
// Mark seed as viewed
UserDefaults.standard.set(true, forKey: "hasViewedSeed")
UserDefaults.standard[.hasViewedSeed] = true
NotificationCenter.default.post(name: .seedViewed, object: nil)
}

@ -182,7 +182,7 @@ final class SeedVC : UIViewController {
self.seedReminderView.subtitle = NSLocalizedString("Make sure to store your recovery phrase in a safe place", comment: "")
}, completion: nil)
seedReminderView.setProgress(1, animated: true)
UserDefaults.standard.set(true, forKey: "hasViewedSeed")
UserDefaults.standard[.hasViewedSeed] = true
NotificationCenter.default.post(name: .seedViewed, object: nil)
}

@ -5,7 +5,7 @@ final class SettingsVC : UIViewController, AvatarViewHelperDelegate {
private var isEditingDisplayName = false { didSet { handleIsEditingDisplayNameChanged() } }
private lazy var userHexEncodedPublicKey: String = {
if let masterHexEncodedPublicKey = UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") {
if let masterHexEncodedPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] {
return masterHexEncodedPublicKey
} else {
return getUserHexEncodedPublicKey()
@ -180,7 +180,7 @@ final class SettingsVC : UIViewController, AvatarViewHelperDelegate {
getSeparator(),
getSettingButton(withTitle: NSLocalizedString("Notifications", comment: ""), color: Colors.text, action: #selector(showNotificationSettings))
]
let isMasterDevice = (UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") == nil)
let isMasterDevice = UserDefaults.standard.isMasterDevice
if isMasterDevice {
result.append(getSeparator())
result.append(getSettingButton(withTitle: NSLocalizedString("Devices", comment: ""), color: Colors.text, action: #selector(showLinkedDevices)))
@ -288,7 +288,7 @@ final class SettingsVC : UIViewController, AvatarViewHelperDelegate {
self?.present(alert, animated: true, completion: nil)
}
}
})
}, requiresSync: true)
}
}

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

@ -428,7 +428,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
@"profile update fails.")];
}];
});
}];
} requiresSync:NO];
}];
}

@ -177,7 +177,7 @@ public class OnboardingProfileViewController: OnboardingBaseViewController {
comment: "Error message shown when a profile update fails."))
})
}
})
}, requiresSync: false)
}
}

@ -339,7 +339,8 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
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 (^)(NSError *))failureBlock;
failure:(void (^)(NSError *))failureBlock
requiresSync:(BOOL)requiresSync;
- (BOOL)isProfileNameTooLong:(nullable NSString *)profileName;

@ -230,6 +230,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
avatarImage:(nullable UIImage *)avatarImage
success:(void (^)(void))successBlockParameter
failure:(void (^)(NSError *))failureBlockParameter
requiresSync:(BOOL)requiresSync
{
OWSAssertDebug(successBlockParameter);
OWSAssertDebug(failureBlockParameter);
@ -244,7 +245,9 @@ 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(error);
@ -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();

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

@ -39,7 +39,7 @@ public class RefreshPreKeysOperation: OWSOperation {
// Loki: Doing this on the global queue to match Signal
DispatchQueue.global().async {
guard self.primaryStorage.currentSignedPrekeyId() == nil else {
print("[Loki] Using existing signed pre key.")
print("[Loki] Skipping pre key refresh; using existing signed pre key.")
return self.reportSuccess()
}

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

@ -7,12 +7,11 @@ public extension LokiAPI {
// MARK: Settings
private static let minimumSnodeCount = 2
private static let targetSnodeCount = 3
private static let maxRandomSnodePoolSize = 1024
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 func dropIfNeeded(_ target: LokiAPITarget, hexEncodedPublicKey: String) {
let swarm = LokiAPI.swarmCache[hexEncodedPublicKey]
@ -39,7 +38,7 @@ public extension LokiAPI {
"method" : "get_n_service_nodes",
"params" : [
"active_only" : true,
"limit" : 24,
"limit" : maxRandomSnodePoolSize,
"fields" : [
"public_ip" : true,
"storage_port" : true,
@ -49,7 +48,7 @@ public extension LokiAPI {
]
])
print("[Loki] Invoking get_n_service_nodes on \(target).")
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { intermediate in
return TSNetworkManager.shared().perform(request).map(on: DispatchQueue.global()) { 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 LokiAPIError.randomSnodePoolUpdatingFailed }
randomSnodePool = try Set(rawTargets.flatMap { rawTarget in
@ -59,13 +58,15 @@ public extension LokiAPI {
}
return LokiAPITarget(address: "https://\(address)", port: UInt16(port), publicKeySet: LokiAPITarget.KeySet(idKey: idKey, encryptionKey: encryptionKey))
})
// randomElement() uses the system's default random generator, which is cryptographically secure
return randomSnodePool.randomElement()!
}.recover(on: DispatchQueue.global()) { error -> Promise<LokiAPITarget> in
}.recover { error -> Promise<LokiAPITarget> in
print("[Loki] Failed to contact seed node at: \(target).")
throw error
}.retryingIfNeeded(maxRetryCount: 16) // The seed nodes have historically been unreliable
} else {
return Promise<LokiAPITarget> { seal in
// randomElement() uses the system's default random generator, which is cryptographically secure
seal.fulfill(randomSnodePool.randomElement()!)
}
}
@ -76,6 +77,7 @@ public extension LokiAPI {
return Promise<[LokiAPITarget]> { $0.fulfill(cachedSwarm) }
} else {
let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ]
// All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works
return getRandomSnode().then(on: DispatchQueue.global()) { invoke(.getSwarm, on: $0, associatedWith: hexEncodedPublicKey, parameters: parameters) }.map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 }
}
}

@ -2,9 +2,26 @@ import PromiseKit
@objc(LKAPI)
public final class LokiAPI : NSObject {
/// Only ever modified from the message processing queue (`OWSBatchMessageProcessor.processingQueue`).
private static var syncMessageTimestamps: [String:Set<UInt64>] = [:]
public static var lastDeviceLinkUpdate: [String:Date] = [:] // Hex encoded public key to date
@objc public static var userHexEncodedPublicKeyCache: [String:Set<String>] = [:] // Thread ID to set of user hex encoded public keys
public static var _lastDeviceLinkUpdate: [String:Date] = [:]
/// A mapping from hex encoded public key to date updated.
public static var lastDeviceLinkUpdate: [String:Date] {
get { stateQueue.sync { _lastDeviceLinkUpdate } }
set { stateQueue.sync { _lastDeviceLinkUpdate = newValue } }
}
private static var _userHexEncodedPublicKeyCache: [String:Set<String>] = [:]
/// A mapping from thread ID to set of user hex encoded public keys.
@objc public static var userHexEncodedPublicKeyCache: [String:Set<String>] {
get { stateQueue.sync { _userHexEncodedPublicKeyCache } }
set { stateQueue.sync { _userHexEncodedPublicKeyCache = newValue } }
}
private static let stateQueue = DispatchQueue(label: "stateQueue")
/// All service node related errors must be handled on this queue to avoid race conditions maintaining e.g. failure counts.
public static let errorHandlingQueue = DispatchQueue(label: "errorHandlingQueue")
// MARK: Convenience
@ -12,12 +29,10 @@ public final class LokiAPI : NSObject {
internal static let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
// MARK: Settings
private static let version = "v1"
private static let apiVersion = "v1"
private static let maxRetryCount: UInt = 8
private static let defaultTimeout: TimeInterval = 20
private static let longPollingTimeout: TimeInterval = 40
private static let receivedMessageHashValuesKey = "receivedMessageHashValuesKey"
private static let receivedMessageHashValuesCollection = "receivedMessageHashValuesCollection"
private static var userIDScanLimit: UInt = 4096
internal static var powDifficulty: UInt = 4
public static let defaultMessageTTL: UInt64 = 24 * 60 * 60 * 1000
@ -69,6 +84,7 @@ public final class LokiAPI : NSObject {
}
public typealias MessageListPromise = Promise<[SSKProtoEnvelope]>
public typealias RawResponsePromise = Promise<RawResponse>
// MARK: Lifecycle
@ -77,7 +93,7 @@ public final class LokiAPI : NSObject {
// MARK: Internal API
internal static func invoke(_ method: LokiAPITarget.Method, on target: LokiAPITarget, associatedWith hexEncodedPublicKey: String,
parameters: [String:Any], headers: [String:String]? = nil, timeout: TimeInterval? = nil) -> RawResponsePromise {
let url = URL(string: "\(target.address):\(target.port)/storage_rpc/\(version)")!
let url = URL(string: "\(target.address):\(target.port)/storage_rpc/\(apiVersion)")!
let request = TSRequest(url: url, method: "POST", parameters: [ "method" : method.rawValue, "params" : parameters ])
if let headers = headers { request.allHTTPHeaderFields = headers }
request.timeoutInterval = timeout ?? defaultTimeout
@ -113,6 +129,7 @@ public final class LokiAPI : NSObject {
}
public static func getDestinations(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> Promise<[Destination]> {
// All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works
let (promise, seal) = Promise<[Destination]>.pending()
func getDestinations(in transaction: YapDatabaseReadTransaction? = nil) {
func getDestinationsInternal(in transaction: YapDatabaseReadTransaction) {
@ -169,7 +186,7 @@ public final class LokiAPI : NSObject {
}
func sendLokiMessageUsingSwarmAPI() -> Promise<Set<RawResponsePromise>> {
notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp))
return lokiMessage.calculatePoW().then(on: DispatchQueue.global()) { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
return lokiMessage.calculatePoW().then { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
notificationCenter.post(name: .contactingNetwork, object: NSNumber(value: signalMessage.timestamp))
return getTargetSnodes(for: destination).map { swarm in
return Set(swarm.map { target in
@ -193,7 +210,7 @@ public final class LokiAPI : NSObject {
return Promise.value([ target ]).mapValues { sendLokiMessage(lokiMessage, to: $0) }.map { Set($0) }.retryingIfNeeded(maxRetryCount: maxRetryCount).get { _ in
LokiP2PAPI.markOnline(destination)
onP2PSuccess()
}.recover(on: DispatchQueue.global()) { error -> Promise<Set<RawResponsePromise>> in
}.recover { error -> Promise<Set<RawResponsePromise>> in
LokiP2PAPI.markOffline(destination)
if lokiMessage.isPing {
print("[Loki] Failed to ping \(destination); marking contact as offline.")
@ -310,7 +327,10 @@ public final class LokiAPI : NSObject {
storage.setLastMessageHash(forServiceNode: target.address, hash: hashValue, expiresAt: expirationDate, transaction: transaction)
}
}
private static let receivedMessageHashValuesKey = "receivedMessageHashValuesKey"
private static let receivedMessageHashValuesCollection = "receivedMessageHashValuesCollection"
private static func getReceivedMessageHashValues() -> Set<String>? {
var result: Set<String>? = nil
storage.dbReadConnection.read { transaction in
@ -398,7 +418,7 @@ public final class LokiAPI : NSObject {
private extension Promise {
fileprivate func recoveringNetworkErrorsIfNeeded() -> Promise<T> {
return recover(on: DispatchQueue.global()) { error -> Promise<T> in
return recover { error -> Promise<T> in
switch error {
case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError
case LokiHTTPClient.HTTPError.networkError(_, _, let underlyingError): throw underlyingError ?? error

@ -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()!
@ -40,6 +41,17 @@ public class LokiDotNetAPI : NSObject {
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
}
}
}
private static func setAuthToken(for server: String, to newValue: String, in transaction: YapDatabaseReadWriteTransaction? = nil) {
func setAuthTokenInternal(in transaction: YapDatabaseReadWriteTransaction) {
@ -147,29 +159,19 @@ public class LokiDotNetAPI : NSObject {
}
}
if server == LokiFileServerAPI.server {
proceed(with: "loki") // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
DispatchQueue.global().async {
proceed(with: "loki") // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
}
} else {
getAuthToken(for: server).done(on: DispatchQueue.global()) { token in
proceed(with: token)
}.catch(on: DispatchQueue.global()) { error in
}.catch { error in
print("[Loki] Couldn't upload attachment due to error: \(error).")
seal.reject(error)
}
}
}
}
// MARK: Internal API
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
}
}
}
// MARK: Private API
private static func requestNewAuthToken(for server: String) -> Promise<String> {
@ -177,7 +179,8 @@ public class LokiDotNetAPI : NSObject {
let queryParameters = "pubKey=\(userHexEncodedPublicKey)"
let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")!
let request = TSRequest(url: url)
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
// All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map(on: DispatchQueue.global()) { 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 LokiDotNetAPIError.parsingFailed
@ -201,6 +204,7 @@ public class LokiDotNetAPI : NSObject {
let url = URL(string: "\(server)/loki/v1/submit_challenge")!
let parameters = [ "pubKey" : userHexEncodedPublicKey, "token" : token ]
let request = TSRequest(url: url, method: "POST", parameters: parameters)
// All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in token }
}

@ -26,13 +26,14 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
/// 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>, in transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise<Set<DeviceLink>> {
// All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works
let hexEncodedPublicKeysDescription = "[ \(hexEncodedPublicKeys.joined(separator: ", ")) ]"
print("[Loki] Getting device links for: \(hexEncodedPublicKeysDescription).")
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(on: DispatchQueue.global()) { $0.responseObject }.map(on: DispatchQueue.global()) { rawResponse -> Set<DeviceLink> in
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).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 LokiDotNetAPIError.parsingFailed
@ -85,7 +86,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
public static func setDeviceLinks(_ deviceLinks: Set<DeviceLink>) -> Promise<Void> {
print("[Loki] Updating device links.")
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
return getAuthToken(for: server).then { token -> Promise<Void> in
let isMaster = deviceLinks.contains { $0.master.hexEncodedPublicKey == userHexEncodedPublicKey }
let deviceLinksAsJSON = deviceLinks.map { $0.toJSON() }
let value = !deviceLinksAsJSON.isEmpty ? [ "isPrimary" : isMaster ? 1 : 0, "authorisations" : deviceLinksAsJSON ] : nil
@ -94,7 +95,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
let url = URL(string: "\(server)/users/me")!
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in }.retryingIfNeeded(maxRetryCount: 8).recover(on: DispatchQueue.global()) { error in
return LokiFileServerProxy(for: server).perform(request).map { _ in }.retryingIfNeeded(maxRetryCount: 8).recover { error in
print("Couldn't update device links due to error: \(error).")
throw error
}
@ -138,9 +139,7 @@ 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)
}
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" ]

@ -1,4 +1,5 @@
import PromiseKit
import SignalMetadataKit
internal class LokiFileServerProxy : LokiHTTPClient {
private let server: String
@ -43,79 +44,85 @@ internal class LokiFileServerProxy : LokiHTTPClient {
}
internal func performLokiFileServerNSURLRequest(_ request: NSURLRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: LokiFileServerProxy.fileServerPublicKey, privateKey: keyPair.privateKey)
guard let symmetricKey = uncheckedSymmetricKey else { return Promise(error: Error.symmetricKeyGenerationFailed) }
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])
let parametersAsString: String
if let tsRequest = request as? TSRequest {
headers["Content-Type"] = "application/json"
let parametersAsData = try JSONSerialization.data(withJSONObject: tsRequest.parameters, options: [])
parametersAsString = !tsRequest.parameters.isEmpty ? String(bytes: parametersAsData, encoding: .utf8)! : "null"
} else {
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
if let parametersAsInputStream = request.httpBodyStream, let parametersAsData = try? Data(from: parametersAsInputStream) {
parametersAsString = "{ \"fileUpload\" : \"\(String(data: parametersAsData.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
} else {
parametersAsString = "null"
return Promise<LokiAPI.RawResponse> { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] seal in
DispatchQueue.global().async {
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: LokiFileServerProxy.fileServerPublicKey, privateKey: keyPair.privateKey)
guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) }
LokiAPI.getRandomSnode().then(on: DispatchQueue.global()) { proxy -> Promise<Any> in
let url = "\(proxy.address):\(proxy.port)/file_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"
let parametersAsData = try JSONSerialization.data(withJSONObject: tsRequest.parameters, options: [])
parametersAsString = !tsRequest.parameters.isEmpty ? String(bytes: parametersAsData, encoding: .utf8)! : "null"
} else {
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
if let parametersAsInputStream = request.httpBodyStream, let parametersAsData = try? Data(from: parametersAsInputStream) {
parametersAsString = "{ \"fileUpload\" : \"\(String(data: parametersAsData.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
} else {
parametersAsString = "null"
}
}
let proxyRequestParameters: JSON = [
"body" : parametersAsString,
"endpoint": endpoint,
"method" : request.httpMethod,
"headers" : headers
]
let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [])
let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
let base64EncodedPublicKey = Data(hex: keyPair.hexEncodedPublicKey).base64EncodedString() // The file server expects an 05 prefixed public key
let proxyRequestHeaders = [
"X-Loki-File-Server-Target" : "/loki/v1/secure_rpc",
"X-Loki-File-Server-Verb" : "POST",
"X-Loki-File-Server-Headers" : "{ \"X-Loki-File-Server-Ephemeral-Key\" : \"\(base64EncodedPublicKey)\" }",
"Connection" : "close", // TODO: Is this necessary?
"Content-Type" : "application/json"
]
let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
proxyRequest.httpBody = "{ \"cipherText64\" : \"\(ivAndCipherText.base64EncodedString())\" }".data(using: String.Encoding.utf8)!
proxyRequest.timeoutInterval = request.timeoutInterval
var task: URLSessionDataTask!
task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
if let error = error {
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
let nsError: NSError = nmError as NSError
nsError.isRetryable = false
resolver.reject(nsError)
} else {
resolver.fulfill(result)
}
}
task.resume()
return promise
}.map(on: DispatchQueue.global()) { rawResponse in
guard let responseAsData = rawResponse as? Data, let responseAsJSON = try? JSONSerialization.jsonObject(with: responseAsData, options: .allowFragments) as? JSON, let base64EncodedCipherText = responseAsJSON["data"] as? String,
let meta = responseAsJSON["meta"] as? JSON, let statusCode = meta["code"] as? Int, let cipherText = Data(base64Encoded: base64EncodedCipherText) else {
print("[Loki] Received an invalid response.")
throw Error.proxyResponseParsingFailed
}
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 () }
let uncheckedJSON = try? JSONSerialization.jsonObject(with: uncheckedJSONAsData, options: .allowFragments) as? JSON
guard let json = uncheckedJSON else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
return json
}.done { rawResponse in
seal.fulfill(rawResponse)
}.catch { error in
print("[Loki] File server proxy request failed with error: \(error.localizedDescription).")
seal.reject(HTTPError.from(error: error) ?? error)
}
}
let proxyRequestParameters: JSON = [
"body" : parametersAsString,
"endpoint": endpoint,
"method" : request.httpMethod,
"headers" : headers
]
let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [])
let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
let base64EncodedPublicKey = Data(hex: keyPair.hexEncodedPublicKey).base64EncodedString() // The file server expects an 05 prefixed public key
let proxyRequestHeaders = [
"X-Loki-File-Server-Target" : "/loki/v1/secure_rpc",
"X-Loki-File-Server-Verb" : "POST",
"X-Loki-File-Server-Headers" : "{ \"X-Loki-File-Server-Ephemeral-Key\" : \"\(base64EncodedPublicKey)\" }",
"Connection" : "close", // TODO: Is this necessary?
"Content-Type" : "application/json"
]
let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
proxyRequest.httpBody = "{ \"cipherText64\" : \"\(ivAndCipherText.base64EncodedString())\" }".data(using: String.Encoding.utf8)!
proxyRequest.timeoutInterval = request.timeoutInterval
var task: URLSessionDataTask!
task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
if let error = error {
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
let nsError: NSError = nmError as NSError
nsError.isRetryable = false
resolver.reject(nsError)
} else {
resolver.fulfill(result)
}
}
task.resume()
return promise
}.map { rawResponse in
guard let responseAsData = rawResponse as? Data, let responseAsJSON = try? JSONSerialization.jsonObject(with: responseAsData, options: .allowFragments) as? JSON, let base64EncodedCipherText = responseAsJSON["data"] as? String,
let meta = responseAsJSON["meta"] as? JSON, let statusCode = meta["code"] as? Int, let cipherText = Data(base64Encoded: base64EncodedCipherText) else {
print("[Loki] Received an invalid response.")
throw Error.proxyResponseParsingFailed
}
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 () }
let uncheckedJSON = try? JSONSerialization.jsonObject(with: uncheckedJSONAsData, options: .allowFragments) as? JSON
guard let json = uncheckedJSON else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
return json
}.recover { error -> Promise<Any> in
print("[Loki] File server proxy request failed with error: \(error.localizedDescription).")
throw HTTPError.from(error: error) ?? error
}
}
}

@ -42,7 +42,7 @@ public final class LokiLongPoller : NSObject {
// MARK: Private API
private func openConnections() {
guard !hasStopped else { return }
LokiAPI.getSwarm(for: userHexEncodedPublicKey).then(on: DispatchQueue.global()) { [weak self] _ -> Guarantee<[Result<Void>]> in
LokiAPI.getSwarm(for: userHexEncodedPublicKey).then { [weak self] _ -> Guarantee<[Result<Void>]> in
guard let strongSelf = self else { return Guarantee.value([Result<Void>]()) }
strongSelf.usedSnodes.removeAll()
let connections: [Promise<Void>] = (0..<strongSelf.connectionCount).map { _ in
@ -52,7 +52,7 @@ public final class LokiLongPoller : NSObject {
}
strongSelf.connections = Set(connections)
return when(resolved: connections)
}.ensure(on: DispatchQueue.global()) { [weak self] in
}.ensure { [weak self] in
guard let strongSelf = self else { return }
Timer.scheduledTimer(withTimeInterval: strongSelf.retryInterval, repeats: false) { _ in
guard let strongSelf = self else { return }

@ -1,4 +1,5 @@
import PromiseKit
import SignalMetadataKit
internal class LokiSnodeProxy : LokiHTTPClient {
private let target: LokiAPITarget
@ -30,63 +31,69 @@ internal class LokiSnodeProxy : LokiHTTPClient {
// MARK: Proxying
override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
guard let targetHexEncodedPublicKeySet = target.publicKeySet else { return Promise(error: Error.targetPublicKeySetMissing) }
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: Data(hex: targetHexEncodedPublicKeySet.encryptionKey), privateKey: keyPair.privateKey)
guard let symmetricKey = uncheckedSymmetricKey else { return Promise(error: Error.symmetricKeyGenerationFailed) }
let headers = getCanonicalHeaders(for: request)
return LokiAPI.getRandomSnode().then { [target = self.target, keyPair = self.keyPair, httpSession = self.httpSession] proxy -> Promise<Any> in
let url = "\(proxy.address):\(proxy.port)/proxy"
print("[Loki] Proxying request to \(target) through \(proxy).")
let parametersAsData = try JSONSerialization.data(withJSONObject: request.parameters, options: [])
let proxyRequestParameters: JSON = [
"method" : request.httpMethod,
"body" : String(bytes: parametersAsData, encoding: .utf8),
"headers" : headers
]
let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [])
let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
let proxyRequestHeaders = [
"X-Sender-Public-Key" : keyPair.publicKey.toHexString(),
"X-Target-Snode-Key" : targetHexEncodedPublicKeySet.idKey
]
let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
proxyRequest.httpBody = ivAndCipherText
proxyRequest.timeoutInterval = request.timeoutInterval
var task: URLSessionDataTask!
task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
if let error = error {
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
let nsError: NSError = nmError as NSError
nsError.isRetryable = false
resolver.reject(nsError)
} else {
resolver.fulfill(result)
return Promise<LokiAPI.RawResponse> { [target = self.target, keyPair = self.keyPair, httpSession = self.httpSession] seal in
DispatchQueue.global().async {
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: Data(hex: targetHexEncodedPublicKeySet.encryptionKey), privateKey: keyPair.privateKey)
guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) }
LokiAPI.getRandomSnode().then(on: DispatchQueue.global()) { proxy -> Promise<LokiAPI.RawResponse> in
let url = "\(proxy.address):\(proxy.port)/proxy"
print("[Loki] Proxying request to \(target) through \(proxy).")
let parametersAsData = try JSONSerialization.data(withJSONObject: request.parameters, options: [])
let proxyRequestParameters: JSON = [
"method" : request.httpMethod,
"body" : String(bytes: parametersAsData, encoding: .utf8),
"headers" : headers
]
let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [])
let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
let proxyRequestHeaders = [
"X-Sender-Public-Key" : keyPair.publicKey.toHexString(),
"X-Target-Snode-Key" : targetHexEncodedPublicKeySet.idKey
]
let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
proxyRequest.httpBody = ivAndCipherText
proxyRequest.timeoutInterval = request.timeoutInterval
var task: URLSessionDataTask!
task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
if let error = error {
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
let nsError: NSError = nmError as NSError
nsError.isRetryable = false
resolver.reject(nsError)
} else {
resolver.fulfill(result)
}
}
task.resume()
return promise
}.map(on: DispatchQueue.global()) { rawResponse in
guard let responseAsData = rawResponse as? Data, let cipherText = Data(base64Encoded: responseAsData) else {
print("[Loki] Received a non-string encoded response.")
return rawResponse
}
let response = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
let uncheckedJSON = try? JSONSerialization.jsonObject(with: response, options: .allowFragments) as? JSON
guard let json = uncheckedJSON, let statusCode = json["status"] as? Int else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
let isSuccess = (200...299) ~= statusCode
var body: Any? = nil
if let bodyAsString = json["body"] as? String {
body = bodyAsString
if let bodyAsJSON = try? JSONSerialization.jsonObject(with: bodyAsString.data(using: .utf8)!, options: .allowFragments) as? JSON {
body = bodyAsJSON
}
}
guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: body, underlyingError: Error.targetSnodeHTTPError(code: statusCode, message: body)) }
return body
}.done { rawResponse in
seal.fulfill(rawResponse)
}.catch { error in
print("[Loki] Proxy request failed with error: \(error.localizedDescription).")
seal.reject(HTTPError.from(error: error) ?? error)
}
}
task.resume()
return promise
}.map { rawResponse in
guard let responseAsData = rawResponse as? Data, let cipherText = Data(base64Encoded: responseAsData) else {
print("[Loki] Received a non-string encoded response.")
return rawResponse
}
let response = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
let uncheckedJSON = try? JSONSerialization.jsonObject(with: response, options: .allowFragments) as? JSON
guard let json = uncheckedJSON, let statusCode = json["status"] as? Int else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
let isSuccess = (200..<300).contains(statusCode)
var body: Any? = nil
if let bodyAsString = json["body"] as? String {
body = bodyAsString
if let bodyAsJSON = try? JSONSerialization.jsonObject(with: bodyAsString.data(using: .utf8)!, options: .allowFragments) as? JSON {
body = bodyAsJSON
}
}
guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: body, underlyingError: Error.targetSnodeHTTPError(code: statusCode, message: body)) }
return body
}.recover { error -> Promise<Any> in
print("[Loki] Proxy request failed with error: \(error.localizedDescription).")
throw HTTPError.from(error: error) ?? error
}
}
}

@ -18,7 +18,7 @@ public final class DeviceLink : NSObject, NSCoding {
@objc public let signature: Data?
@objc public var displayName: String {
if let customDisplayName = UserDefaults.standard.string(forKey: "\(hexEncodedPublicKey)_display_name") {
if let customDisplayName = UserDefaults.standard[.slaveDeviceName(hexEncodedPublicKey)] {
return customDisplayName
} else {
return NSLocalizedString("Unnamed Device", comment: "")

@ -82,7 +82,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
}
let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")!
let request = TSRequest(url: url)
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global()) { 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 LokiDotNetAPIError.parsingFailed
@ -155,33 +155,41 @@ 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: 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")!
let parameters = signedMessage.toJSON()
let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
let displayName = userDisplayName
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
// ISO8601DateFormatter doesn't support milliseconds before iOS 11
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
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 LokiDotNetAPIError.parsingFailed
return Promise<LokiPublicChatMessage> { [privateKey = userKeyPair.privateKey] seal in
DispatchQueue.global().async {
guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(LokiDotNetAPIError.signingFailed) }
getAuthToken(for: server).then { 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")!
let parameters = signedMessage.toJSON()
let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
let displayName = userDisplayName
return LokiFileServerProxy(for: server).perform(request).map { rawResponse in
// ISO8601DateFormatter doesn't support milliseconds before iOS 11
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
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 LokiDotNetAPIError.parsingFailed
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
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 { error -> Promise<LokiPublicChatMessage> in
if let error = error as? NetworkManagerError, error.statusCode == 401 {
print("[Loki] Group chat auth token for: \(server) expired; dropping it.")
storage.dbReadWriteConnection.removeObject(forKey: server, inCollection: authTokenCollection)
}
throw error
}.retryingIfNeeded(maxRetryCount: maxRetryCount).done { message in
seal.fulfill(message)
}.catch { error in
seal.reject(error)
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
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 {
print("[Loki] Group chat auth token for: \(server) expired; dropping it.")
storage.dbReadWriteConnection.removeObject(forKey: server, inCollection: authTokenCollection)
}
throw error
}.retryingIfNeeded(maxRetryCount: maxRetryCount)
}
}
public static func getDeletedMessageServerIDs(for channel: UInt64, on server: String) -> Promise<[UInt64]> {
@ -194,7 +202,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
}
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")!
let request = TSRequest(url: url)
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
return LokiFileServerProxy(for: server).perform(request).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 LokiDotNetAPIError.parsingFailed
@ -212,14 +220,14 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
}
public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, isSentByUser: Bool) -> Promise<Void> {
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
return getAuthToken(for: server).then { token -> Promise<Void> in
let isModerationRequest = !isSentByUser
print("[Loki] Deleting message with ID: \(messageID) for public chat channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
let urlAsString = isSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)"
let url = URL(string: urlAsString)!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).done(on: DispatchQueue.global()) { result -> Void in
return LokiFileServerProxy(for: server).perform(request).done { result -> Void in
print("[Loki] Deleted message with ID: \(messageID) on server: \(server).")
}.retryingIfNeeded(maxRetryCount: maxRetryCount)
}
@ -228,7 +236,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
public static func getModerators(for channel: UInt64, on server: String) -> Promise<Set<String>> {
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")!
let request = TSRequest(url: url)
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
return LokiFileServerProxy(for: server).perform(request).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 LokiDotNetAPIError.parsingFailed
@ -244,34 +252,34 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
}
public static func join(_ channel: UInt64, on server: String) -> Promise<Void> {
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
return getAuthToken(for: server).then { token -> Promise<Void> in
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
let request = TSRequest(url: url, method: "POST", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).done(on: DispatchQueue.global()) { result -> Void in
return LokiFileServerProxy(for: server).perform(request).done { result -> Void in
print("[Loki] Joined channel with ID: \(channel) on server: \(server).")
}.retryingIfNeeded(maxRetryCount: maxRetryCount)
}
}
public static func leave(_ channel: UInt64, on server: String) -> Promise<Void> {
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
return getAuthToken(for: server).then { token -> Promise<Void> in
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).done(on: DispatchQueue.global()) { result -> Void in
return LokiFileServerProxy(for: server).perform(request).done { result -> Void in
print("[Loki] Left channel with ID: \(channel) on server: \(server).")
}.retryingIfNeeded(maxRetryCount: maxRetryCount)
}
}
public static func getUserCount(for channel: UInt64, on server: String) -> Promise<Int> {
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Int> in
return getAuthToken(for: server).then { token -> Promise<Int> in
let queryParameters = "count=200"
let url = URL(string: "\(server)/channels/\(channel)/subscribers?\(queryParameters)")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
return LokiFileServerProxy(for: server).perform(request).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 LokiDotNetAPIError.parsingFailed
@ -291,11 +299,11 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
guard let hexEncodedPublicKeys = displayNameUpdatees[publicChatID] else { return Promise.value(()) }
displayNameUpdatees[publicChatID] = []
print("[Loki] Getting display names for: \(hexEncodedPublicKeys).")
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
return getAuthToken(for: server).then { token -> Promise<Void> 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 LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
return LokiFileServerProxy(for: server).perform(request).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 LokiDotNetAPIError.parsingFailed
@ -320,12 +328,12 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
public static func setDisplayName(to newDisplayName: String?, on server: String) -> Promise<Void> {
print("[Loki] Updating display name on server: \(server).")
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
return getAuthToken(for: server).then { token -> Promise<Void> in
let parameters: JSON = [ "name" : (newDisplayName ?? "") ]
let url = URL(string: "\(server)/users/me")!
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in }.recover(on: DispatchQueue.global()) { error in
return LokiFileServerProxy(for: server).perform(request).map { _ in }.recover { error in
print("Couldn't update display name due to error: \(error).")
throw error
}
@ -334,7 +342,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
public static func setProfilePictureURL(to url: String?, using profileKey: Data, on server: String) -> Promise<Void> {
print("[Loki] Updating profile picture on server: \(server).")
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
return getAuthToken(for: server).then { token -> Promise<Void> in
var annotation: JSON = [ "type" : profilePictureType ]
if let url = url {
annotation["value"] = [ "profileKey" : profileKey.base64EncodedString(), "url" : url ]
@ -343,7 +351,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
let url = URL(string: "\(server)/users/me")!
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in }.recover(on: DispatchQueue.global()) { error in
return LokiFileServerProxy(for: server).perform(request).map { _ in }.recover { error in
print("[Loki] Couldn't update profile picture due to error: \(error).")
throw error
}
@ -353,7 +361,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
public static func getInfo(for channel: UInt64, on server: String) -> Promise<LokiPublicChatInfo> {
let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")!
let request = TSRequest(url: url)
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
return LokiFileServerProxy(for: server).perform(request).map { rawResponse in
guard let json = rawResponse as? JSON,
let data = json["data"] as? JSON,
let annotations = data["annotations"] as? [JSON],

@ -56,7 +56,7 @@ public final class LokiPublicChatManager : NSObject {
return Promise(error: Error.chatCreationFailed)
}
}
return LokiPublicChatAPI.getAuthToken(for: server).then(on: DispatchQueue.global()) { token in
return LokiPublicChatAPI.getAuthToken(for: server).then { token in
return LokiPublicChatAPI.getInfo(for: channel, on: server)
}.map { channelInfo -> LokiPublicChat in
guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed }

@ -206,7 +206,9 @@ public final class LokiPublicChatPoller : NSObject {
}
}
} else {
proceed()
DispatchQueue.global().async {
proceed()
}
}
}
}

@ -15,7 +15,7 @@ public final class SignalMessage : NSObject {
public var ttl: UInt64? { return objc_ttl != 0 ? objc_ttl : nil }
@objc public init(type: SSKProtoEnvelope.SSKProtoEnvelopeType, timestamp: UInt64, senderID: String, senderDeviceID: UInt32,
content: String, recipientID: String, ttl: UInt64, isPing: Bool, isFriendRequest: Bool) {
content: String, recipientID: String, ttl: UInt64, isPing: Bool, isFriendRequest: Bool) {
self.type = type
self.timestamp = timestamp
self.senderID = senderID

@ -1,47 +0,0 @@
import CryptoSwift
import Curve25519Kit
@objc public final class DiffieHellman : NSObject {
@objc public class DiffieHellmanError : NSError { // Not called `Error` for Obj-C interoperablity
@objc public static let decryptionFailed = DiffieHellmanError(domain: "DiffieHellmanErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt data." ])
}
public static let ivLength: Int32 = 16;
private override init() { }
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 DiffieHellmanError.decryptionFailed }
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

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

@ -200,7 +200,7 @@ public class LokiP2PAPI : NSObject {
AssertIsOnMainThread()
guard let message = onlineBroadcastMessage(forThread: thread) else {
print("[Loki] P2P address not set.")
// print("[Loki] P2P address not set.")
return
}
@ -224,7 +224,7 @@ public class LokiP2PAPI : NSObject {
private static func createLokiAddressMessage(for thread: TSThread, isPing: Bool) -> LokiAddressMessage? {
guard let ourAddress = ourP2PAddress else {
print("[Loki] P2P address not set.")
// print("[Loki] P2P address not set.")
return nil
}

@ -0,0 +1,53 @@
import Foundation
public enum LKUserDefaults {
public enum Bool : Swift.String {
case hasLaunchedOnce
case hasSeenOpenGroupSuggestionSheet
case hasViewedSeed
/// Whether the device was unlinked as a slave device (used to notify the user on the landing screen).
case wasUnlinked
}
public enum Double : Swift.String {
case lastDeviceTokenUpload = "lastDeviceTokenUploadTime"
}
public enum String {
case slaveDeviceName(Swift.String)
case deviceToken
/// `nil` if this is a master device or if the user hasn't linked a device.
case masterHexEncodedPublicKey
public var key: Swift.String {
switch self {
case .slaveDeviceName(let hexEncodedPublicKey): return "\(hexEncodedPublicKey)_display_name"
case .deviceToken: return "deviceToken"
case .masterHexEncodedPublicKey: return "masterDeviceHexEncodedPublicKey"
}
}
}
}
public extension UserDefaults {
public subscript(bool: LKUserDefaults.Bool) -> Bool {
get { return self.bool(forKey: bool.rawValue) }
set { set(newValue, forKey: bool.rawValue) }
}
public subscript(double: LKUserDefaults.Double) -> Double {
get { return self.double(forKey: double.rawValue) }
set { set(newValue, forKey: double.rawValue) }
}
public subscript(string: LKUserDefaults.String) -> String? {
get { return self.string(forKey: string.key) }
set { set(newValue, forKey: string.key) }
}
public var isMasterDevice: Bool {
return (self[.masterHexEncodedPublicKey] == nil)
}
}

@ -350,21 +350,21 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
if (attachmentDescription.length > 0 && bodyDescription.length > 0) {
// Attachment with caption.
if ([CurrentAppContext() isRTL]) {
return [[bodyDescription stringByAppendingString:@": "] stringByAppendingString:attachmentDescription];
} else {
// if ([CurrentAppContext() isRTL]) {
// return [[bodyDescription stringByAppendingString:@": "] stringByAppendingString:attachmentDescription];
// } else {
return [[attachmentDescription stringByAppendingString:@": "] stringByAppendingString:bodyDescription];
}
// }
} else if (bodyDescription.length > 0) {
return bodyDescription;
} else if (attachmentDescription.length > 0) {
return attachmentDescription;
} else if (self.contactShare) {
if (CurrentAppContext().isRTL) {
return [self.contactShare.name.displayName stringByAppendingString:@" 👤"];
} else {
// if (CurrentAppContext().isRTL) {
// return [self.contactShare.name.displayName stringByAppendingString:@" 👤"];
// } else {
return [@"👤 " stringByAppendingString:self.contactShare.name.displayName];
}
// }
} else {
// OWSFailDebug(@"message has neither body nor attachment.");
// TODO: We should do better here.

@ -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();
@ -336,7 +336,8 @@ 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) {
@ -425,16 +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];
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
@ -482,11 +489,13 @@ 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.");

@ -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,11 +52,11 @@
#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"
@ -94,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();
@ -1151,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
@ -1403,14 +1399,14 @@ NS_ASSUME_NONNULL_BEGIN
// The envelope source is set during UD decryption.
if ([ECKeyPair isValidHexEncodedPublicKeyWithCandidate:envelope.source]) {
if ([ECKeyPair isValidHexEncodedPublicKeyWithCandidate:envelope.source] && dataMessage.publicChatInfo == nil) { // Handled in LokiPublicChatPoller for open group messages
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));
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 4 * NSEC_PER_SEC));
}
if (groupId.length > 0) {
@ -2017,36 +2013,6 @@ NS_ASSUME_NONNULL_BEGIN
}
}
# pragma mark - Loki Session Handling
- (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

@ -1924,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]];

Loading…
Cancel
Save