This commit is contained in:
nielsandriesse 2020-06-30 16:05:35 +10:00
parent a6908e35a5
commit 832e70f588
21 changed files with 360 additions and 369 deletions

View File

@ -42,13 +42,13 @@ PODS:
- PureLayout (3.1.6)
- Reachability (3.2)
- SAMKeychain (1.5.3)
- SessionAxolotlKit (1.0.2):
- SessionAxolotlKit (1.0.4):
- CocoaLumberjack
- SessionCoreKit (~> 1.0.0)
- SessionCurve25519Kit (~> 2.1.2)
- SessionHKDFKit (~> 0.0.5)
- SwiftProtobuf (~> 1.5.0)
- SessionAxolotlKit/Tests (1.0.2):
- SessionAxolotlKit/Tests (1.0.4):
- CocoaLumberjack
- SessionCoreKit (~> 1.0.0)
- SessionCurve25519Kit (~> 2.1.2)
@ -72,18 +72,18 @@ PODS:
- SessionHKDFKit/Tests (0.0.5):
- CocoaLumberjack
- SessionCoreKit
- SessionMetadataKit (1.0.3):
- SessionMetadataKit (1.0.4):
- CocoaLumberjack
- CryptoSwift (~> 1.3)
- SessionAxolotlKit (~> 1.0.2)
- SessionAxolotlKit (~> 1.0.4)
- SessionCoreKit (~> 1.0.0)
- SessionCurve25519Kit (~> 2.1.2)
- SessionHKDFKit (~> 0.0.5)
- SwiftProtobuf (~> 1.5.0)
- SessionMetadataKit/Tests (1.0.3):
- SessionMetadataKit/Tests (1.0.4):
- CocoaLumberjack
- CryptoSwift (~> 1.3)
- SessionAxolotlKit (~> 1.0.2)
- SessionAxolotlKit (~> 1.0.4)
- SessionCoreKit (~> 1.0.0)
- SessionCurve25519Kit (~> 2.1.2)
- SessionHKDFKit (~> 0.0.5)
@ -98,10 +98,10 @@ PODS:
- PromiseKit (~> 6.0)
- Reachability
- SAMKeychain
- SessionAxolotlKit (~> 1.0.2)
- SessionAxolotlKit (~> 1.0.4)
- SessionCoreKit (~> 1.0.0)
- SessionCurve25519Kit (~> 2.1.3)
- SessionMetadataKit (~> 1.0.3)
- SessionMetadataKit (~> 1.0.4)
- Starscream
- SwiftProtobuf (~> 1.5.0)
- YapDatabase/SQLCipher
@ -115,10 +115,10 @@ PODS:
- PromiseKit (~> 6.0)
- Reachability
- SAMKeychain
- SessionAxolotlKit (~> 1.0.2)
- SessionAxolotlKit (~> 1.0.4)
- SessionCoreKit (~> 1.0.0)
- SessionCurve25519Kit (~> 2.1.3)
- SessionMetadataKit (~> 1.0.3)
- SessionMetadataKit (~> 1.0.4)
- Starscream
- SwiftProtobuf (~> 1.5.0)
- YapDatabase/SQLCipher
@ -277,7 +277,7 @@ CHECKOUT OPTIONS:
:commit: b72c2d1e6132501db906de2cffa8ded7803c54f4
:git: https://github.com/signalapp/Mantle
SessionAxolotlKit:
:commit: 0338147cd5faefbb17e0bbf43cd008615ef64fe2
:commit: e267e0c404d2a6126d889242d551c98fb8945158
:git: https://github.com/loki-project/session-ios-protocol-kit.git
SessionCoreKit:
:commit: 0d66c90657b62cb66ecd2767c57408a951650f23
@ -289,7 +289,7 @@ CHECKOUT OPTIONS:
:commit: 0dcf8cf8a7995ef8663146f7063e6c1d7f5a3274
:git: https://github.com/nielsandriesse/session-ios-hkdf-kit.git
SessionMetadataKit:
:commit: e23212e8494157d7a4daabbd4842be59117d9420
:commit: fbdd35c99a147ea34bd2143ae30e1fd4407c346c
:git: https://github.com/loki-project/session-ios-metadata-kit
Starscream:
:commit: b09ea163c3cb305152c65b299cb024610f52e735
@ -312,12 +312,12 @@ SPEC CHECKSUMS:
PureLayout: bd3c4ec3a3819ad387c99ebb72c6b129c3ed4d2d
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SessionAxolotlKit: 88f72573df989042510d8a1737bd0c0057dda3da
SessionAxolotlKit: 3723011fe66a1e80af2f0e80e7b08460199a658a
SessionCoreKit: 778a3f6e3da788b43497734166646025b6392e88
SessionCurve25519Kit: 9bb9afe199e4bc23578a4b15932ad2c57bd047b1
SessionHKDFKit: b0f4e669411703ab925aba07491c5611564d1419
SessionMetadataKit: 581eb0da986e5a1752d07bfa89cf54bbe1fe3bca
SessionServiceKit: 344dff85e344fd3177d7b0b7aff4647a9d5e9efc
SessionMetadataKit: 2b0e500e6c7f8c425c596781e4307282fdc54bef
SessionServiceKit: 151860f2bd0decc7d735ab94d538d381e98a9be5
SQLCipher: e434ed542b24f38ea7b36468a13f9765e1b5c072
SSZipArchive: 62d4947b08730e4cda640473b0066d209ff033c9
Starscream: 8aaf1a7feb805c816d0e7d3190ef23856f6665b9

2
Pods

@ -1 +1 @@
Subproject commit 8e09671add980ffbb06ba27e1bbb2aa558b5b4cf
Subproject commit 341156442f131ceef3741497db79d67014728946

View File

@ -42,7 +42,7 @@ A Swift/Objective-C library for communicating with the Session messaging service
s.dependency 'CocoaLumberjack'
s.dependency 'CryptoSwift', '~> 1.3'
s.dependency 'AFNetworking'
s.dependency 'SessionAxolotlKit', '~> 1.0.2'
s.dependency 'SessionAxolotlKit', '~> 1.0.4'
s.dependency 'Mantle'
s.dependency 'YapDatabase/SQLCipher'
s.dependency 'Starscream'
@ -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.3'
s.dependency 'SessionMetadataKit', '~> 1.0.4'
s.dependency 'PromiseKit', '~> 6.0'
s.test_spec 'Tests' do |test_spec|

View File

@ -176,8 +176,8 @@ static NSTimeInterval launchStartedAt;
[DDLog flushLog];
// Loki: Stop pollers
[self stopPollerIfNeeded];
[self stopOpenGroupPollersIfNeeded];
[self stopPoller];
[self stopOpenGroupPollers];
}
- (void)applicationWillEnterForeground:(UIApplication *)application
@ -197,8 +197,8 @@ static NSTimeInterval launchStartedAt;
[DDLog flushLog];
// Loki: Stop pollers
[self stopPollerIfNeeded];
[self stopOpenGroupPollersIfNeeded];
[self stopPoller];
[self stopOpenGroupPollers];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
@ -1387,7 +1387,7 @@ static NSTimeInterval launchStartedAt;
[self.poller startIfNeeded];
}
- (void)stopPoller { [self.lokiPoller stop]; }
- (void)stopPoller { [self.poller stop]; }
- (void)startClosedGroupPollerIfNeeded
{
@ -1413,10 +1413,8 @@ static NSTimeInterval launchStartedAt;
[SSKEnvironment.shared.messageSenderJobQueue clearAllJobs];
[SSKEnvironment.shared.identityManager clearIdentityKey];
[LKSnodeAPI clearSnodePool];
[self stopPollerIfNeeded];
[self stopOpenGroupPollersIfNeeded];
[self.lokiNewsFeedPoller stop];
[self.lokiMessengerUpdatesFeedPoller stop];
[self stopPoller];
[self stopOpenGroupPollers];
[LKPublicChatManager.shared stopPollers];
bool wasUnlinked = [NSUserDefaults.standardUserDefaults boolForKey:@"wasUnlinked"];
[SignalApp resetAppData:^{

View File

@ -149,6 +149,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
if OWSIdentityManager.shared().identityKeyPair() != nil {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.startPollerIfNeeded()
appDelegate.startClosedGroupPollerIfNeeded()
appDelegate.startOpenGroupPollersIfNeeded()
}
// Populate onion request path countries cache

View File

@ -162,7 +162,7 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
func handleDeviceLinkingModalDismissed() {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.stopPollerIfNeeded()
appDelegate.stopPoller()
TSAccountManager.sharedInstance().resetForReregistration()
}

View File

@ -169,7 +169,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
return showError(title: NSLocalizedString("A closed group cannot have more than 20 members", comment: ""))
}
let selectedContacts = self.selectedContacts
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in
let _ = FileServerAPI.getDeviceLinks(associatedWith: selectedContacts).ensure2 {
var thread: TSGroupThread!
try! Storage.writeSync { transaction in

View File

@ -1262,7 +1262,7 @@ typedef enum : NSUInteger {
- (void)restoreSession {
dispatch_async(dispatch_get_main_queue(), ^{
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKSessionManagementProtocol startSessionResetInThread:self.thread using:transaction];
[LKSessionManagementProtocol startSessionResetInThread:self.thread transaction:transaction];
} error:nil];
});
}

View File

@ -681,8 +681,8 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
[SSKEnvironment.shared.identityManager clearIdentityKey];
[LKSnodeAPI clearSnodePool];
AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
[appDelegate stopPollerIfNeeded];
[appDelegate stopOpenGroupPollersIfNeeded];
[appDelegate stopPoller];
[appDelegate stopOpenGroupPollers];
[SSKEnvironment.shared.tsAccountManager resetForReregistration];
UIViewController *rootViewController = [[OnboardingController new] initialViewController];
OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:rootViewController];

View File

@ -132,15 +132,6 @@ 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;

View File

@ -169,7 +169,7 @@ public final class SnodeAPI : NSObject {
try! Storage.writeSync { transaction in
Storage.pruneLastMessageHashInfoIfExpired(for: snode, associatedWith: publicKey, using: transaction)
}
let lastHash = Storage.getLastMessageHash(for: snode, associatedWith: publicKey)
let lastHash = Storage.getLastMessageHash(for: snode, associatedWith: publicKey) ?? ""
let parameters = [ "pubKey" : publicKey, "lastHash" : lastHash ]
return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters)
}

View File

@ -0,0 +1,44 @@
public final class ClosedGroupRatchet : NSObject, NSCoding {
public let chainKey: String
public let keyIndex: UInt
public let messageKeys: [String]
// MARK: Initialization
public init(chainKey: String, keyIndex: UInt, messageKeys: [String]) {
self.chainKey = chainKey
self.keyIndex = keyIndex
self.messageKeys = messageKeys
}
// MARK: Coding
public init?(coder: NSCoder) {
guard let chainKey = coder.decodeObject(forKey: "chainKey") as? String,
let keyIndex = coder.decodeObject(forKey: "keyIndex") as? UInt,
let messageKeys = coder.decodeObject(forKey: "messageKeys") as? [String] else { return nil }
self.chainKey = chainKey
self.keyIndex = UInt(keyIndex)
self.messageKeys = messageKeys
super.init()
}
public func encode(with coder: NSCoder) {
coder.encode(chainKey, forKey: "chainKey")
coder.encode(keyIndex, forKey: "keyIndex")
coder.encode(messageKeys, forKey: "messageKeys")
}
// MARK: Equality
override public func isEqual(_ other: Any?) -> Bool {
guard let other = other as? ClosedGroupRatchet else { return false }
return chainKey == other.chainKey && keyIndex == other.keyIndex && messageKeys == other.messageKeys
}
// MARK: Hashing
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
return chainKey.hashValue ^ keyIndex.hashValue ^ messageKeys.hashValue
}
// MARK: Description
override public var description: String { return "[ chainKey : \(chainKey), keyIndex : \(keyIndex), messageKeys : \(messageKeys.prettifiedDescription) ]" }
}

View File

@ -3,6 +3,10 @@
internal final class ClosedGroupUpdateMessage : TSOutgoingMessage {
private let kind: Kind
@objc internal var isGroupCreationMessage: Bool {
if case .new = kind { return true } else { return false }
}
// MARK: Settings
@objc internal override var ttl: UInt32 { return UInt32(TTLUtilities.getTTL(for: .closedGroupUpdate)) }

View File

@ -1,4 +1,3 @@
import CryptoSwift
import PromiseKit
// A few notes about making changes in this file:
@ -13,57 +12,6 @@ import PromiseKit
/// See [the documentation](https://github.com/loki-project/session-protocol-docs/wiki/Medium-Size-Groups) for more information.
@objc(LKClosedGroupsProtocol)
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 bundled in a `ClosedGroupUpdateMessage` to the group. 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.
// When a user kicks a member from the group, they re-generate ratchets for everyone and send
// those out to all members (minus the member that was just kicked) in a
// `ClosedGroupUpdateMessage` using established channels.
public struct Ratchet {
public let chainKey: String
public let keyIndex: UInt
public let messageKeys: [String]
}
public enum RatchetingError : LocalizedError {
case loadingFailed(groupPublicKey: String, senderPublicKey: String)
case messageKeyMissing(targetKeyIndex: UInt, groupPublicKey: String, senderPublicKey: String)
public var errorDescription: String? {
switch self {
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.
@ -72,13 +20,15 @@ public final class ClosedGroupsProtocol : NSObject {
let userPublicKey = getUserHexEncodedPublicKey()
// Generate a key pair for the group
let groupKeyPair = Curve25519.generateKeyPair()
let groupPublicKey = groupKeyPair.publicKey.toHexString()
let groupPublicKey = groupKeyPair.hexEncodedPublicKey
// 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) }
let ratchets = members.map {
SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: $0, using: transaction)
}
// Create the group
let admins = [ UserDefaults.standard[.masterHexEncodedPublicKey] ?? userPublicKey ]
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
@ -89,12 +39,16 @@ public final class ClosedGroupsProtocol : NSObject {
SSKEnvironment.shared.profileManager.addThread(toProfileWhitelist: thread)
// Send a closed group update message to all members involved
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)
for member in members {
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: Data(hex: groupPublicKey), 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)
Storage.setClosedGroupPrivateKey(groupKeyPair.privateKey.toHexString(), for: groupPublicKey, using: transaction)
// Notify the user
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate)
infoMessage.save(with: transaction)
@ -104,144 +58,13 @@ public final class ClosedGroupsProtocol : NSObject {
return thread
}
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(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, transaction: transaction)
return ratchet
}
private static func step(_ ratchet: Ratchet) throws -> Ratchet {
let nextMessageKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(1) ])
let nextChainKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(2) ])
let nextKeyIndex = ratchet.keyIndex + 1
return Ratchet(chainKey: nextChainKey.toHexString(), keyIndex: nextKeyIndex, messageKeys: ratchet.messageKeys + [ nextMessageKey.toHexString() ])
}
/// - Note: Sync. Don't call from the main thread.
private static func stepRatchetOnce(for groupPublicKey: String, senderPublicKey: String, transaction: YapDatabaseReadWriteTransaction) throws -> Ratchet {
#if DEBUG
assert(!Thread.isMainThread)
#endif
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(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
return result
} catch {
print("[Loki] Couldn't step ratchet due to error: \(error).")
throw error
}
}
private static func stepRatchetOnceAsync(for groupPublicKey: String, senderPublicKey: String) -> Promise<Ratchet> {
let (promise, seal) = Promise<Ratchet>.pending()
SnodeAPI.workQueue.async {
try! Storage.writeSync { transaction in
do {
let result = try stepRatchetOnce(for: groupPublicKey, senderPublicKey: senderPublicKey, transaction: transaction)
seal.fulfill(result)
} catch {
seal.reject(error)
}
}
}
return promise
}
/// - Note: Sync. Don't call from the main thread.
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(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) else {
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
print("[Loki] \(error.errorDescription!)")
throw error
}
if targetKeyIndex < ratchet.keyIndex {
// 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
}
return ratchet
} else {
var currentKeyIndex = ratchet.keyIndex
var result = ratchet
while currentKeyIndex < targetKeyIndex {
do {
result = try step(result)
} catch {
print("[Loki] Couldn't step ratchet due to error: \(error).")
throw error
}
}
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
return result
}
}
private static func stepRatchetAsync(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt) -> Promise<Ratchet> {
let (promise, seal) = Promise<Ratchet>.pending()
SnodeAPI.workQueue.async {
try! Storage.writeSync { transaction in
do {
let result = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: targetKeyIndex, transaction: transaction)
seal.fulfill(result)
} catch {
seal.reject(error)
}
}
}
return promise
}
@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)
let messageKey = ratchet.messageKeys.last!
let aes = try AES(key: Data(hex: messageKey).bytes, blockMode: gcm, padding: .noPadding)
return Data(try aes.decrypt(ciphertext.bytes))
}
}
@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 groupPublicKey = closedGroupUpdate.groupPublicKey.toHexString()
let name = closedGroupUpdate.name
let groupPrivateKey = closedGroupUpdate.groupPrivateKey!
let chainKeys = closedGroupUpdate.chainKeys
@ -249,19 +72,18 @@ public final class ClosedGroupsProtocol : NSObject {
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)
let ratchet = ClosedGroupRatchet(chainKey: chainKey.toHexString(), keyIndex: 0, messageKeys: [])
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: member, ratchet: ratchet, using: transaction)
}
// Create the group
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey.toHexString())
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)
// Add the group to the user's set of public keys to poll for
let groupKeyPair = ECKeyPair(publicKey: groupPublicKey, privateKey: groupPrivateKey)!
Storage.addClosedGroupKeyPair(groupKeyPair)
Storage.setClosedGroupPrivateKey(groupPrivateKey.toHexString(), for: groupPublicKey, using: transaction)
// Notify the user
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate)
infoMessage.save(with: transaction)

View File

@ -0,0 +1,171 @@
import CryptoSwift
import PromiseKit
import SessionMetadataKit
@objc(LKSharedSenderKeysImplementation)
public final class SharedSenderKeysImplementation : NSObject, SharedSenderKeysProtocol {
private static let gcmTagSize: UInt = 16
private static let ivSize: UInt = 12
// MARK: Documentation
// 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 bundled in a `ClosedGroupUpdateMessage` to the group. 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.
// When a user kicks a member from the group, they re-generate ratchets for everyone and send
// those out to all members (minus the member that was just kicked) in a
// `ClosedGroupUpdateMessage` using established channels.
// MARK: Ratcheting Error
public enum RatchetingError : LocalizedError {
case loadingFailed(groupPublicKey: String, senderPublicKey: String)
case messageKeyMissing(targetKeyIndex: UInt, groupPublicKey: String, senderPublicKey: String)
public var errorDescription: String? {
switch self {
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)."
}
}
}
// MARK: Initialization
@objc public static let shared = SharedSenderKeysImplementation()
private override init() { }
// MARK: Private/Internal API
internal func generateRatchet(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> ClosedGroupRatchet {
let rootChainKey = Data.getSecureRandomData(ofSize: 32)!.toHexString()
let ratchet = ClosedGroupRatchet(chainKey: rootChainKey, keyIndex: 0, messageKeys: [])
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, using: transaction)
return ratchet
}
private func step(_ ratchet: ClosedGroupRatchet) throws -> ClosedGroupRatchet {
let nextMessageKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(1) ])
let nextChainKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(2) ])
let nextKeyIndex = ratchet.keyIndex + 1
return ClosedGroupRatchet(chainKey: nextChainKey.toHexString(), keyIndex: nextKeyIndex, messageKeys: ratchet.messageKeys + [ nextMessageKey.toHexString() ])
}
/// - Note: Sync. Don't call from the main thread.
private func stepRatchetOnce(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws -> ClosedGroupRatchet {
#if DEBUG
assert(!Thread.isMainThread)
#endif
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(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, using: transaction)
return result
} catch {
print("[Loki] Couldn't step ratchet due to error: \(error).")
throw error
}
}
/// - Note: Sync. Don't call from the main thread.
private func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction) throws -> ClosedGroupRatchet {
#if DEBUG
assert(!Thread.isMainThread)
#endif
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 {
// 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
}
return ratchet
} else {
var currentKeyIndex = ratchet.keyIndex
var result = ratchet
while currentKeyIndex < targetKeyIndex {
do {
result = try step(result)
currentKeyIndex = result.keyIndex
} catch {
print("[Loki] Couldn't step ratchet due to error: \(error).")
throw error
}
}
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, using: transaction)
return result
}
}
@objc(encrypt:forGroupWithPublicKey:senderPublicKey:protocolContext:error:)
public func encrypt(_ plaintext: Data, forGroupWithPublicKey groupPublicKey: String, senderPublicKey: String, protocolContext: Any) throws -> [Any] {
let transaction = protocolContext as! YapDatabaseReadWriteTransaction
let (ivAndCiphertext, keyIndex) = try encrypt(plaintext, for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
return [ ivAndCiphertext, NSNumber(value: keyIndex) ]
}
public func encrypt(_ plaintext: Data, for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws -> (ivAndCiphertext: Data, keyIndex: UInt) {
let ratchet = try stepRatchetOnce(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
let iv = Data.getSecureRandomData(ofSize: SharedSenderKeysImplementation.ivSize)!
let gcm = GCM(iv: iv.bytes, tagLength: Int(SharedSenderKeysImplementation.gcmTagSize), mode: .combined)
let messageKey = ratchet.messageKeys.last!
let aes = try AES(key: Data(hex: messageKey).bytes, blockMode: gcm, padding: .noPadding)
let ciphertext = try aes.encrypt(plaintext.bytes)
return (ivAndCiphertext: iv + Data(bytes: ciphertext), ratchet.keyIndex)
}
@objc(decrypt:forGroupWithPublicKey:senderPublicKey:keyIndex:protocolContext:error:)
public func decrypt(_ ivAndCiphertext: Data, forGroupWithPublicKey groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, protocolContext: Any) throws -> Data {
let transaction = protocolContext as! YapDatabaseReadWriteTransaction
return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction)
}
public func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction) throws -> Data {
let ratchet = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex, using: transaction)
let iv = ivAndCiphertext[0..<Int(SharedSenderKeysImplementation.ivSize)]
let ciphertext = ivAndCiphertext[Int(SharedSenderKeysImplementation.ivSize)...]
let gcm = GCM(iv: iv.bytes, tagLength: Int(SharedSenderKeysImplementation.gcmTagSize), mode: .combined)
let messageKey = ratchet.messageKeys.last!
let aes = try AES(key: Data(hex: messageKey).bytes, blockMode: gcm, padding: .noPadding)
return Data(try aes.decrypt(ciphertext.bytes))
}
public func isClosedGroup(_ publicKey: String) -> Bool {
return Storage.getUserClosedGroupPublicKeys().contains(publicKey)
}
public func getKeyPair(forGroupWithPublicKey groupPublicKey: String) -> ECKeyPair {
let privateKey = Storage.getClosedGroupPrivateKey(for: groupPublicKey)!
return ECKeyPair(publicKey: Data(hex: groupPublicKey.removing05PrefixIfNeeded()), privateKey: Data(hex: privateKey))!
}
}

View File

@ -4,16 +4,16 @@ internal extension Storage {
// MARK: Ratchets
internal static let closedGroupRatchetCollection = "LokiClosedGroupRatchetCollection"
internal static func getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String) -> ClosedGroupsProtocol.Ratchet? {
internal static func getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String) -> ClosedGroupRatchet? {
let key = "\(groupPublicKey).\(senderPublicKey)"
var result: ClosedGroupsProtocol.Ratchet?
var result: ClosedGroupRatchet?
read { transaction in
result = transaction.object(forKey: key, inCollection: closedGroupRatchetCollection) as? ClosedGroupsProtocol.Ratchet
result = transaction.object(forKey: key, inCollection: closedGroupRatchetCollection) as? ClosedGroupRatchet
}
return result
}
internal static func setClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupsProtocol.Ratchet, transaction: YapDatabaseReadWriteTransaction) {
internal static func setClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, using transaction: YapDatabaseReadWriteTransaction) {
let key = "\(groupPublicKey).\(senderPublicKey)"
transaction.setObject(ratchet, forKey: key, inCollection: closedGroupRatchetCollection)
}
@ -21,29 +21,27 @@ internal extension Storage {
@objc internal extension Storage {
// MARK: Key Pairs
internal static let closedGroupKeyPairCollection = "LokiClosedGroupKeyPairCollection"
// MARK: Private Keys
internal static let closedGroupPrivateKeyCollection = "LokiClosedGroupPrivateKeyCollection"
internal static func getUserClosedGroupPublicKeys() -> Set<String> {
var result: Set<String> = []
read { transaction in
result = Set(transaction.allKeys(inCollection: closedGroupKeyPairCollection))
result = Set(transaction.allKeys(inCollection: closedGroupPrivateKeyCollection))
}
return result
}
@objc(getKeyPairForClosedGroupWithPublicKey:)
internal static func getClosedGroupKeyPair(for publicKey: String) -> ECKeyPair? {
var result: ECKeyPair?
@objc(getPrivateKeyForClosedGroupWithPublicKey:)
internal static func getClosedGroupPrivateKey(for publicKey: String) -> String? {
var result: String?
read { transaction in
result = transaction.object(forKey: publicKey, inCollection: closedGroupKeyPairCollection) as? ECKeyPair
result = transaction.object(forKey: publicKey, inCollection: closedGroupPrivateKeyCollection) as? String
}
return result
}
internal static func addClosedGroupKeyPair(_ keyPair: ECKeyPair) {
try! writeSync { transaction in
transaction.setObject(keyPair, forKey: keyPair.hexEncodedPublicKey, inCollection: closedGroupKeyPairCollection)
}
internal static func setClosedGroupPrivateKey(_ privateKey: String, for publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(privateKey, forKey: publicKey, inCollection: closedGroupPrivateKeyCollection)
}
}

View File

@ -1117,7 +1117,7 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
NSError *error;
SSKProtoDataMessage *_Nullable dataProto = [builder buildAndReturnError:&error];
if (error || dataProto == nil) {
if (error != nil || dataProto == nil) {
OWSFailDebug(@"Couldn't build protobuf due to error: %@.", error);
return nil;
}
@ -1127,7 +1127,7 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
- (nullable id)prepareCustomContentBuilder:(SignalRecipient *)recipient {
SSKProtoDataMessage *_Nullable dataMessage = [self buildDataMessage:recipient.recipientId];
if (!dataMessage) {
if (dataMessage == nil) {
OWSFailDebug(@"Couldn't build protobuf.");
return nil;
}
@ -1144,7 +1144,7 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
NSError *error;
NSData *_Nullable contentData = [contentBuilder buildSerializedDataAndReturnError:&error];
if (error || !contentData) {
if (error != nil || contentData == nil) {
OWSFailDebug(@"Couldn't serialize protobuf due to error: %@.", error);
return nil;
}

View File

@ -489,10 +489,11 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
NSError *cipherError;
SMKSecretSessionCipher *_Nullable cipher =
[[SMKSecretSessionCipher alloc] initWithSessionResetImplementation:self.sessionResetImplementation
sessionStore:self.primaryStorage
preKeyStore:self.primaryStorage
signedPreKeyStore:self.primaryStorage
identityStore:self.identityManager
sessionStore:self.primaryStorage
preKeyStore:self.primaryStorage
signedPreKeyStore:self.primaryStorage
identityStore:self.identityManager
sharedSenderKeysImplementation:LKSharedSenderKeysImplementation.shared
error:&cipherError];
if (cipherError || !cipher) {
@ -503,17 +504,20 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
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];
NSString *groupPrivateKey = [LKStorage getPrivateKeyForClosedGroupWithPublicKey:envelope.source];
if (groupPrivateKey != nil) {
keyPair = [[ECKeyPair alloc] initWithPublicKey:[NSData dataFromHexString:[envelope.source removing05PrefixIfNeeded]] privateKey:[NSData dataFromHexString:groupPrivateKey]];
}
}
NSError *decryptError;
SMKDecryptResult *_Nullable decryptResult =
[cipher throwswrapped_decryptMessageWithCertificateValidator:certificateValidator
senderPublicKey:envelope.source
cipherTextData:encryptedData
timestamp:serverTimestamp
localRecipientId:localRecipientId
localDeviceId:localDeviceId
keyPair:keyPair
protocolContext:transaction
error:&decryptError];
@ -585,7 +589,6 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
return;
}
OWSFailDebug(@"%@", underlyingError);
failureBlock(underlyingError);
return;
}
@ -688,8 +691,8 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
envelope:(SSKProtoEnvelope *)envelope
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
NSString *hexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source;
TSThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction];
NSString *masterPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source;
TSThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:masterPublicKey transaction:transaction];
[SSKEnvironment.shared.notificationsManager notifyUserForErrorMessage:errorMessage
thread:contactThread
transaction:transaction];

View File

@ -38,6 +38,8 @@ NSString *envelopeAddress(SSKProtoEnvelope *envelope)
return @"UnidentifiedSender";
case SSKProtoEnvelopeTypeFriendRequest:
return @"LokiFriendRequest";
case SSKProtoEnvelopeTypeClosedGroupCiphertext:
return @"ClosedGroupCiphertext";
default:
// Shouldn't happen
OWSProdFail([OWSAnalyticsEvents messageManagerErrorEnvelopeTypeOther]);

View File

@ -273,6 +273,7 @@ NS_ASSUME_NONNULL_BEGIN
case SSKProtoEnvelopeTypeFriendRequest:
case SSKProtoEnvelopeTypeCiphertext:
case SSKProtoEnvelopeTypePrekeyBundle:
case SSKProtoEnvelopeTypeClosedGroupCiphertext:
case SSKProtoEnvelopeTypeUnidentifiedSender:
if (!plaintextData) {
OWSFailDebug(@"missing decrypted data for envelope: %@", [self descriptionForEnvelope:envelope]);
@ -424,27 +425,9 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
// Loki: Decrypt closed group message if applicable
NSData *sharedSenderKeysPlaintext = nil;
if ([[LKStorage getUserClosedGroupPublicKeys] containsObject:envelope.source]) {
if (envelope.content != nil) {
NSError *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];
SSKProtoContent *_Nullable contentProto = [SSKProtoContent parseData:plaintextData error:&error];
if (error != nil || contentProto == nil) {
OWSFailDebug(@"Couldn't parse proto due to error: %@.", error);
return;
@ -564,7 +547,7 @@ NS_ASSUME_NONNULL_BEGIN
}
}
BOOL usesSharedSenderKeys = [LKClosedGroupsProtocol handleSharedSenderKeysUpdateIfNeeded:dataMessage transaction:transaction];
[LKClosedGroupsProtocol handleSharedSenderKeysUpdateIfNeeded:dataMessage transaction:transaction];
if (dataMessage.group) {
TSGroupThread *_Nullable groupThread =
@ -1033,7 +1016,7 @@ NS_ASSUME_NONNULL_BEGIN
// Loki: Handle closed groups sync message
[LKSyncMessagesProtocol handleClosedGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope transaction:transaction];
} else if (syncMessage.openGroups != nil) {
// Loki: Handle open groups sync message
// Loki: Handle open group sync message
[LKSyncMessagesProtocol handleOpenGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope transaction:transaction];
} else {
OWSLogWarn(@"Ignoring unsupported sync message.");
@ -1359,10 +1342,8 @@ NS_ASSUME_NONNULL_BEGIN
BOOL wasCurrentUserRemovedFromGroup = [removedMemberIds containsObject:userMasterPublicKey];
if (!wasCurrentUserRemovedFromGroup) {
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];
}
// 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

View File

@ -387,9 +387,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
//
// So we're using YDB behavior to ensure this invariant, which is a bit
// unorthodox.
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[allAttachmentIds addObjectsFromArray:[OutgoingMessagePreparer prepareMessageForSending:message transaction:transaction]];
} error:nil];
if (message.attachmentIds.count > 0) {
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[allAttachmentIds addObjectsFromArray:[OutgoingMessagePreparer prepareMessageForSending:message transaction:transaction]];
} error:nil];
}
NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message];
@ -516,11 +518,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
} else if (thread.isGroupThread) {
TSGroupThread *groupThread = (TSGroupThread *)thread;
recipientIds = [LKSessionMetaProtocol getDestinationsForOutgoingGroupMessage:message inThread:thread];
__block NSString *userMasterHexEncodedPublicKey;
__block NSString *userMasterPublicKey;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
userMasterHexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userPublicKey in:transaction] ?: userPublicKey;
userMasterPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userPublicKey in:transaction] ?: userPublicKey;
}];
if ([recipientIds containsObject:userMasterHexEncodedPublicKey]) {
if ([recipientIds containsObject:userMasterPublicKey]) {
OWSFailDebug(@"Message send recipients should not include self.");
}
} else if ([thread isKindOfClass:TSContactThread.class]) {
@ -1063,21 +1065,21 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:message.uniqueThreadId transaction: transaction];
}];
if (publicChat != nil) {
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
NSString *displayName = SSKEnvironment.shared.profileManager.localProfileName;
if (displayName == nil) { displayName = @"Anonymous"; }
TSQuotedMessage *quote = message.quotedMessage;
uint64_t quoteID = quote.timestamp;
NSString *quoteeHexEncodedPublicKey = quote.authorId;
NSString *quoteePublicKey = quote.authorId;
__block uint64_t quotedMessageServerID = 0;
if (quoteID != 0) {
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
quotedMessageServerID = [LKDatabaseUtilities getServerIDForQuoteWithID:quoteID quoteeHexEncodedPublicKey:quoteeHexEncodedPublicKey threadID:messageSend.thread.uniqueId transaction:transaction];
quotedMessageServerID = [LKDatabaseUtilities getServerIDForQuoteWithID:quoteID quoteeHexEncodedPublicKey:quoteePublicKey threadID:messageSend.thread.uniqueId transaction:transaction];
}];
}
NSString *body = (message.body != nil && message.body.length > 0) ? message.body : [NSString stringWithFormat:@"%@", @(message.timestamp)]; // Workaround for the fact that the back-end doesn't accept messages without a body
LKGroupMessage *groupMessage = [[LKGroupMessage alloc] initWithHexEncodedPublicKey:userHexEncodedPublicKey displayName:displayName body:body type:LKPublicChatAPI.publicChatMessageType
timestamp:message.timestamp quotedMessageTimestamp:quoteID quoteeHexEncodedPublicKey:quoteeHexEncodedPublicKey quotedMessageBody:quote.body quotedMessageServerID:quotedMessageServerID signatureData:nil signatureVersion:0];
LKGroupMessage *groupMessage = [[LKGroupMessage alloc] initWithHexEncodedPublicKey:userPublicKey displayName:displayName body:body type:LKPublicChatAPI.publicChatMessageType
timestamp:message.timestamp quotedMessageTimestamp:quoteID quoteeHexEncodedPublicKey:quoteePublicKey quotedMessageBody:quote.body quotedMessageServerID:quotedMessageServerID signatureData:nil signatureVersion:0];
OWSLinkPreview *linkPreview = message.linkPreview;
if (linkPreview != nil) {
TSAttachmentStream *attachment = [TSAttachmentStream fetchObjectWithUniqueID:linkPreview.imageAttachmentId];
@ -1092,7 +1094,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
NSUInteger height = attachment.shouldHaveImageSize ? @(attachment.imageSize.height).unsignedIntegerValue : 0;
[groupMessage addAttachmentWithKind:@"attachment" server:publicChat.server serverID:attachment.serverId contentType:attachment.contentType size:attachment.byteCount fileName:attachment.sourceFilename flags:0 width:width height:height caption:attachment.caption url:attachment.downloadURL linkPreviewURL:nil linkPreviewTitle:nil];
}
message.actualSenderHexEncodedPublicKey = userHexEncodedPublicKey;
message.actualSenderHexEncodedPublicKey = userPublicKey;
[[LKPublicChatAPI sendMessage:groupMessage toGroup:publicChat.channel onServer:publicChat.server]
.thenOn(OWSDispatch.sendingQueue, ^(LKGroupMessage *groupMessage) {
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@ -1105,13 +1107,13 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
failedMessageSend(error);
}) retainUntilComplete];
} else {
NSString *targetHexEncodedPublicKey = recipient.recipientId;
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
NSString *targetPublicKey = recipient.recipientId;
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
__block BOOL isUserLinkedDevice;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
isUserLinkedDevice = [LKDatabaseUtilities isUserLinkedDevice:targetHexEncodedPublicKey in:transaction];
isUserLinkedDevice = [LKDatabaseUtilities isUserLinkedDevice:targetPublicKey in:transaction];
}];
if ([targetHexEncodedPublicKey isEqual:userHexEncodedPublicKey]) {
if ([targetPublicKey isEqual:userPublicKey]) {
[LKLogger print:[NSString stringWithFormat:@"[Loki] Sending %@ to self.", message.class]];
} else if (isUserLinkedDevice) {
[LKLogger print:[NSString stringWithFormat:@"[Loki] Sending %@ to %@ (one of the current user's linked devices).", message.class, recipient.recipientId]];
@ -1122,9 +1124,16 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
SSKProtoEnvelopeType type = ((NSNumber *)signalMessageInfo[@"type"]).integerValue;
if ([message isKindOfClass:OWSEndSessionMessage.class]) {
type = SSKProtoEnvelopeTypeFriendRequest;
} else if ([messageSend.thread isKindOfClass:TSGroupThread.class] && ((TSGroupThread *)messageSend.thread).usesSharedSenderKeys) {
type = SSKProtoEnvelopeTypeClosedGroupCiphertext;
}
uint64_t timestamp = message.timestamp;
NSString *senderID = type == SSKProtoEnvelopeTypeUnidentifiedSender ? @"" : userHexEncodedPublicKey;
NSString *senderID = (type == SSKProtoEnvelopeTypeUnidentifiedSender) ? @"" : userPublicKey;
if ([messageSend.thread isKindOfClass:TSGroupThread.class] && ((TSGroupThread *)messageSend.thread).usesSharedSenderKeys) {
senderID = [LKGroupUtilities getDecodedGroupID:((TSGroupThread *)messageSend.thread).groupModel.groupId];
} else {
OWSAssertDebug([senderID isEqual:@""]);
}
uint32_t senderDeviceID = type == SSKProtoEnvelopeTypeUnidentifiedSender ? 0 : OWSDevicePrimaryDeviceId;
NSString *content = signalMessageInfo[@"content"];
NSString *recipientID = signalMessageInfo[@"destination"];
@ -1472,22 +1481,22 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
OWSOutgoingSentMessageTranscript *sentMessageTranscript =
[[OWSOutgoingSentMessageTranscript alloc] initWithOutgoingMessage:message isRecipientUpdate:isRecipientUpdate];
NSString *currentDevice = self.tsAccountManager.localNumber;
NSString *userPublicKey = self.tsAccountManager.localNumber;
// Loki: Send to the other device, but not self
__block NSSet<NSString *> *linkedDevices;
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
linkedDevices = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:currentDevice in:transaction];
linkedDevices = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userPublicKey in:transaction];
}];
NSString *otherDevice;
for (NSString *device in linkedDevices) {
if (![device isEqual:currentDevice]) {
if (![device isEqual:userPublicKey]) {
otherDevice = device;
break;
}
}
NSString *recipientId = otherDevice ?: currentDevice;
NSString *recipientId = otherDevice ?: userPublicKey;
__block SignalRecipient *recipient;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
recipient = [SignalRecipient markRecipientAsRegisteredAndGet:recipientId transaction:transaction];
@ -1715,8 +1724,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
OWSPrimaryStorage *storage = self.primaryStorage;
TSOutgoingMessage *message = messageSend.message;
// This may throw an exception
if ([LKSessionManagementProtocol isSessionRequiredForMessage:messageSend.message]
if ([LKSessionManagementProtocol isSessionRequiredForMessage:message]
&& ![storage containsSession:recipientID deviceId:@(OWSDevicePrimaryDeviceId).intValue protocolContext:transaction]) {
NSString *missingSessionException = @"missingSessionException";
OWSRaiseException(missingSessionException,
@ -1725,10 +1733,10 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
@(OWSDevicePrimaryDeviceId));
}
BOOL isFriendRequestMessage = [messageSend.message isKindOfClass:LKFriendRequestMessage.class];
BOOL isSessionRequestMessage = [messageSend.message isKindOfClass:LKSessionRequestMessage.class];
BOOL isDeviceLinkMessage = [messageSend.message isKindOfClass:LKDeviceLinkMessage.class]
&& ((LKDeviceLinkMessage *)messageSend.message).kind == LKDeviceLinkMessageKindRequest;
BOOL isFriendRequestMessage = [message isKindOfClass:LKFriendRequestMessage.class];
BOOL isSessionRequestMessage = [message isKindOfClass:LKSessionRequestMessage.class];
BOOL isDeviceLinkMessage = [message isKindOfClass:LKDeviceLinkMessage.class]
&& ((LKDeviceLinkMessage *)message).kind == LKDeviceLinkMessageKindRequest;
SessionCipher *cipher = [[SessionCipher alloc] initWithSessionStore:storage
preKeyStore:storage
@ -1741,66 +1749,35 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
TSWhisperMessageType messageType;
if (messageSend.isUDSend) {
NSError *error;
LKSessionResetImplementation *sessionResetImplementation = [[LKSessionResetImplementation alloc] initWithStorage:self.primaryStorage];
SMKSecretSessionCipher *_Nullable secretCipher =
[[SMKSecretSessionCipher alloc] initWithSessionStore:self.primaryStorage
preKeyStore:self.primaryStorage
signedPreKeyStore:self.primaryStorage
identityStore:self.identityManager
error:&error];
[[SMKSecretSessionCipher alloc] initWithSessionResetImplementation:sessionResetImplementation
sessionStore:self.primaryStorage
preKeyStore:self.primaryStorage
signedPreKeyStore:self.primaryStorage
identityStore:self.identityManager
sharedSenderKeysImplementation:LKSharedSenderKeysImplementation.shared
error:&error];
if (error || !secretCipher) {
OWSRaiseException(@"SecretSessionCipherFailure", @"Can't create secret session cipher.");
}
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];
}
serializedMessage = [secretCipher throwswrapped_encryptMessageWithRecipientPublicKey:recipientID
deviceID:@(OWSDevicePrimaryDeviceId).intValue
paddedPlaintext:plainText.paddedMessageBody
senderCertificate:messageSend.senderCertificate
protocolContext:transaction
useFallbackSessionCipher:isFriendRequestMessage || isSessionRequestMessage || isDeviceLinkMessage
error:&error];
SCKRaiseIfExceptionWrapperError(error);
if (!serializedMessage || error) {
if (serializedMessage == nil || error != nil) {
OWSFailDebug(@"Error while UD encrypting message: %@.", error);
return nil;
}
messageType = TSUnidentifiedSenderMessageType;
} else {
// This may throw an exception
id<CipherMessage> encryptedMessage =
[cipher throws_encryptMessage:[plainText paddedMessageBody] protocolContext:transaction];
serializedMessage = encryptedMessage.serialized;
@ -1809,7 +1786,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
BOOL isSilent = message.isSilent;
BOOL isOnline = message.isOnline;
BOOL isPing = NO;
OWSMessageServiceParams *messageParams =
[[OWSMessageServiceParams alloc] initWithType:messageType
@ -1820,12 +1796,12 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
isOnline:isOnline
registrationId:[cipher throws_remoteRegistrationId:transaction]
ttl:message.ttl
isPing:isPing];
isPing:NO];
NSError *error;
NSDictionary *jsonDict = [MTLJSONAdapter JSONDictionaryFromModel:messageParams error:&error];
if (error) {
if (error != nil) {
OWSProdError([OWSAnalyticsEvents messageSendErrorCouldNotSerializeMessageJson]);
return nil;
}