Implement polling, encryption & decryption

This commit is contained in:
nielsandriesse 2020-06-25 17:36:32 +10:00
parent 31dd2673e1
commit 80dcca627a
64 changed files with 1757 additions and 1203 deletions

View File

@ -22,7 +22,7 @@ final class NotificationServiceExtension : UNNotificationServiceExtension {
// Modify the notification content here...
let base64EncodedData = notificationContent.userInfo["ENCRYPTED_DATA"] as! String
let data = Data(base64Encoded: base64EncodedData)!
let envelope = try? LokiMessageWrapper.unwrap(data: data)
let envelope = try? MessageWrapper.unwrap(data: data)
let envelopeData = try? envelope?.serializedData()
let decrypter = SSKEnvironment.shared.messageDecrypter
if (envelope != nil && envelopeData != nil) {

View File

@ -60,10 +60,10 @@ PODS:
- SessionCoreKit/Tests (1.0.0):
- CocoaLumberjack
- GRKOpenSSLFramework
- SessionCurve25519Kit (2.1.2):
- SessionCurve25519Kit (2.1.3):
- CocoaLumberjack
- SessionCoreKit (~> 1.0.0)
- SessionCurve25519Kit/Tests (2.1.2):
- SessionCurve25519Kit/Tests (2.1.3):
- CocoaLumberjack
- SessionCoreKit (~> 1.0.0)
- SessionHKDFKit (0.0.5):
@ -72,7 +72,7 @@ PODS:
- SessionHKDFKit/Tests (0.0.5):
- CocoaLumberjack
- SessionCoreKit
- SessionMetadataKit (1.0.2):
- SessionMetadataKit (1.0.3):
- CocoaLumberjack
- CryptoSwift (~> 1.3)
- SessionAxolotlKit (~> 1.0.2)
@ -80,7 +80,7 @@ PODS:
- SessionCurve25519Kit (~> 2.1.2)
- SessionHKDFKit (~> 0.0.5)
- SwiftProtobuf (~> 1.5.0)
- SessionMetadataKit/Tests (1.0.2):
- SessionMetadataKit/Tests (1.0.3):
- CocoaLumberjack
- CryptoSwift (~> 1.3)
- SessionAxolotlKit (~> 1.0.2)
@ -100,8 +100,8 @@ PODS:
- SAMKeychain
- SessionAxolotlKit (~> 1.0.2)
- SessionCoreKit (~> 1.0.0)
- SessionCurve25519Kit (~> 2.1.2)
- SessionMetadataKit (~> 1.0.2)
- SessionCurve25519Kit (~> 2.1.3)
- SessionMetadataKit (~> 1.0.3)
- Starscream
- SwiftProtobuf (~> 1.5.0)
- YapDatabase/SQLCipher
@ -117,8 +117,8 @@ PODS:
- SAMKeychain
- SessionAxolotlKit (~> 1.0.2)
- SessionCoreKit (~> 1.0.0)
- SessionCurve25519Kit (~> 2.1.2)
- SessionMetadataKit (~> 1.0.2)
- SessionCurve25519Kit (~> 2.1.3)
- SessionMetadataKit (~> 1.0.3)
- Starscream
- SwiftProtobuf (~> 1.5.0)
- YapDatabase/SQLCipher
@ -283,13 +283,13 @@ CHECKOUT OPTIONS:
:commit: 0d66c90657b62cb66ecd2767c57408a951650f23
:git: https://github.com/loki-project/session-ios-core-kit.git
SessionCurve25519Kit:
:commit: ed4ae4576003905267d1573e336a003364564793
:commit: c3bc075d1e1c8339eebe2af184869de1a007d855
:git: https://github.com/loki-project/session-ios-curve-25519-kit
SessionHKDFKit:
:commit: 0dcf8cf8a7995ef8663146f7063e6c1d7f5a3274
:git: https://github.com/nielsandriesse/session-ios-hkdf-kit.git
SessionMetadataKit:
:commit: bc6a425ac96dd6482b4e10df1eee1349153b2eca
:commit: e23212e8494157d7a4daabbd4842be59117d9420
:git: https://github.com/loki-project/session-ios-metadata-kit
Starscream:
:commit: b09ea163c3cb305152c65b299cb024610f52e735
@ -314,13 +314,13 @@ SPEC CHECKSUMS:
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SessionAxolotlKit: 88f72573df989042510d8a1737bd0c0057dda3da
SessionCoreKit: 778a3f6e3da788b43497734166646025b6392e88
SessionCurve25519Kit: cedc6c713f8182e6ee02bf9bd1b337552e00ebbb
SessionCurve25519Kit: 9bb9afe199e4bc23578a4b15932ad2c57bd047b1
SessionHKDFKit: b0f4e669411703ab925aba07491c5611564d1419
SessionMetadataKit: 29d6b1c0f8dbe630ccddd73362b9eb3b1abc8a6d
SessionServiceKit: c792d64fc86aaa0f7325beaa36e1a9211a887359
SessionMetadataKit: 581eb0da986e5a1752d07bfa89cf54bbe1fe3bca
SessionServiceKit: 344dff85e344fd3177d7b0b7aff4647a9d5e9efc
SQLCipher: e434ed542b24f38ea7b36468a13f9765e1b5c072
SSZipArchive: 62d4947b08730e4cda640473b0066d209ff033c9
Starscream: ef3ece99d765eeccb67de105bfa143f929026cf5
Starscream: 8aaf1a7feb805c816d0e7d3190ef23856f6665b9
SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2
YapDatabase: b418a4baa6906e8028748938f9159807fd039af4
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54

2
Pods

@ -1 +1 @@
Subproject commit 3353c303e2731e9266f62bf9b72cb7a49eeb969b
Subproject commit 8e09671add980ffbb06ba27e1bbb2aa558b5b4cf

View File

@ -38,7 +38,7 @@ A Swift/Objective-C library for communicating with the Session messaging service
s.resources = ["SignalServiceKit/Resources/Certificates/*", "SignalServiceKit/src/Loki/Mnemonic/*.txt"]
s.dependency 'SessionCurve25519Kit', '~> 2.1.2'
s.dependency 'SessionCurve25519Kit', '~> 2.1.3'
s.dependency 'CocoaLumberjack'
s.dependency 'CryptoSwift', '~> 1.3'
s.dependency 'AFNetworking'
@ -52,7 +52,7 @@ A Swift/Objective-C library for communicating with the Session messaging service
s.dependency 'Reachability'
s.dependency 'SwiftProtobuf', '~> 1.5.0'
s.dependency 'SessionCoreKit', '~> 1.0.0'
s.dependency 'SessionMetadataKit', '~> 1.0.2'
s.dependency 'SessionMetadataKit', '~> 1.0.3'
s.dependency 'PromiseKit', '~> 6.0'
s.test_spec 'Tests' do |test_spec|

View File

@ -2789,6 +2789,7 @@
isa = PBXGroup;
children = (
B8CCF63B239757C10091D419 /* Components */,
C32B405424A961E1001117B5 /* Dependencies */,
B8BFFF392355426100102A27 /* Shelved */,
B8CCF63C239757DB0091D419 /* Utilities */,
B8CCF63D2397580E0091D419 /* View Controllers */,
@ -2909,7 +2910,6 @@
B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */,
C31A6C59247F214E001123EF /* UIView+Glow.swift */,
C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */,
C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -2950,6 +2950,14 @@
path = "View Controllers";
sourceTree = "<group>";
};
C32B405424A961E1001117B5 /* Dependencies */ = {
isa = PBXGroup;
children = (
C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */,
);
path = Dependencies;
sourceTree = "<group>";
};
C35E8AA42485C83B00ACB629 /* CSV */ = {
isa = PBXGroup;
children = (

View File

@ -9,8 +9,10 @@ extern NSString *const AppDelegateStoryboardMain;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
- (void)startPollerIfNeeded;
- (void)stopPollerIfNeeded;
- (void)stopPoller;
- (void)startClosedGroupPollerIfNeeded;
- (void)stopClosedGroupPoller;
- (void)startOpenGroupPollersIfNeeded;
- (void)stopOpenGroupPollersIfNeeded;
- (void)stopOpenGroupPollers;
@end

View File

@ -65,9 +65,8 @@ static NSTimeInterval launchStartedAt;
@property (nonatomic) BOOL didAppLaunchFail;
// Loki
@property (nonatomic) LKPoller *lokiPoller;
@property (nonatomic) LKRSSFeedPoller *lokiNewsFeedPoller;
@property (nonatomic) LKRSSFeedPoller *lokiMessengerUpdatesFeedPoller;
@property (nonatomic) LKPoller *poller;
@property (nonatomic) LKClosedGroupPoller *closedGroupPoller;
@end
@ -1379,53 +1378,41 @@ static NSTimeInterval launchStartedAt;
#pragma mark - Loki
- (void)setUpPollerIfNeeded
{
if (self.lokiPoller != nil) { return; }
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
if (userHexEncodedPublicKey == nil) { return; }
self.lokiPoller = [[LKPoller alloc] initOnMessagesReceived:^(NSArray<SSKProtoEnvelope *> *messages) {
if (messages.count != 0) {
[LKLogger print:@"[Loki] Received new messages."];
}
for (SSKProtoEnvelope *message in messages) {
NSData *data = [message serializedDataAndReturnError:nil];
if (data != nil) {
[SSKEnvironment.shared.messageReceiver handleReceivedEnvelopeData:data];
} else {
[LKLogger print:@"[Loki] Failed to deserialize envelope."];
}
}
}];
}
- (void)startPollerIfNeeded
{
[self setUpPollerIfNeeded];
[self.lokiPoller startIfNeeded];
if (self.poller != nil) { return; }
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
if (userPublicKey == nil) { return; }
self.poller = [[LKPoller alloc] init];
[self.poller startIfNeeded];
}
- (void)stopPollerIfNeeded
- (void)stopPoller { [self.lokiPoller stop]; }
- (void)startClosedGroupPollerIfNeeded
{
[self.lokiPoller stopIfNeeded];
if (self.closedGroupPoller != nil) { return; }
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
if (userPublicKey == nil) { return; }
self.closedGroupPoller = [[LKClosedGroupPoller alloc] init];
[self.closedGroupPoller startIfNeeded];
}
- (void)stopClosedGroupPoller { [self.closedGroupPoller stop]; }
- (void)startOpenGroupPollersIfNeeded
{
[LKPublicChatManager.shared startPollersIfNeeded];
[SSKEnvironment.shared.attachmentDownloads continueDownloadIfPossible];
}
- (void)stopOpenGroupPollersIfNeeded
{
[LKPublicChatManager.shared stopPollers];
}
- (void)stopOpenGroupPollers { [LKPublicChatManager.shared stopPollers]; }
- (void)handleDataNukeRequested:(NSNotification *)notification {
[ThreadUtil deleteAllContent];
[SSKEnvironment.shared.messageSenderJobQueue clearAllJobs];
[SSKEnvironment.shared.identityManager clearIdentityKey];
[LKAPI clearSnodePool];
[LKSnodeAPI clearSnodePool];
[self stopPollerIfNeeded];
[self stopOpenGroupPollersIfNeeded];
[self.lokiNewsFeedPoller stop];

View File

@ -199,7 +199,7 @@ public class MessageFetcherJob: NSObject {
}
private func fetchUndeliveredMessages() -> Promise<Set<Promise<[SSKProtoEnvelope]>>> {
return LokiAPI.getMessages()
return SnodeAPI.getMessages(for: getUserHexEncodedPublicKey())
}
private func acknowledgeDelivery(envelope: SSKProtoEnvelope) {

View File

@ -3,6 +3,24 @@ class BaseVC : UIViewController {
override var preferredStatusBarStyle: UIStatusBarStyle { return isLightMode ? .default : .lightContent }
lazy var navBarTitleLabel: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
result.alpha = 1
result.textAlignment = .center
return result
}()
lazy var crossfadeLabel: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
result.alpha = 0
result.textAlignment = .center
return result
}()
override func viewDidLoad() {
setNeedsStatusBarAppearanceUpdate()
NotificationCenter.default.addObserver(self, selector: #selector(handleUnexpectedDeviceLinkRequestReceivedNotification), name: .unexpectedDeviceLinkRequestReceived, object: nil)
@ -23,11 +41,18 @@ class BaseVC : UIViewController {
}
internal func setNavBarTitle(_ title: String, customFontSize: CGFloat? = nil) {
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: customFontSize ?? Values.veryLargeFontSize)
navigationItem.titleView = titleLabel
let container = UIView()
navBarTitleLabel.text = title
crossfadeLabel.text = title
if let customFontSize = customFontSize {
navBarTitleLabel.font = .boldSystemFont(ofSize: customFontSize)
crossfadeLabel.font = .boldSystemFont(ofSize: customFontSize)
}
container.addSubview(navBarTitleLabel)
navBarTitleLabel.pin(to: container)
container.addSubview(crossfadeLabel)
crossfadeLabel.pin(to: container)
navigationItem.titleView = container
}
internal func setUpNavBarSessionIcon() {

View File

@ -182,7 +182,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
let linkingAuthorizationMessage = DeviceLinkingUtilities.getLinkingAuthorizationMessage(for: deviceLink)
let master = DeviceLink.Device(hexEncodedPublicKey: deviceLink.master.hexEncodedPublicKey, signature: linkingAuthorizationMessage.masterSignature)
let signedDeviceLink = DeviceLink(between: master, and: deviceLink.slave)
LokiFileServerAPI.addDeviceLink(signedDeviceLink).done(on: DispatchQueue.main) { [weak self] in
FileServerAPI.addDeviceLink(signedDeviceLink).done(on: DispatchQueue.main) { [weak self] in
SSKEnvironment.shared.messageSender.send(linkingAuthorizationMessage, success: {
let storage = OWSPrimaryStorage.shared()
let slaveHexEncodedPublicKey = deviceLink.slave.hexEncodedPublicKey
@ -205,7 +205,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
}
}, failure: { error in
print("[Loki] Failed to send device link authorization message.")
let _ = LokiFileServerAPI.removeDeviceLink(signedDeviceLink) // Attempt to roll back
let _ = FileServerAPI.removeDeviceLink(signedDeviceLink) // Attempt to roll back
DispatchQueue.main.async {
self?.close()
let alert = UIAlertController(title: NSLocalizedString("Device Linking Failed", comment: ""), message: NSLocalizedString("Please check your internet connection and try again", comment: ""), preferredStyle: .alert)
@ -233,7 +233,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
subtitleLabel.text = NSLocalizedString("Your device has been linked successfully", comment: "")
mnemonicLabel.isHidden = true
buttonStackView.isHidden = true
LokiFileServerAPI.addDeviceLink(deviceLink).catch { error in
FileServerAPI.addDeviceLink(deviceLink).catch { error in
print("[Loki] Failed to add device link due to error: \(error).")
}
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in

View File

@ -140,7 +140,7 @@ final class DeviceLinksVC : BaseVC, UITableViewDataSource, UITableViewDelegate,
}
private func removeDeviceLink(_ deviceLink: DeviceLink) {
LokiFileServerAPI.removeDeviceLink(deviceLink).done { [weak self] in
FileServerAPI.removeDeviceLink(deviceLink).done { [weak self] in
let linkedDeviceHexEncodedPublicKey = deviceLink.other.hexEncodedPublicKey
guard let thread = TSContactThread.fetch(uniqueId: TSContactThread.threadId(fromContactId: linkedDeviceHexEncodedPublicKey)) else { return }
let unlinkDeviceMessage = UnlinkDeviceMessage(thread: thread)

View File

@ -45,6 +45,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
let bottomInset = Values.newConversationButtonBottomOffset + Values.newConversationButtonExpandedSize + Values.largeSpacing + Values.newConversationButtonCollapsedSize
result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
result.showsVerticalScrollIndicator = false
return result
}()
@ -166,7 +167,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
publicKeys.insert(publicKey)
}
}
let _ = LokiFileServerAPI.getDeviceLinks(associatedWith: publicKeys)
let _ = FileServerAPI.getDeviceLinks(associatedWith: publicKeys)
// Do initial update
reload()
}

View File

@ -1,5 +1,5 @@
final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate {
final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, UIScrollViewDelegate {
private var selectedContacts: Set<String> = []
private lazy var contacts: [String] = {
@ -8,22 +8,22 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
storage.dbReadConnection.read { transaction in
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let thread = object as? TSContactThread, thread.shouldThreadBeVisible && thread.isContactFriend else { return }
let hexEncodedPublicKey = thread.contactIdentifier()
guard UserDisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) != nil else { return }
let publicKey = thread.contactIdentifier()
guard UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) != nil else { return }
// We shouldn't be able to add slave devices to groups
guard storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) == nil else { return }
result.append(hexEncodedPublicKey)
guard storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) == nil else { return }
result.append(publicKey)
}
}
func getDisplayName(for hexEncodedPublicKey: String) -> String {
return UserDisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) ?? "Unknown Contact"
}
let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
var linkedDeviceHexEncodedPublicKeys: Set<String> = [ userHexEncodedPublicKey ]
let userPublicKey = getUserHexEncodedPublicKey()
var userLinkedDevices: Set<String> = [ userPublicKey ]
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
linkedDeviceHexEncodedPublicKeys = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userHexEncodedPublicKey, in: transaction)
userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
}
result = result.filter { !linkedDeviceHexEncodedPublicKeys.contains($0) }
result = result.filter { !userLinkedDevices.contains($0) }
result = result.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
return result
}()
@ -38,7 +38,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
result.register(Cell.self, forCellReuseIdentifier: "Cell")
result.separatorStyle = .none
result.backgroundColor = .clear
result.showsVerticalScrollIndicator = false
result.isScrollEnabled = false
return result
}()
@ -47,7 +47,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
super.viewDidLoad()
setUpGradientBackground()
setUpNavBarStyle()
let customTitleFontSize = isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize
let customTitleFontSize = Values.largeFontSize
setNavBarTitle(NSLocalizedString("New Closed Group", comment: ""), customFontSize: customTitleFontSize)
// Set up navigation bar buttons
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
@ -58,33 +58,29 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
navigationItem.rightBarButtonItem = doneButton
// Set up content
if !contacts.isEmpty {
view.addSubview(nameTextField)
nameTextField.pin(.leading, to: .leading, of: view, withInset: Values.largeSpacing)
nameTextField.pin(.top, to: .top, of: view, withInset: Values.mediumSpacing)
nameTextField.pin(.trailing, to: .trailing, of: view, withInset: -Values.largeSpacing)
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("Closed groups support up to 10 members and provide the same privacy protections as one-on-one sessions.", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
view.addSubview(explanationLabel)
explanationLabel.pin(.leading, to: .leading, of: view, withInset: Values.largeSpacing)
explanationLabel.pin(.top, to: .bottom, of: nameTextField, withInset: Values.mediumSpacing)
explanationLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.largeSpacing)
let mainStackView = UIStackView()
mainStackView.axis = .vertical
nameTextField.delegate = self
let nameTextFieldContainer = UIView()
nameTextFieldContainer.addSubview(nameTextField)
nameTextField.pin(.leading, to: .leading, of: nameTextFieldContainer, withInset: Values.largeSpacing)
nameTextField.pin(.top, to: .top, of: nameTextFieldContainer, withInset: Values.mediumSpacing)
nameTextFieldContainer.pin(.trailing, to: .trailing, of: nameTextField, withInset: Values.largeSpacing)
nameTextFieldContainer.pin(.bottom, to: .bottom, of: nameTextField, withInset: Values.largeSpacing)
mainStackView.addArrangedSubview(nameTextFieldContainer)
let separator = UIView()
separator.backgroundColor = Colors.separator
separator.set(.height, to: Values.separatorThickness)
view.addSubview(separator)
separator.pin(.leading, to: .leading, of: view)
separator.pin(.top, to: .bottom, of: explanationLabel, withInset: Values.largeSpacing)
separator.pin(.trailing, to: .trailing, of: view)
view.addSubview(tableView)
tableView.pin(.leading, to: .leading, of: view)
tableView.pin(.top, to: .bottom, of: separator)
tableView.pin(.trailing, to: .trailing, of: view)
tableView.pin(.bottom, to: .bottom, of: view)
mainStackView.addArrangedSubview(separator)
tableView.set(.height, to: CGFloat(contacts.count * 67)) // A cell is exactly 67 points high
tableView.set(.width, to: UIScreen.main.bounds.width)
mainStackView.addArrangedSubview(tableView)
let scrollView = UIScrollView(wrapping: mainStackView, withInsets: UIEdgeInsets.zero)
scrollView.showsVerticalScrollIndicator = false
scrollView.delegate = self
view.addSubview(scrollView)
scrollView.set(.width, to: UIScreen.main.bounds.width)
scrollView.pin(to: view)
} else {
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
@ -122,6 +118,22 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
}
// MARK: Interaction
func textFieldDidEndEditing(_ textField: UITextField) {
crossfadeLabel.text = textField.text!.isEmpty ? NSLocalizedString("New Closed Group", comment: "") : textField.text!
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if nameTextField.isFirstResponder {
nameTextField.resignFirstResponder()
}
let nameTextFieldCenterY = nameTextField.convert(nameTextField.bounds.center, to: scrollView).y
let tableViewOriginY = tableView.convert(tableView.bounds.origin, to: scrollView).y
let titleLabelAlpha = 1 - (scrollView.contentOffset.y - nameTextFieldCenterY) / (tableViewOriginY - nameTextFieldCenterY)
let crossfadeLabelAlpha = 1 - titleLabelAlpha
navBarTitleLabel.alpha = titleLabelAlpha
crossfadeLabel.alpha = crossfadeLabelAlpha
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let contact = contacts[indexPath.row]
if !selectedContacts.contains(contact) {
@ -153,38 +165,20 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
guard selectedContacts.count >= 2 else {
return showError(title: NSLocalizedString("Please pick at least 2 group members", comment: ""))
}
guard selectedContacts.count <= 10 else {
return showError(title: NSLocalizedString("A closed group cannot have more than 10 members", comment: ""))
guard selectedContacts.count <= 20 else {
return showError(title: NSLocalizedString("A closed group cannot have more than 20 members", comment: ""))
}
let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
let storage = OWSPrimaryStorage.shared()
var masterHexEncodedPublicKey = ""
storage.dbReadConnection.read { transaction in
masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: userHexEncodedPublicKey, in: transaction) ?? userHexEncodedPublicKey
}
let members = selectedContacts + [ masterHexEncodedPublicKey ]
let admins = [ masterHexEncodedPublicKey ]
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(Randomness.generateRandomBytes(kGroupIdLength)!.toHexString())
let group = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins)
let thread = TSGroupThread.getOrCreateThread(with: group)
OWSProfileManager.shared().addThread(toProfileWhitelist: thread)
let selectedContacts = self.selectedContacts
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in
let message = TSOutgoingMessage(in: thread, groupMetaMessage: .new, expiresInSeconds: 0)
message.update(withCustomMessage: "Closed group created")
DispatchQueue.main.async {
SSKEnvironment.shared.messageSender.send(message, success: {
DispatchQueue.main.async {
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 {
self?.presentingViewController?.dismiss(animated: true, completion: nil)
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
}
})
let _ = FileServerAPI.getDeviceLinks(associatedWith: selectedContacts).ensure2 {
var thread: TSGroupThread!
try! Storage.writeSync { transaction in
thread = ClosedGroupsProtocol.createClosedGroup(name: name, members: selectedContacts, transaction: transaction)
}
DispatchQueue.main.async {
self?.presentingViewController?.dismiss(animated: true, completion: nil)
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
}
}
}
}

View File

@ -156,7 +156,7 @@ final class PathVC : BaseVC {
return stackView
}
private func getPathRow(snode: LokiAPITarget, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
private func getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
let country = IP2Country.isInitialized ? (IP2Country.shared.countryNamesCache[snode.ip] ?? "Resolving...") : "Resolving..."
let title = isGuardSnode ? NSLocalizedString("Entry Node", comment: "") : NSLocalizedString("Service Node", comment: "")
return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval)

View File

@ -263,7 +263,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
var isMaxFileSizeExceeded = false
if let error = error as? LokiDotNetAPI.LokiDotNetAPIError {
if let error = error as? DotNetAPI.DotNetAPIError {
isMaxFileSizeExceeded = (error == .maxFileSizeExceeded)
}
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : NSLocalizedString("Couldn't Update Profile", comment: "")

View File

@ -679,7 +679,7 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[ThreadUtil deleteAllContent];
[SSKEnvironment.shared.identityManager clearIdentityKey];
[LKAPI clearSnodePool];
[LKSnodeAPI clearSnodePool];
AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
[appDelegate stopPollerIfNeeded];
[appDelegate stopOpenGroupPollersIfNeeded];

View File

@ -2806,8 +2806,8 @@
"Search GIFs?" = "Search GIFs?";
"You will not have full metadata protection when sending GIFs." = "You will not have full metadata protection when sending GIFs.";
"The ability to add members to a closed group is coming soon." = "The ability to add members to a closed group is coming soon.";
"A closed group cannot have more than 10 members" = "A closed group cannot have more than 10 members";
"Closed groups support up to 10 members and provide the same privacy protections as one-on-one sessions." = "Closed groups support up to 10 members and provide the same privacy protections as one-on-one sessions.";
"A closed group cannot have more than 20 members" = "A closed group cannot have more than 20 members";
"Closed groups support up to 20 members" = "Closed groups support up to 20 members";
"No messages yet" = "No messages yet";
"Would you like to join the Session Public Chat?" = "Would you like to join the Session Public Chat?";
"Join Public Chat" = "Join Public Chat";
@ -2842,3 +2842,4 @@
"Learn More" = "Learn More";
"Please ask the open group operator to add you to the group." = "Please ask the open group operator to add you to the group.";
"Unauthorized" = "Unauthorized";
"Closed group created" = "Closed group created";

View File

@ -229,7 +229,6 @@ NSString *const kSyncManagerLastContactSyncKey = @"kTSStorageManagerOWSSyncManag
return;
}
if ([LKSyncMessagesProtocol shouldSkipConfigurationSyncMessage]) { return; }
[self sendConfigurationSyncMessage_AppReady];
}];
}
@ -270,7 +269,7 @@ NSString *const kSyncManagerLastContactSyncKey = @"kTSStorageManagerOWSSyncManag
- (AnyPromise *)syncContact:(NSString *)hexEncodedPubKey transaction:(YapDatabaseReadTransaction *)transaction
{
return [LKSyncMessagesProtocol syncContactWithPublicKey:hexEncodedPubKey in:transaction];
return [LKSyncMessagesProtocol syncContactWithPublicKey:hexEncodedPubKey];
}
- (AnyPromise *)syncAllContacts

View File

@ -16,13 +16,14 @@ option java_outer_classname = "SignalServiceProtos";
message Envelope {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
RECEIPT = 5;
UNIDENTIFIED_SENDER = 6;
FRIEND_REQUEST = 101; // Loki: Contains prekeys and a message; uses simple encryption
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
RECEIPT = 5;
UNIDENTIFIED_SENDER = 6;
CLOSED_GROUP_CIPHERTEXT = 7; // Loki
FRIEND_REQUEST = 101; // Loki: Contains pre keys and a message; uses simple encryption
}
// @required
@ -131,6 +132,15 @@ message CallMessage {
optional bytes profileKey = 6;
}
message ClosedGroupCiphertext {
// @required
optional bytes ciphertext = 1;
// @required
optional string senderPublicKey = 2;
// @required
optional uint32 keyIndex = 3;
}
message DataMessage {
enum Flags {
END_SESSION = 1;
@ -235,22 +245,25 @@ message DataMessage {
optional AttachmentPointer image = 3;
}
// Loki: A custom message for our profile
message LokiProfile {
message LokiProfile { // Loki
optional string displayName = 1;
optional string profilePicture = 2;
}
message ClosedGroupUpdate { // Loki
enum Type {
NEW = 0; // groupPublicKey, name, groupPrivateKey, chainKeys, members, admins
}
optional string name = 1;
// @required
optional string name = 1;
optional bytes groupPublicKey = 2;
optional bytes groupPrivateKey = 3;
repeated bytes chainKeys = 4;
repeated string members = 5;
repeated string admins = 6;
// @required
optional string groupID = 2;
// @required
optional string sharedSecret = 3;
// @required
optional string senderKey = 4;
repeated string members = 5;
optional Type type = 7;
}
optional string body = 1;
@ -327,7 +340,7 @@ message SyncMessage {
// @required
optional string url = 1;
// @required
optional uint64 channelId = 2;
optional uint64 channelID = 2;
}
message Blocked {
@ -445,7 +458,7 @@ message GroupDetails {
optional uint32 expireTimer = 6;
optional string color = 7;
optional bool blocked = 8;
repeated string admins = 9; // Loki
repeated string admins = 9; // Loki
}
// Internal - DO NOT SEND

View File

@ -19,6 +19,7 @@ extern NSString *const TSGroupThread_NotificationKey_UniqueId;
@property (nonatomic, strong) TSGroupModel *groupModel;
@property (nonatomic, readonly) BOOL isRSSFeed;
@property (nonatomic, readonly) BOOL isPublicChat;
@property (nonatomic) BOOL usesSharedSenderKeys;
+ (instancetype)getOrCreateThreadWithGroupModel:(TSGroupModel *)groupModel;
+ (instancetype)getOrCreateThreadWithGroupModel:(TSGroupModel *)groupModel

View File

@ -25,12 +25,14 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
OWSAssertDebug(groupModel);
OWSAssertDebug(groupModel.groupId.length > 0);
OWSAssertDebug(groupModel.groupMemberIds.count > 0);
for (NSString *recipientId in groupModel.groupMemberIds) {
OWSAssertDebug(recipientId.length > 0);
}
NSString *uniqueIdentifier = [[self class] threadIdFromGroupId:groupModel.groupId];
self = [super initWithUniqueId:uniqueIdentifier];
if (!self) {
return self;
}
@ -55,6 +57,7 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
adminIds:@[ localNumber ]];
self = [self initWithGroupModel:groupModel];
if (!self) {
return self;
}
@ -77,10 +80,12 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
OWSAssertDebug(transaction);
TSGroupThread *thread = [self fetchObjectWithUniqueID:[self threadIdFromGroupId:groupId] transaction:transaction];
if (!thread) {
thread = [[self alloc] initWithGroupId:groupId groupType:groupType];
[thread saveWithTransaction:transaction];
}
return thread;
}
@ -89,9 +94,11 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
OWSAssertDebug(groupId.length > 0);
__block TSGroupThread *thread;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
thread = [self getOrCreateThreadWithGroupId:groupId groupType:groupType transaction:transaction];
} error:nil];
return thread;
}
@ -108,6 +115,7 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
thread = [[TSGroupThread alloc] initWithGroupModel:groupModel];
[thread saveWithTransaction:transaction];
}
return thread;
}
@ -117,15 +125,18 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
OWSAssertDebug(groupModel.groupId.length > 0);
__block TSGroupThread *thread;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
thread = [self getOrCreateThreadWithGroupModel:groupModel transaction:transaction];
} error:nil];
return thread;
}
+ (NSString *)threadIdFromGroupId:(NSData *)groupId
{
OWSAssertDebug(groupId.length > 0);
return [TSGroupThreadPrefix stringByAppendingString:[[LKGroupUtilities getDecodedGroupIDAsData:groupId] base64EncodedString]];
}
@ -188,12 +199,19 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
return (self.groupModel.groupType == rssFeed);
}
- (BOOL)isContactFriend
{
return false;
}
- (BOOL)isLocalUserInGroup
{
__block BOOL result = NO;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
result = [self isCurrentUserInGroupWithTransaction:transaction];
}];
return result;
}
@ -234,6 +252,7 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
- (void)setGroupModel:(TSGroupModel *)newGroupModel withTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
self.groupModel = newGroupModel;
[self saveWithTransaction:transaction];
[transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{
@ -251,10 +270,10 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
- (void)leaveGroupWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
NSMutableSet<NSString *> *newGroupMemberIDs = [NSMutableSet setWithArray:self.groupModel.groupMemberIds];
NSString *userHexEncodedPublicKey = TSAccountManager.localNumber;
if (userHexEncodedPublicKey == nil) { return; }
NSSet<NSString *> *linkedDeviceHexEncodedPublicKeys = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userHexEncodedPublicKey in:transaction];
[newGroupMemberIDs minusSet:linkedDeviceHexEncodedPublicKeys];
NSString *userPublicKey = TSAccountManager.localNumber;
if (userPublicKey == nil) { return; }
NSSet<NSString *> *userLinkedDevices = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userPublicKey in:transaction];
[newGroupMemberIDs minusSet:userLinkedDevices];
self.groupModel.groupMemberIds = newGroupMemberIDs.allObjects;
[self saveWithTransaction:transaction];
[transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{
@ -315,11 +334,6 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
return [self.class stableColorNameForNewConversationWithString:[self threadIdFromGroupId:groupId]];
}
- (BOOL)isContactFriend
{
return false;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -60,7 +60,7 @@ public enum ProofOfWork {
/// - Returns: A nonce string or `nil` if it failed
public static func calculate(data: String, pubKey: String, timestamp: UInt64, ttl: UInt64) -> String? {
let payload = createPayload(pubKey: pubKey, data: data, timestamp: timestamp, ttl: ttl)
let target = calcTarget(ttl: ttl, payloadLength: payload.count, nonceTrials: Int(LokiAPI.powDifficulty))
let target = calcTarget(ttl: ttl, payloadLength: payload.count, nonceTrials: Int(SnodeAPI.powDifficulty))
// Start with the max value
var trialValue = UInt64.max

View File

@ -37,19 +37,19 @@ internal class LokiFileServerProxy : LokiHTTPClient {
}
// MARK: Proxying
override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
let isLokiFileServer = (server == LokiFileServerAPI.server)
override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
let isLokiFileServer = (server == FileServerAPI.server)
guard isLokiFileServer else { return super.perform(request, withCompletionQueue: queue) } // Don't proxy open group requests for now
return performLokiFileServerNSURLRequest(request, withCompletionQueue: queue)
}
internal func performLokiFileServerNSURLRequest(_ request: NSURLRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
internal func performLokiFileServerNSURLRequest(_ request: NSURLRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
var headers = getCanonicalHeaders(for: request)
return Promise<LokiAPI.RawResponse> { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] seal in
return Promise<SnodeAPI.RawResponse> { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] seal in
DispatchQueue.global(qos: .userInitiated).async {
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: LokiFileServerProxy.fileServerPublicKey, privateKey: keyPair.privateKey)
guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) }
LokiAPI.getRandomSnode().then2 { proxy -> Promise<Any> in
SnodeAPI.getRandomSnode().then2 { 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 }
@ -84,7 +84,7 @@ internal class LokiFileServerProxy : LokiHTTPClient {
"Connection" : "close", // TODO: Is this necessary?
"Content-Type" : "application/json"
]
let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
let (promise, resolver) = SnodeAPI.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)!

View File

@ -14,8 +14,8 @@ public class LokiHTTPClient {
return result
}()
internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map2 { $0.responseObject }.recover2 { error -> LokiAPI.RawResponsePromise in
internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map2 { $0.responseObject }.recover2 { error -> SnodeAPI.RawResponsePromise in
throw HTTPError.from(error: error) ?? error
}
}

View File

@ -1,8 +1,8 @@
import PromiseKit
import SessionMetadataKit
/// Base class for `LokiFileServerAPI` and `LokiPublicChatAPI`.
public class LokiDotNetAPI : NSObject {
/// Base class for `FileServerAPI` and `PublicChatAPI`.
public class DotNetAPI : NSObject {
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
internal static var userKeyPair: ECKeyPair { OWSIdentityManager.shared().identityKeyPair()! }
@ -11,17 +11,18 @@ public class LokiDotNetAPI : NSObject {
private static let attachmentType = "network.loki"
// MARK: Error
@objc public class LokiDotNetAPIError : NSError { // Not called `Error` for Obj-C interoperablity
@objc(LKDotNetAPIError)
public class DotNetAPIError : NSError { // Not called `Error` for Obj-C interoperablity
@objc public static let generic = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "An error occurred." ])
@objc public static let parsingFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Invalid file server response." ])
@objc public static let signingFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Couldn't sign message." ])
@objc public static let encryptionFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Couldn't encrypt file." ])
@objc public static let decryptionFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt file." ])
@objc public static let maxFileSizeExceeded = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 6, userInfo: [ NSLocalizedDescriptionKey : "Maximum file size exceeded." ])
@objc public static let generic = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "An error occurred." ])
@objc public static let parsingFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Invalid file server response." ])
@objc public static let signingFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Couldn't sign message." ])
@objc public static let encryptionFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Couldn't encrypt file." ])
@objc public static let decryptionFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt file." ])
@objc public static let maxFileSizeExceeded = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 6, userInfo: [ NSLocalizedDescriptionKey : "Maximum file size exceeded." ])
}
// MARK: Database
// MARK: Storage
/// To be overridden by subclasses.
internal class var authTokenCollection: String { preconditionFailure("authTokenCollection is abstract and must be overridden.") }
@ -70,7 +71,7 @@ public class LokiDotNetAPI : NSObject {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { 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
throw DotNetAPIError.parsingFailed
}
// Discard the "05" prefix if needed
if serverPublicKey.count == 33 {
@ -80,7 +81,7 @@ public class LokiDotNetAPI : NSObject {
// The challenge is prefixed by the 16 bit IV
guard let tokenAsData = try? DiffieHellman.decrypt(challenge, publicKey: serverPublicKey, privateKey: userKeyPair.privateKey),
let token = String(bytes: tokenAsData, encoding: .utf8) else {
throw LokiDotNetAPIError.decryptionFailed
throw DotNetAPIError.decryptionFailed
}
return token
}
@ -101,14 +102,14 @@ public class LokiDotNetAPI : NSObject {
}
public static func uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> Promise<Void> {
let isEncryptionRequired = (server == LokiFileServerAPI.server)
let isEncryptionRequired = (server == FileServerAPI.server)
return Promise<Void>() { seal in
func proceed(with token: String) {
// Get the attachment
let data: Data
guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else {
print("[Loki] Couldn't read attachment from disk.")
return seal.reject(LokiDotNetAPIError.generic)
return seal.reject(DotNetAPIError.generic)
}
// Encrypt the attachment if needed
if isEncryptionRequired {
@ -116,7 +117,7 @@ public class LokiDotNetAPI : NSObject {
var digest = NSData()
guard let encryptedAttachmentData = Cryptography.encryptAttachmentData(unencryptedAttachmentData, outKey: &encryptionKey, outDigest: &digest) else {
print("[Loki] Couldn't encrypt attachment.")
return seal.reject(LokiDotNetAPIError.encryptionFailed)
return seal.reject(DotNetAPIError.encryptionFailed)
}
attachment.encryptionKey = encryptionKey as Data
attachment.digest = digest as Data
@ -125,9 +126,9 @@ public class LokiDotNetAPI : NSObject {
data = unencryptedAttachmentData
}
// Check the file size if needed
let isLokiFileServer = (server == LokiFileServerAPI.server)
if isLokiFileServer && data.count > LokiFileServerAPI.maxFileSize {
return seal.reject(LokiDotNetAPIError.maxFileSizeExceeded)
let isLokiFileServer = (server == FileServerAPI.server)
if isLokiFileServer && data.count > FileServerAPI.maxFileSize {
return seal.reject(DotNetAPIError.maxFileSizeExceeded)
}
// Create the request
let url = "\(server)/files"
@ -146,7 +147,7 @@ public class LokiDotNetAPI : NSObject {
// Parse the server ID & download URL
guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse attachment from: \(responseObject).")
return seal.reject(LokiDotNetAPIError.parsingFailed)
return seal.reject(DotNetAPIError.parsingFailed)
}
// Update the attachment
attachment.serverId = serverID
@ -155,7 +156,7 @@ public class LokiDotNetAPI : NSObject {
attachment.save()
seal.fulfill(())
}
let isProxyingRequired = (server == LokiFileServerAPI.server) // Don't proxy open group requests for now
let isProxyingRequired = (server == FileServerAPI.server) // Don't proxy open group requests for now
if isProxyingRequired {
attachment.isUploaded = false
attachment.save()
@ -181,14 +182,14 @@ public class LokiDotNetAPI : NSObject {
let isSuccessful = (200...299) ~= statusCode
guard isSuccessful else {
print("[Loki] Couldn't upload attachment.")
return seal.reject(LokiDotNetAPIError.generic)
return seal.reject(DotNetAPIError.generic)
}
parseResponse(responseObject)
})
task.resume()
}
}
if server == LokiFileServerAPI.server {
if server == FileServerAPI.server {
DispatchQueue.global(qos: .userInitiated).async {
proceed(with: "loki") // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
}
@ -211,7 +212,7 @@ internal extension Promise {
return recover2 { error -> Promise<T> in
if let error = error as? NetworkManagerError, (error.statusCode == 401 || error.statusCode == 403) {
print("[Loki] Group chat auth token for: \(server) expired; dropping it.")
LokiDotNetAPI.clearAuthToken(for: server)
DotNetAPI.clearAuthToken(for: server)
}
throw error
}

View File

@ -1,15 +1,16 @@
import PromiseKit
@objc(LKFileServerAPI)
public final class LokiFileServerAPI : LokiDotNetAPI {
public final class FileServerAPI : DotNetAPI {
// MARK: Settings
@objc public static let server = "https://file.getsession.org"
public static let maxFileSize = 10_000_000 // 10 MB
private static let deviceLinkType = "network.loki.messenger.devicemapping"
private static let attachmentType = "net.app.core.oembed"
// MARK: Database
public static let maxFileSize = 10_000_000 // 10 MB
@objc public static let server = "https://file.getsession.org"
// MARK: Storage
override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" }
// MARK: Device Links
@ -41,7 +42,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { 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
throw DotNetAPIError.parsingFailed
}
return Set(data.flatMap { data -> [DeviceLink] in
guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] }
@ -98,7 +99,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 attempt(maxRetryCount: 8, recoveringOn: LokiAPI.workQueue) {
return attempt(maxRetryCount: 8, recoveringOn: SnodeAPI.workQueue) {
LokiFileServerProxy(for: server).perform(request).map2 { _ in }
}.handlingInvalidAuthTokenIfNeeded(for: server).recover2 { error in
print("Couldn't update device links due to error: \(error).")
@ -138,7 +139,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
}
public static func uploadProfilePicture(_ profilePicture: Data) -> Promise<String> {
guard profilePicture.count < maxFileSize else { return Promise(error: LokiDotNetAPIError.maxFileSizeExceeded) }
guard profilePicture.count < maxFileSize else { return Promise(error: DotNetAPIError.maxFileSizeExceeded) }
let url = "\(server)/files"
let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
var error: NSError?
@ -154,7 +155,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).map2 { responseObject in
guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse profile picture from: \(responseObject).")
throw LokiDotNetAPIError.parsingFailed
throw DotNetAPIError.parsingFailed
}
UserDefaults.standard[.lastProfilePictureUpload] = Date()
return downloadURL

View File

@ -1,188 +0,0 @@
import PromiseKit
public extension LokiAPI {
fileprivate static let seedNodePool: Set<String> = [ "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ]
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
internal static var snodeFailureCount: [LokiAPITarget:UInt] = [:]
// TODO: Read/write this directly from/to the database
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
internal static var snodePool: Set<LokiAPITarget> = []
// TODO: Read/write this directly from/to the database
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
internal static var swarmCache: [String:[LokiAPITarget]] = [:]
// MARK: Settings
private static let minimumSnodePoolCount = 32
private static let minimumSwarmSnodeCount = 2
private static let targetSwarmSnodeCount = 2
internal static let snodeFailureThreshold = 2
// MARK: Internal API
internal static func getRandomSnode() -> Promise<LokiAPITarget> {
if snodePool.count < minimumSnodePoolCount {
storage.dbReadConnection.read { transaction in
snodePool = storage.getSnodePool(in: transaction)
}
}
if snodePool.count < minimumSnodePoolCount {
let target = seedNodePool.randomElement()!
let url = "\(target)/json_rpc"
let parameters: JSON = [
"method" : "get_n_service_nodes",
"params" : [
"active_only" : true,
"fields" : [
"public_ip" : true, "storage_port" : true, "pubkey_ed25519" : true, "pubkey_x25519" : true
]
]
]
print("[Loki] Populating snode pool using: \(target).")
let (promise, seal) = Promise<LokiAPITarget>.pending()
attempt(maxRetryCount: 4, recoveringOn: LokiAPI.workQueue) {
HTTP.execute(.post, url, parameters: parameters).map2 { json -> LokiAPITarget in
guard let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw LokiAPIError.randomSnodePoolUpdatingFailed }
snodePool = try Set(rawTargets.flatMap { rawTarget in
guard let address = rawTarget["public_ip"] as? String, let port = rawTarget["storage_port"] as? Int, let ed25519PublicKey = rawTarget["pubkey_ed25519"] as? String, let x25519PublicKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else {
print("[Loki] Failed to parse target from: \(rawTarget).")
return nil
}
return LokiAPITarget(address: "https://\(address)", port: UInt16(port), publicKeySet: LokiAPITarget.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
})
// randomElement() uses the system's default random generator, which is cryptographically secure
return snodePool.randomElement()!
}
}.done2 { snode in
seal.fulfill(snode)
try! Storage.writeSync { transaction in
print("[Loki] Persisting snode pool to database.")
storage.setSnodePool(LokiAPI.snodePool, in: transaction)
}
}.catch2 { error in
print("[Loki] Failed to contact seed node at: \(target).")
seal.reject(error)
}
return promise
} else {
return Promise<LokiAPITarget> { seal in
// randomElement() uses the system's default random generator, which is cryptographically secure
seal.fulfill(snodePool.randomElement()!)
}
}
}
internal static func getSwarm(for hexEncodedPublicKey: String, isForcedReload: Bool = false) -> Promise<[LokiAPITarget]> {
if swarmCache[hexEncodedPublicKey] == nil {
storage.dbReadConnection.read { transaction in
swarmCache[hexEncodedPublicKey] = storage.getSwarm(for: hexEncodedPublicKey, in: transaction)
}
}
if let cachedSwarm = swarmCache[hexEncodedPublicKey], cachedSwarm.count >= minimumSwarmSnodeCount && !isForcedReload {
return Promise<[LokiAPITarget]> { $0.fulfill(cachedSwarm) }
} else {
print("[Loki] Getting swarm for: \(hexEncodedPublicKey).")
let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ]
return getRandomSnode().then2 {
invoke(.getSwarm, on: $0, associatedWith: hexEncodedPublicKey, parameters: parameters)
}.map2 { rawSnodes in
let swarm = parseTargets(from: rawSnodes)
swarmCache[hexEncodedPublicKey] = swarm
try! Storage.writeSync { transaction in
storage.setSwarm(swarm, for: hexEncodedPublicKey, in: transaction)
}
return swarm
}
}
}
internal static func getTargetSnodes(for hexEncodedPublicKey: String) -> Promise<[LokiAPITarget]> {
// shuffled() uses the system's default random generator, which is cryptographically secure
return getSwarm(for: hexEncodedPublicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) }
}
internal static func dropSnodeFromSnodePool(_ target: LokiAPITarget) {
LokiAPI.snodePool.remove(target)
try! Storage.writeSync { transaction in
storage.dropSnodeFromSnodePool(target, in: transaction)
}
}
internal static func dropSnodeFromSwarmIfNeeded(_ target: LokiAPITarget, hexEncodedPublicKey: String) {
let swarm = LokiAPI.swarmCache[hexEncodedPublicKey]
if var swarm = swarm, let index = swarm.firstIndex(of: target) {
swarm.remove(at: index)
LokiAPI.swarmCache[hexEncodedPublicKey] = swarm
try! Storage.writeSync { transaction in
storage.setSwarm(swarm, for: hexEncodedPublicKey, in: transaction)
}
}
}
// MARK: Public API
@objc public static func clearSnodePool() {
snodePool.removeAll()
try! Storage.writeSync { transaction in
storage.clearSnodePool(in: transaction)
}
}
// MARK: Parsing
private static func parseTargets(from rawResponse: Any) -> [LokiAPITarget] {
guard let json = rawResponse as? JSON, let rawTargets = json["snodes"] as? [JSON] else {
print("[Loki] Failed to parse targets from: \(rawResponse).")
return []
}
return rawTargets.flatMap { rawTarget in
guard let address = rawTarget["ip"] as? String, let portAsString = rawTarget["port"] as? String, let port = UInt16(portAsString), let ed25519PublicKey = rawTarget["pubkey_ed25519"] as? String, let x25519PublicKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else {
print("[Loki] Failed to parse target from: \(rawTarget).")
return nil
}
return LokiAPITarget(address: "https://\(address)", port: port, publicKeySet: LokiAPITarget.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
}
}
}
// MARK: Snode Error Handling
internal extension Promise {
internal func handlingSnodeErrorsIfNeeded(for target: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise<T> {
return recover2 { error -> Promise<T> in
if let error = error as? LokiHTTPClient.HTTPError {
switch error.statusCode {
case 0, 400, 500, 503:
// The snode is unreachable
let oldFailureCount = LokiAPI.snodeFailureCount[target] ?? 0
let newFailureCount = oldFailureCount + 1
LokiAPI.snodeFailureCount[target] = newFailureCount
print("[Loki] Couldn't reach snode at: \(target); setting failure count to \(newFailureCount).")
if newFailureCount >= LokiAPI.snodeFailureThreshold {
print("[Loki] Failure threshold reached for: \(target); dropping it.")
LokiAPI.dropSnodeFromSwarmIfNeeded(target, hexEncodedPublicKey: hexEncodedPublicKey)
LokiAPI.dropSnodeFromSnodePool(target)
LokiAPI.snodeFailureCount[target] = 0
}
case 406:
print("[Loki] The user's clock is out of sync with the service node network.")
throw LokiAPI.LokiAPIError.clockOutOfSync
case 421:
// The snode isn't associated with the given public key anymore
print("[Loki] Invalidating swarm for: \(hexEncodedPublicKey).")
LokiAPI.dropSnodeFromSwarmIfNeeded(target, hexEncodedPublicKey: hexEncodedPublicKey)
case 432:
// The PoW difficulty is too low
if case LokiHTTPClient.HTTPError.networkError(_, let result, _) = error, let json = result as? JSON, let powDifficulty = json["difficulty"] as? Int {
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
LokiAPI.powDifficulty = UInt(powDifficulty)
} else {
print("[Loki] Failed to update proof of work difficulty.")
}
break
default: break
}
}
throw error
}
}
}

View File

@ -1,206 +0,0 @@
import PromiseKit
@objc(LKAPI)
public final class LokiAPI : NSObject {
internal static let workQueue = DispatchQueue(label: "LokiAPI.workQueue", qos: .userInitiated)
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
// MARK: Settings
private static let maxRetryCount: UInt = 4
private static let defaultTimeout: TimeInterval = 20
internal static var powDifficulty: UInt = 1
/// - Note: Changing this on the fly is not recommended.
internal static var useOnionRequests = true
// MARK: Types
public typealias RawResponse = Any
@objc public class LokiAPIError : NSError { // Not called `Error` for Obj-C interoperablity
@objc public static let proofOfWorkCalculationFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Failed to calculate proof of work." ])
@objc public static let messageConversionFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Failed to construct message." ])
@objc public static let clockOutOfSync = LokiAPIError(domain: "LokiAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Your clock is out of sync with the service node network." ])
@objc public static let randomSnodePoolUpdatingFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Failed to update random service node pool." ])
@objc public static let missingSnodeVersion = LokiAPIError(domain: "LokiAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Missing service node version." ])
}
public typealias MessageListPromise = Promise<[SSKProtoEnvelope]>
public typealias RawResponsePromise = Promise<RawResponse>
// MARK: Lifecycle
override private init() { }
// MARK: Internal API
internal static func invoke(_ method: LokiAPITarget.Method, on target: LokiAPITarget, associatedWith hexEncodedPublicKey: String,
parameters: JSON, headers: [String:String]? = nil, timeout: TimeInterval? = nil) -> RawResponsePromise {
let url = URL(string: "\(target.address):\(target.port)/storage_rpc/v1")!
if useOnionRequests {
return OnionRequestAPI.sendOnionRequest(invoking: method, on: target, with: parameters, associatedWith: hexEncodedPublicKey).map2 { $0 as Any }
} else {
let request = TSRequest(url: url, method: "POST", parameters: [ "method" : method.rawValue, "params" : parameters ])
if let headers = headers { request.allHTTPHeaderFields = headers }
request.timeoutInterval = timeout ?? defaultTimeout
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global(qos: .default))
.map2 { $0.responseObject }
.handlingSnodeErrorsIfNeeded(for: target, associatedWith: hexEncodedPublicKey)
.recoveringNetworkErrorsIfNeeded()
}
}
internal static func getRawMessages(from target: LokiAPITarget) -> RawResponsePromise {
let lastHashValue = getLastMessageHashValue(for: target) ?? ""
let parameters = [ "pubKey" : getUserHexEncodedPublicKey(), "lastHash" : lastHashValue ]
return invoke(.getMessages, on: target, associatedWith: getUserHexEncodedPublicKey(), parameters: parameters)
}
// MARK: Public API
public static func getMessages() -> Promise<Set<MessageListPromise>> {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) {
getTargetSnodes(for: getUserHexEncodedPublicKey()).mapValues2 { targetSnode in
getRawMessages(from: targetSnode).map2 { parseRawMessagesResponse($0, from: targetSnode) }
}.map2 { Set($0) }
}
}
@objc(sendSignalMessage:onP2PSuccess:)
public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> AnyPromise {
let promise = sendSignalMessage(signalMessage, onP2PSuccess: onP2PSuccess).mapValues2 { AnyPromise.from($0) }.map2 { Set($0) }
return AnyPromise.from(promise)
}
public static func sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> Promise<Set<RawResponsePromise>> {
// Convert the message to a Loki message
guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: LokiAPIError.messageConversionFailed) }
let notificationCenter = NotificationCenter.default
let destination = lokiMessage.destination
notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp))
// Calculate proof of work
return lokiMessage.calculatePoW().then2 { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
notificationCenter.post(name: .routing, object: NSNumber(value: signalMessage.timestamp))
// Get the target snodes
return getTargetSnodes(for: destination).map2 { snodes in
notificationCenter.post(name: .messageSending, object: NSNumber(value: signalMessage.timestamp))
let parameters = lokiMessageWithPoW.toJSON()
return Set(snodes.map { snode in
// Send the message to the target snode
return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) {
invoke(.sendMessage, on: snode, associatedWith: destination, parameters: parameters)
}.map2 { rawResponse in
if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int {
guard powDifficulty != LokiAPI.powDifficulty, powDifficulty < 100 else { return rawResponse }
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
LokiAPI.powDifficulty = UInt(powDifficulty)
} else {
print("[Loki] Failed to update proof of work difficulty from: \(rawResponse).")
}
return rawResponse
}
})
}
}
}
// MARK: Parsing
// The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions.
internal static func parseRawMessagesResponse(_ rawResponse: Any, from target: LokiAPITarget) -> [SSKProtoEnvelope] {
guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] }
updateLastMessageHashValueIfPossible(for: target, from: rawMessages)
let newRawMessages = removeDuplicates(from: rawMessages)
let newMessages = parseProtoEnvelopes(from: newRawMessages)
let newMessageCount = newMessages.count
return newMessages
}
private static func updateLastMessageHashValueIfPossible(for target: LokiAPITarget, from rawMessages: [JSON]) {
if let lastMessage = rawMessages.last, let hashValue = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? Int {
setLastMessageHashValue(for: target, hashValue: hashValue, expirationDate: UInt64(expirationDate))
// FIXME: Move this out of here
if UserDefaults.standard[.isUsingFullAPNs] {
LokiPushNotificationManager.acknowledgeDelivery(forMessageWithHash: hashValue, expiration: expirationDate, hexEncodedPublicKey: getUserHexEncodedPublicKey())
}
} else if (!rawMessages.isEmpty) {
print("[Loki] Failed to update last message hash value from: \(rawMessages).")
}
}
private static func removeDuplicates(from rawMessages: [JSON]) -> [JSON] {
var receivedMessageHashValues = getReceivedMessageHashValues() ?? []
return rawMessages.filter { rawMessage in
guard let hashValue = rawMessage["hash"] as? String else {
print("[Loki] Missing hash value for message: \(rawMessage).")
return false
}
let isDuplicate = receivedMessageHashValues.contains(hashValue)
receivedMessageHashValues.insert(hashValue)
setReceivedMessageHashValues(to: receivedMessageHashValues)
return !isDuplicate
}
}
private static func parseProtoEnvelopes(from rawMessages: [JSON]) -> [SSKProtoEnvelope] {
return rawMessages.compactMap { rawMessage in
guard let base64EncodedData = rawMessage["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else {
print("[Loki] Failed to decode data for message: \(rawMessage).")
return nil
}
guard let envelope = try? LokiMessageWrapper.unwrap(data: data) else {
print("[Loki] Failed to unwrap data for message: \(rawMessage).")
return nil
}
return envelope
}
}
// MARK: Message Hash Caching
private static func getLastMessageHashValue(for target: LokiAPITarget) -> String? {
var result: String? = nil
// Uses a read/write connection because getting the last message hash value also removes expired messages as needed
// TODO: This shouldn't be the case; a getter shouldn't have an unexpected side effect
try! Storage.writeSync { transaction in
result = storage.getLastMessageHash(forSnode: target.address, transaction: transaction)
}
return result
}
private static func setLastMessageHashValue(for target: LokiAPITarget, hashValue: String, expirationDate: UInt64) {
try! Storage.writeSync { transaction in
storage.setLastMessageHash(forSnode: 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
result = transaction.object(forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection) as! Set<String>?
}
return result
}
private static func setReceivedMessageHashValues(to receivedMessageHashValues: Set<String>) {
try! Storage.writeSync { transaction in
transaction.setObject(receivedMessageHashValues, forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection)
}
}
}
// MARK: Error Handling
private extension Promise {
fileprivate func recoveringNetworkErrorsIfNeeded() -> Promise<T> {
return recover2 { error -> Promise<T> in
switch error {
case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError
case LokiHTTPClient.HTTPError.networkError(_, _, let underlyingError): throw underlyingError ?? error
default: throw error
}
}
}
}

View File

@ -1,8 +1,8 @@
import PromiseKit
public struct LokiMessage {
/// The hex encoded public key of the receiver.
let destination: String
/// The hex encoded public key of the recipient.
let recipientPublicKey: String
/// The content of the message.
let data: LosslessStringConvertible
/// The time to live for the message in milliseconds.
@ -19,7 +19,7 @@ public struct LokiMessage {
private(set) var nonce: String? = nil
private init(destination: String, data: LosslessStringConvertible, ttl: UInt64, isPing: Bool) {
self.destination = destination
self.recipientPublicKey = destination
self.data = data
self.ttl = ttl
self.isPing = isPing
@ -31,7 +31,7 @@ public struct LokiMessage {
public static func from(signalMessage: SignalMessage) -> LokiMessage? {
// To match the desktop application, we have to wrap the data in an envelope and then wrap that in a websocket object
do {
let wrappedMessage = try LokiMessageWrapper.wrap(message: signalMessage)
let wrappedMessage = try MessageWrapper.wrap(message: signalMessage)
let data = wrappedMessage.base64EncodedString()
let destination = signalMessage.recipientPublicKey
var ttl = TTLUtilities.fallbackMessageTTL
@ -52,20 +52,20 @@ public struct LokiMessage {
DispatchQueue.global(qos: .userInitiated).async {
let now = NSDate.ows_millisecondTimeStamp()
let dataAsString = self.data as! String // Safe because of how from(signalMessage:with:) is implemented
if let nonce = ProofOfWork.calculate(data: dataAsString, pubKey: self.destination, timestamp: now, ttl: self.ttl) {
if let nonce = ProofOfWork.calculate(data: dataAsString, pubKey: self.recipientPublicKey, timestamp: now, ttl: self.ttl) {
var result = self
result.timestamp = now
result.nonce = nonce
seal.fulfill(result)
} else {
seal.reject(LokiAPI.LokiAPIError.proofOfWorkCalculationFailed)
seal.reject(SnodeAPI.SnodeAPIError.proofOfWorkCalculationFailed)
}
}
}
}
public func toJSON() -> JSON {
var result = [ "pubKey" : destination, "data" : data.description, "ttl" : String(ttl) ]
var result = [ "pubKey" : recipientPublicKey, "data" : data.description, "ttl" : String(ttl) ]
if let timestamp = timestamp, let nonce = nonce {
result["timestamp"] = String(timestamp)
result["nonce"] = nonce

View File

@ -1,5 +1,5 @@
public enum LokiMessageWrapper {
public enum MessageWrapper {
public enum Error : LocalizedError {
case failedToWrapData

View File

@ -18,7 +18,7 @@ extension OnionRequestAPI {
}
/// - Note: Sync. Don't call from the main thread.
private static func encrypt(_ plaintext: Data, forSnode snode: LokiAPITarget) throws -> EncryptionResult {
private static func encrypt(_ plaintext: Data, forSnode snode: Snode) throws -> EncryptionResult {
guard !Thread.isMainThread else { preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.") }
guard let hexEncodedSnodeX25519PublicKey = snode.publicKeySet?.x25519Key else { throw Error.snodePublicKeySetMissing }
let snodeX25519PublicKey = Data(hex: hexEncodedSnodeX25519PublicKey)
@ -31,7 +31,7 @@ extension OnionRequestAPI {
}
/// Encrypts `payload` for `snode` and returns the result. Use this to build the core of an onion request.
internal static func encrypt(_ payload: JSON, forTargetSnode snode: LokiAPITarget) -> Promise<EncryptionResult> {
internal static func encrypt(_ payload: JSON, forTargetSnode snode: Snode) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
do {
@ -51,7 +51,7 @@ extension OnionRequestAPI {
}
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
internal static func encryptHop(from lhs: LokiAPITarget, to rhs: LokiAPITarget, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
internal static func encryptHop(from lhs: Snode, to rhs: Snode, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
let parameters: JSON = [

View File

@ -3,12 +3,12 @@ import PromiseKit
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
public enum OnionRequestAPI {
public static var guardSnodes: Set<LokiAPITarget> = []
public static var guardSnodes: Set<Snode> = []
public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user
private static var snodePool: Set<LokiAPITarget> {
let unreliableSnodes = Set(LokiAPI.snodeFailureCount.keys)
return LokiAPI.snodePool.subtracting(unreliableSnodes)
private static var snodePool: Set<Snode> {
let unreliableSnodes = Set(SnodeAPI.snodeFailureCount.keys)
return SnodeAPI.snodePool.subtracting(unreliableSnodes)
}
// MARK: Settings
@ -40,14 +40,14 @@ public enum OnionRequestAPI {
}
// MARK: Path
public typealias Path = [LokiAPITarget]
public typealias Path = [Snode]
// MARK: Onion Building Result
private typealias OnionBuildingResult = (guardSnode: LokiAPITarget, finalEncryptionResult: EncryptionResult, targetSnodeSymmetricKey: Data)
private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: EncryptionResult, targetSnodeSymmetricKey: Data)
// MARK: Private API
/// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise.
private static func testSnode(_ snode: LokiAPITarget) -> Promise<Void> {
private static func testSnode(_ snode: Snode) -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending()
DispatchQueue.global(qos: .userInitiated).async {
let url = "\(snode.address):\(snode.port)/get_stats/v1"
@ -69,22 +69,22 @@ public enum OnionRequestAPI {
/// Finds `guardSnodeCount` guard snodes to use for path building. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available.
private static func getGuardSnodes() -> Promise<Set<LokiAPITarget>> {
private static func getGuardSnodes() -> Promise<Set<Snode>> {
if guardSnodes.count >= guardSnodeCount {
return Promise<Set<LokiAPITarget>> { $0.fulfill(guardSnodes) }
return Promise<Set<Snode>> { $0.fulfill(guardSnodes) }
} else {
print("[Loki] [Onion Request API] Populating guard snode cache.")
return LokiAPI.getRandomSnode().then2 { _ -> Promise<Set<LokiAPITarget>> in // Just used to populate the snode pool
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<Set<Snode>> in // Just used to populate the snode pool
var unusedSnodes = snodePool // Sync on LokiAPI.workQueue
guard unusedSnodes.count >= guardSnodeCount else { throw Error.insufficientSnodes }
func getGuardSnode() -> Promise<LokiAPITarget> {
func getGuardSnode() -> Promise<Snode> {
// randomElement() uses the system's default random generator, which is cryptographically secure
guard let candidate = unusedSnodes.randomElement() else { return Promise<LokiAPITarget> { $0.reject(Error.insufficientSnodes) } }
guard let candidate = unusedSnodes.randomElement() else { return Promise<Snode> { $0.reject(Error.insufficientSnodes) } }
unusedSnodes.remove(candidate) // All used snodes should be unique
print("[Loki] [Onion Request API] Testing guard snode: \(candidate).")
// Loop until a reliable guard snode is found
return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in
withDelay(0.25, completionQueue: LokiAPI.workQueue) { getGuardSnode() }
withDelay(0.25, completionQueue: SnodeAPI.workQueue) { getGuardSnode() }
}
}
let promises = (0..<guardSnodeCount).map { _ in getGuardSnode() }
@ -104,7 +104,7 @@ public enum OnionRequestAPI {
DispatchQueue.main.async {
NotificationCenter.default.post(name: .buildingPaths, object: nil)
}
return LokiAPI.getRandomSnode().then2 { _ -> Promise<[Path]> in // Just used to populate the snode pool
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<[Path]> in // Just used to populate the snode pool
return getGuardSnodes().map2 { guardSnodes -> [Path] in
var unusedSnodes = snodePool.subtracting(guardSnodes)
let pathSnodeCount = guardSnodeCount * pathSize - guardSnodeCount
@ -137,7 +137,7 @@ public enum OnionRequestAPI {
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
///
/// - Note: Exposed for testing purposes.
internal static func getPath(excluding snode: LokiAPITarget) -> Promise<Path> {
internal static func getPath(excluding snode: Snode) -> Promise<Path> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
if paths.count < pathCount {
let storage = OWSPrimaryStorage.shared()
@ -167,13 +167,13 @@ public enum OnionRequestAPI {
}
}
private static func dropGuardSnode(_ snode: LokiAPITarget) {
private static func dropGuardSnode(_ snode: Snode) {
guardSnodes = guardSnodes.filter { $0 != snode }
}
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: JSON, targetedAt snode: LokiAPITarget) -> Promise<OnionBuildingResult> {
var guardSnode: LokiAPITarget!
private static func buildOnion(around payload: JSON, targetedAt snode: Snode) -> Promise<OnionBuildingResult> {
var guardSnode: Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the target snode
var encryptionResult: EncryptionResult!
return getPath(excluding: snode).then2 { path -> Promise<EncryptionResult> in
@ -204,9 +204,9 @@ public enum OnionRequestAPI {
// MARK: Internal API
/// Sends an onion request to `snode`. Builds new paths as needed.
internal static func sendOnionRequest(invoking method: LokiAPITarget.Method, on snode: LokiAPITarget, with parameters: JSON, associatedWith hexEncodedPublicKey: String) -> Promise<JSON> {
internal static func sendOnionRequest(invoking method: Snode.Method, on snode: Snode, with parameters: JSON, associatedWith publicKey: String) -> Promise<JSON> {
let (promise, seal) = Promise<JSON>.pending()
var guardSnode: LokiAPITarget!
var guardSnode: Snode!
DispatchQueue.global(qos: .userInitiated).async {
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
buildOnion(around: payload, targetedAt: snode).done2 { intermediate in
@ -232,7 +232,7 @@ public enum OnionRequestAPI {
let bodyAsString = json["body"] as? String, let statusCode = json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) }
if statusCode == 406 { // Clock out of sync
print("[Loki] The user's clock is out of sync with the service node network.")
seal.reject(LokiAPI.LokiAPIError.clockOutOfSync)
seal.reject(SnodeAPI.SnodeAPIError.clockOutOfSync)
} else {
guard let bodyAsData = bodyAsString.data(using: .utf8),
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) }
@ -254,51 +254,10 @@ public enum OnionRequestAPI {
dropAllPaths() // A snode in the path is bad; retry with a different path
dropGuardSnode(guardSnode)
}
promise.handlingErrorsIfNeeded(forTargetSnode: snode, associatedWith: hexEncodedPublicKey)
promise.recover2 { error -> Promise<JSON> in
guard case OnionRequestAPI.Error.httpRequestFailedAtTargetSnode(let statusCode, let json) = error else { throw error }
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
}
return promise
}
}
// MARK: Target Snode Error Handling
private extension Promise where T == JSON {
func handlingErrorsIfNeeded(forTargetSnode snode: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise<JSON> {
return recover2 { error -> Promise<JSON> in // Must be invoked on LokiAPI.errorHandlingQueue
// The code below is very similar to that in LokiAPI.handlingSnodeErrorsIfNeeded(for:associatedWith:), but unfortunately slightly
// different due to the fact that OnionRequestAPI uses the newer HTTP API, whereas LokiAPI still uses TSNetworkManager
guard case OnionRequestAPI.Error.httpRequestFailedAtTargetSnode(let statusCode, let json) = error else { throw error }
switch statusCode {
case 0, 400, 500, 503:
// The snode is unreachable
let oldFailureCount = LokiAPI.snodeFailureCount[snode] ?? 0
let newFailureCount = oldFailureCount + 1
LokiAPI.snodeFailureCount[snode] = newFailureCount
print("[Loki] Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).")
if newFailureCount >= LokiAPI.snodeFailureThreshold {
print("[Loki] Failure threshold reached for: \(snode); dropping it.")
LokiAPI.dropSnodeFromSwarmIfNeeded(snode, hexEncodedPublicKey: hexEncodedPublicKey)
LokiAPI.dropSnodeFromSnodePool(snode)
LokiAPI.snodeFailureCount[snode] = 0
}
case 406:
print("[Loki] The user's clock is out of sync with the service node network.")
throw LokiAPI.LokiAPIError.clockOutOfSync
case 421:
// The snode isn't associated with the given public key anymore
print("[Loki] Invalidating swarm for: \(hexEncodedPublicKey).")
LokiAPI.dropSnodeFromSwarmIfNeeded(snode, hexEncodedPublicKey: hexEncodedPublicKey)
case 432:
// The proof of work difficulty is too low
if let powDifficulty = json["difficulty"] as? Int {
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
LokiAPI.powDifficulty = UInt(powDifficulty)
} else {
print("[Loki] Failed to update proof of work difficulty.")
}
break
default: break
}
throw error
}
}
}

View File

@ -1,7 +1,7 @@
import PromiseKit
@objc(LKPublicChatAPI)
public final class LokiPublicChatAPI : LokiDotNetAPI {
public final class LokiPublicChatAPI : DotNetAPI {
private static var moderators: [String:[UInt64:Set<String>]] = [:] // Server URL to (channel ID to set of moderator IDs)
@objc public static let defaultChats: [LokiPublicChat] = [] // Currently unused
@ -94,7 +94,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { 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
throw DotNetAPIError.parsingFailed
}
return rawMessages.flatMap { message in
let isDeleted = (message["is_deleted"] as? Int == 1)
@ -174,7 +174,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).")
let (promise, seal) = Promise<LokiPublicChatMessage>.pending()
DispatchQueue.global(qos: .userInitiated).async { [privateKey = userKeyPair.privateKey] in
guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(LokiDotNetAPIError.signingFailed) }
guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(DotNetAPIError.signingFailed) }
attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<LokiPublicChatMessage> in
let url = URL(string: "\(server)/channels/\(channel)/messages")!
@ -189,7 +189,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String,
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw LokiDotNetAPIError.parsingFailed
throw DotNetAPIError.parsingFailed
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: getUserHexEncodedPublicKey(), displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
@ -220,7 +220,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { 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
throw DotNetAPIError.parsingFailed
}
return deletions.flatMap { deletion in
guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else {
@ -269,7 +269,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { 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
throw DotNetAPIError.parsingFailed
}
try! Storage.writeSync { transaction in
data.forEach { data in
@ -398,7 +398,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
let countInfo = data["counts"] as? JSON,
let memberCount = countInfo["subscribers"] as? Int else {
print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw LokiDotNetAPIError.parsingFailed
throw DotNetAPIError.parsingFailed
}
let storage = OWSPrimaryStorage.shared()
try! Storage.writeSync { transaction in
@ -460,7 +460,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { 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
throw DotNetAPIError.parsingFailed
}
let moderatorsAsSet = Set(moderators);
if self.moderators.keys.contains(server) {

View File

@ -8,7 +8,6 @@ public final class LokiPublicChatPoller : NSObject {
private var pollForModeratorsTimer: Timer? = nil
private var pollForDisplayNamesTimer: Timer? = nil
private var hasStarted = false
private let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
// MARK: Settings
private let pollForNewMessagesInterval: TimeInterval = 4
@ -56,7 +55,7 @@ public final class LokiPublicChatPoller : NSObject {
public func pollForNewMessages() -> Promise<Void> {
let publicChat = self.publicChat
let userHexEncodedPublicKey = self.userHexEncodedPublicKey
let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
return LokiPublicChatAPI.getMessages(for: publicChat.channel, on: publicChat.server).done(on: DispatchQueue.global(qos: .default)) { messages in
let uniqueHexEncodedPublicKeys = Set(messages.map { $0.hexEncodedPublicKey })
func proceed() {
@ -194,13 +193,13 @@ public final class LokiPublicChatPoller : NSObject {
return timeSinceLastUpdate > MultiDeviceProtocol.deviceLinkUpdateInterval
}
if !hexEncodedPublicKeysToUpdate.isEmpty {
LokiFileServerAPI.getDeviceLinks(associatedWith: hexEncodedPublicKeysToUpdate).done(on: DispatchQueue.global(qos: .default)) { _ in
FileServerAPI.getDeviceLinks(associatedWith: hexEncodedPublicKeysToUpdate).done(on: DispatchQueue.global(qos: .default)) { _ in
proceed()
hexEncodedPublicKeysToUpdate.forEach {
MultiDeviceProtocol.lastDeviceLinkUpdate[$0] = Date() // TODO: Doing this from a global queue seems a bit iffy
}
}.catch(on: DispatchQueue.global(qos: .default)) { error in
if (error as? LokiDotNetAPI.LokiDotNetAPIError) == LokiDotNetAPI.LokiDotNetAPIError.parsingFailed {
if (error as? DotNetAPI.DotNetAPIError) == DotNetAPI.DotNetAPIError.parsingFailed {
// Don't immediately re-fetch in case of failure due to a parsing error
hexEncodedPublicKeysToUpdate.forEach {
MultiDeviceProtocol.lastDeviceLinkUpdate[$0] = Date() // TODO: Doing this from a global queue seems a bit iffy

View File

@ -1,16 +1,14 @@
import PromiseKit
@objc(LKPoller)
public final class LokiPoller : NSObject {
private let onMessagesReceived: ([SSKProtoEnvelope]) -> Void
public final class Poller : NSObject {
private let storage = OWSPrimaryStorage.shared()
private var hasStarted = false
private var hasStopped = false
private var usedSnodes = Set<LokiAPITarget>()
private var isPolling = false
private var usedSnodes = Set<Snode>()
private var pollCount = 0
// MARK: Settings
private static let pollInterval: TimeInterval = 1
private static let pollInterval: TimeInterval = 2
private static let retryInterval: TimeInterval = 0.25
/// After polling a given snode this many times we always switch to a new one.
///
@ -29,41 +27,32 @@ public final class LokiPoller : NSObject {
}
}
// MARK: Initialization
@objc public init(onMessagesReceived: @escaping ([SSKProtoEnvelope]) -> Void) {
self.onMessagesReceived = onMessagesReceived
super.init()
}
// MARK: Public API
@objc public func startIfNeeded() {
guard !hasStarted else { return }
guard !isPolling else { return }
print("[Loki] Started polling.")
hasStarted = true
hasStopped = false
isPolling = true
setUpPolling()
}
@objc public func stopIfNeeded() {
guard !hasStopped else { return }
@objc public func stop() {
print("[Loki] Stopped polling.")
hasStarted = false
hasStopped = true
isPolling = false
usedSnodes.removeAll()
}
// MARK: Private API
private func setUpPolling() {
guard !hasStopped else { return }
LokiAPI.getSwarm(for: getUserHexEncodedPublicKey(), isForcedReload: true).then2 { [weak self] _ -> Promise<Void> in
guard isPolling else { return }
SnodeAPI.getSwarm(for: getUserHexEncodedPublicKey(), isForcedReload: true).then2 { [weak self] _ -> Promise<Void> in
guard let strongSelf = self else { return Promise { $0.fulfill(()) } }
strongSelf.usedSnodes.removeAll()
let (promise, seal) = Promise<Void>.pending()
strongSelf.pollNextSnode(seal: seal)
return promise
}.ensure(on: DispatchQueue.main) { [weak self] in // Timers don't do well on background queues
guard let strongSelf = self, !strongSelf.hasStopped else { return }
Timer.scheduledTimer(withTimeInterval: LokiPoller.retryInterval, repeats: false) { _ in
guard let strongSelf = self, strongSelf.isPolling else { return }
Timer.scheduledTimer(withTimeInterval: Poller.retryInterval, repeats: false) { _ in
guard let strongSelf = self else { return }
strongSelf.setUpPolling()
}
@ -72,7 +61,7 @@ public final class LokiPoller : NSObject {
private func pollNextSnode(seal: Resolver<Void>) {
let userPublicKey = getUserHexEncodedPublicKey()
let swarm = LokiAPI.swarmCache[userPublicKey] ?? []
let swarm = SnodeAPI.swarmCache[userPublicKey] ?? []
let unusedSnodes = Set(swarm).subtracting(usedSnodes)
if !unusedSnodes.isEmpty {
// randomElement() uses the system's default random generator, which is cryptographically secure
@ -85,7 +74,7 @@ public final class LokiPoller : NSObject {
self?.pollCount = 0
} else {
print("[Loki] Polling \(nextSnode) failed; dropping it and switching to next snode.")
LokiAPI.dropSnodeFromSwarmIfNeeded(nextSnode, hexEncodedPublicKey: userPublicKey)
SnodeAPI.dropSnodeFromSwarmIfNeeded(nextSnode, publicKey: userPublicKey)
}
self?.pollNextSnode(seal: seal)
}
@ -94,19 +83,30 @@ public final class LokiPoller : NSObject {
}
}
private func poll(_ target: LokiAPITarget, seal longTermSeal: Resolver<Void>) -> Promise<Void> {
guard !hasStopped else { return Promise { $0.fulfill(()) } }
return LokiAPI.getRawMessages(from: target).then(on: DispatchQueue.main) { [weak self] rawResponse -> Promise<Void> in
guard let strongSelf = self, !strongSelf.hasStopped else { return Promise { $0.fulfill(()) } }
let messages = LokiAPI.parseRawMessagesResponse(rawResponse, from: target)
strongSelf.onMessagesReceived(messages)
private func poll(_ snode: Snode, seal longTermSeal: Resolver<Void>) -> Promise<Void> {
guard isPolling else { return Promise { $0.fulfill(()) } }
let userPublicKey = getUserHexEncodedPublicKey()
return SnodeAPI.getRawMessages(from: snode, associatedWith: userPublicKey).then(on: DispatchQueue.main) { [weak self] rawResponse -> Promise<Void> in
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
let messages = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: userPublicKey)
if !messages.isEmpty {
print("[Loki] Received \(messages.count) new message(s).")
}
messages.forEach { message in
do {
let data = try message.serializedData()
SSKEnvironment.shared.messageReceiver.handleReceivedEnvelopeData(data)
} catch {
print("[Loki] Failed to deserialize envelope due to error: \(error).")
}
}
strongSelf.pollCount += 1
if strongSelf.pollCount == LokiPoller.maxPollCount {
if strongSelf.pollCount == Poller.maxPollCount {
throw Error.pollLimitReached
} else {
return withDelay(LokiPoller.pollInterval, completionQueue: LokiAPI.workQueue) {
guard let strongSelf = self, !strongSelf.hasStopped else { return Promise { $0.fulfill(()) } }
return strongSelf.poll(target, seal: longTermSeal)
return withDelay(Poller.pollInterval, completionQueue: SnodeAPI.workQueue) {
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
return strongSelf.poll(snode, seal: longTermSeal)
}
}
}

View File

@ -13,7 +13,7 @@ public enum LokiRSSFeedProxy {
}
public static func fetchContent(for url: String) -> Promise<String> {
let server = LokiFileServerAPI.server
let server = FileServerAPI.server
let endpoints = [ "messenger-updates/feed" : "loki/v1/rss/messenger", "loki.network/feed" : "loki/v1/rss/loki" ]
let endpoint = endpoints.first { url.lowercased().contains($0.key) }!.value
let url = URL(string: server + "/" + endpoint)!

View File

@ -1,8 +1,5 @@
public typealias Snode = LokiAPITarget
/// Either a service node or another client if P2P is enabled.
public final class LokiAPITarget : NSObject, NSCoding {
public final class Snode : NSObject, NSCoding {
public let address: String
public let port: UInt16
internal let publicKeySet: KeySet?
@ -55,7 +52,7 @@ public final class LokiAPITarget : NSObject, NSCoding {
// MARK: Equality
override public func isEqual(_ other: Any?) -> Bool {
guard let other = other as? LokiAPITarget else { return false }
guard let other = other as? Snode else { return false }
return address == other.address && port == other.port
}

View File

@ -0,0 +1,336 @@
import PromiseKit
@objc(LKSnodeAPI)
public final class SnodeAPI : NSObject {
internal static let workQueue = DispatchQueue(label: "SnodeAPI.workQueue", qos: .userInitiated) // It's important that this is a serial queue
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
internal static var snodeFailureCount: [Snode:UInt] = [:]
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
internal static var snodePool: Set<Snode> = []
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
internal static var swarmCache: [String:[Snode]] = [:]
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
// MARK: Settings
internal static let snodeFailureThreshold = 2
private static let maxRetryCount: UInt = 4
private static let minimumSnodePoolCount = 32
private static let minimumSwarmSnodeCount = 2
private static let seedNodePool: Set<String> = [ "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ]
private static let targetSwarmSnodeCount = 2
internal static var powDifficulty: UInt = 1
/// - Note: Changing this on the fly is not recommended.
internal static var useOnionRequests = true
// MARK: Error
@objc(LKSnodeAPIError)
public class SnodeAPIError : NSError { // Not called `Error` for Obj-C interoperablity
@objc public static let proofOfWorkCalculationFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Failed to calculate proof of work." ])
@objc public static let messageConversionFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Failed to construct message." ])
@objc public static let clockOutOfSync = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Your clock is out of sync with the service node network." ])
@objc public static let randomSnodePoolUpdatingFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Failed to update random service node pool." ])
@objc public static let missingSnodeVersion = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Missing service node version." ])
}
// MARK: Type Aliases
public typealias MessageListPromise = Promise<[SSKProtoEnvelope]>
public typealias RawResponse = Any
public typealias RawResponsePromise = Promise<RawResponse>
// MARK: Lifecycle
override private init() { }
// MARK: Core
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise {
if useOnionRequests {
return OnionRequestAPI.sendOnionRequest(invoking: method, on: snode, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
} else {
let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in
guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error else { throw error }
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
}
}
}
internal static func getRandomSnode() -> Promise<Snode> {
if snodePool.count < minimumSnodePoolCount {
storage.dbReadConnection.read { transaction in
snodePool = storage.getSnodePool(in: transaction)
}
}
if snodePool.count < minimumSnodePoolCount {
let target = seedNodePool.randomElement()!
let url = "\(target)/json_rpc"
let parameters: JSON = [
"method" : "get_n_service_nodes",
"params" : [
"active_only" : true,
"fields" : [
"public_ip" : true, "storage_port" : true, "pubkey_ed25519" : true, "pubkey_x25519" : true
]
]
]
print("[Loki] Populating snode pool using: \(target).")
let (promise, seal) = Promise<Snode>.pending()
attempt(maxRetryCount: 4, recoveringOn: SnodeAPI.workQueue) {
HTTP.execute(.post, url, parameters: parameters).map2 { json -> Snode in
guard let intermediate = json["result"] as? JSON, let rawSnodes = intermediate["service_node_states"] as? [JSON] else { throw SnodeAPIError.randomSnodePoolUpdatingFailed }
snodePool = try Set(rawSnodes.flatMap { rawSnode in
guard let address = rawSnode["public_ip"] as? String, let port = rawSnode["storage_port"] as? Int,
let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
print("[Loki] Failed to parse target from: \(rawSnode).")
return nil
}
return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
})
// randomElement() uses the system's default random generator, which is cryptographically secure
return snodePool.randomElement()!
}
}.done2 { snode in
seal.fulfill(snode)
try! Storage.writeSync { transaction in
print("[Loki] Persisting snode pool to database.")
storage.setSnodePool(SnodeAPI.snodePool, in: transaction)
}
}.catch2 { error in
print("[Loki] Failed to contact seed node at: \(target).")
seal.reject(error)
}
return promise
} else {
return Promise<Snode> { seal in
// randomElement() uses the system's default random generator, which is cryptographically secure
seal.fulfill(snodePool.randomElement()!)
}
}
}
internal static func getSwarm(for publicKey: String, isForcedReload: Bool = false) -> Promise<[Snode]> {
if swarmCache[publicKey] == nil {
storage.dbReadConnection.read { transaction in
swarmCache[publicKey] = storage.getSwarm(for: publicKey, in: transaction)
}
}
if let cachedSwarm = swarmCache[publicKey], cachedSwarm.count >= minimumSwarmSnodeCount && !isForcedReload {
return Promise<[Snode]> { $0.fulfill(cachedSwarm) }
} else {
print("[Loki] Getting swarm for: \(publicKey).")
let parameters: [String:Any] = [ "pubKey" : publicKey ]
return getRandomSnode().then2 {
invoke(.getSwarm, on: $0, associatedWith: publicKey, parameters: parameters)
}.map2 { rawSnodes in
let swarm = parseSnodes(from: rawSnodes)
swarmCache[publicKey] = swarm
try! Storage.writeSync { transaction in
storage.setSwarm(swarm, for: publicKey, in: transaction)
}
return swarm
}
}
}
internal static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> {
// shuffled() uses the system's default random generator, which is cryptographically secure
return getSwarm(for: publicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) }
}
internal static func dropSnodeFromSnodePool(_ snode: Snode) {
SnodeAPI.snodePool.remove(snode)
try! Storage.writeSync { transaction in
storage.dropSnodeFromSnodePool(snode, in: transaction)
}
}
@objc public static func clearSnodePool() {
snodePool.removeAll()
try! Storage.writeSync { transaction in
storage.clearSnodePool(in: transaction)
}
}
internal static func dropSnodeFromSwarmIfNeeded(_ snode: Snode, publicKey: String) {
let swarm = SnodeAPI.swarmCache[publicKey]
if var swarm = swarm, let index = swarm.firstIndex(of: snode) {
swarm.remove(at: index)
SnodeAPI.swarmCache[publicKey] = swarm
try! Storage.writeSync { transaction in
storage.setSwarm(swarm, for: publicKey, in: transaction)
}
}
}
// MARK: Receiving
internal static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
try! Storage.writeSync { transaction in
Storage.pruneLastMessageHashInfoIfExpired(for: snode, associatedWith: publicKey, using: transaction)
}
let lastHash = Storage.getLastMessageHash(for: snode, associatedWith: publicKey)
let parameters = [ "pubKey" : publicKey, "lastHash" : lastHash ]
return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters)
}
public static func getMessages(for publicKey: String) -> Promise<Set<MessageListPromise>> {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: SnodeAPI.workQueue) {
getTargetSnodes(for: publicKey).mapValues2 { targetSnode in
getRawMessages(from: targetSnode, associatedWith: publicKey).map2 {
parseRawMessagesResponse($0, from: targetSnode, associatedWith: publicKey)
}
}.map2 { Set($0) }
}
}
// MARK: Sending
@objc(sendSignalMessage:)
public static func objc_sendSignalMessage(_ signalMessage: SignalMessage) -> AnyPromise {
let promise = sendSignalMessage(signalMessage).mapValues2 { AnyPromise.from($0) }.map2 { Set($0) }
return AnyPromise.from(promise)
}
public static func sendSignalMessage(_ signalMessage: SignalMessage) -> Promise<Set<RawResponsePromise>> {
// Convert the message to a Loki message
guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: SnodeAPIError.messageConversionFailed) }
let publicKey = lokiMessage.recipientPublicKey
let notificationCenter = NotificationCenter.default
notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp))
// Calculate proof of work
return lokiMessage.calculatePoW().then2 { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
notificationCenter.post(name: .routing, object: NSNumber(value: signalMessage.timestamp))
// Get the target snodes
return getTargetSnodes(for: publicKey).map2 { snodes in
notificationCenter.post(name: .messageSending, object: NSNumber(value: signalMessage.timestamp))
let parameters = lokiMessageWithPoW.toJSON()
return Set(snodes.map { snode in
// Send the message to the target snode
return attempt(maxRetryCount: maxRetryCount, recoveringOn: SnodeAPI.workQueue) {
invoke(.sendMessage, on: snode, associatedWith: publicKey, parameters: parameters)
}.map2 { rawResponse in
if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int {
guard powDifficulty != SnodeAPI.powDifficulty, powDifficulty < 100 else { return rawResponse }
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
SnodeAPI.powDifficulty = UInt(powDifficulty)
} else {
print("[Loki] Failed to update proof of work difficulty from: \(rawResponse).")
}
return rawResponse
}
})
}
}
}
// MARK: Parsing
// The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions.
private static func parseSnodes(from rawResponse: Any) -> [Snode] {
guard let json = rawResponse as? JSON, let rawSnodes = json["snodes"] as? [JSON] else {
print("[Loki] Failed to parse targets from: \(rawResponse).")
return []
}
return rawSnodes.flatMap { rawSnode in
guard let address = rawSnode["ip"] as? String, let portAsString = rawSnode["port"] as? String, let port = UInt16(portAsString), let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
print("[Loki] Failed to parse target from: \(rawSnode).")
return nil
}
return Snode(address: "https://\(address)", port: port, publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
}
}
internal static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Snode, associatedWith publicKey: String) -> [SSKProtoEnvelope] {
guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] }
if let (lastHash, expirationDate) = updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: rawMessages),
UserDefaults.standard[.isUsingFullAPNs] {
LokiPushNotificationManager.acknowledgeDelivery(forMessageWithHash: lastHash, expiration: expirationDate, hexEncodedPublicKey: getUserHexEncodedPublicKey())
}
let rawNewMessages = removeDuplicates(from: rawMessages, associatedWith: publicKey)
let newMessages = parseProtoEnvelopes(from: rawNewMessages)
return newMessages
}
private static func updateLastMessageHashValueIfPossible(for snode: Snode, associatedWith publicKey: String, from rawMessages: [JSON]) -> (String, UInt64)? {
if let lastMessage = rawMessages.last, let lastHash = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? UInt64 {
try! Storage.writeSync { transaction in
Storage.setLastMessageHashInfo(for: snode, associatedWith: publicKey, to: [ "hash" : lastHash, "expirationDate" : NSNumber(value: expirationDate) ], using: transaction)
}
return (lastHash, expirationDate)
} else if (!rawMessages.isEmpty) {
print("[Loki] Failed to update last message hash value from: \(rawMessages).")
}
return nil
}
private static func removeDuplicates(from rawMessages: [JSON], associatedWith publicKey: String) -> [JSON] {
var receivedMessages = Storage.getReceivedMessages(for: publicKey) ?? []
return rawMessages.filter { rawMessage in
guard let hash = rawMessage["hash"] as? String else {
print("[Loki] Missing hash value for message: \(rawMessage).")
return false
}
let isDuplicate = receivedMessages.contains(hash)
receivedMessages.insert(hash)
try! Storage.writeSync { transaction in
Storage.setReceivedMessages(to: receivedMessages, for: publicKey, using: transaction)
}
return !isDuplicate
}
}
private static func parseProtoEnvelopes(from rawMessages: [JSON]) -> [SSKProtoEnvelope] {
return rawMessages.compactMap { rawMessage in
guard let base64EncodedData = rawMessage["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else {
print("[Loki] Failed to decode data for message: \(rawMessage).")
return nil
}
guard let envelope = try? MessageWrapper.unwrap(data: data) else {
print("[Loki] Failed to unwrap data for message: \(rawMessage).")
return nil
}
return envelope
}
}
// MARK: Error Handling
/// - Note: Should only be invoked from `LokiAPI.workQueue` to avoid race conditions.
internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Snode, associatedWith publicKey: String) -> Error? {
#if DEBUG
assertOnQueue(SnodeAPI.workQueue)
#endif
switch statusCode {
case 0, 400, 500, 503:
// The snode is unreachable
let oldFailureCount = SnodeAPI.snodeFailureCount[snode] ?? 0
let newFailureCount = oldFailureCount + 1
SnodeAPI.snodeFailureCount[snode] = newFailureCount
print("[Loki] Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).")
if newFailureCount >= SnodeAPI.snodeFailureThreshold {
print("[Loki] Failure threshold reached for: \(snode); dropping it.")
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
SnodeAPI.dropSnodeFromSnodePool(snode)
SnodeAPI.snodeFailureCount[snode] = 0
}
case 406:
print("[Loki] The user's clock is out of sync with the service node network.")
return SnodeAPI.SnodeAPIError.clockOutOfSync
case 421:
// The snode isn't associated with the given public key anymore
print("[Loki] Invalidating swarm for: \(publicKey).")
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
case 432:
// The proof of work difficulty is too low
if let powDifficulty = json?["difficulty"] as? UInt {
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
SnodeAPI.powDifficulty = UInt(powDifficulty)
} else {
print("[Loki] Failed to update proof of work difficulty.")
}
break
default: break
}
return nil
}
}

View File

@ -0,0 +1,58 @@
internal extension Storage {
// MARK: Last Message Hash
private static let lastMessageHashCollection = "LokiLastMessageHashCollection"
internal static func getLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String) -> JSON? {
let key = "\(snode.address):\(snode.port).\(publicKey)"
var result: JSON?
read { transaction in
result = transaction.object(forKey: key, inCollection: lastMessageHashCollection) as? JSON
}
if let result = result {
guard result["hash"] as? String != nil else { return nil }
guard result["expirationDate"] as? NSNumber != nil else { return nil }
}
return result
}
internal static func pruneLastMessageHashInfoIfExpired(for snode: Snode, associatedWith publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
guard let lastMessageHashInfo = getLastMessageHashInfo(for: snode, associatedWith: publicKey),
let hash = lastMessageHashInfo["hash"] as? String, let expirationDate = (lastMessageHashInfo["expirationDate"] as? NSNumber)?.uint64Value else { return }
let now = NSDate.ows_millisecondTimeStamp()
if now >= expirationDate {
removeLastMessageHashInfo(for: snode, associatedWith: publicKey, using: transaction)
}
}
internal static func getLastMessageHash(for snode: Snode, associatedWith publicKey: String) -> String? {
return getLastMessageHashInfo(for: snode, associatedWith: publicKey)?["hash"] as? String
}
internal static func removeLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
let key = "\(snode.address):\(snode.port).\(publicKey)"
transaction.removeObject(forKey: key, inCollection: lastMessageHashCollection)
}
internal static func setLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: YapDatabaseReadWriteTransaction) {
let key = "\(snode.address):\(snode.port).\(publicKey)"
guard lastMessageHashInfo.count == 2 && lastMessageHashInfo["hash"] as? String != nil && lastMessageHashInfo["expirationDate"] as? NSNumber != nil else { return }
transaction.setObject(lastMessageHashInfo, forKey: key, inCollection: lastMessageHashCollection)
}
// MARK: Received Messages
private static let receivedMessagesCollection = "LokiReceivedMessagesCollection"
internal static func getReceivedMessages(for publicKey: String) -> Set<String>? {
var result: Set<String>?
read { transaction in
result = transaction.object(forKey: publicKey, inCollection: receivedMessagesCollection) as? Set<String>
}
return result
}
internal static func setReceivedMessages(to receivedMessages: Set<String>, for publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(receivedMessages, forKey: publicKey, inCollection: receivedMessagesCollection)
}
}

View File

@ -154,37 +154,6 @@
[LKLogger print:[NSString stringWithFormat:@"[Loki] Removed pre key bundle from: %@.", hexEncodedPublicKey]];
}
# pragma mark - Last Message Hash
#define LKLastMessageHashCollection @"LKLastMessageHashCollection"
- (NSString *_Nullable)getLastMessageHashForSnode:(NSString *)snode transaction:(YapDatabaseReadWriteTransaction *)transaction {
NSDictionary *_Nullable dict = [transaction objectForKey:snode inCollection:LKLastMessageHashCollection];
if (dict == nil) { return nil; }
NSString *_Nullable hash = dict[@"hash"];
if (hash == nil) { return nil; }
// Check if the hash has expired
uint64_t now = NSDate.ows_millisecondTimeStamp;
NSNumber *_Nullable expiresAt = dict[@"expiresAt"];
if (expiresAt && expiresAt.unsignedLongLongValue <= now) {
[self removeLastMessageHashForSnode:snode transaction:transaction];
return nil;
}
return hash;
}
- (void)setLastMessageHashForSnode:(NSString *)snode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction {
NSDictionary *dict = @{ @"hash" : hash, @"expiresAt": @(expiresAt) };
[transaction setObject:dict forKey:snode inCollection:LKLastMessageHashCollection];
}
- (void)removeLastMessageHashForSnode:(NSString *)snode transaction:(YapDatabaseReadWriteTransaction *)transaction {
[transaction removeObjectForKey:snode inCollection:LKLastMessageHashCollection];
}
# pragma mark - Open Groups
#define LKMessageIDCollection @"LKMessageIDCollection"

View File

@ -4,7 +4,7 @@
public extension OWSPrimaryStorage {
// MARK: Snode Pool
public func setSnodePool(_ snodePool: Set<LokiAPITarget>, in transaction: YapDatabaseReadWriteTransaction) {
public func setSnodePool(_ snodePool: Set<Snode>, in transaction: YapDatabaseReadWriteTransaction) {
clearSnodePool(in: transaction)
snodePool.forEach { snode in
transaction.setObject(snode, forKey: snode.description, inCollection: Storage.snodePoolCollection)
@ -15,16 +15,16 @@ public extension OWSPrimaryStorage {
transaction.removeAllObjects(inCollection: Storage.snodePoolCollection)
}
public func getSnodePool(in transaction: YapDatabaseReadTransaction) -> Set<LokiAPITarget> {
var result: Set<LokiAPITarget> = []
public func getSnodePool(in transaction: YapDatabaseReadTransaction) -> Set<Snode> {
var result: Set<Snode> = []
transaction.enumerateKeysAndObjects(inCollection: Storage.snodePoolCollection) { _, object, _ in
guard let snode = object as? LokiAPITarget else { return }
guard let snode = object as? Snode else { return }
result.insert(snode)
}
return result
}
public func dropSnodeFromSnodePool(_ snode: LokiAPITarget, in transaction: YapDatabaseReadWriteTransaction) {
public func dropSnodeFromSnodePool(_ snode: Snode, in transaction: YapDatabaseReadWriteTransaction) {
transaction.removeObject(forKey: snode.description, inCollection: Storage.snodePoolCollection)
}
@ -72,12 +72,12 @@ public extension OWSPrimaryStorage {
public func getOnionRequestPaths(in transaction: YapDatabaseReadTransaction) -> [OnionRequestAPI.Path] {
let collection = Storage.onionRequestPathCollection
guard
let path0Snode0 = transaction.object(forKey: "0-0", inCollection: collection) as? LokiAPITarget,
let path0Snode1 = transaction.object(forKey: "0-1", inCollection: collection) as? LokiAPITarget,
let path0Snode2 = transaction.object(forKey: "0-2", inCollection: collection) as? LokiAPITarget,
let path1Snode0 = transaction.object(forKey: "1-0", inCollection: collection) as? LokiAPITarget,
let path1Snode1 = transaction.object(forKey: "1-1", inCollection: collection) as? LokiAPITarget,
let path1Snode2 = transaction.object(forKey: "1-2", inCollection: collection) as? LokiAPITarget else { return [] }
let path0Snode0 = transaction.object(forKey: "0-0", inCollection: collection) as? Snode,
let path0Snode1 = transaction.object(forKey: "0-1", inCollection: collection) as? Snode,
let path0Snode2 = transaction.object(forKey: "0-2", inCollection: collection) as? Snode,
let path1Snode0 = transaction.object(forKey: "1-0", inCollection: collection) as? Snode,
let path1Snode1 = transaction.object(forKey: "1-1", inCollection: collection) as? Snode,
let path1Snode2 = transaction.object(forKey: "1-2", inCollection: collection) as? Snode else { return [] }
return [ [ path0Snode0, path0Snode1, path0Snode2 ], [ path1Snode0, path1Snode1, path1Snode2 ] ]
}

View File

@ -0,0 +1,69 @@
import PromiseKit
@objc(LKClosedGroupPoller)
public final class ClosedGroupPoller : NSObject {
private var isPolling = false
private var timer: Timer?
// MARK: Settings
private static let pollInterval: TimeInterval = 4
// MARK: Error
private enum Error : LocalizedError {
case insufficientSnodes
case pollingCanceled
internal var errorDescription: String? {
switch self {
case .insufficientSnodes: return "No snodes left to poll."
case .pollingCanceled: return "Polling canceled."
}
}
}
// MARK: Public API
@objc public func startIfNeeded() {
AssertIsOnMainThread() // Timers don't do well on background queues
guard !isPolling else { return }
isPolling = true
timer = Timer.scheduledTimer(withTimeInterval: ClosedGroupPoller.pollInterval, repeats: true) { [weak self] _ in
self?.poll()
}
}
@objc public func stop() {
isPolling = false
timer?.invalidate()
}
// MARK: Private API
private func poll() {
guard isPolling else { return }
let publicKeys = Storage.getUserClosedGroupPublicKeys()
publicKeys.forEach { publicKey in
SnodeAPI.getSwarm(for: publicKey).then2 { [weak self] swarm -> Promise<[SSKProtoEnvelope]> in
// randomElement() uses the system's default random generator, which is cryptographically secure
guard let snode = swarm.randomElement() else { return Promise(error: Error.insufficientSnodes) }
guard let self = self, self.isPolling else { return Promise(error: Error.pollingCanceled) }
return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).map2 {
SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: publicKey)
}
}.done2 { [weak self] messages in
guard let self = self, self.isPolling else { return }
if !messages.isEmpty {
print("[Loki] Received \(messages.count) new message(s) in closed group with public key: \(publicKey).")
}
messages.forEach { message in
do {
let data = try message.serializedData()
SSKEnvironment.shared.messageReceiver.handleReceivedEnvelopeData(data)
} catch {
print("[Loki] Failed to deserialize envelope due to error: \(error).")
}
}
}.catch2 { error in
print("[Loki] Polling failed for closed group with public key: \(publicKey) due to error: \(error).")
}
}
}
}

View File

@ -1,41 +1,49 @@
@objc(LKClosedGroupUpdateMessage)
internal final class ClosedGroupUpdateMessage : TSOutgoingMessage {
private let name: String
private let id: String
private let sharedSecret: String
private let senderKey: String
private let members: Set<String>
private let kind: Kind
// MARK: Settings
@objc internal override var ttl: UInt32 { return UInt32(TTLUtilities.getTTL(for: .closedGroupUpdate)) }
@objc internal override func shouldBeSaved() -> Bool { return false }
@objc internal override func shouldSyncTranscript() -> Bool { return false }
@objc internal init(thread: TSThread, name: String, id: String, sharedSecret: String, senderKey: String, members: Set<String>) {
self.name = name
self.id = id
self.sharedSecret = sharedSecret
self.senderKey = senderKey
self.members = members
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageBody: "Closed group created",
// MARK: Kind
internal enum Kind {
case new(groupPublicKey: Data, name: String, groupPrivateKey: Data, chainKeys: [Data], members: [String], admins: [String])
}
// MARK: Initialization
internal init(thread: TSThread, kind: Kind) {
self.kind = kind
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageBody: "",
attachmentIds: NSMutableArray(), expiresInSeconds: 0, expireStartedAt: 0, isVoiceMessage: false,
groupMetaMessage: .new, quotedMessage: nil, contactShare: nil, linkPreview: nil)
groupMetaMessage: .unspecified, quotedMessage: nil, contactShare: nil, linkPreview: nil)
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(thread:name:id:secretKey:senderKey:members:) instead.")
preconditionFailure("Use init(thread:kind:) instead.")
}
required init(dictionary: [String:Any]) throws {
preconditionFailure("Use init(thread:name:id:secretKey:senderKey:members:) instead.")
preconditionFailure("Use init(thread:kind:) instead.")
}
// MARK: Building
@objc internal override func dataMessageBuilder() -> Any? {
guard let builder = super.dataMessageBuilder() as? SSKProtoDataMessage.SSKProtoDataMessageBuilder else { return nil }
let closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(name: name, groupID: id, sharedSecret: sharedSecret, senderKey: senderKey)
closedGroupUpdate.setMembers([String](members))
do {
let closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate.SSKProtoDataMessageClosedGroupUpdateBuilder
switch kind {
case .new(let groupPublicKey, let name, let groupPrivateKey, let chainKeys, let members, let admins):
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .new)
closedGroupUpdate.setName(name)
closedGroupUpdate.setGroupPrivateKey(groupPrivateKey)
closedGroupUpdate.setChainKeys(chainKeys)
closedGroupUpdate.setMembers(members)
closedGroupUpdate.setAdmins(admins)
}
builder.setClosedGroupUpdate(try closedGroupUpdate.build())
} catch {
owsFailDebug("Failed to build closed group update due to error: \(error).")

View File

@ -16,6 +16,34 @@ public final class ClosedGroupsProtocol : NSObject {
private static let gcmTagSize: UInt = 16
private static let ivSize: UInt = 12
// A quick overview of how shared sender key based closed groups work:
//
// When a user creates the group, they generate a key pair for the group along with a ratchet for
// every member of the group. They bundle this together with some other group info such as the group
// name in a `ClosedGroupUpdateMessage` and send that using established channels to every member of
// the group. Note that because a user can only pick from their existing contacts when selecting
// the group members they don't need to establish sessions before being able to send the
// `ClosedGroupUpdateMessage`. Another way to optimize the performance of the group creation process
// is to batch fetch the device links of all members involved ahead of time, rather than letting
// the sending pipeline do it separately for every user the `ClosedGroupUpdateMessage` is sent to.
// After the group is created, every user polls for the public key associated with the group.
// Upon receiving a `ClosedGroupUpdateMessage` of type `.new`, a user sends session requests to all
// other members of the group they don't yet have a session with for reasons outlined below.
// When a user sends a message they step their ratchet and use the resulting message key to encrypt
// the message.
// When another user receives that message, they step the ratchet associated with the sender and
// use the resulting message key to decrypt the message.
// When a user leaves the group, new ratchets must be generated for all members to ensure that the
// user that left can't decrypt messages going forward. To this end every user deletes all ratchets
// associated with the group in question upon receiving a group update message that indicates that
// a user left. They then generate a new ratchet for themselves and send it out to all members of
// the group (again fetching device links ahead of time). The user should already have established
// sessions with all other members at this point because of the behavior outlined a few points above.
// When a user adds a new member to the group, they generate a ratchet for that new member and
// send that to all existing members of the group in a `ClosedGroupUpdateMessage`. They send a
// `ClosedGroupUpdateMessage` with the newly generated ratchet but also the existing ratchets of
// every other member of the group to the user that joined.
public struct Ratchet {
public let chainKey: String
public let keyIndex: UInt
@ -23,46 +51,60 @@ public final class ClosedGroupsProtocol : NSObject {
}
public enum RatchetingError : LocalizedError {
case loadingFailed(groupID: String, senderPublicKey: String)
case messageKeyMissing(targetKeyIndex: UInt, groupID: String, senderPublicKey: String)
case loadingFailed(groupPublicKey: String, senderPublicKey: String)
case messageKeyMissing(targetKeyIndex: UInt, groupPublicKey: String, senderPublicKey: String)
public var errorDescription: String? {
switch self {
case .loadingFailed(let groupID, let senderPublicKey): return "Couldn't get ratchet for closed group with ID: \(groupID), sender public key: \(senderPublicKey)."
case .messageKeyMissing(let targetKeyIndex, let groupID, let senderPublicKey): return "Couldn't find message key for old key index: \(targetKeyIndex), group ID: \(groupID), sender public key: \(senderPublicKey)."
case .loadingFailed(let groupPublicKey, let senderPublicKey): return "Couldn't get ratchet for closed group with public key: \(groupPublicKey), sender public key: \(senderPublicKey)."
case .messageKeyMissing(let targetKeyIndex, let groupPublicKey, let senderPublicKey): return "Couldn't find message key for old key index: \(targetKeyIndex), public key: \(groupPublicKey), sender public key: \(senderPublicKey)."
}
}
}
/// - Note: It's recommended to batch fetch the device links for the given set of members before invoking this, to avoid
/// the message sending pipeline making a request for each member.
public static func createClosedGroup(name: String, members: Set<String>, transaction: YapDatabaseReadWriteTransaction) {
// Generate a key pair for the group
let keyPair = Curve25519.generateKeyPair()
// The group ID is its public key (hex encoded)
let groupID = keyPair.publicKey.toHexString()
// Create the ratchet
public static func createClosedGroup(name: String, members membersAsSet: Set<String>, transaction: YapDatabaseReadWriteTransaction) -> TSGroupThread {
var membersAsSet = membersAsSet
let userPublicKey = getUserHexEncodedPublicKey()
let ratchet = generateRatchet(for: groupID, senderPublicKey: userPublicKey, transaction: transaction)
// Get the shared secret and sender key to include in the closed group update message
let sharedSecret = keyPair.privateKey.toHexString()
let senderKey = ratchet.chainKey
// Generate a key pair for the group
let groupKeyPair = Curve25519.generateKeyPair()
let groupPublicKey = groupKeyPair.publicKey.toHexString()
// Ensure the current user's master device is included in the member list
membersAsSet.remove(userPublicKey)
membersAsSet.insert(UserDefaults.standard[.masterHexEncodedPublicKey] ?? userPublicKey)
// Create ratchets for all users involved
let members = [String](membersAsSet)
let ratchets = members.map { generateRatchet(for: groupPublicKey, senderPublicKey: $0, transaction: transaction) }
// Create the group
let admins = [ UserDefaults.standard[.masterHexEncodedPublicKey] ?? userPublicKey ]
let wrappedGroupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupID)
let group = TSGroupModel(title: name, memberIds: [String](members), image: nil, groupId: wrappedGroupID, groupType: .closedGroup, adminIds: admins)
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let group = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins)
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
thread.usesSharedSenderKeys = true
thread.save(with: transaction)
SSKEnvironment.shared.profileManager.addThread(toProfileWhitelist: thread)
// Send a closed group update message to all members involved
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, name: name, id: groupID, sharedSecret: sharedSecret, senderKey: senderKey, members: members)
let chainKeys = ratchets.map { Data(hex: $0.chainKey) }
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: groupKeyPair.publicKey, name: name, groupPrivateKey: groupKeyPair.privateKey, chainKeys: chainKeys, members: members, admins: admins)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction)
// Store the group's key pair
Storage.addClosedGroupKeyPair(groupKeyPair)
// Notify the user
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate)
infoMessage.save(with: transaction)
// The user can only pick from existing contacts when selecting closed group
// members so there's no need to establish sessions
// Return
return thread
}
private static func generateRatchet(for groupID: String, senderPublicKey: String, transaction: YapDatabaseReadWriteTransaction) -> Ratchet {
let rootChainKey = Randomness.generateRandomBytes(32)!.toHexString()
private static func generateRatchet(for groupPublicKey: String, senderPublicKey: String, transaction: YapDatabaseReadWriteTransaction) -> Ratchet {
let rootChainKey = Data.getSecureRandomData(ofSize: 32)!.toHexString()
let ratchet = Ratchet(chainKey: rootChainKey, keyIndex: 0, messageKeys: [])
Storage.setClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey, ratchet: ratchet, transaction: transaction)
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, transaction: transaction)
return ratchet
}
@ -74,18 +116,18 @@ public final class ClosedGroupsProtocol : NSObject {
}
/// - Note: Sync. Don't call from the main thread.
private static func stepRatchetOnce(for groupID: String, senderPublicKey: String, transaction: YapDatabaseReadWriteTransaction) throws -> Ratchet {
private static func stepRatchetOnce(for groupPublicKey: String, senderPublicKey: String, transaction: YapDatabaseReadWriteTransaction) throws -> Ratchet {
#if DEBUG
assert(!Thread.isMainThread)
#endif
guard let ratchet = Storage.getClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey) else {
let error = RatchetingError.loadingFailed(groupID: groupID, senderPublicKey: senderPublicKey)
guard let ratchet = Storage.getClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) else {
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
print("[Loki] \(error.errorDescription!)")
throw error
}
do {
let result = try step(ratchet)
Storage.setClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
return result
} catch {
print("[Loki] Couldn't step ratchet due to error: \(error).")
@ -93,12 +135,12 @@ public final class ClosedGroupsProtocol : NSObject {
}
}
private static func stepRatchetOnceAsync(for groupID: String, senderPublicKey: String) -> Promise<Ratchet> {
private static func stepRatchetOnceAsync(for groupPublicKey: String, senderPublicKey: String) -> Promise<Ratchet> {
let (promise, seal) = Promise<Ratchet>.pending()
LokiAPI.workQueue.async {
SnodeAPI.workQueue.async {
try! Storage.writeSync { transaction in
do {
let result = try stepRatchetOnce(for: groupID, senderPublicKey: senderPublicKey, transaction: transaction)
let result = try stepRatchetOnce(for: groupPublicKey, senderPublicKey: senderPublicKey, transaction: transaction)
seal.fulfill(result)
} catch {
seal.reject(error)
@ -109,28 +151,23 @@ public final class ClosedGroupsProtocol : NSObject {
}
/// - Note: Sync. Don't call from the main thread.
private static func stepRatchet(for groupID: String, senderPublicKey: String, until targetKeyIndex: UInt, transaction: YapDatabaseReadWriteTransaction) throws -> Ratchet {
private static func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, transaction: YapDatabaseReadWriteTransaction) throws -> Ratchet {
#if DEBUG
assert(!Thread.isMainThread)
#endif
guard let ratchet = Storage.getClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey) else {
let error = RatchetingError.loadingFailed(groupID: groupID, senderPublicKey: senderPublicKey)
guard let ratchet = Storage.getClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) else {
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
print("[Loki] \(error.errorDescription!)")
throw error
}
if targetKeyIndex < ratchet.keyIndex {
// In the case where this function is invoked for an old key index, just remove the key generated
// earlier from the database. There's no need to advance the ratchet.
guard let messageKey = (ratchet.messageKeys.count > targetKeyIndex) ? ratchet.messageKeys[Int(targetKeyIndex)] : nil else {
let error = RatchetingError.messageKeyMissing(targetKeyIndex: targetKeyIndex, groupID: groupID, senderPublicKey: senderPublicKey)
// There's no need to advance the ratchet if this is invoked for an old key index
guard ratchet.messageKeys.count > targetKeyIndex else {
let error = RatchetingError.messageKeyMissing(targetKeyIndex: targetKeyIndex, groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
print("[Loki] \(error.errorDescription!)")
throw error
}
var messageKeysCopy = ratchet.messageKeys
messageKeysCopy.remove(at: Int(targetKeyIndex))
let result = Ratchet(chainKey: ratchet.chainKey, keyIndex: ratchet.keyIndex, messageKeys: messageKeysCopy)
Storage.setClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
return result
return ratchet
} else {
var currentKeyIndex = ratchet.keyIndex
var result = ratchet
@ -142,17 +179,17 @@ public final class ClosedGroupsProtocol : NSObject {
throw error
}
}
Storage.setClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
return result
}
}
private static func stepRatchetAsync(for groupID: String, senderPublicKey: String, until targetKeyIndex: UInt) -> Promise<Ratchet> {
private static func stepRatchetAsync(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt) -> Promise<Ratchet> {
let (promise, seal) = Promise<Ratchet>.pending()
LokiAPI.workQueue.async {
SnodeAPI.workQueue.async {
try! Storage.writeSync { transaction in
do {
let result = try stepRatchet(for: groupID, senderPublicKey: senderPublicKey, until: targetKeyIndex, transaction: transaction)
let result = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: targetKeyIndex, transaction: transaction)
seal.fulfill(result)
} catch {
seal.reject(error)
@ -162,8 +199,30 @@ public final class ClosedGroupsProtocol : NSObject {
return promise
}
public static func decrypt(_ ivAndCiphertext: Data, for groupID: String, senderPublicKey: String, keyIndex: UInt) -> Promise<Data> {
return stepRatchetAsync(for: groupID, senderPublicKey: senderPublicKey, until: keyIndex).map2 { ratchet in
@objc(encryptPlaintext:forGroupWithPublicKey:senderPublicKey:)
static func objc_encrypt(_ plaintext: Data, for groupPublicKey: String, senderPublicKey: String) -> [Any]? {
guard let (ivAndCiphertext, keyIndex) = try? encrypt(plaintext, for: groupPublicKey, senderPublicKey: senderPublicKey).wait() else { return nil }
return [ ivAndCiphertext, NSNumber(value: keyIndex) ]
}
public static func encrypt(_ plaintext: Data, for groupPublicKey: String, senderPublicKey: String) -> Promise<(ivAndCiphertext: Data, keyIndex: UInt)> {
return stepRatchetOnceAsync(for: groupPublicKey, senderPublicKey: senderPublicKey).map2 { ratchet in
let iv = Data.getSecureRandomData(ofSize: ivSize)!
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
let messageKey = ratchet.messageKeys.last!
let aes = try AES(key: messageKey.bytes, blockMode: gcm, padding: .noPadding)
let ciphertext = try aes.encrypt(plaintext.bytes)
return (ivAndCiphertext: iv + Data(bytes: ciphertext), ratchet.keyIndex)
}
}
@objc(decryptCiphertext:forGroupWithPublicKey:senderPublicKey:keyIndex:)
static func objc_decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt) -> Data? {
return try? decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex).wait()
}
public static func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt) -> Promise<Data> {
return stepRatchetAsync(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex).map2 { ratchet in
let iv = ivAndCiphertext[0..<Int(ivSize)]
let ciphertext = ivAndCiphertext[Int(ivSize)...]
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
@ -173,14 +232,40 @@ public final class ClosedGroupsProtocol : NSObject {
}
}
public static func encrypt(_ plaintext: Data, for groupID: String, senderPublicKey: String) -> Promise<Data> {
return stepRatchetOnceAsync(for: groupID, senderPublicKey: senderPublicKey).map2 { ratchet in
let iv = Data.getSecureRandomData(ofSize: ivSize)!
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
let messageKey = ratchet.messageKeys.last!
let aes = try AES(key: messageKey.bytes, blockMode: gcm, padding: .noPadding)
let ciphertext = try aes.encrypt(plaintext.bytes)
return iv + Data(bytes: ciphertext)
@objc(handleSharedSenderKeysUpdateIfNeeded:transaction:)
public static func handleSharedSenderKeysUpdateIfNeeded(_ dataMessage: SSKProtoDataMessage, using transaction: YapDatabaseReadWriteTransaction) -> Bool {
guard let closedGroupUpdate = dataMessage.closedGroupUpdate else { return false }
switch closedGroupUpdate.type {
case .new:
// Unwrap the message
let groupPublicKey = closedGroupUpdate.groupPublicKey
let name = closedGroupUpdate.name
let groupPrivateKey = closedGroupUpdate.groupPrivateKey!
let chainKeys = closedGroupUpdate.chainKeys
let members = closedGroupUpdate.members
let admins = closedGroupUpdate.admins
// Persist the ratchets
zip(members, chainKeys).forEach { (member, chainKey) in
let ratchet = Ratchet(chainKey: chainKey.toHexString(), keyIndex: 0, messageKeys: [])
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey.toHexString(), senderPublicKey: member, ratchet: ratchet, transaction: transaction)
}
// Create the group
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey.toHexString())
let group = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins)
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
thread.usesSharedSenderKeys = true
thread.save(with: transaction)
SSKEnvironment.shared.profileManager.addThread(toProfileWhitelist: thread)
// Add the group to the user's set of public keys to poll for
let groupKeyPair = ECKeyPair(publicKey: groupPublicKey, privateKey: groupPrivateKey)!
Storage.addClosedGroupKeyPair(groupKeyPair)
// Notify the user
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate)
infoMessage.save(with: transaction)
// Establish sessions if needed
establishSessionsIfNeeded(with: members, in: thread, using: transaction)
// Return
return true
}
}
@ -188,10 +273,10 @@ public final class ClosedGroupsProtocol : NSObject {
public static func shouldIgnoreClosedGroupMessage(_ dataMessage: SSKProtoDataMessage, in thread: TSThread, wrappedIn envelope: SSKProtoEnvelope) -> Bool {
guard let thread = thread as? TSGroupThread, thread.groupModel.groupType == .closedGroup,
dataMessage.group?.type == .deliver else { return false }
let sender = envelope.source! // Set during UD decryption
let publicKey = envelope.source! // Set during UD decryption
var result = false
Storage.read { transaction in
result = !thread.isUser(inGroup: sender, transaction: transaction)
result = !thread.isUser(inGroup: publicKey, transaction: transaction)
}
return result
}
@ -199,16 +284,17 @@ public final class ClosedGroupsProtocol : NSObject {
@objc(shouldIgnoreClosedGroupUpdateMessage:inThread:wrappedIn:)
public static func shouldIgnoreClosedGroupUpdateMessage(_ dataMessage: SSKProtoDataMessage, in thread: TSGroupThread?, wrappedIn envelope: SSKProtoEnvelope) -> Bool {
guard let thread = thread else { return false }
let sender = envelope.source! // Set during UD decryption
let publicKey = envelope.source! // Set during UD decryption
var result = false
Storage.read { transaction in
result = !thread.isUserAdmin(inGroup: sender, transaction: transaction)
result = !thread.isUserAdmin(inGroup: publicKey, transaction: transaction)
}
return result
}
@objc(establishSessionsIfNeededWithClosedGroupMembers:inThread:transaction:)
public static func establishSessionsIfNeeded(with closedGroupMembers: [String], in thread: TSGroupThread, using transaction: YapDatabaseReadWriteTransaction) {
guard thread.groupModel.groupType == .closedGroup else { return }
closedGroupMembers.forEach { member in
SessionManagementProtocol.establishSessionIfNeeded(with: member, using: transaction)
}

View File

@ -0,0 +1,49 @@
internal extension Storage {
// MARK: Ratchets
internal static let closedGroupRatchetCollection = "LokiClosedGroupRatchetCollection"
internal static func getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String) -> ClosedGroupsProtocol.Ratchet? {
let key = "\(groupPublicKey).\(senderPublicKey)"
var result: ClosedGroupsProtocol.Ratchet?
read { transaction in
result = transaction.object(forKey: key, inCollection: closedGroupRatchetCollection) as? ClosedGroupsProtocol.Ratchet
}
return result
}
internal static func setClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupsProtocol.Ratchet, transaction: YapDatabaseReadWriteTransaction) {
let key = "\(groupPublicKey).\(senderPublicKey)"
transaction.setObject(ratchet, forKey: key, inCollection: closedGroupRatchetCollection)
}
}
@objc internal extension Storage {
// MARK: Key Pairs
internal static let closedGroupKeyPairCollection = "LokiClosedGroupKeyPairCollection"
internal static func getUserClosedGroupPublicKeys() -> Set<String> {
var result: Set<String> = []
read { transaction in
result = Set(transaction.allKeys(inCollection: closedGroupKeyPairCollection))
}
return result
}
@objc(getKeyPairForClosedGroupWithPublicKey:)
internal static func getClosedGroupKeyPair(for publicKey: String) -> ECKeyPair? {
var result: ECKeyPair?
read { transaction in
result = transaction.object(forKey: publicKey, inCollection: closedGroupKeyPairCollection) as? ECKeyPair
}
return result
}
internal static func addClosedGroupKeyPair(_ keyPair: ECKeyPair) {
try! writeSync { transaction in
transaction.setObject(keyPair, forKey: keyPair.hexEncodedPublicKey, inCollection: closedGroupKeyPairCollection)
}
}
}

View File

@ -1,21 +0,0 @@
public extension Storage {
public static let closedGroupRatchetCollection = "LokiClosedGroupRatchetCollection"
public static func getClosedGroupRatchet(groupID: String, senderPublicKey: String) -> ClosedGroupsProtocol.Ratchet? {
let collection = closedGroupRatchetCollection
let key = "\(groupID).\(senderPublicKey)"
var result: ClosedGroupsProtocol.Ratchet?
read { transaction in
result = transaction.object(forKey: key, inCollection: collection) as? ClosedGroupsProtocol.Ratchet
}
return result
}
public static func setClosedGroupRatchet(groupID: String, senderPublicKey: String, ratchet: ClosedGroupsProtocol.Ratchet, transaction: YapDatabaseReadWriteTransaction) {
let collection = closedGroupRatchetCollection
let key = "\(groupID).\(senderPublicKey)"
transaction.setObject(ratchet, forKey: key, inCollection: collection)
}
}

View File

@ -36,9 +36,14 @@ public final class SessionMetaProtocol : NSObject {
}
}
} else {
result = Set(outgoingGroupMessage.sendingRecipientIds())
.intersection(thread.groupModel.groupMemberIds)
.subtracting(MultiDeviceProtocol.getUserLinkedDevices())
if let groupThread = thread as? TSGroupThread, groupThread.usesSharedSenderKeys {
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupThread.groupModel.groupId)
result = [ groupPublicKey ]
} else {
result = Set(outgoingGroupMessage.sendingRecipientIds())
.intersection(thread.groupModel.groupMemberIds)
.subtracting(MultiDeviceProtocol.getUserLinkedDevices())
}
}
return NSMutableSet(set: result)
}
@ -72,23 +77,23 @@ public final class SessionMetaProtocol : NSObject {
// MARK: Typing Indicators
/// Invoked only if typing indicators are enabled in the settings. Provides an opportunity
/// to avoid sending them if certain conditions are met.
@objc(shouldSendTypingIndicatorForThread:)
public static func shouldSendTypingIndicator(for thread: TSThread) -> Bool {
guard !thread.isGroupThread(), let contactID = thread.contactIdentifier() else { return false }
@objc(shouldSendTypingIndicatorInThread:)
public static func shouldSendTypingIndicator(in thread: TSThread) -> Bool {
guard !thread.isGroupThread(), let publicKey = thread.contactIdentifier() else { return false }
var isContactFriend = false
storage.dbReadConnection.read { transaction in
isContactFriend = (storage.getFriendRequestStatus(for: contactID, transaction: transaction) == .friends)
isContactFriend = (storage.getFriendRequestStatus(for: publicKey, transaction: transaction) == .friends)
}
return isContactFriend
}
// MARK: Receipts
@objc(shouldSendReceiptForThread:)
public static func shouldSendReceipt(for thread: TSThread) -> Bool {
guard !thread.isGroupThread(), let contactID = thread.contactIdentifier() else { return false }
@objc(shouldSendReceiptInThread:)
public static func shouldSendReceipt(in thread: TSThread) -> Bool {
guard !thread.isGroupThread(), let publicKey = thread.contactIdentifier() else { return false }
var isContactFriend = false
storage.dbReadConnection.read { transaction in
isContactFriend = (storage.getFriendRequestStatus(for: contactID, transaction: transaction) == .friends)
isContactFriend = (storage.getFriendRequestStatus(for: publicKey, transaction: transaction) == .friends)
}
return isContactFriend
}
@ -102,7 +107,7 @@ public final class SessionMetaProtocol : NSObject {
return result.source == getUserHexEncodedPublicKey() // Should never occur
}
@objc(updateDisplayNameIfNeededForHexEncodedPublicKey:using:transaction:)
@objc(updateDisplayNameIfNeededForPublicKey:using:transaction:)
public static func updateDisplayNameIfNeeded(for publicKey: String, using dataMessage: SSKProtoDataMessage, in transaction: YapDatabaseReadWriteTransaction) {
guard let profile = dataMessage.profile, let rawDisplayName = profile.displayName, !rawDisplayName.isEmpty else { return }
let shortID = publicKey.substring(from: publicKey.index(publicKey.endIndex, offsetBy: -8))

View File

@ -24,7 +24,7 @@ public final class MultiDeviceProtocol : NSObject {
// MARK: Multi Device Destination
public struct MultiDeviceDestination : Hashable {
public let hexEncodedPublicKey: String
public let publicKey: String
public let isMaster: Bool
}
@ -63,7 +63,7 @@ public final class MultiDeviceProtocol : NSObject {
private static func copy(_ messageSend: OWSMessageSend, for destination: MultiDeviceDestination, with seal: Resolver<Void>) -> OWSMessageSend {
var recipient: SignalRecipient!
storage.dbReadConnection.read { transaction in
recipient = SignalRecipient.getOrBuildUnsavedRecipient(forRecipientId: destination.hexEncodedPublicKey, transaction: transaction)
recipient = SignalRecipient.getOrBuildUnsavedRecipient(forRecipientId: destination.publicKey, transaction: transaction)
}
// TODO: Why is it okay that the thread, sender certificate, etc. don't get changed?
return OWSMessageSend(message: messageSend.message, thread: messageSend.thread, recipient: recipient,
@ -78,11 +78,11 @@ public final class MultiDeviceProtocol : NSObject {
let (threadPromise, threadPromiseSeal) = Promise<TSThread>.pending()
if messageSend.message.thread.isGroupThread() {
threadPromiseSeal.fulfill(messageSend.message.thread)
} else if let thread = TSContactThread.getWithContactId(destination.hexEncodedPublicKey, transaction: transaction) {
} else if let thread = TSContactThread.getWithContactId(destination.publicKey, transaction: transaction) {
threadPromiseSeal.fulfill(thread)
} else {
Storage.write { transaction in
let thread = TSContactThread.getOrCreateThread(withContactId: destination.hexEncodedPublicKey, transaction: transaction)
let thread = TSContactThread.getOrCreateThread(withContactId: destination.publicKey, transaction: transaction)
threadPromiseSeal.fulfill(thread)
}
}
@ -100,7 +100,7 @@ public final class MultiDeviceProtocol : NSObject {
}
} else {
Storage.write { transaction in
getAutoGeneratedMultiDeviceFRMessageSend(for: destination.hexEncodedPublicKey, in: transaction, seal: seal)
getAutoGeneratedMultiDeviceFRMessageSend(for: destination.publicKey, in: transaction, seal: seal)
.done(on: OWSDispatch.sendingQueue()) { autoGeneratedFRMessageSend in
messageSender.sendMessage(autoGeneratedFRMessageSend)
}
@ -111,7 +111,7 @@ public final class MultiDeviceProtocol : NSObject {
}
/// See [Multi Device Message Sending](https://github.com/loki-project/session-protocol-docs/wiki/Multi-Device-Message-Sending) for more information.
@objc(sendMessageToDestinationAndLinkedDevices:in:)
@objc(sendMessageToDestinationAndLinkedDevices:transaction:)
public static func sendMessageToDestinationAndLinkedDevices(_ messageSend: OWSMessageSend, in transaction: YapDatabaseReadTransaction) {
if !messageSend.isUDSend && messageSend.recipient.recipientId() != getUserHexEncodedPublicKey() {
#if DEBUG
@ -128,8 +128,8 @@ public final class MultiDeviceProtocol : NSObject {
return
}
print("[Loki] Sending \(type(of: message)) message using multi device routing.")
let recipientID = messageSend.recipient.recipientId()
getMultiDeviceDestinations(for: recipientID, in: transaction).done2 { destinations in
let publicKey = messageSend.recipient.recipientId()
getMultiDeviceDestinations(for: publicKey, in: transaction).done2 { destinations in
var promises: [Promise<Void>] = []
let masterDestination = destinations.first { $0.isMaster }
if let masterDestination = masterDestination {
@ -167,32 +167,25 @@ public final class MultiDeviceProtocol : NSObject {
}
/// See [Auto-Generated Friend Requests](https://github.com/loki-project/session-protocol-docs/wiki/Auto-Generated-Friend-Requests) for more information.
@objc(getAutoGeneratedMultiDeviceFRMessageForHexEncodedPublicKey:in:)
public static func getAutoGeneratedMultiDeviceFRMessage(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> FriendRequestMessage {
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
public static func getAutoGeneratedMultiDeviceFRMessage(for publicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> FriendRequestMessage {
let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction)
return FriendRequestMessage(timestamp: NSDate.ows_millisecondTimeStamp(), thread: thread, body: "Please accept to enable messages to be synced across devices")
}
/// See [Auto-Generated Friend Requests](https://github.com/loki-project/session-protocol-docs/wiki/Auto-Generated-Friend-Requests) for more information.
@objc(getAutoGeneratedMultiDeviceFRMessageSendForHexEncodedPublicKey:in:)
public static func objc_getAutoGeneratedMultiDeviceFRMessageSend(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> AnyPromise {
return AnyPromise.from(getAutoGeneratedMultiDeviceFRMessageSend(for: hexEncodedPublicKey, in: transaction))
}
/// See [Auto-Generated Friend Requests](https://github.com/loki-project/session-protocol-docs/wiki/Auto-Generated-Friend-Requests) for more information.
public static func getAutoGeneratedMultiDeviceFRMessageSend(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction, seal externalSeal: Resolver<Void>? = nil) -> Promise<OWSMessageSend> {
public static func getAutoGeneratedMultiDeviceFRMessageSend(for publicKey: String, in transaction: YapDatabaseReadWriteTransaction, seal externalSeal: Resolver<Void>? = nil) -> Promise<OWSMessageSend> {
// We don't update the friend request status; that's done in OWSMessageSender.sendMessage(_:)
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let message = getAutoGeneratedMultiDeviceFRMessage(for: hexEncodedPublicKey, in: transaction)
let recipient = SignalRecipient.getOrBuildUnsavedRecipient(forRecipientId: hexEncodedPublicKey, transaction: transaction)
let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction)
let message = getAutoGeneratedMultiDeviceFRMessage(for: publicKey, in: transaction)
let recipient = SignalRecipient.getOrBuildUnsavedRecipient(forRecipientId: publicKey, transaction: transaction)
let udManager = SSKEnvironment.shared.udManager
let senderCertificate = udManager.getSenderCertificate()
SSKEnvironment.shared.profileManager.ensureProfileCachedForContact(withID: hexEncodedPublicKey, with: transaction) // Prevent the line below from starting a write transaction
SSKEnvironment.shared.profileManager.ensureProfileCachedForContact(withID: publicKey, with: transaction) // Prevent the line below from starting a write transaction
let (promise, seal) = Promise<OWSMessageSend>.pending()
LokiAPI.workQueue.async {
SnodeAPI.workQueue.async {
var recipientUDAccess: OWSUDAccess?
if let senderCertificate = senderCertificate {
recipientUDAccess = udManager.udAccess(forRecipientId: hexEncodedPublicKey, requireSyncAccess: true)
recipientUDAccess = udManager.udAccess(forRecipientId: publicKey, requireSyncAccess: true)
}
let messageSend = OWSMessageSend(message: message, thread: thread, recipient: recipient, senderCertificate: senderCertificate,
udAccess: recipientUDAccess, localNumber: getUserHexEncodedPublicKey(), success: {
@ -205,21 +198,19 @@ public final class MultiDeviceProtocol : NSObject {
return promise
}
@objc(updateDeviceLinksIfNeededForHexEncodedPublicKey:in:)
public static func updateDeviceLinksIfNeeded(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> AnyPromise {
let promise = getMultiDeviceDestinations(for: hexEncodedPublicKey, in: transaction)
return AnyPromise.from(promise)
@objc(updateDeviceLinksIfNeededForPublicKey:transaction:)
public static func updateDeviceLinksIfNeeded(for publicKey: String, in transaction: YapDatabaseReadTransaction) -> AnyPromise {
return AnyPromise.from(getMultiDeviceDestinations(for: publicKey, in: transaction))
}
// MARK: - Receiving
@objc(handleDeviceLinkMessageIfNeeded:wrappedIn:using:)
@objc(handleDeviceLinkMessageIfNeeded:wrappedIn:transaction:)
public static func handleDeviceLinkMessageIfNeeded(_ protoContent: SSKProtoContent, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
let publicKey = envelope.source! // Set during UD decryption
guard let deviceLinkMessage = protoContent.lokiDeviceLinkMessage, let master = deviceLinkMessage.masterPublicKey,
let slave = deviceLinkMessage.slavePublicKey, let slaveSignature = deviceLinkMessage.slaveSignature else {
print("[Loki] Received an invalid device link message.")
return
return print("[Loki] Received an invalid device link message.")
}
let deviceLinkingSession = DeviceLinkingSession.current
if let masterSignature = deviceLinkMessage.masterSignature { // Authorization
@ -244,29 +235,30 @@ public final class MultiDeviceProtocol : NSObject {
}
}
@objc(handleUnlinkDeviceMessage:wrappedIn:using:)
@objc(handleUnlinkDeviceMessage:wrappedIn:transaction:)
public static func handleUnlinkDeviceMessage(_ dataMessage: SSKProtoDataMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
let sender = envelope.source! // Set during UD decryption
let publicKey = envelope.source! // Set during UD decryption
// Check that the request was sent by our master device
guard let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: getUserHexEncodedPublicKey(), in: transaction) else { return }
let wasSentByMasterDevice = (masterPublicKey == sender)
let userPublicKey = getUserHexEncodedPublicKey()
guard let userMasterPublicKey = storage.getMasterHexEncodedPublicKey(for: userPublicKey, in: transaction) else { return }
let wasSentByMasterDevice = (userMasterPublicKey == publicKey)
guard wasSentByMasterDevice else { return }
// Ignore the request if we don't know about the device link in question
let masterDeviceLinks = storage.getDeviceLinks(for: masterPublicKey, in: transaction)
let masterDeviceLinks = storage.getDeviceLinks(for: userMasterPublicKey, in: transaction)
if !masterDeviceLinks.contains(where: {
$0.master.hexEncodedPublicKey == masterPublicKey && $0.slave.hexEncodedPublicKey == getUserHexEncodedPublicKey()
$0.master.hexEncodedPublicKey == userMasterPublicKey && $0.slave.hexEncodedPublicKey == userPublicKey
}) {
return
}
LokiFileServerAPI.getDeviceLinks(associatedWith: getUserHexEncodedPublicKey()).done2 { slaveDeviceLinks in
FileServerAPI.getDeviceLinks(associatedWith: userPublicKey).done2 { slaveDeviceLinks in
// Check that the device link IS present on the file server.
// Note that the device link as seen from the master device's perspective has been deleted at this point, but the
// device link as seen from the slave perspective hasn't.
if slaveDeviceLinks.contains(where: {
$0.master.hexEncodedPublicKey == masterPublicKey && $0.slave.hexEncodedPublicKey == getUserHexEncodedPublicKey()
$0.master.hexEncodedPublicKey == userMasterPublicKey && $0.slave.hexEncodedPublicKey == userPublicKey
}) {
for deviceLink in slaveDeviceLinks { // In theory there should only be one
LokiFileServerAPI.removeDeviceLink(deviceLink) // Attempt to clean up on the file server
FileServerAPI.removeDeviceLink(deviceLink) // Attempt to clean up on the file server
}
UserDefaults.standard[.wasUnlinked] = true
DispatchQueue.main.async {
@ -288,10 +280,10 @@ public extension MultiDeviceProtocol {
storage.dbReadConnection.read { transaction in
var destinations: Set<MultiDeviceDestination> = []
let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey
let masterDestination = MultiDeviceDestination(hexEncodedPublicKey: masterPublicKey, isMaster: true)
let masterDestination = MultiDeviceDestination(publicKey: masterPublicKey, isMaster: true)
destinations.insert(masterDestination)
let deviceLinks = storage.getDeviceLinks(for: masterPublicKey, in: transaction)
let slaveDestinations = deviceLinks.map { MultiDeviceDestination(hexEncodedPublicKey: $0.slave.hexEncodedPublicKey, isMaster: false) }
let slaveDestinations = deviceLinks.map { MultiDeviceDestination(publicKey: $0.slave.hexEncodedPublicKey, isMaster: false) }
destinations.formUnion(slaveDestinations)
seal.fulfill(destinations)
}
@ -304,11 +296,11 @@ public extension MultiDeviceProtocol {
}
if timeSinceLastUpdate > deviceLinkUpdateInterval {
let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey
LokiFileServerAPI.getDeviceLinks(associatedWith: masterPublicKey).done2 { _ in
FileServerAPI.getDeviceLinks(associatedWith: masterPublicKey).done2 { _ in
getDestinations()
lastDeviceLinkUpdate[publicKey] = Date()
}.catch2 { error in
if (error as? LokiDotNetAPI.LokiDotNetAPIError) == LokiDotNetAPI.LokiDotNetAPIError.parsingFailed {
if (error as? DotNetAPI.DotNetAPIError) == DotNetAPI.DotNetAPIError.parsingFailed {
// Don't immediately re-fetch in case of failure due to a parsing error
lastDeviceLinkUpdate[publicKey] = Date()
getDestinations()

View File

@ -63,6 +63,7 @@ public final class SessionManagementProtocol : NSObject {
if message is FriendRequestMessage { return false }
else if message is SessionRequestMessage { return false }
else if let message = message as? DeviceLinkMessage, message.kind == .request { return false }
else if (message.thread as? TSGroupThread)?.usesSharedSenderKeys == true { return false }
return true
}
@ -110,7 +111,7 @@ public final class SessionManagementProtocol : NSObject {
messageSenderJobQueue.add(message: sessionRequestMessage, transaction: transaction)
}
@objc(sendSessionEstablishedMessageToPublicKey:in:)
@objc(sendSessionEstablishedMessageToPublicKey:transaction:)
public static func sendSessionEstablishedMessage(to publicKey: String, in transaction: YapDatabaseReadWriteTransaction) {
let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction)
thread.save(with: transaction)
@ -119,6 +120,9 @@ public final class SessionManagementProtocol : NSObject {
messageSenderJobQueue.add(message: ephemeralMessage, transaction: transaction)
}
/// DEPRECATED.
///
/// Only relevant for closed groups that don't use shared sender keys.
@objc(shouldIgnoreMissingPreKeyBundleExceptionForMessage:to:)
public static func shouldIgnoreMissingPreKeyBundleException(for message: TSOutgoingMessage, to hexEncodedPublicKey: String) -> Bool {
// When a closed group is created, members try to establish sessions with eachother in the background through
@ -126,15 +130,15 @@ public final class SessionManagementProtocol : NSObject {
// bundles contained in the session requests and replied with background messages to finalize the session
// creation, a given user won't be able to successfully send a message to all members of a group. This check
// is so that until we can do better on this front the user at least won't see this as an error in the UI.
return (message.thread as? TSGroupThread)?.groupModel.groupType == .closedGroup
guard let groupThread = message.thread as? TSGroupThread else { return false }
return groupThread.groupModel.groupType == .closedGroup && !groupThread.usesSharedSenderKeys
}
@objc(startSessionResetInThread:using:)
@objc(startSessionResetInThread:transaction:)
public static func startSessionReset(in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
// Check preconditions
guard let thread = thread as? TSContactThread else {
print("[Loki] Can't restore session for non contact thread.")
return
return print("[Loki] Can't restore session for non contact thread.")
}
// Send session restoration request messages to the devices requiring session restoration
let devices = thread.sessionRestoreDevices // TODO: Rename this to something that reads better
@ -157,7 +161,7 @@ public final class SessionManagementProtocol : NSObject {
// MARK: - Receiving
@objc(handleDecryptionError:forPublicKey:using:)
@objc(handleDecryptionError:forPublicKey:transaction:)
public static func handleDecryptionError(_ rawValue: Int32, for publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
let type = TSErrorMessageType(rawValue: rawValue)
let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey
@ -171,47 +175,47 @@ public final class SessionManagementProtocol : NSObject {
}
}
@objc(handlePreKeyBundleMessageIfNeeded:wrappedIn:using:)
@objc(handlePreKeyBundleMessageIfNeeded:wrappedIn:transaction:)
public static func handlePreKeyBundleMessageIfNeeded(_ protoContent: SSKProtoContent, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
let sender = envelope.source! // Set during UD decryption
let publicKey = envelope.source! // Set during UD decryption
guard let preKeyBundleMessage = protoContent.prekeyBundleMessage else { return }
print("[Loki] Received a pre key bundle message from: \(sender).")
print("[Loki] Received a pre key bundle message from: \(publicKey).")
guard let preKeyBundle = preKeyBundleMessage.getPreKeyBundle(with: transaction) else {
print("[Loki] Couldn't parse pre key bundle received from: \(sender).")
return
return print("[Loki] Couldn't parse pre key bundle received from: \(publicKey).")
}
if isSessionRequestMessage(protoContent.dataMessage),
let sentSessionRequestTimestamp = storage.getSessionRequestTimestamp(for: sender, in: transaction),
let sentSessionRequestTimestamp = storage.getSessionRequestTimestamp(for: publicKey, in: transaction),
envelope.timestamp < NSDate.ows_millisecondsSince1970(for: sentSessionRequestTimestamp) {
// We sent a session request after this one was sent
print("[Loki] Ignoring session request from: \(sender).")
return
return print("[Loki] Ignoring session request from: \(publicKey).")
}
storage.setPreKeyBundle(preKeyBundle, forContact: sender, transaction: transaction)
storage.setPreKeyBundle(preKeyBundle, forContact: publicKey, transaction: transaction)
}
/// - Note: Must be invoked after `handlePreKeyBundleMessageIfNeeded(_:wrappedIn:using:)`.
@objc(handleSessionRequestMessage:wrappedIn:using:)
public static func handleSessionRequestMessage(_ dataMessage: SSKProtoDataMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
let sender = envelope.source! // Set during UD decryption
if let sentSessionRequestTimestamp = storage.getSessionRequestTimestamp(for: sender, in: transaction),
@objc(handleSessionRequestMessageIfNeeded:wrappedIn:transaction:)
public static func handleSessionRequestMessageIfNeeded(_ protoContent: SSKProtoContent, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) -> Bool {
guard isSessionRequestMessage(protoContent.dataMessage) else { return false }
let publicKey = envelope.source! // Set during UD decryption
if let sentSessionRequestTimestamp = storage.getSessionRequestTimestamp(for: publicKey, in: transaction),
envelope.timestamp < NSDate.ows_millisecondsSince1970(for: sentSessionRequestTimestamp) {
// We sent a session request after this one was sent
print("[Loki] Ignoring session request from: \(sender).")
return
print("[Loki] Ignoring session request from: \(publicKey).")
return false
}
sendSessionEstablishedMessage(to: sender, in: transaction)
sendSessionEstablishedMessage(to: publicKey, in: transaction)
return true
}
@objc(handleEndSessionMessageReceivedInThread:using:)
public static func handleEndSessionMessageReceived(in thread: TSContactThread, using transaction: YapDatabaseReadWriteTransaction) {
let sender = thread.contactIdentifier()
print("[Loki] End session message received from: \(sender).")
let publicKey = thread.contactIdentifier()
print("[Loki] End session message received from: \(publicKey).")
// Notify the user
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeLokiSessionResetInProgress)
infoMessage.save(with: transaction)
// Archive all sessions
storage.archiveAllSessions(forContact: sender, protocolContext: transaction)
storage.archiveAllSessions(forContact: publicKey, protocolContext: transaction)
// Update the session reset status
thread.sessionResetStatus = .requestReceived
thread.save(with: transaction)

View File

@ -22,10 +22,10 @@ public final class SyncMessagesProtocol : NSObject {
@objc public static func syncProfile() {
try! Storage.writeSync { transaction in
let userPublicKey = getUserHexEncodedPublicKey()
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
for publicKey in linkedDevices {
guard publicKey != userPublicKey else { continue }
let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction)
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
for device in userLinkedDevices {
guard device != userPublicKey else { continue }
let thread = TSContactThread.getOrCreateThread(withContactId: device, transaction: transaction)
thread.save(with: transaction)
let syncMessage = OWSOutgoingSyncMessage.init(in: thread, messageBody: "", attachmentId: nil)
syncMessage.save(with: transaction)
@ -60,14 +60,14 @@ public final class SyncMessagesProtocol : NSObject {
}
@objc public static func syncAllClosedGroups() -> AnyPromise {
var groups: [TSGroupThread] = []
var closedGroups: [TSGroupThread] = []
TSGroupThread.enumerateCollectionObjects { object, _ in
guard let group = object as? TSGroupThread, group.groupModel.groupType == .closedGroup,
group.shouldThreadBeVisible else { return }
groups.append(group)
guard let closedGroup = object as? TSGroupThread, closedGroup.groupModel.groupType == .closedGroup,
closedGroup.shouldThreadBeVisible else { return }
closedGroups.append(closedGroup)
}
let syncManager = SSKEnvironment.shared.syncManager
let promises = groups.map { group -> Promise<Void> in
let promises = closedGroups.map { group -> Promise<Void> in
return Promise(syncManager.syncGroup(for: group)).map2 { _ in }
}
return AnyPromise.from(when(fulfilled: promises))
@ -87,8 +87,8 @@ public final class SyncMessagesProtocol : NSObject {
// MARK: - Receiving
@objc(isValidSyncMessage:in:)
public static func isValidSyncMessage(_ envelope: SSKProtoEnvelope, in transaction: YapDatabaseReadTransaction) -> Bool {
@objc(isValidSyncMessage:transaction:)
public static func isValidSyncMessage(_ envelope: SSKProtoEnvelope, transaction: YapDatabaseReadTransaction) -> Bool {
let publicKey = envelope.source! // Set during UD decryption
return LokiDatabaseUtilities.isUserLinkedDevice(publicKey, transaction: transaction)
}
@ -99,7 +99,7 @@ public final class SyncMessagesProtocol : NSObject {
syncMessageTimestamps[publicKey] = timestamps
}
@objc(isDuplicateSyncMessage:fromHexEncodedPublicKey:)
@objc(isDuplicateSyncMessage:fromPublicKey:)
public static func isDuplicateSyncMessage(_ protoContent: SSKProtoContent, from publicKey: String) -> Bool {
guard let syncMessage = protoContent.syncMessage?.sent else { return false }
var timestamps: Set<UInt64> = syncMessageTimestamps[publicKey] ?? []
@ -111,41 +111,47 @@ public final class SyncMessagesProtocol : NSObject {
return result
}
@objc(updateProfileFromSyncMessageIfNeeded:wrappedIn:using:)
@objc(updateProfileFromSyncMessageIfNeeded:wrappedIn:transaction:)
public static func updateProfileFromSyncMessageIfNeeded(_ dataMessage: SSKProtoDataMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
let sender = envelope.source! // Set during UD decryption
let publicKey = envelope.source! // Set during UD decryption
guard let userMasterPublicKey = storage.getMasterHexEncodedPublicKey(for: getUserHexEncodedPublicKey(), in: transaction) else { return }
let wasSentByMasterDevice = (userMasterPublicKey == sender)
let wasSentByMasterDevice = (userMasterPublicKey == publicKey)
guard wasSentByMasterDevice else { return }
SessionMetaProtocol.updateDisplayNameIfNeeded(for: userMasterPublicKey, using: dataMessage, in: transaction)
SessionMetaProtocol.updateProfileKeyIfNeeded(for: userMasterPublicKey, using: dataMessage)
}
@objc(handleClosedGroupUpdatedSyncMessageIfNeeded:using:)
public static func handleClosedGroupUpdatedSyncMessageIfNeeded(_ transcript: OWSIncomingSentMessageTranscript, using transaction: YapDatabaseReadWriteTransaction) {
@objc(handleClosedGroupUpdateSyncMessageIfNeeded:wrappedIn:transaction:)
public static func handleClosedGroupUpdateSyncMessageIfNeeded(_ transcript: OWSIncomingSentMessageTranscript, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
// Check preconditions
guard let group = transcript.dataMessage.group, let name = group.name else { return }
let publicKey = envelope.source! // Set during UD decryption
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey)
guard wasSentByLinkedDevice, let group = transcript.dataMessage.group, let name = group.name else { return }
// Create or update the group
let id = group.id
let members = group.members
let newGroupThread = TSGroupThread.getOrCreateThread(withGroupId: id, groupType: .closedGroup, transaction: transaction)
let newGroupModel = TSGroupModel(title: name, memberIds: members, image: nil, groupId: id, groupType: .closedGroup, adminIds: group.admins)
newGroupThread.groupModel = newGroupModel // TODO: Should this use the setGroupModel method on TSGroupThread?
newGroupThread.save(with: transaction)
newGroupThread.setGroupModel(newGroupModel, with: transaction)
OWSDisappearingMessagesJob.shared().becomeConsistent(withDisappearingDuration: transcript.dataMessage.expireTimer, thread: newGroupThread, createdByRemoteRecipientId: nil, createdInExistingGroup: true, transaction: transaction)
// Try to establish sessions with all members for which none exists yet when a group is created or updated
ClosedGroupsProtocol.establishSessionsIfNeeded(with: members, in: newGroupThread, using: transaction)
OWSDisappearingMessagesJob.shared().becomeConsistent(withDisappearingDuration: transcript.dataMessage.expireTimer, thread: newGroupThread, createdByRemoteRecipientId: nil, createdInExistingGroup: true, transaction: transaction)
// Notify the user
let contactsManager = SSKEnvironment.shared.contactsManager
let groupUpdatedMessageDescription = newGroupThread.groupModel.getInfoStringAboutUpdate(to: newGroupModel, contactsManager: contactsManager)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: newGroupThread, messageType: .typeGroupUpdate, customMessage: groupUpdatedMessageDescription)
let infoMessageText = newGroupThread.groupModel.getInfoStringAboutUpdate(to: newGroupModel, contactsManager: contactsManager)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: newGroupThread, messageType: .typeGroupUpdate, customMessage: infoMessageText)
infoMessage.save(with: transaction)
}
@objc(handleClosedGroupQuitSyncMessageIfNeeded:using:)
public static func handleClosedGroupQuitSyncMessageIfNeeded(_ transcript: OWSIncomingSentMessageTranscript, using transaction: YapDatabaseReadWriteTransaction) {
@objc(handleClosedGroupQuitSyncMessageIfNeeded:wrappedIn:transaction:)
public static func handleClosedGroupQuitSyncMessageIfNeeded(_ transcript: OWSIncomingSentMessageTranscript, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
// Check preconditions
guard let group = transcript.dataMessage.group else { return }
let publicKey = envelope.source! // Set during UD decryption
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey)
guard wasSentByLinkedDevice, let group = transcript.dataMessage.group else { return }
// Leave the group
let groupThread = TSGroupThread.getOrCreateThread(withGroupId: group.id, groupType: .closedGroup, transaction: transaction)
groupThread.save(with: transaction)
@ -155,12 +161,12 @@ public final class SyncMessagesProtocol : NSObject {
infoMessage.save(with: transaction)
}
@objc(handleContactSyncMessageIfNeeded:wrappedIn:using:)
@objc(handleContactSyncMessageIfNeeded:wrappedIn:transaction:)
public static func handleContactSyncMessageIfNeeded(_ syncMessage: SSKProtoSyncMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
let publicKey = envelope.source! // Set during UD decryption
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey)
guard wasSentByLinkedDevice, let contacts = syncMessage.contacts, let contactsAsData = contacts.data, contactsAsData.count > 0 else { return }
guard wasSentByLinkedDevice, let contacts = syncMessage.contacts, let contactsAsData = contacts.data, !contactsAsData.isEmpty else { return }
print("[Loki] Contact sync message received.")
handleContactSyncMessageData(contactsAsData, using: transaction)
}
@ -169,10 +175,10 @@ public final class SyncMessagesProtocol : NSObject {
let parser = ContactParser(data: data)
let publicKeys = parser.parseHexEncodedPublicKeys()
let userPublicKey = getUserHexEncodedPublicKey()
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
// Try to establish sessions
for publicKey in publicKeys {
guard !linkedDevices.contains(publicKey) else { continue } // Skip self and linked devices
guard !userLinkedDevices.contains(publicKey) else { continue } // Skip self and linked devices
// We don't update the friend request status; that's done in OWSMessageSender.sendMessage(_:)
let friendRequestStatus = storage.getFriendRequestStatus(for: publicKey, transaction: transaction)
switch friendRequestStatus {
@ -199,42 +205,43 @@ public final class SyncMessagesProtocol : NSObject {
}
}
@objc(handleClosedGroupSyncMessageIfNeeded:wrappedIn:using:)
@objc(handleClosedGroupSyncMessageIfNeeded:wrappedIn:transaction:)
public static func handleClosedGroupSyncMessageIfNeeded(_ syncMessage: SSKProtoSyncMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
let publicKey = envelope.source! // Set during UD decryption
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey)
guard wasSentByLinkedDevice, let groups = syncMessage.groups, let groupsAsData = groups.data, groupsAsData.count > 0 else { return }
guard wasSentByLinkedDevice, let groups = syncMessage.groups, let groupsAsData = groups.data, !groupsAsData.isEmpty else { return }
print("[Loki] Closed group sync message received.")
let parser = ClosedGroupParser(data: groupsAsData)
let groupModels = parser.parseGroupModels()
for groupModel in groupModels {
var thread: TSGroupThread! = TSGroupThread(groupId: groupModel.groupId, transaction: transaction)
let closedGroups = parser.parseGroupModels()
for closedGroup in closedGroups {
var thread: TSGroupThread! = TSGroupThread(groupId: closedGroup.groupId, transaction: transaction)
if thread == nil {
thread = TSGroupThread.getOrCreateThread(with: groupModel, transaction: transaction)
thread = TSGroupThread.getOrCreateThread(with: closedGroup, transaction: transaction)
thread.shouldThreadBeVisible = true
thread.save(with: transaction)
}
ClosedGroupsProtocol.establishSessionsIfNeeded(with: groupModel.groupMemberIds, in: thread, using: transaction)
ClosedGroupsProtocol.establishSessionsIfNeeded(with: closedGroup.groupMemberIds, in: thread, using: transaction)
}
}
@objc(handleOpenGroupSyncMessageIfNeeded:wrappedIn:using:)
@objc(handleOpenGroupSyncMessageIfNeeded:wrappedIn:transaction:)
public static func handleOpenGroupSyncMessageIfNeeded(_ syncMessage: SSKProtoSyncMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
let publicKey = envelope.source! // Set during UD decryption
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey)
guard wasSentByLinkedDevice else { return }
let groups = syncMessage.openGroups
guard groups.count > 0 else { return }
let openGroups = syncMessage.openGroups
guard !openGroups.isEmpty else { return }
print("[Loki] Open group sync message received.")
for openGroup in groups {
let openGroupManager = LokiPublicChatManager.shared
let openGroupManager = LokiPublicChatManager.shared
let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey()
let userDisplayName = SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: userPublicKey, transaction: transaction)
for openGroup in openGroups {
guard openGroupManager.getChat(server: openGroup.url, channel: openGroup.channelID) == nil else { return }
let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey()
let displayName = SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: userPublicKey, transaction: transaction)
LokiPublicChatAPI.setDisplayName(to: displayName, on: openGroup.url)
openGroupManager.addChat(server: openGroup.url, channel: openGroup.channelID)
LokiPublicChatAPI.setDisplayName(to: userDisplayName, on: openGroup.url)
// TODO: Should we also set the profile picture here?
}
}
}

View File

@ -103,7 +103,7 @@ public final class LokiPushNotificationManager : NSObject {
}
@objc(acknowledgeDeliveryForMessageWithHash:expiration:hexEncodedPublicKey:)
static func acknowledgeDelivery(forMessageWithHash hash: String, expiration: Int, hexEncodedPublicKey: String) {
static func acknowledgeDelivery(forMessageWithHash hash: String, expiration: UInt64, hexEncodedPublicKey: String) {
let parameters: JSON = [ "lastHash" : hash, "pubKey" : hexEncodedPublicKey, "expiration" : expiration]
let url = URL(string: server + "acknowledge_message_delivery")!
let request = TSRequest(url: url, method: "POST", parameters: parameters)

View File

@ -3,74 +3,74 @@ import PromiseKit
public extension Thenable {
func then2<U>(_ body: @escaping (T) throws -> U) -> Promise<U.T> where U : Thenable {
return then(on: LokiAPI.workQueue, body)
return then(on: SnodeAPI.workQueue, body)
}
func map2<U>(_ transform: @escaping (T) throws -> U) -> Promise<U> {
return map(on: LokiAPI.workQueue, transform)
return map(on: SnodeAPI.workQueue, transform)
}
func done2(_ body: @escaping (T) throws -> Void) -> Promise<Void> {
return done(on: LokiAPI.workQueue, body)
return done(on: SnodeAPI.workQueue, body)
}
func get2(_ body: @escaping (T) throws -> Void) -> Promise<T> {
return get(on: LokiAPI.workQueue, body)
return get(on: SnodeAPI.workQueue, body)
}
}
public extension Thenable where T: Sequence {
func mapValues2<U>(_ transform: @escaping (T.Iterator.Element) throws -> U) -> Promise<[U]> {
return mapValues(on: LokiAPI.workQueue, transform)
return mapValues(on: SnodeAPI.workQueue, transform)
}
}
public extension Guarantee {
func then2<U>(_ body: @escaping (T) -> Guarantee<U>) -> Guarantee<U> {
return then(on: LokiAPI.workQueue, body)
return then(on: SnodeAPI.workQueue, body)
}
func map2<U>(_ body: @escaping (T) -> U) -> Guarantee<U> {
return map(on: LokiAPI.workQueue, body)
return map(on: SnodeAPI.workQueue, body)
}
func done2(_ body: @escaping (T) -> Void) -> Guarantee<Void> {
return done(on: LokiAPI.workQueue, body)
return done(on: SnodeAPI.workQueue, body)
}
func get2(_ body: @escaping (T) -> Void) -> Guarantee<T> {
return get(on: LokiAPI.workQueue, body)
return get(on: SnodeAPI.workQueue, body)
}
}
public extension CatchMixin {
func catch2(_ body: @escaping (Error) -> Void) -> PMKFinalizer {
return self.catch(on: LokiAPI.workQueue, body)
return self.catch(on: SnodeAPI.workQueue, body)
}
func recover2<U: Thenable>(_ body: @escaping(Error) throws -> U) -> Promise<T> where U.T == T {
return recover(on: LokiAPI.workQueue, body)
return recover(on: SnodeAPI.workQueue, body)
}
func recover2(_ body: @escaping(Error) -> Guarantee<T>) -> Guarantee<T> {
return recover(on: LokiAPI.workQueue, body)
return recover(on: SnodeAPI.workQueue, body)
}
func ensure2(_ body: @escaping () -> Void) -> Promise<T> {
return ensure(on: LokiAPI.workQueue, body)
return ensure(on: SnodeAPI.workQueue, body)
}
}
public extension CatchMixin where T == Void {
func recover2(_ body: @escaping(Error) -> Void) -> Guarantee<Void> {
return recover(on: LokiAPI.workQueue, body)
return recover(on: SnodeAPI.workQueue, body)
}
func recover2(_ body: @escaping(Error) throws -> Void) -> Promise<Void> {
return recover(on: LokiAPI.workQueue, body)
return recover(on: SnodeAPI.workQueue, body)
}
}

View File

@ -62,6 +62,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertDebug(envelopeData);
self = [super initWithUniqueId:[NSUUID new].UUIDString];
if (!self) {
return self;
}
@ -259,6 +260,7 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
OWSSingletonAssert();
self = [super init];
if (!self) {
return self;
}
@ -418,6 +420,7 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
AssertOnDispatchQueue(self.serialQueue);
NSMutableArray<OWSMessageContentJob *> *processedJobs = [NSMutableArray new];
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (OWSMessageContentJob *job in jobs) {

View File

@ -283,6 +283,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
// Return to avoid double-acknowledging.
return;
}
case SSKProtoEnvelopeTypeClosedGroupCiphertext: // Loki: Fall through
case SSKProtoEnvelopeTypeUnidentifiedSender: {
[self decryptUnidentifiedSender:envelope
successBlock:^(OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
@ -336,9 +337,9 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
NSString *recipientId = envelope.source;
ECKeyPair *identityKeyPair = self.identityManager.identityKeyPair;
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientId:recipientId privateKey:identityKeyPair.privateKey];
NSData *_Nullable plaintextData = [[cipher decryptWithMessage:encryptedData] removePadding];
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientPublicKey:recipientId privateKey:identityKeyPair.privateKey];
NSData *_Nullable plaintextData = [[cipher decrypt:encryptedData] removePadding];
if (!plaintextData) {
NSString *errorString = [NSString stringWithFormat:@"Failed to decrypt friend request message from: %@.", recipientId];
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, errorString);
@ -500,6 +501,11 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
return failureBlock(cipherError);
}
ECKeyPair *keyPair = nil; // Loki: SMKSecretSessionCipher will fall back on the user's key pair if this is nil
if (envelope.type == SSKProtoEnvelopeTypeClosedGroupCiphertext) {
keyPair = [LKStorage getKeyPairForClosedGroupWithPublicKey:envelope.source];
}
NSError *decryptError;
SMKDecryptResult *_Nullable decryptResult =
[cipher throwswrapped_decryptMessageWithCertificateValidator:certificateValidator
@ -507,6 +513,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
timestamp:serverTimestamp
localRecipientId:localRecipientId
localDeviceId:localDeviceId
keyPair:keyPair
protocolContext:transaction
error:&decryptError];
@ -543,7 +550,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
identifiedEnvelope = [identifiedEnvelopeBuilder buildAndReturnError:&identifiedEnvelopeBuilderError];
if (identifiedEnvelopeBuilderError) {
OWSFailDebug(@"failure identifiedEnvelopeBuilderError: %@", identifiedEnvelopeBuilderError);
OWSFailDebug(@"identifiedEnvelopeBuilderError: %@", identifiedEnvelopeBuilderError);
}
}
OWSAssert(underlyingError);
@ -578,6 +585,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
return;
}
OWSFailDebug(@"%@", underlyingError);
failureBlock(underlyingError);
return;
}
@ -596,7 +604,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
long sourceDeviceId = decryptResult.senderDeviceId;
if (sourceDeviceId < 1 || sourceDeviceId > UINT32_MAX) {
NSString *errorDescription = @"Invalid UD sender device id.";
NSString *errorDescription = @"Invalid UD sender device ID.";
OWSFailDebug(@"%@", errorDescription);
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptUDMessage, errorDescription);
return failureBlock(error);
@ -670,7 +678,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
OWSAssertDebug(errorMessage);
if (errorMessage != nil) {
[errorMessage saveWithTransaction:transaction];
[LKSessionManagementProtocol handleDecryptionError:errorMessage.errorType forPublicKey:envelope.source using:transaction];
[LKSessionManagementProtocol handleDecryptionError:errorMessage.errorType forPublicKey:envelope.source transaction:transaction];
[self notifyUserForErrorMessage:errorMessage envelope:envelope transaction:transaction];
}
} error:nil];

View File

@ -403,16 +403,16 @@ NS_ASSUME_NONNULL_BEGIN
}
OWSPrimaryStorage *storage = OWSPrimaryStorage.sharedManager;
__block NSSet<NSString *> *linkedDeviceHexEncodedPublicKeys;
__block NSSet<NSString *> *senderLinkedDevices;
[storage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
linkedDeviceHexEncodedPublicKeys = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:envelope.source in:transaction];
senderLinkedDevices = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:envelope.source in:transaction];
}];
BOOL duplicateEnvelope = NO;
for (NSString *hexEncodedPublicKey in linkedDeviceHexEncodedPublicKeys) {
for (NSString *publicKey in senderLinkedDevices) {
duplicateEnvelope = duplicateEnvelope
|| [self.incomingMessageFinder existsMessageWithTimestamp:envelope.timestamp
sourceId:hexEncodedPublicKey
sourceId:publicKey
sourceDeviceId:envelope.sourceDevice
transaction:transaction];
}
@ -423,43 +423,65 @@ NS_ASSUME_NONNULL_BEGIN
envelope.timestamp);
return;
}
if (envelope.content != nil) {
// Loki: Decrypt closed group message if applicable
NSData *sharedSenderKeysPlaintext = nil;
if ([[LKStorage getUserClosedGroupPublicKeys] containsObject:envelope.source]) {
NSError *error;
SSKProtoContent *_Nullable contentProto = [SSKProtoContent parseData:plaintextData error:&error];
if (error || !contentProto) {
OWSFailDebug(@"Could not parse proto due to error: %@.", error);
SSKProtoClosedGroupCiphertext *_Nullable closedGroupCiphertextProto = [SSKProtoClosedGroupCiphertext parseData:plaintextData error:&error];
if (error != nil || closedGroupCiphertextProto == nil) {
OWSFailDebug(@"Couldn't parse proto due to error: %@.", error);
return;
}
NSString *senderPublicKey = closedGroupCiphertextProto.senderPublicKey;
uint32_t keyIndex = closedGroupCiphertextProto.keyIndex;
sharedSenderKeysPlaintext = [LKClosedGroupsProtocol decryptCiphertext:closedGroupCiphertextProto.ciphertext forGroupWithPublicKey:envelope.source senderPublicKey:senderPublicKey keyIndex:keyIndex];
if (sharedSenderKeysPlaintext == nil) {
OWSFailDebug(@"Couldn't parse proto due to error: %@.", error);
return;
}
}
if (envelope.content != nil || sharedSenderKeysPlaintext != nil) {
NSError *error;
SSKProtoContent *_Nullable contentProto = [SSKProtoContent parseData:(sharedSenderKeysPlaintext ?: plaintextData) error:&error];
if (error != nil || contentProto == nil) {
OWSFailDebug(@"Couldn't parse proto due to error: %@.", error);
return;
}
OWSLogInfo(@"Handling content: <Content: %@>.", [self descriptionForContent:contentProto]);
// Loki: Ignore any friend requests from before restoration
// Loki: Ignore friend requests from before restoration
if ([LKFriendRequestProtocol isFriendRequestFromBeforeRestoration:envelope]) {
[LKLogger print:@"[Loki] Ignoring friend request from before restoration."];
return;
}
// Loki: Ignore any duplicate sync transcripts
if ([LKSyncMessagesProtocol isDuplicateSyncMessage:contentProto fromHexEncodedPublicKey:envelope.source]) { return; }
// Loki: Ignore duplicate sync transcripts
if ([LKSyncMessagesProtocol isDuplicateSyncMessage:contentProto fromPublicKey:envelope.source]) {
[LKLogger print:@"[Loki] Ignoring duplicate sync transcript."];
return;
}
// Loki: Handle pre key bundle message if needed
[LKSessionManagementProtocol handlePreKeyBundleMessageIfNeeded:contentProto wrappedIn:envelope using:transaction];
[LKSessionManagementProtocol handlePreKeyBundleMessageIfNeeded:contentProto wrappedIn:envelope transaction:transaction];
// Loki: Handle session request if needed
if ([LKSessionManagementProtocol isSessionRequestMessage:contentProto.dataMessage]) {
[LKSessionManagementProtocol handleSessionRequestMessage:contentProto.dataMessage wrappedIn:envelope using:transaction];
return; // Don't process the message any further
if ([LKSessionManagementProtocol handleSessionRequestMessageIfNeeded:contentProto wrappedIn:envelope transaction:transaction]) {
return;
}
// Loki: Handle session restoration request if needed
if ([LKSessionManagementProtocol isSessionRestorationRequest:contentProto.dataMessage]) { return; } // Don't process the message any further
if ([LKSessionManagementProtocol isSessionRestorationRequest:contentProto.dataMessage]) {
return;
}
// Loki: Handle friend request acceptance if needed
[LKFriendRequestProtocol handleFriendRequestAcceptanceIfNeeded:envelope in:transaction];
// Loki: Handle device linking message if needed
if (contentProto.lokiDeviceLinkMessage != nil) {
[LKMultiDeviceProtocol handleDeviceLinkMessageIfNeeded:contentProto wrappedIn:envelope using:transaction];
[LKMultiDeviceProtocol handleDeviceLinkMessageIfNeeded:contentProto wrappedIn:envelope transaction:transaction];
} else if (contentProto.syncMessage) {
[self throws_handleIncomingEnvelope:envelope
withSyncMessage:contentProto.syncMessage
@ -542,13 +564,15 @@ NS_ASSUME_NONNULL_BEGIN
}
}
BOOL usesSharedSenderKeys = [LKClosedGroupsProtocol handleSharedSenderKeysUpdateIfNeeded:dataMessage transaction:transaction];
if (dataMessage.group) {
TSGroupThread *_Nullable groupThread =
[TSGroupThread threadWithGroupId:dataMessage.group.id transaction:transaction];
if (groupThread) {
// Loki: Ignore closed group message if needed
if ([LKClosedGroupsProtocol shouldIgnoreClosedGroupMessage:dataMessage inThread:groupThread wrappedIn:envelope using:transaction]) { return; }
if ([LKClosedGroupsProtocol shouldIgnoreClosedGroupMessage:dataMessage inThread:groupThread wrappedIn:envelope]) { return; }
if (dataMessage.group.type != SSKProtoGroupContextTypeUpdate) {
if (![groupThread isCurrentUserInGroupWithTransaction:transaction]) {
@ -557,9 +581,9 @@ NS_ASSUME_NONNULL_BEGIN
}
}
} else {
// Unknown group.
// Unknown group
if (dataMessage.group.type == SSKProtoGroupContextTypeUpdate) {
// Accept group updates for unknown groups.
// Accept group updates for unknown groups
} else if (dataMessage.group.type == SSKProtoGroupContextTypeDeliver) {
[self sendGroupInfoRequest:dataMessage.group.id envelope:envelope transaction:transaction];
return;
@ -577,7 +601,7 @@ NS_ASSUME_NONNULL_BEGIN
} else if ((dataMessage.flags & SSKProtoDataMessageFlagsProfileKeyUpdate) != 0) {
[self handleProfileKeyMessageWithEnvelope:envelope dataMessage:dataMessage];
} else if ([LKMultiDeviceProtocol isUnlinkDeviceMessage:dataMessage]) {
[LKMultiDeviceProtocol handleUnlinkDeviceMessage:dataMessage wrappedIn:envelope using:transaction];
[LKMultiDeviceProtocol handleUnlinkDeviceMessage:dataMessage wrappedIn:envelope transaction:transaction];
} else if (dataMessage.attachments.count > 0) {
[self handleReceivedMediaWithEnvelope:envelope
dataMessage:dataMessage
@ -846,6 +870,7 @@ NS_ASSUME_NONNULL_BEGIN
}
TSThread *_Nullable thread = [self threadForEnvelope:envelope dataMessage:dataMessage transaction:transaction];
if (!thread) {
OWSFailDebug(@"Ignoring media message for unknown group.");
return;
@ -895,7 +920,7 @@ NS_ASSUME_NONNULL_BEGIN
}
// Loki: Take into account multi device when checking sync message validity
if (![LKSyncMessagesProtocol isValidSyncMessage:envelope in:transaction]) {
if (![LKSyncMessagesProtocol isValidSyncMessage:envelope transaction:transaction]) {
OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorSyncMessageFromUnknownSource], envelope);
return;
}
@ -911,7 +936,7 @@ NS_ASSUME_NONNULL_BEGIN
}
// Loki: Update profile if needed (i.e. if the sync message came from the master device)
[LKSyncMessagesProtocol updateProfileFromSyncMessageIfNeeded:dataMessage wrappedIn:envelope using:transaction];
[LKSyncMessagesProtocol updateProfileFromSyncMessageIfNeeded:dataMessage wrappedIn:envelope transaction:transaction];
NSString *destination = syncMessage.sent.destination;
if (dataMessage && destination.length > 0 && dataMessage.hasProfileKey) {
@ -950,10 +975,10 @@ NS_ASSUME_NONNULL_BEGIN
} else {
if (transcript.isGroupUpdate) {
// Loki: Handle closed group updated sync message
[LKSyncMessagesProtocol handleClosedGroupUpdatedSyncMessageIfNeeded:transcript using:transaction];
[LKSyncMessagesProtocol handleClosedGroupUpdateSyncMessageIfNeeded:transcript wrappedIn:envelope transaction:transaction];
} else if (transcript.isGroupQuit) {
// Loki: Handle closed group quit sync message
[LKSyncMessagesProtocol handleClosedGroupQuitSyncMessageIfNeeded:transcript using:transaction];
[LKSyncMessagesProtocol handleClosedGroupQuitSyncMessageIfNeeded:transcript wrappedIn:envelope transaction:transaction];
} else {
[OWSRecordTranscriptJob
processIncomingSentMessageTranscript:transcript
@ -1003,13 +1028,13 @@ NS_ASSUME_NONNULL_BEGIN
[self.identityManager throws_processIncomingSyncMessage:syncMessage.verified transaction:transaction];
} else if (syncMessage.contacts != nil) {
// Loki: Handle contact sync message
[LKSyncMessagesProtocol handleContactSyncMessageIfNeeded:syncMessage wrappedIn:envelope using:transaction];
[LKSyncMessagesProtocol handleContactSyncMessageIfNeeded:syncMessage wrappedIn:envelope transaction:transaction];
} else if (syncMessage.groups != nil) {
// Loki: Handle closed groups sync message
[LKSyncMessagesProtocol handleClosedGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope using:transaction];
[LKSyncMessagesProtocol handleClosedGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope transaction:transaction];
} else if (syncMessage.openGroups != nil) {
// Loki: Handle open groups sync message
[LKSyncMessagesProtocol handleOpenGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope using:transaction];
[LKSyncMessagesProtocol handleOpenGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope transaction:transaction];
} else {
OWSLogWarn(@"Ignoring unsupported sync message.");
}
@ -1033,8 +1058,7 @@ NS_ASSUME_NONNULL_BEGIN
}
// Loki: Handle session reset
NSString *sender = envelope.source;
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:sender transaction:transaction];
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:envelope.source transaction:transaction];
[LKSessionManagementProtocol handleEndSessionMessageReceivedInThread:thread using:transaction];
}
@ -1057,7 +1081,7 @@ NS_ASSUME_NONNULL_BEGIN
TSThread *_Nullable thread = [self threadForEnvelope:envelope dataMessage:dataMessage transaction:transaction];
if (!thread) {
OWSFailDebug(@"ignoring expiring messages update for unknown group.");
OWSFailDebug(@"Ignoring expiring messages update for unknown group.");
return;
}
@ -1107,6 +1131,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSFailDebug(@"Received a profile key message without a profile key from: %@.", envelopeAddress(envelope));
return;
}
NSData *profileKey = dataMessage.profileKey;
if (profileKey.length != kAES256_KeyByteLength) {
OWSFailDebug(@"received profile key of unexpected length: %lu, from: %@",
@ -1263,7 +1288,7 @@ NS_ASSUME_NONNULL_BEGIN
if ([ECKeyPair isValidHexEncodedPublicKeyWithCandidate:envelope.source] && dataMessage.publicChatInfo == nil) { // Handled in LokiPublicChatPoller for open group messages
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
[[LKMultiDeviceProtocol updateDeviceLinksIfNeededForHexEncodedPublicKey:envelope.source in:transaction].ensureOn(queue, ^() {
[[LKMultiDeviceProtocol updateDeviceLinksIfNeededForPublicKey:envelope.source transaction:transaction].ensureOn(queue, ^() {
dispatch_semaphore_signal(semaphore);
}).catchOn(queue, ^(NSError *error) {
dispatch_semaphore_signal(semaphore);
@ -1277,16 +1302,15 @@ NS_ASSUME_NONNULL_BEGIN
NSMutableSet *removedMemberIds = [NSMutableSet new];
for (NSString *recipientId in newMemberIds) {
if (![ECKeyPair isValidHexEncodedPublicKeyWithCandidate:recipientId]) {
OWSLogVerbose(
@"incoming group update has invalid group member: %@", [self descriptionForEnvelope:envelope]);
OWSFailDebug(@"incoming group update has invalid group member");
OWSLogVerbose(@"Incoming group update has invalid group member: %@", [self descriptionForEnvelope:envelope]);
OWSFailDebug(@"Incoming group update has invalid group member");
return nil;
}
}
NSString *senderMasterHexEncodedPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source);
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
NSString *userMasterHexEncodedPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userHexEncodedPublicKey in:transaction] ?: userHexEncodedPublicKey);
NSString *senderMasterPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source);
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
NSString *userMasterPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userPublicKey in:transaction] ?: userPublicKey);
// Group messages create the group if it doesn't already exist.
//
@ -1306,18 +1330,18 @@ NS_ASSUME_NONNULL_BEGIN
}
// Loki: Handle profile key update if needed
[LKSessionMetaProtocol updateProfileKeyIfNeededForPublicKey:senderMasterHexEncodedPublicKey using:dataMessage];
[LKSessionMetaProtocol updateProfileKeyIfNeededForPublicKey:senderMasterPublicKey using:dataMessage];
// Loki: Handle display name update if needed
[LKSessionMetaProtocol updateDisplayNameIfNeededForPublicKey:senderMasterHexEncodedPublicKey using:dataMessage transaction:transaction];
[LKSessionMetaProtocol updateDisplayNameIfNeededForPublicKey:senderMasterPublicKey using:dataMessage transaction:transaction];
switch (dataMessage.group.type) {
case SSKProtoGroupContextTypeUpdate: {
// Loki: Ignore updates from non-admins
if ([LKClosedGroupsProtocol shouldIgnoreClosedGroupUpdateMessage:dataMessage inThread:oldGroupThread wrappedIn:envelope using:transaction]) {
if ([LKClosedGroupsProtocol shouldIgnoreClosedGroupUpdateMessage:dataMessage inThread:oldGroupThread wrappedIn:envelope]) {
return nil;
}
// Ensures that the thread exists but doesn't update it.
// Ensures that the thread exists but don't update it.
TSGroupThread *newGroupThread =
[TSGroupThread getOrCreateThreadWithGroupId:groupId groupType:oldGroupThread.groupModel.groupType transaction:transaction];
@ -1333,10 +1357,12 @@ NS_ASSUME_NONNULL_BEGIN
[newGroupThread setGroupModel:newGroupModel withTransaction:transaction];
BOOL wasCurrentUserRemovedFromGroup = [removedMemberIds containsObject:userMasterHexEncodedPublicKey];
BOOL wasCurrentUserRemovedFromGroup = [removedMemberIds containsObject:userMasterPublicKey];
if (!wasCurrentUserRemovedFromGroup) {
// Loki: Try to establish sessions with all members when a group is created or updated
[LKClosedGroupsProtocol establishSessionsIfNeededWithClosedGroupMembers:newMemberIds.allObjects inThread:newGroupThread using:transaction];
if (!newGroupThread.usesSharedSenderKeys) {
// Loki: Try to establish sessions with all members involved when a group is created or updated
[LKClosedGroupsProtocol establishSessionsIfNeededWithClosedGroupMembers:newMemberIds.allObjects inThread:newGroupThread transaction:transaction];
}
}
[[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer
@ -1365,12 +1391,12 @@ NS_ASSUME_NONNULL_BEGIN
return nil;
}
newMemberIds = [NSMutableSet setWithArray:oldGroupThread.groupModel.groupMemberIds];
[newMemberIds removeObject:senderMasterHexEncodedPublicKey];
[newMemberIds removeObject:senderMasterPublicKey];
oldGroupThread.groupModel.groupMemberIds = [newMemberIds.allObjects mutableCopy];
[oldGroupThread saveWithTransaction:transaction];
NSString *nameString =
[self.contactsManager displayNameForPhoneIdentifier:senderMasterHexEncodedPublicKey transaction:transaction];
[self.contactsManager displayNameForPhoneIdentifier:senderMasterPublicKey transaction:transaction];
NSString *updateGroupInfo =
[NSString stringWithFormat:NSLocalizedString(@"GROUP_MEMBER_LEFT", @""), nameString];
// MJK TODO - should be safe to remove senderTimestamp
@ -1382,7 +1408,7 @@ NS_ASSUME_NONNULL_BEGIN
// If we were the one that quit then we need to leave the group (only relevant for slave
// devices in a multi device context)
// TODO: This needs more documentation
if (![newMemberIds containsObject:userMasterHexEncodedPublicKey]) {
if (![newMemberIds containsObject:userMasterPublicKey]) {
[oldGroupThread leaveGroupWithTransaction:transaction];
}
@ -1390,13 +1416,13 @@ NS_ASSUME_NONNULL_BEGIN
}
case SSKProtoGroupContextTypeDeliver: {
if (!oldGroupThread) {
OWSFailDebug(@"ignoring deliver group message from unknown group.");
OWSFailDebug(@"Ignoring deliver group message from unknown group.");
return nil;
}
[[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer
thread:oldGroupThread
createdByRemoteRecipientId:senderMasterHexEncodedPublicKey
createdByRemoteRecipientId:senderMasterPublicKey
createdInExistingGroup:NO
transaction:transaction];
@ -1414,7 +1440,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSLogError(@"linkPreviewError: %@", linkPreviewError);
}
OWSLogDebug(@"incoming message from: %@ for group: %@ with timestamp: %lu",
OWSLogDebug(@"Incoming message from: %@ for group: %@ with timestamp: %lu",
envelopeAddress(envelope),
groupId,
(unsigned long)timestamp);
@ -1423,7 +1449,7 @@ NS_ASSUME_NONNULL_BEGIN
TSIncomingMessage *incomingMessage =
[[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:timestamp
inThread:oldGroupThread
authorId:senderMasterHexEncodedPublicKey
authorId:senderMasterPublicKey
sourceDeviceId:envelope.sourceDevice
messageBody:body
attachmentIds:@[]
@ -1449,13 +1475,13 @@ NS_ASSUME_NONNULL_BEGIN
if (body.length == 0 && attachmentPointers.count < 1 && !contact) {
OWSLogWarn(@"Ignoring empty incoming message from: %@ for group: %@ with timestamp: %lu.",
senderMasterHexEncodedPublicKey,
senderMasterPublicKey,
groupId,
(unsigned long)timestamp);
return nil;
}
// Loki: Cache the user hex encoded public key (for mentions)
// Loki: Cache the user public key (for mentions)
dispatch_async(dispatch_get_main_queue(), ^{
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[LKMentionsManager populateUserPublicKeyCacheIfNeededFor:oldGroupThread.uniqueId in:transaction];
@ -1486,16 +1512,16 @@ NS_ASSUME_NONNULL_BEGIN
// Loki: A message from a slave device should appear as if it came from the master device; the underlying
// friend request logic, however, should still be specific to the slave device.
NSString *hexEncodedPublicKey = envelope.source;
NSString *masterHexEncodedPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source);
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction];
TSContactThread *masterThread = [TSContactThread getOrCreateThreadWithContactId:masterHexEncodedPublicKey transaction:transaction];
NSString *publicKey = envelope.source;
NSString *masterPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source);
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:publicKey transaction:transaction];
TSContactThread *masterThread = [TSContactThread getOrCreateThreadWithContactId:masterPublicKey transaction:transaction];
OWSLogDebug(@"Incoming message from: %@ with timestamp: %lu.", hexEncodedPublicKey, (unsigned long)timestamp);
OWSLogDebug(@"Incoming message from: %@ with timestamp: %lu.", publicKey, (unsigned long)timestamp);
[[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer
thread:masterThread
createdByRemoteRecipientId:hexEncodedPublicKey
createdByRemoteRecipientId:publicKey
createdInExistingGroup:NO
transaction:transaction];
@ -1528,7 +1554,10 @@ NS_ASSUME_NONNULL_BEGIN
serverTimestamp:serverTimestamp
wasReceivedByUD:wasReceivedByUD];
// Loki: Handle display name update if needed
[LKSessionMetaProtocol updateDisplayNameIfNeededForPublicKey:incomingMessage.authorId using:dataMessage transaction:transaction];
// Loki: Handle profile key update if needed
[LKSessionMetaProtocol updateProfileKeyIfNeededForPublicKey:thread.contactIdentifier using:dataMessage];
NSArray<TSAttachmentPointer *> *attachmentPointers =
@ -1544,33 +1573,22 @@ NS_ASSUME_NONNULL_BEGIN
if (body.length == 0 && attachmentPointers.count < 1 && !contact) {
if (envelope.type == SSKProtoEnvelopeTypeFriendRequest) {
// Loki: This is needed for compatibility with refactored desktop clients
[LKSessionManagementProtocol sendSessionEstablishedMessageToPublicKey:hexEncodedPublicKey in:transaction];
[LKSessionManagementProtocol sendSessionEstablishedMessageToPublicKey:publicKey transaction:transaction];
} else {
OWSLogWarn(@"Ignoring empty incoming message from: %@ with timestamp: %lu.", hexEncodedPublicKey, (unsigned long)timestamp);
OWSLogWarn(@"Ignoring empty incoming message from: %@ with timestamp: %lu.", publicKey, (unsigned long)timestamp);
}
return nil;
}
// Loki: This is needed for compatibility with refactored desktop clients
LKFriendRequestStatus friendRequestStatus = [self.primaryStorage getFriendRequestStatusForContact:hexEncodedPublicKey transaction:transaction];
LKFriendRequestStatus friendRequestStatus = [self.primaryStorage getFriendRequestStatusForContact:publicKey transaction:transaction];
if (friendRequestStatus == LKFriendRequestStatusNone || friendRequestStatus == LKFriendRequestStatusRequestExpired) {
[self.primaryStorage setFriendRequestStatus:LKFriendRequestStatusRequestReceived forContact:hexEncodedPublicKey transaction:transaction];
[self.primaryStorage setFriendRequestStatus:LKFriendRequestStatusRequestReceived forContact:publicKey transaction:transaction];
} else if (friendRequestStatus == LKFriendRequestStatusRequestSent) {
[self.primaryStorage setFriendRequestStatus:LKFriendRequestStatusFriends forContact:hexEncodedPublicKey transaction:transaction];
[LKFriendRequestProtocol sendFriendRequestAcceptedMessageToPublicKey:hexEncodedPublicKey using:transaction];
[LKSyncMessagesProtocol syncContactWithPublicKey:masterHexEncodedPublicKey in:transaction];
[self.primaryStorage setFriendRequestStatus:LKFriendRequestStatusFriends forContact:publicKey transaction:transaction];
[LKFriendRequestProtocol sendFriendRequestAcceptedMessageToPublicKey:publicKey using:transaction];
[LKSyncMessagesProtocol syncContactWithPublicKey:masterPublicKey];
}
// Loki: If we received a message from a contact in the last 2 minutes that wasn't P2P, then we need to ping them.
// We assume this occurred because they don't have our P2P details.
/*
if (!envelope.isPtpMessage && hexEncodedPublicKey != nil) {
uint64_t timestamp = envelope.timestamp;
uint64_t now = NSDate.ows_millisecondTimeStamp;
uint64_t ageInSeconds = (now - timestamp) / 1000;
if (ageInSeconds <= 120) { [LKP2PAPI pingContact:hexEncodedPublicKey]; }
}
*/
[self finalizeIncomingMessage:incomingMessage
thread:thread

View File

@ -125,6 +125,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
- (OWSMessageDecryptJob *_Nullable)nextJob
{
__block OWSMessageDecryptJob *_Nullable job = nil;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
YapDatabaseViewTransaction *viewTransaction = [transaction ext:OWSMessageDecryptJobFinderExtensionName];
OWSAssertDebug(viewTransaction != nil);
@ -406,7 +407,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
}
// We persist the decrypted envelope data in the same transaction within which
// it was decrypted to prevent data loss. If the new job isn't persisted,
// it was decrypted to prevent data loss. If the new job isn't persisted,
// the session state side effects of its decryption are also rolled back.
//
// NOTE: We use envelopeData from the decrypt result, not job.envelopeData,

View File

@ -612,7 +612,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
if ([LKMultiDeviceProtocol isMultiDeviceRequiredForMessage:message]) { // Avoid the write transaction if possible
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[LKMultiDeviceProtocol sendMessageToDestinationAndLinkedDevices:messageSend in:transaction];
[LKMultiDeviceProtocol sendMessageToDestinationAndLinkedDevices:messageSend transaction:transaction];
}];
} else {
[self sendMessage:messageSend];
@ -944,7 +944,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
if ([TSPreKeyManager isAppLockedDueToPreKeyUpdateFailures]) {
OWSProdError([OWSAnalyticsEvents messageSendErrorFailedDueToPrekeyUpdateFailures]);
// Retry pr ekey update every time user tries to send a message while the app
// Retry pre key update every time user tries to send a message while the app
// is disabled due to pre key update failures.
//
// Only try to update the signed pre key; updating it is sufficient to
@ -1142,7 +1142,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}
} error:nil];
// Convenience
void (^onP2PSuccess)() = ^() { message.isP2P = YES; };
void (^handleError)(NSError *error) = ^(NSError *error) {
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
if (!message.skipSave) {
@ -1157,7 +1156,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
failedMessageSend(error);
};
// Send the message
[[LKAPI sendSignalMessage:signalMessage onP2PSuccess:onP2PSuccess]
[[LKSnodeAPI sendSignalMessage:signalMessage]
.thenOn(OWSDispatch.sendingQueue, ^(id result) {
NSSet<AnyPromise *> *promises = (NSSet<AnyPromise *> *)result;
__block BOOL isSuccess = NO;
@ -1291,7 +1290,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
switch (statusCode) {
case 0: { // Loki
NSError *error;
if ([responseError isKindOfClass:LokiAPIError.class] || [responseError isKindOfClass:LokiDotNetAPIError.class]
if ([responseError isKindOfClass:LKSnodeAPIError.class] || [responseError isKindOfClass:LKDotNetAPIError.class]
|| [responseError isKindOfClass:DiffieHellmanError.class]) {
error = responseError;
} else {
@ -1672,10 +1671,10 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
TSOutgoingMessage *message = messageSend.message;
ECKeyPair *identityKeyPair = self.identityManager.identityKeyPair;
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientId:recipientId privateKey:identityKeyPair.privateKey];
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientPublicKey:recipientId privateKey:identityKeyPair.privateKey];
// This will return nil if encryption failed
NSData *_Nullable serializedMessage = [cipher encryptWithMessage:[plainText paddedMessageBody]];
NSData *_Nullable serializedMessage = [cipher encrypt:[plainText paddedMessageBody]];
if (!serializedMessage) {
OWSFailDebug(@"Failed to encrypt friend request for: %@.", recipientId);
return nil;
@ -1752,13 +1751,47 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
OWSRaiseException(@"SecretSessionCipherFailure", @"Can't create secret session cipher.");
}
serializedMessage = [secretCipher throwswrapped_encryptMessageWithRecipientId:recipientID
deviceId:@(OWSDevicePrimaryDeviceId).intValue
paddedPlaintext:[plainText paddedMessageBody]
senderCertificate:messageSend.senderCertificate
protocolContext:transaction
useFallbackSessionCipher:isFriendRequestMessage || isSessionRequestMessage || isDeviceLinkMessage
error:&error];
if ([messageSend.thread isKindOfClass:TSGroupThread.class] && ((TSGroupThread *)messageSend.thread).usesSharedSenderKeys) {
NSString *groupPublicKey = [LKGroupUtilities getDecodedGroupID:((TSGroupThread *)messageSend.thread).groupModel.groupId];
NSString *senderPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
NSArray *ciphertextAndKeyIndex = [LKClosedGroupsProtocol encryptPlaintext:plainText.paddedMessageBody forGroupWithPublicKey:groupPublicKey senderPublicKey:senderPublicKey];
if (ciphertextAndKeyIndex.count != 2) {
OWSFailDebug(@"Couldn't encrypt closed group message.");
return nil;
}
NSData *ivAndCiphertext = ciphertextAndKeyIndex[0];
NSNumber *keyIndex = ciphertextAndKeyIndex[1];
SSKProtoClosedGroupCiphertextBuilder *builder = [SSKProtoClosedGroupCiphertext builderWithCiphertext:ivAndCiphertext senderPublicKey:senderPublicKey keyIndex:keyIndex.unsignedIntValue];
SSKProtoClosedGroupCiphertext *closedGroupCiphertext = [builder buildAndReturnError:&error];
if (closedGroupCiphertext == nil) {
OWSFailDebug(@"Couldn't build closed group message due to error: %@.", error);
return nil;
}
ECKeyPair *keyPair = [LKStorage getKeyPairForClosedGroupWithPublicKey:groupPublicKey];
if (keyPair == nil) {
OWSFailDebug(@"Missing key pair for closed group with public key: %@.", groupPublicKey);
return nil;
}
serializedMessage = [secretCipher throwswrapped_encryptMessageWithRecipientId:recipientID
deviceId:@(OWSDevicePrimaryDeviceId).intValue
paddedPlaintext:nil
closedGroupCiphertext:closedGroupCiphertext
senderCertificate:messageSend.senderCertificate
keyPair:keyPair
protocolContext:transaction
useFallbackSessionCipher:NO
error:&error];
} else {
serializedMessage = [secretCipher throwswrapped_encryptMessageWithRecipientId:recipientID
deviceId:@(OWSDevicePrimaryDeviceId).intValue
paddedPlaintext:plainText.paddedMessageBody
closedGroupCiphertext:nil
senderCertificate:messageSend.senderCertificate
keyPair:nil
protocolContext:transaction
useFallbackSessionCipher:isFriendRequestMessage || isSessionRequestMessage || isDeviceLinkMessage
error:&error];
}
SCKRaiseIfExceptionWrapperError(error);
if (!serializedMessage || error) {

View File

@ -174,7 +174,7 @@ NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptMa
TSThread *thread = [TSContactThread getOrCreateThreadWithContactId:recipientId];
if (![LKSessionMetaProtocol shouldSendReceiptForThread:thread]) {
if (![LKSessionMetaProtocol shouldSendReceiptInThread:thread]) {
continue;
}

View File

@ -286,7 +286,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
self.toLinkedDevicesReadReceiptMap[threadUniqueId] = newReadReceipt;
}
if (![LKSessionMetaProtocol shouldSendReceiptForThread:message.thread]) {
if (![LKSessionMetaProtocol shouldSendReceiptInThread:message.thread]) {
return;
}

View File

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

View File

@ -23,6 +23,7 @@ public enum SSKProtoError: Error {
case prekeyBundle = 3
case receipt = 5
case unidentifiedSender = 6
case closedGroupCiphertext = 7
case friendRequest = 101
}
@ -34,6 +35,7 @@ public enum SSKProtoError: Error {
case .prekeyBundle: return .prekeyBundle
case .receipt: return .receipt
case .unidentifiedSender: return .unidentifiedSender
case .closedGroupCiphertext: return .closedGroupCiphertext
case .friendRequest: return .friendRequest
}
}
@ -46,6 +48,7 @@ public enum SSKProtoError: Error {
case .prekeyBundle: return .prekeyBundle
case .receipt: return .receipt
case .unidentifiedSender: return .unidentifiedSender
case .closedGroupCiphertext: return .closedGroupCiphertext
case .friendRequest: return .friendRequest
}
}
@ -1707,6 +1710,133 @@ extension SSKProtoCallMessage.SSKProtoCallMessageBuilder {
#endif
// MARK: - SSKProtoClosedGroupCiphertext
@objc public class SSKProtoClosedGroupCiphertext: NSObject {
// MARK: - SSKProtoClosedGroupCiphertextBuilder
@objc public class func builder(ciphertext: Data, senderPublicKey: String, keyIndex: UInt32) -> SSKProtoClosedGroupCiphertextBuilder {
return SSKProtoClosedGroupCiphertextBuilder(ciphertext: ciphertext, senderPublicKey: senderPublicKey, keyIndex: keyIndex)
}
// asBuilder() constructs a builder that reflects the proto's contents.
@objc public func asBuilder() -> SSKProtoClosedGroupCiphertextBuilder {
let builder = SSKProtoClosedGroupCiphertextBuilder(ciphertext: ciphertext, senderPublicKey: senderPublicKey, keyIndex: keyIndex)
return builder
}
@objc public class SSKProtoClosedGroupCiphertextBuilder: NSObject {
private var proto = SignalServiceProtos_ClosedGroupCiphertext()
@objc fileprivate override init() {}
@objc fileprivate init(ciphertext: Data, senderPublicKey: String, keyIndex: UInt32) {
super.init()
setCiphertext(ciphertext)
setSenderPublicKey(senderPublicKey)
setKeyIndex(keyIndex)
}
@objc public func setCiphertext(_ valueParam: Data) {
proto.ciphertext = valueParam
}
@objc public func setSenderPublicKey(_ valueParam: String) {
proto.senderPublicKey = valueParam
}
@objc public func setKeyIndex(_ valueParam: UInt32) {
proto.keyIndex = valueParam
}
@objc public func build() throws -> SSKProtoClosedGroupCiphertext {
return try SSKProtoClosedGroupCiphertext.parseProto(proto)
}
@objc public func buildSerializedData() throws -> Data {
return try SSKProtoClosedGroupCiphertext.parseProto(proto).serializedData()
}
}
fileprivate let proto: SignalServiceProtos_ClosedGroupCiphertext
@objc public let ciphertext: Data
@objc public let senderPublicKey: String
@objc public let keyIndex: UInt32
private init(proto: SignalServiceProtos_ClosedGroupCiphertext,
ciphertext: Data,
senderPublicKey: String,
keyIndex: UInt32) {
self.proto = proto
self.ciphertext = ciphertext
self.senderPublicKey = senderPublicKey
self.keyIndex = keyIndex
}
@objc
public func serializedData() throws -> Data {
return try self.proto.serializedData()
}
@objc public class func parseData(_ serializedData: Data) throws -> SSKProtoClosedGroupCiphertext {
let proto = try SignalServiceProtos_ClosedGroupCiphertext(serializedData: serializedData)
return try parseProto(proto)
}
fileprivate class func parseProto(_ proto: SignalServiceProtos_ClosedGroupCiphertext) throws -> SSKProtoClosedGroupCiphertext {
guard proto.hasCiphertext else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: ciphertext")
}
let ciphertext = proto.ciphertext
guard proto.hasSenderPublicKey else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: senderPublicKey")
}
let senderPublicKey = proto.senderPublicKey
guard proto.hasKeyIndex else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: keyIndex")
}
let keyIndex = proto.keyIndex
// MARK: - Begin Validation Logic for SSKProtoClosedGroupCiphertext -
// MARK: - End Validation Logic for SSKProtoClosedGroupCiphertext -
let result = SSKProtoClosedGroupCiphertext(proto: proto,
ciphertext: ciphertext,
senderPublicKey: senderPublicKey,
keyIndex: keyIndex)
return result
}
@objc public override var debugDescription: String {
return "\(proto)"
}
}
#if DEBUG
extension SSKProtoClosedGroupCiphertext {
@objc public func serializedDataIgnoringErrors() -> Data? {
return try! self.serializedData()
}
}
extension SSKProtoClosedGroupCiphertext.SSKProtoClosedGroupCiphertextBuilder {
@objc public func buildIgnoringErrors() -> SSKProtoClosedGroupCiphertext? {
return try! self.build()
}
}
#endif
// MARK: - SSKProtoDataMessageQuoteQuotedAttachment
@objc public class SSKProtoDataMessageQuoteQuotedAttachment: NSObject {
@ -3288,16 +3418,42 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
@objc public class SSKProtoDataMessageClosedGroupUpdate: NSObject {
// MARK: - SSKProtoDataMessageClosedGroupUpdateType
@objc public enum SSKProtoDataMessageClosedGroupUpdateType: Int32 {
case new = 0
}
private class func SSKProtoDataMessageClosedGroupUpdateTypeWrap(_ value: SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum) -> SSKProtoDataMessageClosedGroupUpdateType {
switch value {
case .new: return .new
}
}
private class func SSKProtoDataMessageClosedGroupUpdateTypeUnwrap(_ value: SSKProtoDataMessageClosedGroupUpdateType) -> SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum {
switch value {
case .new: return .new
}
}
// MARK: - SSKProtoDataMessageClosedGroupUpdateBuilder
@objc public class func builder(name: String, groupID: String, sharedSecret: String, senderKey: String) -> SSKProtoDataMessageClosedGroupUpdateBuilder {
return SSKProtoDataMessageClosedGroupUpdateBuilder(name: name, groupID: groupID, sharedSecret: sharedSecret, senderKey: senderKey)
@objc public class func builder(groupPublicKey: Data, type: SSKProtoDataMessageClosedGroupUpdateType) -> SSKProtoDataMessageClosedGroupUpdateBuilder {
return SSKProtoDataMessageClosedGroupUpdateBuilder(groupPublicKey: groupPublicKey, type: type)
}
// asBuilder() constructs a builder that reflects the proto's contents.
@objc public func asBuilder() -> SSKProtoDataMessageClosedGroupUpdateBuilder {
let builder = SSKProtoDataMessageClosedGroupUpdateBuilder(name: name, groupID: groupID, sharedSecret: sharedSecret, senderKey: senderKey)
let builder = SSKProtoDataMessageClosedGroupUpdateBuilder(groupPublicKey: groupPublicKey, type: type)
if let _value = name {
builder.setName(_value)
}
if let _value = groupPrivateKey {
builder.setGroupPrivateKey(_value)
}
builder.setChainKeys(chainKeys)
builder.setMembers(members)
builder.setAdmins(admins)
return builder
}
@ -3307,29 +3463,33 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
@objc fileprivate override init() {}
@objc fileprivate init(name: String, groupID: String, sharedSecret: String, senderKey: String) {
@objc fileprivate init(groupPublicKey: Data, type: SSKProtoDataMessageClosedGroupUpdateType) {
super.init()
setName(name)
setGroupID(groupID)
setSharedSecret(sharedSecret)
setSenderKey(senderKey)
setGroupPublicKey(groupPublicKey)
setType(type)
}
@objc public func setName(_ valueParam: String) {
proto.name = valueParam
}
@objc public func setGroupID(_ valueParam: String) {
proto.groupID = valueParam
@objc public func setGroupPublicKey(_ valueParam: Data) {
proto.groupPublicKey = valueParam
}
@objc public func setSharedSecret(_ valueParam: String) {
proto.sharedSecret = valueParam
@objc public func setGroupPrivateKey(_ valueParam: Data) {
proto.groupPrivateKey = valueParam
}
@objc public func setSenderKey(_ valueParam: String) {
proto.senderKey = valueParam
@objc public func addChainKeys(_ valueParam: Data) {
var items = proto.chainKeys
items.append(valueParam)
proto.chainKeys = items
}
@objc public func setChainKeys(_ wrappedItems: [Data]) {
proto.chainKeys = wrappedItems
}
@objc public func addMembers(_ valueParam: String) {
@ -3342,6 +3502,20 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
proto.members = wrappedItems
}
@objc public func addAdmins(_ valueParam: String) {
var items = proto.admins
items.append(valueParam)
proto.admins = items
}
@objc public func setAdmins(_ wrappedItems: [String]) {
proto.admins = wrappedItems
}
@objc public func setType(_ valueParam: SSKProtoDataMessageClosedGroupUpdateType) {
proto.type = SSKProtoDataMessageClosedGroupUpdateTypeUnwrap(valueParam)
}
@objc public func build() throws -> SSKProtoDataMessageClosedGroupUpdate {
return try SSKProtoDataMessageClosedGroupUpdate.parseProto(proto)
}
@ -3353,28 +3527,48 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
fileprivate let proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate
@objc public let name: String
@objc public let groupPublicKey: Data
@objc public let groupID: String
@objc public let type: SSKProtoDataMessageClosedGroupUpdateType
@objc public let sharedSecret: String
@objc public var name: String? {
guard proto.hasName else {
return nil
}
return proto.name
}
@objc public var hasName: Bool {
return proto.hasName
}
@objc public let senderKey: String
@objc public var groupPrivateKey: Data? {
guard proto.hasGroupPrivateKey else {
return nil
}
return proto.groupPrivateKey
}
@objc public var hasGroupPrivateKey: Bool {
return proto.hasGroupPrivateKey
}
@objc public var chainKeys: [Data] {
return proto.chainKeys
}
@objc public var members: [String] {
return proto.members
}
@objc public var admins: [String] {
return proto.admins
}
private init(proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate,
name: String,
groupID: String,
sharedSecret: String,
senderKey: String) {
groupPublicKey: Data,
type: SSKProtoDataMessageClosedGroupUpdateType) {
self.proto = proto
self.name = name
self.groupID = groupID
self.sharedSecret = sharedSecret
self.senderKey = senderKey
self.groupPublicKey = groupPublicKey
self.type = type
}
@objc
@ -3388,35 +3582,23 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
}
fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate) throws -> SSKProtoDataMessageClosedGroupUpdate {
guard proto.hasName else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: name")
guard proto.hasGroupPublicKey else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: groupPublicKey")
}
let name = proto.name
let groupPublicKey = proto.groupPublicKey
guard proto.hasGroupID else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: groupID")
guard proto.hasType else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: type")
}
let groupID = proto.groupID
guard proto.hasSharedSecret else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: sharedSecret")
}
let sharedSecret = proto.sharedSecret
guard proto.hasSenderKey else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: senderKey")
}
let senderKey = proto.senderKey
let type = SSKProtoDataMessageClosedGroupUpdateTypeWrap(proto.type)
// MARK: - Begin Validation Logic for SSKProtoDataMessageClosedGroupUpdate -
// MARK: - End Validation Logic for SSKProtoDataMessageClosedGroupUpdate -
let result = SSKProtoDataMessageClosedGroupUpdate(proto: proto,
name: name,
groupID: groupID,
sharedSecret: sharedSecret,
senderKey: senderKey)
groupPublicKey: groupPublicKey,
type: type)
return result
}

View File

@ -131,7 +131,10 @@ struct SignalServiceProtos_Envelope {
case receipt // = 5
case unidentifiedSender // = 6
/// Loki: Contains prekeys and a message; uses simple encryption
/// Loki
case closedGroupCiphertext // = 7
/// Loki: Contains pre keys and a message; uses simple encryption
case friendRequest // = 101
init() {
@ -146,6 +149,7 @@ struct SignalServiceProtos_Envelope {
case 3: self = .prekeyBundle
case 5: self = .receipt
case 6: self = .unidentifiedSender
case 7: self = .closedGroupCiphertext
case 101: self = .friendRequest
default: return nil
}
@ -159,6 +163,7 @@ struct SignalServiceProtos_Envelope {
case .prekeyBundle: return 3
case .receipt: return 5
case .unidentifiedSender: return 6
case .closedGroupCiphertext: return 7
case .friendRequest: return 101
}
}
@ -724,6 +729,50 @@ struct SignalServiceProtos_CallMessage {
fileprivate var _profileKey: Data? = nil
}
struct SignalServiceProtos_ClosedGroupCiphertext {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// @required
var ciphertext: Data {
get {return _ciphertext ?? SwiftProtobuf.Internal.emptyData}
set {_ciphertext = newValue}
}
/// Returns true if `ciphertext` has been explicitly set.
var hasCiphertext: Bool {return self._ciphertext != nil}
/// Clears the value of `ciphertext`. Subsequent reads from it will return its default value.
mutating func clearCiphertext() {self._ciphertext = nil}
/// @required
var senderPublicKey: String {
get {return _senderPublicKey ?? String()}
set {_senderPublicKey = newValue}
}
/// Returns true if `senderPublicKey` has been explicitly set.
var hasSenderPublicKey: Bool {return self._senderPublicKey != nil}
/// Clears the value of `senderPublicKey`. Subsequent reads from it will return its default value.
mutating func clearSenderPublicKey() {self._senderPublicKey = nil}
/// @required
var keyIndex: UInt32 {
get {return _keyIndex ?? 0}
set {_keyIndex = newValue}
}
/// Returns true if `keyIndex` has been explicitly set.
var hasKeyIndex: Bool {return self._keyIndex != nil}
/// Clears the value of `keyIndex`. Subsequent reads from it will return its default value.
mutating func clearKeyIndex() {self._keyIndex = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
fileprivate var _ciphertext: Data? = nil
fileprivate var _senderPublicKey: String? = nil
fileprivate var _keyIndex: UInt32? = nil
}
struct SignalServiceProtos_DataMessage {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
@ -1454,7 +1503,7 @@ struct SignalServiceProtos_DataMessage {
fileprivate var _image: SignalServiceProtos_AttachmentPointer? = nil
}
/// Loki: A custom message for our profile
/// Loki
struct LokiProfile {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
@ -1492,7 +1541,6 @@ struct SignalServiceProtos_DataMessage {
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// @required
var name: String {
get {return _name ?? String()}
set {_name = newValue}
@ -1503,45 +1551,73 @@ struct SignalServiceProtos_DataMessage {
mutating func clearName() {self._name = nil}
/// @required
var groupID: String {
get {return _groupID ?? String()}
set {_groupID = newValue}
var groupPublicKey: Data {
get {return _groupPublicKey ?? SwiftProtobuf.Internal.emptyData}
set {_groupPublicKey = newValue}
}
/// Returns true if `groupID` has been explicitly set.
var hasGroupID: Bool {return self._groupID != nil}
/// Clears the value of `groupID`. Subsequent reads from it will return its default value.
mutating func clearGroupID() {self._groupID = nil}
/// Returns true if `groupPublicKey` has been explicitly set.
var hasGroupPublicKey: Bool {return self._groupPublicKey != nil}
/// Clears the value of `groupPublicKey`. Subsequent reads from it will return its default value.
mutating func clearGroupPublicKey() {self._groupPublicKey = nil}
/// @required
var sharedSecret: String {
get {return _sharedSecret ?? String()}
set {_sharedSecret = newValue}
var groupPrivateKey: Data {
get {return _groupPrivateKey ?? SwiftProtobuf.Internal.emptyData}
set {_groupPrivateKey = newValue}
}
/// Returns true if `sharedSecret` has been explicitly set.
var hasSharedSecret: Bool {return self._sharedSecret != nil}
/// Clears the value of `sharedSecret`. Subsequent reads from it will return its default value.
mutating func clearSharedSecret() {self._sharedSecret = nil}
/// Returns true if `groupPrivateKey` has been explicitly set.
var hasGroupPrivateKey: Bool {return self._groupPrivateKey != nil}
/// Clears the value of `groupPrivateKey`. Subsequent reads from it will return its default value.
mutating func clearGroupPrivateKey() {self._groupPrivateKey = nil}
/// @required
var senderKey: String {
get {return _senderKey ?? String()}
set {_senderKey = newValue}
}
/// Returns true if `senderKey` has been explicitly set.
var hasSenderKey: Bool {return self._senderKey != nil}
/// Clears the value of `senderKey`. Subsequent reads from it will return its default value.
mutating func clearSenderKey() {self._senderKey = nil}
var chainKeys: [Data] = []
var members: [String] = []
var admins: [String] = []
/// @required
var type: SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum {
get {return _type ?? .new}
set {_type = newValue}
}
/// Returns true if `type` has been explicitly set.
var hasType: Bool {return self._type != nil}
/// Clears the value of `type`. Subsequent reads from it will return its default value.
mutating func clearType() {self._type = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
enum TypeEnum: SwiftProtobuf.Enum {
typealias RawValue = Int
/// groupPublicKey, name, groupPrivateKey, chainKeys, members, admins
case new // = 0
init() {
self = .new
}
init?(rawValue: Int) {
switch rawValue {
case 0: self = .new
default: return nil
}
}
var rawValue: Int {
switch self {
case .new: return 0
}
}
}
init() {}
fileprivate var _name: String? = nil
fileprivate var _groupID: String? = nil
fileprivate var _sharedSecret: String? = nil
fileprivate var _senderKey: String? = nil
fileprivate var _groupPublicKey: Data? = nil
fileprivate var _groupPrivateKey: Data? = nil
fileprivate var _type: SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum? = nil
}
init() {}
@ -1580,6 +1656,10 @@ extension SignalServiceProtos_DataMessage.Contact.PostalAddress.TypeEnum: CaseIt
// Support synthesized by the compiler.
}
extension SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum: CaseIterable {
// Support synthesized by the compiler.
}
#endif // swift(>=4.2)
struct SignalServiceProtos_NullMessage {
@ -2839,6 +2919,7 @@ extension SignalServiceProtos_Envelope.TypeEnum: SwiftProtobuf._ProtoNameProvidi
3: .same(proto: "PREKEY_BUNDLE"),
5: .same(proto: "RECEIPT"),
6: .same(proto: "UNIDENTIFIED_SENDER"),
7: .same(proto: "CLOSED_GROUP_CIPHERTEXT"),
101: .same(proto: "FRIEND_REQUEST"),
]
}
@ -3308,6 +3389,47 @@ extension SignalServiceProtos_CallMessage.Hangup: SwiftProtobuf.Message, SwiftPr
}
}
extension SignalServiceProtos_ClosedGroupCiphertext: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".ClosedGroupCiphertext"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "ciphertext"),
2: .same(proto: "senderPublicKey"),
3: .same(proto: "keyIndex"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
switch fieldNumber {
case 1: try decoder.decodeSingularBytesField(value: &self._ciphertext)
case 2: try decoder.decodeSingularStringField(value: &self._senderPublicKey)
case 3: try decoder.decodeSingularUInt32Field(value: &self._keyIndex)
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._ciphertext {
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
}
if let v = self._senderPublicKey {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
if let v = self._keyIndex {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 3)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SignalServiceProtos_ClosedGroupCiphertext, rhs: SignalServiceProtos_ClosedGroupCiphertext) -> Bool {
if lhs._ciphertext != rhs._ciphertext {return false}
if lhs._senderPublicKey != rhs._senderPublicKey {return false}
if lhs._keyIndex != rhs._keyIndex {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".DataMessage"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
@ -3938,20 +4060,24 @@ extension SignalServiceProtos_DataMessage.ClosedGroupUpdate: SwiftProtobuf.Messa
static let protoMessageName: String = SignalServiceProtos_DataMessage.protoMessageName + ".ClosedGroupUpdate"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "name"),
2: .same(proto: "groupID"),
3: .same(proto: "sharedSecret"),
4: .same(proto: "senderKey"),
2: .same(proto: "groupPublicKey"),
3: .same(proto: "groupPrivateKey"),
4: .same(proto: "chainKeys"),
5: .same(proto: "members"),
6: .same(proto: "admins"),
7: .same(proto: "type"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
switch fieldNumber {
case 1: try decoder.decodeSingularStringField(value: &self._name)
case 2: try decoder.decodeSingularStringField(value: &self._groupID)
case 3: try decoder.decodeSingularStringField(value: &self._sharedSecret)
case 4: try decoder.decodeSingularStringField(value: &self._senderKey)
case 2: try decoder.decodeSingularBytesField(value: &self._groupPublicKey)
case 3: try decoder.decodeSingularBytesField(value: &self._groupPrivateKey)
case 4: try decoder.decodeRepeatedBytesField(value: &self.chainKeys)
case 5: try decoder.decodeRepeatedStringField(value: &self.members)
case 6: try decoder.decodeRepeatedStringField(value: &self.admins)
case 7: try decoder.decodeSingularEnumField(value: &self._type)
default: break
}
}
@ -3961,32 +4087,46 @@ extension SignalServiceProtos_DataMessage.ClosedGroupUpdate: SwiftProtobuf.Messa
if let v = self._name {
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
}
if let v = self._groupID {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
if let v = self._groupPublicKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
}
if let v = self._sharedSecret {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
if let v = self._groupPrivateKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 3)
}
if let v = self._senderKey {
try visitor.visitSingularStringField(value: v, fieldNumber: 4)
if !self.chainKeys.isEmpty {
try visitor.visitRepeatedBytesField(value: self.chainKeys, fieldNumber: 4)
}
if !self.members.isEmpty {
try visitor.visitRepeatedStringField(value: self.members, fieldNumber: 5)
}
if !self.admins.isEmpty {
try visitor.visitRepeatedStringField(value: self.admins, fieldNumber: 6)
}
if let v = self._type {
try visitor.visitSingularEnumField(value: v, fieldNumber: 7)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SignalServiceProtos_DataMessage.ClosedGroupUpdate, rhs: SignalServiceProtos_DataMessage.ClosedGroupUpdate) -> Bool {
if lhs._name != rhs._name {return false}
if lhs._groupID != rhs._groupID {return false}
if lhs._sharedSecret != rhs._sharedSecret {return false}
if lhs._senderKey != rhs._senderKey {return false}
if lhs._groupPublicKey != rhs._groupPublicKey {return false}
if lhs._groupPrivateKey != rhs._groupPrivateKey {return false}
if lhs.chainKeys != rhs.chainKeys {return false}
if lhs.members != rhs.members {return false}
if lhs.admins != rhs.admins {return false}
if lhs._type != rhs._type {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "NEW"),
]
}
extension SignalServiceProtos_NullMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".NullMessage"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
@ -4370,7 +4510,7 @@ extension SignalServiceProtos_SyncMessage.OpenGroupDetails: SwiftProtobuf.Messag
static let protoMessageName: String = SignalServiceProtos_SyncMessage.protoMessageName + ".OpenGroupDetails"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "url"),
2: .same(proto: "channelId"),
2: .same(proto: "channelID"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {

View File

@ -323,7 +323,7 @@ public class TypingIndicatorsImpl: NSObject, TypingIndicators {
return
}
if !SessionMetaProtocol.shouldSendTypingIndicator(for: thread) { return }
if !SessionMetaProtocol.shouldSendTypingIndicator(in: thread) { return }
let message = TypingIndicatorMessage(thread: thread, action: action)
messageSender.sendPromise(message: message).retainUntilComplete()