Implement rough group modification logic

This commit is contained in:
nielsandriesse 2020-07-03 14:27:28 +10:00
parent 79655169bd
commit c604b40632
17 changed files with 289 additions and 296 deletions

2
Pods

@ -1 +1 @@
Subproject commit c33c86d927843d7383dde6b4c9350052e0f3f482
Subproject commit 776821513e5208897eb79150f2801b62653ec5b7

View File

@ -244,6 +244,8 @@ message DataMessage {
message ClosedGroupUpdate { // Loki
enum Type {
NEW = 0; // groupPublicKey, name, groupPrivateKey, chainKeys, members, admins
INFO = 1; // groupPublicKey, name, chainKeys, members, admins
CHAIN_KEY = 2; // groupPublicKey, chainKeys
}
optional string name = 1;

View File

@ -39,14 +39,14 @@ extern NSString *const TSGroupThread_NotificationKey_UniqueId;
- (BOOL)isLocalUserInGroup;
- (BOOL)isCurrentUserInGroupWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (BOOL)isUserInGroup:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction;
- (BOOL)isUserMemberInGroup:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction;
- (BOOL)isUserAdminInGroup:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction;
// all group threads containing recipient as a member
+ (NSArray<TSGroupThread *> *)groupThreadsWithRecipientId:(NSString *)recipientId
transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)setGroupModel:(TSGroupModel *)newGroupModel withTransaction:(YapDatabaseReadTransaction *)transaction;
- (void)setGroupModel:(TSGroupModel *)newGroupModel withTransaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)leaveGroupWithSneakyTransaction;
- (void)leaveGroupWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;

View File

@ -218,10 +218,10 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
- (BOOL)isCurrentUserInGroupWithTransaction:(YapDatabaseReadTransaction *)transaction
{
NSString *userHexEncodedPublicKey = TSAccountManager.localNumber;
return [self isUserInGroup:userHexEncodedPublicKey transaction:transaction];
return [self isUserMemberInGroup:userHexEncodedPublicKey transaction:transaction];
}
- (BOOL)isUserInGroup:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction
- (BOOL)isUserMemberInGroup:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction
{
if (hexEncodedPublicKey == nil) { return NO; }
NSSet<NSString *> *linkedDeviceHexEncodedPublicKeys = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:hexEncodedPublicKey in:transaction];

View File

@ -8,7 +8,7 @@ public final class Poller : NSObject {
private var pollCount = 0
// MARK: Settings
private static let pollInterval: TimeInterval = 2
private static let pollInterval: TimeInterval = 1
private static let retryInterval: TimeInterval = 0.25
/// After polling a given snode this many times we always switch to a new one.
///

View File

@ -6,7 +6,7 @@ public final class ClosedGroupPoller : NSObject {
private var timer: Timer?
// MARK: Settings
private static let pollInterval: TimeInterval = 4
private static let pollInterval: TimeInterval = 2
// MARK: Error
private enum Error : LocalizedError {

View File

@ -3,10 +3,6 @@
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)) }
@ -16,6 +12,8 @@ internal final class ClosedGroupUpdateMessage : TSOutgoingMessage {
// MARK: Kind
internal enum Kind {
case new(groupPublicKey: Data, name: String, groupPrivateKey: Data, chainKeys: [Data], members: [String], admins: [String])
case info(groupPublicKey: Data, name: String, chainKeys: [Data], members: [String], admins: [String])
case chainKey(groupPublicKey: Data, chainKey: Data)
}
// MARK: Initialization
@ -47,6 +45,15 @@ internal final class ClosedGroupUpdateMessage : TSOutgoingMessage {
closedGroupUpdate.setChainKeys(chainKeys)
closedGroupUpdate.setMembers(members)
closedGroupUpdate.setAdmins(admins)
case .info(let groupPublicKey, let name, let chainKeys, let members, let admins):
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .info)
closedGroupUpdate.setName(name)
closedGroupUpdate.setChainKeys(chainKeys)
closedGroupUpdate.setMembers(members)
closedGroupUpdate.setAdmins(admins)
case .chainKey(let groupPublicKey, let chainKey):
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .chainKey)
closedGroupUpdate.setChainKeys([ chainKey ])
}
builder.setClosedGroupUpdate(try closedGroupUpdate.build())
} catch {

View File

@ -58,57 +58,232 @@ public final class ClosedGroupsProtocol : NSObject {
return thread
}
@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.toHexString()
let name = closedGroupUpdate.name
let groupPrivateKey = closedGroupUpdate.groupPrivateKey!
let chainKeys = closedGroupUpdate.chainKeys
let members = closedGroupUpdate.members
let admins = closedGroupUpdate.admins
// Persist the ratchets
zip(members, chainKeys).forEach { (member, chainKey) in
let ratchet = 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)
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
public static func addUser(_ publicKey: String, to groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
// Prepare
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
let groupID = LKGroupUtilities.getEncodedClosedGroupID(groupPublicKey)
guard let thread1 = TSGroupThread.fetch(uniqueId: groupID, transaction: transaction) else {
return print("[Loki] Can't add user to nonexistent closed group.")
}
let group = thread1.groupModel
let name = group.groupName!
let admins = group.groupAdminIds
// Add the user
var members = group.groupMemberIds
members.append(publicKey)
// Establish sessions if needed (it's important that this happens before the code below)
establishSessionsIfNeeded(with: members, using: transaction)
// Generate a ratchet for the new member
let ratchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: publicKey, using: transaction)
let chainKey = Data(hex: ratchet.chainKey)
// Send the update to the group
let closedGroupUpdateMessageKind1 = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, chainKeys: [ chainKey ], members: members, admins: admins)
let closedGroupUpdateMessage1 = ClosedGroupUpdateMessage(thread: thread1, kind: closedGroupUpdateMessageKind1)
messageSenderJobQueue.add(message: closedGroupUpdateMessage1, transaction: transaction)
// Notify the added user
let allChainKeys = Storage.getAllClosedGroupRatchets(for: groupPublicKey).map { Data(hex: $0.chainKey) } + [ chainKey ] // TODO: I think we need to include the key index here as well
let closedGroupUpdateMessageKind2 = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, chainKeys: allChainKeys, members: members, admins: admins)
let thread2 = TSContactThread.getOrCreateThread(contactId: publicKey)
thread2.save(with: transaction)
let closedGroupUpdateMessage2 = ClosedGroupUpdateMessage(thread: thread2, kind: closedGroupUpdateMessageKind2)
messageSenderJobQueue.add(message: closedGroupUpdateMessage2, transaction: transaction)
// Notify the user
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread1, messageType: .typeGroupUpdate)
infoMessage.save(with: transaction)
}
public static func removeUser(_ publicKey: String, from groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
// Prepare
let groupID = LKGroupUtilities.getEncodedClosedGroupID(groupPublicKey)
guard let thread = TSGroupThread.fetch(uniqueId: groupID, transaction: transaction) else {
return print("[Loki] Can't remove user from nonexistent closed group.")
}
let group = thread.groupModel
let name = group.groupName!
let admins = group.groupAdminIds
// Remove the user
var members = group.groupMemberIds
guard let indexOfUser = members.firstIndex(of: publicKey) else {
return print("[Loki] Can't remove user from group.")
}
members.remove(at: indexOfUser)
// Establish sessions if needed (it's important that this happens before the code below)
establishSessionsIfNeeded(with: members, using: transaction)
// Generate new ratchets for everyone except the member that was removed
let ratchets = members.map {
SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: $0, using: transaction)
}
let chainKeys = ratchets.map { Data(hex: $0.chainKey) }
// Send a closed group update message to all members involved
for member in members {
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction)
SSKEnvironment.shared.profileManager.addThread(toProfileWhitelist: thread)
// Add the group to the user's set of public keys to poll for
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)
// Establish sessions if needed
establishSessionsIfNeeded(with: members, in: thread, using: transaction)
// Return
return true
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, chainKeys: chainKeys, members: members, admins: admins)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction)
}
// Notify the removed user
SessionManagementProtocol.establishSessionIfNeeded(with: publicKey, using: transaction)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, chainKeys: [], members: members, admins: admins)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction)
// Notify the user
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate)
infoMessage.save(with: transaction)
}
public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
// Prepare
let groupID = LKGroupUtilities.getEncodedClosedGroupID(groupPublicKey)
guard let thread = TSGroupThread.fetch(uniqueId: groupID, transaction: transaction) else {
return print("[Loki] Can't leave nonexistent closed group.")
}
let group = thread.groupModel
let name = group.groupName!
// Leave the group
var members = group.groupMemberIds
guard let indexOfSelf = members.firstIndex(of: getUserHexEncodedPublicKey()) else {
return print("[Loki] Can't leave group.")
}
members.remove(at: indexOfSelf)
let admins = group.groupAdminIds
// Send the update to the group (don't include new ratchets as everyone should generate new ratchets
// individually in this case)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, chainKeys: [], members: members, admins: admins)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction)
// Delete all ratchets
Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction)
}
@objc(handleSharedSenderKeysUpdateIfNeeded:from:transaction:)
public static func handleSharedSenderKeysUpdateIfNeeded(_ dataMessage: SSKProtoDataMessage, from publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
// Note that `publicKey` is either the public key of the group or the public key of the
// sender, depending on how the message was sent
guard let closedGroupUpdate = dataMessage.closedGroupUpdate else { return }
switch closedGroupUpdate.type {
case .new: handleNewGroupMessage(closedGroupUpdate, using: transaction)
case .info: handleInfoMessage(closedGroupUpdate, using: transaction)
case .chainKey: handleChainKeyMessage(closedGroupUpdate, from: publicKey, using: transaction)
}
}
private static func handleNewGroupMessage(_ closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate, using transaction: YapDatabaseReadWriteTransaction) {
// Unwrap the message
let groupPublicKey = closedGroupUpdate.groupPublicKey.toHexString()
let name = closedGroupUpdate.name
let groupPrivateKey = closedGroupUpdate.groupPrivateKey!
let chainKeys = closedGroupUpdate.chainKeys
let members = closedGroupUpdate.members
let admins = closedGroupUpdate.admins
// Persist the ratchets
zip(members, chainKeys).forEach { (member, chainKey) in
let ratchet = ClosedGroupRatchet(chainKey: chainKey.toHexString(), keyIndex: 0, messageKeys: [])
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: member, ratchet: ratchet, using: transaction)
}
// Create the group
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
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)
// Establish sessions if needed
establishSessionsIfNeeded(with: members, using: transaction)
}
/// 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, using transaction: YapDatabaseReadWriteTransaction) {
// TODO: Check that the sender was an admin
// Unwrap the message
let groupPublicKey = closedGroupUpdate.groupPublicKey.toHexString()
let name = closedGroupUpdate.name
let chainKeys = closedGroupUpdate.chainKeys
let members = closedGroupUpdate.members
let admins = closedGroupUpdate.admins
// Get the group
let groupID = LKGroupUtilities.getEncodedClosedGroupID(groupPublicKey)
guard let thread = TSGroupThread.fetch(uniqueId: groupID, transaction: transaction) else {
return print("[Loki] Ignoring closed group update for nonexistent group.")
}
let group = thread.groupModel
// 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 handling removed members)
let oldMembers = group.groupMemberIds
let newMembers = members.filter { !oldMembers.contains($0) }
if newMembers.count == chainKeys.count { // If someone was kicked the message won't have any chain keys
zip(newMembers, chainKeys).forEach { (member, chainKey) in
let ratchet = ClosedGroupRatchet(chainKey: chainKey.toHexString(), keyIndex: 0, 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
if Set(members).intersection(oldMembers) != Set(oldMembers) {
Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction)
let userPublicKey = getUserHexEncodedPublicKey()
let newRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction)
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, ratchet: newRatchet, using: transaction)
for member in members {
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.chainKey(groupPublicKey: Data(hex: groupPublicKey), chainKey: Data(hex: newRatchet.chainKey))
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction)
}
}
// Update the group
let groupIDAsData = groupID.data(using: String.Encoding.utf8)!
let newGroupModel = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupIDAsData, groupType: .closedGroup, adminIds: admins)
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate)
infoMessage.save(with: transaction)
}
/// Invoked upon receiving a chain key from another user.
private static func handleChainKeyMessage(_ closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate, from senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
let groupPublicKey = closedGroupUpdate.groupPublicKey.toHexString()
guard let chainKey = closedGroupUpdate.chainKeys.first else {
return print("[Loki] Ignoring invalid closed group update.")
}
let ratchet = ClosedGroupRatchet(chainKey: chainKey.toHexString(), keyIndex: 0, messageKeys: [])
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, using: transaction)
}
@objc(establishSessionsIfNeededWithClosedGroupMembers:transaction:)
public static func establishSessionsIfNeeded(with closedGroupMembers: [String], using transaction: YapDatabaseReadWriteTransaction) {
closedGroupMembers.forEach { publicKey in
SessionManagementProtocol.establishSessionIfNeeded(with: publicKey, using: transaction)
}
}
@objc(shouldIgnoreClosedGroupMessage:inThread:wrappedIn:)
public static func shouldIgnoreClosedGroupMessage(_ dataMessage: SSKProtoDataMessage, in thread: TSThread, wrappedIn envelope: SSKProtoEnvelope) -> Bool {
guard let thread = thread as? TSGroupThread, thread.groupModel.groupType == .closedGroup,
dataMessage.group?.type == .deliver else { return false }
public static func shouldIgnoreClosedGroupMessage(_ dataMessage: SSKProtoDataMessage, in thread: TSGroupThread, wrappedIn envelope: SSKProtoEnvelope) -> Bool {
guard thread.groupModel.groupType == .closedGroup else { return true }
let publicKey = envelope.source! // Set during UD decryption
var result = false
Storage.read { transaction in
result = !thread.isUser(inGroup: publicKey, transaction: transaction)
result = !thread.isUserMember(inGroup: publicKey, transaction: transaction)
}
return result
}
@objc(shouldIgnoreClosedGroupUpdateMessage:inThread:wrappedIn:)
public static func shouldIgnoreClosedGroupUpdateMessage(_ dataMessage: SSKProtoDataMessage, in thread: TSGroupThread?, wrappedIn envelope: SSKProtoEnvelope) -> Bool {
guard let thread = thread else { return false }
public static func shouldIgnoreClosedGroupUpdateMessage(_ dataMessage: SSKProtoDataMessage, in thread: TSGroupThread, wrappedIn envelope: SSKProtoEnvelope) -> Bool {
guard thread.groupModel.groupType == .closedGroup else { return true }
let publicKey = envelope.source! // Set during UD decryption
var result = false
Storage.read { transaction in
@ -116,13 +291,5 @@ public final class ClosedGroupsProtocol : NSObject {
}
return result
}
@objc(establishSessionsIfNeededWithClosedGroupMembers:inThread:transaction:)
public static func establishSessionsIfNeeded(with closedGroupMembers: [String], in thread: TSGroupThread, using transaction: YapDatabaseReadWriteTransaction) {
guard thread.groupModel.groupType == .closedGroup else { return }
closedGroupMembers.forEach { member in
SessionManagementProtocol.establishSessionIfNeeded(with: member, using: transaction)
}
}
}

View File

@ -61,7 +61,7 @@ public final class SharedSenderKeysImplementation : NSObject, SharedSenderKeysPr
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)
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, using: transaction)
return ratchet
}
@ -77,14 +77,14 @@ public final class SharedSenderKeysImplementation : NSObject, SharedSenderKeysPr
#if DEBUG
assert(!Thread.isMainThread)
#endif
guard let ratchet = Storage.getClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) else {
guard let ratchet = Storage.getClosedGroupRatchet(for: 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)
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, using: transaction)
return result
} catch {
print("[Loki] Couldn't step ratchet due to error: \(error).")
@ -97,7 +97,7 @@ public final class SharedSenderKeysImplementation : NSObject, SharedSenderKeysPr
#if DEBUG
assert(!Thread.isMainThread)
#endif
guard let ratchet = Storage.getClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) else {
guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey) else {
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
print("[Loki] \(error.errorDescription!)")
throw error
@ -122,7 +122,7 @@ public final class SharedSenderKeysImplementation : NSObject, SharedSenderKeysPr
throw error
}
}
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, using: transaction)
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, using: transaction)
return result
}
}

View File

@ -2,20 +2,39 @@
internal extension Storage {
// MARK: Ratchets
internal static let closedGroupRatchetCollection = "LokiClosedGroupRatchetCollection"
internal static func getClosedGroupRatchetCollection(for groupPublicKey: String) -> String {
return "LokiClosedGroupRatchetCollection.\(groupPublicKey)"
}
internal static func getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String) -> ClosedGroupRatchet? {
let key = "\(groupPublicKey).\(senderPublicKey)"
internal static func getClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String) -> ClosedGroupRatchet? {
let collection = getClosedGroupRatchetCollection(for: groupPublicKey)
var result: ClosedGroupRatchet?
read { transaction in
result = transaction.object(forKey: key, inCollection: closedGroupRatchetCollection) as? ClosedGroupRatchet
result = transaction.object(forKey: senderPublicKey, inCollection: collection) as? ClosedGroupRatchet
}
return result
}
internal static func setClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, using transaction: YapDatabaseReadWriteTransaction) {
let key = "\(groupPublicKey).\(senderPublicKey)"
transaction.setObject(ratchet, forKey: key, inCollection: closedGroupRatchetCollection)
internal static func setClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, using transaction: YapDatabaseReadWriteTransaction) {
let collection = getClosedGroupRatchetCollection(for: groupPublicKey)
transaction.setObject(ratchet, forKey: senderPublicKey, inCollection: collection)
}
internal static func getAllClosedGroupRatchets(for groupPublicKey: String) -> [ClosedGroupRatchet] {
let collection = getClosedGroupRatchetCollection(for: groupPublicKey)
var result: [ClosedGroupRatchet] = []
read { transaction in
transaction.enumerateRows(inCollection: collection) { _, object, _, _ in
guard let ratchet = object as? ClosedGroupRatchet else { return }
result.append(ratchet)
}
}
return result
}
internal static func removeAllClosedGroupRatchets(for groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
let collection = getClosedGroupRatchetCollection(for: groupPublicKey)
transaction.removeAllObjects(inCollection: collection)
}
}

View File

@ -104,7 +104,7 @@ public final class SessionManagementProtocol : NSObject {
let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction)
thread.save(with: transaction)
// Send the session request
print("[Loki] Establishing session with: \(publicKey).")
print("[Loki] Sending session request to: \(publicKey).")
storage.setSessionRequestTimestamp(for: publicKey, to: Date(), in: transaction)
let sessionRequestMessage = SessionRequestMessage(thread: thread)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue

View File

@ -137,7 +137,7 @@ public final class SyncMessagesProtocol : NSObject {
newGroupThread.setGroupModel(newGroupModel, with: transaction)
OWSDisappearingMessagesJob.shared().becomeConsistent(withDisappearingDuration: transcript.dataMessage.expireTimer, thread: newGroupThread, createdByRemoteRecipientId: nil, createdInExistingGroup: true, transaction: transaction)
// Try to establish sessions with all members for which none exists yet when a group is created or updated
ClosedGroupsProtocol.establishSessionsIfNeeded(with: members, in: newGroupThread, using: transaction)
ClosedGroupsProtocol.establishSessionsIfNeeded(with: members, using: transaction)
// Notify the user
let contactsManager = SSKEnvironment.shared.contactsManager
let infoMessageText = newGroupThread.groupModel.getInfoStringAboutUpdate(to: newGroupModel, contactsManager: contactsManager)
@ -221,7 +221,7 @@ public final class SyncMessagesProtocol : NSObject {
thread.shouldThreadBeVisible = true
thread.save(with: transaction)
}
ClosedGroupsProtocol.establishSessionsIfNeeded(with: closedGroup.groupMemberIds, in: thread, using: transaction)
ClosedGroupsProtocol.establishSessionsIfNeeded(with: closedGroup.groupMemberIds, using: transaction)
}
}

View File

@ -502,14 +502,6 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
return failureBlock(cipherError);
}
ECKeyPair *keyPair = nil; // Loki: SMKSecretSessionCipher will fall back on the user's key pair if this is nil
if (envelope.type == SSKProtoEnvelopeTypeClosedGroupCiphertext) {
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

View File

@ -547,7 +547,7 @@ NS_ASSUME_NONNULL_BEGIN
}
}
[LKClosedGroupsProtocol handleSharedSenderKeysUpdateIfNeeded:dataMessage transaction:transaction];
[LKClosedGroupsProtocol handleSharedSenderKeysUpdateIfNeeded:dataMessage from:envelope.source transaction:transaction];
if (dataMessage.group) {
TSGroupThread *_Nullable groupThread =
@ -1192,7 +1192,7 @@ NS_ASSUME_NONNULL_BEGIN
}
// Ensure sender is in the group.
if (![gThread isUserInGroup:envelope.source transaction:transaction]) {
if (![gThread isUserMemberInGroup:envelope.source transaction:transaction]) {
OWSLogWarn(@"Ignoring 'Request Group Info' message for non-member of group. %@ not in %@",
envelope.source,
gThread.groupModel.groupMemberIds);
@ -1321,7 +1321,7 @@ NS_ASSUME_NONNULL_BEGIN
switch (dataMessage.group.type) {
case SSKProtoGroupContextTypeUpdate: {
// Loki: Ignore updates from non-admins
if ([LKClosedGroupsProtocol shouldIgnoreClosedGroupUpdateMessage:dataMessage inThread:oldGroupThread wrappedIn:envelope]) {
if (oldGroupThread != nil && [LKClosedGroupsProtocol shouldIgnoreClosedGroupUpdateMessage:dataMessage inThread:oldGroupThread wrappedIn:envelope]) {
return nil;
}
// Ensures that the thread exists but don't update it.
@ -1343,7 +1343,7 @@ NS_ASSUME_NONNULL_BEGIN
BOOL wasCurrentUserRemovedFromGroup = [removedMemberIds containsObject:userMasterPublicKey];
if (!wasCurrentUserRemovedFromGroup) {
// Loki: Try to establish sessions with all members involved when a group is created or updated
[LKClosedGroupsProtocol establishSessionsIfNeededWithClosedGroupMembers:newMemberIds.allObjects inThread:newGroupThread transaction:transaction];
[LKClosedGroupsProtocol establishSessionsIfNeededWithClosedGroupMembers:newMemberIds.allObjects transaction:transaction];
}
[[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer

View File

@ -1710,133 +1710,6 @@ extension SSKProtoCallMessage.SSKProtoCallMessageBuilder {
#endif
// MARK: - SSKProtoClosedGroupCiphertext
@objc public class SSKProtoClosedGroupCiphertext: NSObject {
// MARK: - SSKProtoClosedGroupCiphertextBuilder
@objc public class func builder(ciphertext: Data, senderPublicKey: String, keyIndex: UInt32) -> SSKProtoClosedGroupCiphertextBuilder {
return SSKProtoClosedGroupCiphertextBuilder(ciphertext: ciphertext, senderPublicKey: senderPublicKey, keyIndex: keyIndex)
}
// asBuilder() constructs a builder that reflects the proto's contents.
@objc public func asBuilder() -> SSKProtoClosedGroupCiphertextBuilder {
let builder = SSKProtoClosedGroupCiphertextBuilder(ciphertext: ciphertext, senderPublicKey: senderPublicKey, keyIndex: keyIndex)
return builder
}
@objc public class SSKProtoClosedGroupCiphertextBuilder: NSObject {
private var proto = SignalServiceProtos_ClosedGroupCiphertext()
@objc fileprivate override init() {}
@objc fileprivate init(ciphertext: Data, senderPublicKey: String, keyIndex: UInt32) {
super.init()
setCiphertext(ciphertext)
setSenderPublicKey(senderPublicKey)
setKeyIndex(keyIndex)
}
@objc public func setCiphertext(_ valueParam: Data) {
proto.ciphertext = valueParam
}
@objc public func setSenderPublicKey(_ valueParam: String) {
proto.senderPublicKey = valueParam
}
@objc public func setKeyIndex(_ valueParam: UInt32) {
proto.keyIndex = valueParam
}
@objc public func build() throws -> SSKProtoClosedGroupCiphertext {
return try SSKProtoClosedGroupCiphertext.parseProto(proto)
}
@objc public func buildSerializedData() throws -> Data {
return try SSKProtoClosedGroupCiphertext.parseProto(proto).serializedData()
}
}
fileprivate let proto: SignalServiceProtos_ClosedGroupCiphertext
@objc public let ciphertext: Data
@objc public let senderPublicKey: String
@objc public let keyIndex: UInt32
private init(proto: SignalServiceProtos_ClosedGroupCiphertext,
ciphertext: Data,
senderPublicKey: String,
keyIndex: UInt32) {
self.proto = proto
self.ciphertext = ciphertext
self.senderPublicKey = senderPublicKey
self.keyIndex = keyIndex
}
@objc
public func serializedData() throws -> Data {
return try self.proto.serializedData()
}
@objc public class func parseData(_ serializedData: Data) throws -> SSKProtoClosedGroupCiphertext {
let proto = try SignalServiceProtos_ClosedGroupCiphertext(serializedData: serializedData)
return try parseProto(proto)
}
fileprivate class func parseProto(_ proto: SignalServiceProtos_ClosedGroupCiphertext) throws -> SSKProtoClosedGroupCiphertext {
guard proto.hasCiphertext else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: ciphertext")
}
let ciphertext = proto.ciphertext
guard proto.hasSenderPublicKey else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: senderPublicKey")
}
let senderPublicKey = proto.senderPublicKey
guard proto.hasKeyIndex else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: keyIndex")
}
let keyIndex = proto.keyIndex
// MARK: - Begin Validation Logic for SSKProtoClosedGroupCiphertext -
// MARK: - End Validation Logic for SSKProtoClosedGroupCiphertext -
let result = SSKProtoClosedGroupCiphertext(proto: proto,
ciphertext: ciphertext,
senderPublicKey: senderPublicKey,
keyIndex: keyIndex)
return result
}
@objc public override var debugDescription: String {
return "\(proto)"
}
}
#if DEBUG
extension SSKProtoClosedGroupCiphertext {
@objc public func serializedDataIgnoringErrors() -> Data? {
return try! self.serializedData()
}
}
extension SSKProtoClosedGroupCiphertext.SSKProtoClosedGroupCiphertextBuilder {
@objc public func buildIgnoringErrors() -> SSKProtoClosedGroupCiphertext? {
return try! self.build()
}
}
#endif
// MARK: - SSKProtoDataMessageQuoteQuotedAttachment
@objc public class SSKProtoDataMessageQuoteQuotedAttachment: NSObject {
@ -3422,17 +3295,23 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
@objc public enum SSKProtoDataMessageClosedGroupUpdateType: Int32 {
case new = 0
case info = 1
case chainKey = 2
}
private class func SSKProtoDataMessageClosedGroupUpdateTypeWrap(_ value: SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum) -> SSKProtoDataMessageClosedGroupUpdateType {
switch value {
case .new: return .new
case .info: return .info
case .chainKey: return .chainKey
}
}
private class func SSKProtoDataMessageClosedGroupUpdateTypeUnwrap(_ value: SSKProtoDataMessageClosedGroupUpdateType) -> SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum {
switch value {
case .new: return .new
case .info: return .info
case .chainKey: return .chainKey
}
}

View File

@ -729,50 +729,6 @@ struct SignalServiceProtos_CallMessage {
fileprivate var _profileKey: Data? = nil
}
struct SignalServiceProtos_ClosedGroupCiphertext {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// @required
var ciphertext: Data {
get {return _ciphertext ?? SwiftProtobuf.Internal.emptyData}
set {_ciphertext = newValue}
}
/// Returns true if `ciphertext` has been explicitly set.
var hasCiphertext: Bool {return self._ciphertext != nil}
/// Clears the value of `ciphertext`. Subsequent reads from it will return its default value.
mutating func clearCiphertext() {self._ciphertext = nil}
/// @required
var senderPublicKey: String {
get {return _senderPublicKey ?? String()}
set {_senderPublicKey = newValue}
}
/// Returns true if `senderPublicKey` has been explicitly set.
var hasSenderPublicKey: Bool {return self._senderPublicKey != nil}
/// Clears the value of `senderPublicKey`. Subsequent reads from it will return its default value.
mutating func clearSenderPublicKey() {self._senderPublicKey = nil}
/// @required
var keyIndex: UInt32 {
get {return _keyIndex ?? 0}
set {_keyIndex = newValue}
}
/// Returns true if `keyIndex` has been explicitly set.
var hasKeyIndex: Bool {return self._keyIndex != nil}
/// Clears the value of `keyIndex`. Subsequent reads from it will return its default value.
mutating func clearKeyIndex() {self._keyIndex = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
fileprivate var _ciphertext: Data? = nil
fileprivate var _senderPublicKey: String? = nil
fileprivate var _keyIndex: UInt32? = nil
}
struct SignalServiceProtos_DataMessage {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
@ -1593,6 +1549,12 @@ struct SignalServiceProtos_DataMessage {
/// groupPublicKey, name, groupPrivateKey, chainKeys, members, admins
case new // = 0
/// groupPublicKey, name, chainKeys, members, admins
case info // = 1
/// groupPublicKey, chainKeys
case chainKey // = 2
init() {
self = .new
}
@ -1600,6 +1562,8 @@ struct SignalServiceProtos_DataMessage {
init?(rawValue: Int) {
switch rawValue {
case 0: self = .new
case 1: self = .info
case 2: self = .chainKey
default: return nil
}
}
@ -1607,6 +1571,8 @@ struct SignalServiceProtos_DataMessage {
var rawValue: Int {
switch self {
case .new: return 0
case .info: return 1
case .chainKey: return 2
}
}
@ -3389,47 +3355,6 @@ extension SignalServiceProtos_CallMessage.Hangup: SwiftProtobuf.Message, SwiftPr
}
}
extension SignalServiceProtos_ClosedGroupCiphertext: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".ClosedGroupCiphertext"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "ciphertext"),
2: .same(proto: "senderPublicKey"),
3: .same(proto: "keyIndex"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
switch fieldNumber {
case 1: try decoder.decodeSingularBytesField(value: &self._ciphertext)
case 2: try decoder.decodeSingularStringField(value: &self._senderPublicKey)
case 3: try decoder.decodeSingularUInt32Field(value: &self._keyIndex)
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._ciphertext {
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
}
if let v = self._senderPublicKey {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
if let v = self._keyIndex {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 3)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SignalServiceProtos_ClosedGroupCiphertext, rhs: SignalServiceProtos_ClosedGroupCiphertext) -> Bool {
if lhs._ciphertext != rhs._ciphertext {return false}
if lhs._senderPublicKey != rhs._senderPublicKey {return false}
if lhs._keyIndex != rhs._keyIndex {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".DataMessage"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
@ -4124,6 +4049,8 @@ extension SignalServiceProtos_DataMessage.ClosedGroupUpdate: SwiftProtobuf.Messa
extension SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "NEW"),
1: .same(proto: "INFO"),
2: .same(proto: "CHAIN_KEY"),
]
}