Update for multi device

Still to do is including the sender public key in sender key messages so that we can correctly handle slave devices, and also to get rid of the ordering requirement
This commit is contained in:
nielsandriesse 2020-07-06 14:22:09 +10:00
parent 4e1a14ae05
commit 29fbf34442
1 changed files with 63 additions and 51 deletions

View File

@ -14,21 +14,29 @@ import PromiseKit
public final class ClosedGroupsProtocol : NSObject {
public static let isSharedSenderKeysEnabled = true
/// - 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.
/// - 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 membersAsSet: Set<String>, transaction: YapDatabaseReadWriteTransaction) -> TSGroupThread {
// Prepare
var membersAsSet = membersAsSet
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
let userPublicKey = getUserHexEncodedPublicKey()
// Generate a key pair for the group
let groupKeyPair = Curve25519.generateKeyPair()
let groupPublicKey = groupKeyPair.hexEncodedPublicKey
// Ensure the current user's master device is included in the member list
let groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
// Ensure the current user's master device is the one that's included in the member list
membersAsSet.remove(userPublicKey)
membersAsSet.insert(UserDefaults.standard[.masterHexEncodedPublicKey] ?? userPublicKey)
// Create ratchets for all users involved
let members = [String](membersAsSet) // On the receiving side it's assumed that the member list and chain key list are ordered the same
let ratchets = members.map {
// Create ratchets for all members (and their linked devices). The sorting that happens is needed because the receiving end assumes that the member
// list and sender key list are ordered the same.
var membersAndLinkedDevicesAsSet: Set<String> = []
for member in membersAsSet {
let deviceLinks = OWSPrimaryStorage.shared().getDeviceLinks(for: member, in: transaction)
membersAndLinkedDevicesAsSet.formUnion(deviceLinks.flatMap { [ $0.master.hexEncodedPublicKey, $0.slave.hexEncodedPublicKey ] })
}
let members = [String](membersAsSet).sorted()
let membersAndLinkedDevices = [String](membersAndLinkedDevicesAsSet).sorted()
let ratchets = membersAndLinkedDevices.map {
SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: $0, using: transaction)
}
// Create the group
@ -39,15 +47,15 @@ public final class ClosedGroupsProtocol : NSObject {
thread.usesSharedSenderKeys = true
thread.save(with: transaction)
SSKEnvironment.shared.profileManager.addThread(toProfileWhitelist: thread)
// Establish sessions if needed (shouldn't be necessary under normal circumstances as
// the user can only pick from existing contacts)
establishSessionsIfNeeded(with: members, using: transaction)
// Send a closed group update message to all members involved using established channels
// Establish sessions if needed
establishSessionsIfNeeded(with: members, using: transaction) // Not `membersAndLinkedDevices` as this internally takes care of multi device already
// Send a closed group update message to all members (and their linked devices) using established channels
let senderKeys = ratchets.map { ClosedGroupSenderKey(chainKey: Data(hex: $0.chainKey), keyIndex: $0.keyIndex) }
for member in members {
for member in members { // Not `membersAndLinkedDevices` as this internally takes care of multi device already
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, senderKeys: senderKeys, members: members, admins: admins)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name,
groupPrivateKey: groupKeyPair.privateKey, senderKeys: senderKeys, members: members, admins: admins)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction)
}
@ -76,28 +84,35 @@ public final class ClosedGroupsProtocol : NSObject {
// Add the members to the member list
var members = group.groupMemberIds
members.append(contentsOf: newMembersAsSet)
// Establish sessions if needed (shouldn't be necessary under normal circumstances as
// the user can only pick from existing contacts)
establishSessionsIfNeeded(with: members, using: transaction)
// Generate ratchets for the new members
let newMembers = [String](newMembersAsSet) // On the receiving side it's assumed that the member list and chain key list are ordered the same
let ratchets = newMembers.map {
// Generate ratchets for the new members (and their linked devices). The sorting that happens is needed because the receiving end assumes that the member
// list and sender key list are ordered the same.
var newMembersAndLinkedDevicesAsSet: Set<String> = []
for member in newMembersAsSet {
let deviceLinks = OWSPrimaryStorage.shared().getDeviceLinks(for: member, in: transaction)
newMembersAndLinkedDevicesAsSet.formUnion(deviceLinks.flatMap { [ $0.master.hexEncodedPublicKey, $0.slave.hexEncodedPublicKey ] })
}
members = members.sorted()
let newMembersAndLinkedDevices = [String](newMembersAndLinkedDevicesAsSet).sorted()
let ratchets = newMembersAndLinkedDevices.map {
SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: $0, using: transaction)
}
// Send a closed group update message to the existing members with the new members' ratchets (this message is
// aimed at the group)
// Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
let senderKeys = ratchets.map { ClosedGroupSenderKey(chainKey: Data(hex: $0.chainKey), keyIndex: $0.keyIndex) }
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: senderKeys, members: members, admins: admins)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: senderKeys,
members: members, admins: admins)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction)
// Send closed group update messages to the new members using established channels
// Establish sessions if needed
establishSessionsIfNeeded(with: [String](newMembersAsSet), using: transaction) // Not `newMembersAndLinkedDevices` as this internally takes care of multi device already
// Send closed group update messages to the new members (and their linked devices) using established channels
let allSenderKeys = Storage.getAllClosedGroupRatchets(for: groupPublicKey).map { // This includes the newly generated ratchets
ClosedGroupSenderKey(chainKey: Data(hex: $0.chainKey), keyIndex: $0.keyIndex)
}
for member in newMembers {
for member in newMembersAsSet { // Not `newMembersAndLinkedDevices` as this internally takes care of multi device already
let thread = TSContactThread.getOrCreateThread(contactId: member)
thread.save(with: transaction)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, groupPrivateKey: Data(hex: groupPrivateKey), senderKeys: allSenderKeys, members: members, admins: admins)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name,
groupPrivateKey: Data(hex: groupPrivateKey), senderKeys: allSenderKeys, members: members, admins: admins)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction)
}
@ -115,12 +130,12 @@ public final class ClosedGroupsProtocol : NSObject {
}
public static func removeMembers(_ membersToRemove: Set<String>, from groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
// Prepare
let userPublicKey = getUserHexEncodedPublicKey()
let isUserLeaving = membersToRemove.contains(userPublicKey)
guard !isUserLeaving || membersToRemove.count == 1 else {
return print("[Loki] Can't remove self and others simultaneously.")
}
// Prepare
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
let groupID = LKGroupUtilities.getEncodedClosedGroupID(groupPublicKey)
guard let thread = TSGroupThread.fetch(uniqueId: groupID, transaction: transaction) else {
@ -137,26 +152,26 @@ public final class ClosedGroupsProtocol : NSObject {
}
indexes.forEach { members.remove(at: $0) }
// Send the update to the group (don't include new ratchets as everyone should generate new ratchets individually)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [], members: members, admins: admins)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [],
members: members, admins: admins)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction)
// Delete all ratchets (it's important that this happens after sending out the update)
Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction)
// Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate
// a new ratchet and send it out to all members (minus the removed ones) using established channels.
// Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and send it out to all
// members (minus the removed ones) and their linked devices using established channels.
if isUserLeaving {
Storage.removeClosedGroupPrivateKey(for: groupPublicKey, using: transaction)
} else {
// Establish sessions if needed (shouldn't be necessary under normal circumstances as
// sessions would've already been established previously)
establishSessionsIfNeeded(with: members, using: transaction)
// Send out the user's new ratchet to all members (minus the removed ones) using established channels
let newRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction)
let newSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: newRatchet.chainKey), keyIndex: newRatchet.keyIndex)
for member in members {
// Establish sessions if needed
establishSessionsIfNeeded(with: members, using: transaction) // This internally takes care of multi device
// Send out the user's new ratchet to all members (minus the removed ones) and their linked devices using established channels
let userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction)
let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex)
for member in members { // This internally takes care of multi device
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: newSenderKey)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: userSenderKey)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction)
}
@ -210,12 +225,13 @@ public final class ClosedGroupsProtocol : NSObject {
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate)
infoMessage.save(with: transaction)
// Establish sessions if needed
establishSessionsIfNeeded(with: members, using: transaction)
establishSessionsIfNeeded(with: members, using: transaction) // This internally takes care of multi device
}
/// Invoked upon receiving a group update. A group update is sent out when a group's name is changed, when new users
/// are added, when users leave or are kicked, or if the group admins are changed.
private static func handleInfoMessage(_ closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate, from senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
/// Invoked upon receiving a group update. A group update is sent out when a group's name is changed, when new users are added, when users leave or are
/// kicked, or if the group admins are changed.
private static func handleInfoMessage(_ closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate, from senderPublicKey: String,
using transaction: YapDatabaseReadWriteTransaction) {
// Unwrap the message
let groupPublicKey = closedGroupUpdate.groupPublicKey.toHexString()
let name = closedGroupUpdate.name
@ -237,19 +253,15 @@ public final class ClosedGroupsProtocol : NSObject {
return print("[Loki] Ignoring closed group update from non-admin.")
}
// Establish sessions if needed (it's important that this happens before the code below)
establishSessionsIfNeeded(with: members, using: transaction)
// Parse out any new members and store their ratchets (it's important that
// this happens before the code below)
establishSessionsIfNeeded(with: members, using: transaction) // This internally takes care of multi device
// Parse out any new members and store their ratchets (it's important that this happens before the code below)
let oldMembers = group.groupMemberIds
let newMembers = members.filter { !oldMembers.contains($0) }
if newMembers.count == senderKeys.count { // If someone left or was kicked the message won't have any sender keys
zip(newMembers, senderKeys).forEach { (member, senderKey) in
let ratchet = ClosedGroupRatchet(chainKey: senderKey.chainKey.toHexString(), keyIndex: UInt(senderKey.keyIndex), messageKeys: [])
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: member, ratchet: ratchet, using: transaction)
}
zip(newMembers, senderKeys).forEach { (member, senderKey) in
let ratchet = ClosedGroupRatchet(chainKey: senderKey.chainKey.toHexString(), keyIndex: UInt(senderKey.keyIndex), messageKeys: [])
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: member, ratchet: ratchet, using: transaction)
}
// Delete all ratchets and send out the user's new ratchet using established
// channels if any member of the group left or was removed
// Delete all ratchets and send out the user's new ratchet using established channels if any member of the group left or was removed
if Set(members).intersection(oldMembers) != Set(oldMembers) {
Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction)
let userPublicKey = getUserHexEncodedPublicKey()