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
pull/218/head
nielsandriesse 5 years ago
parent 4e1a14ae05
commit 29fbf34442

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

Loading…
Cancel
Save