Signal-iOS/Signal/src/ViewControllers/DebugUI/DebugUIGroupsV2.swift

442 lines
20 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
import SignalServiceKit
import SignalUI
#if USE_DEBUG_UI
class DebugUIGroupsV2: DebugUIPage, Dependencies {
let name = "Groups v2"
func section(thread: TSThread?) -> OWSTableSection? {
var sectionItems = [OWSTableItem]()
if let groupThread = thread as? TSGroupThread {
sectionItems.append(OWSTableItem(title: "Send group update.") { [weak self] in
self?.sendGroupUpdate(groupThread: groupThread)
})
if let groupModelV2 = groupThread.groupModel as? TSGroupModelV2 {
// v2 Group
sectionItems.append(OWSTableItem(title: "Kick other group members.") { [weak self] in
self?.kickOtherGroupMembers(groupModel: groupModelV2)
})
}
}
if let contactThread = thread as? TSContactThread {
sectionItems.append(OWSTableItem(title: "Send invalid group messages.") { [weak self] in
self?.sendInvalidGroupMessages(contactThread: contactThread)
})
}
if let groupThread = thread as? TSGroupThread,
groupThread.isGroupV2Thread {
sectionItems.append(OWSTableItem(title: "Send partially-invalid group messages.") { [weak self] in
self?.sendPartiallyInvalidGroupMessages(groupThread: groupThread)
})
sectionItems.append(OWSTableItem(title: "Update v2 group immediately.") { [weak self] in
self?.updateV2GroupImmediately(groupThread: groupThread)
})
}
return OWSTableSection(title: "Groups v2", items: sectionItems)
}
// MARK: -
private func kickOtherGroupMembers(groupModel: TSGroupModelV2) {
guard let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aci else {
return owsFailDebug("Missing localAddress.")
}
let serviceIdsToKick = groupModel.groupMembership.allMembersOfAnyKind
.compactMap({ $0.serviceId }).filter({ $0 != localAci })
GroupManager.removeFromGroupOrRevokeInviteV2(groupModel: groupModel, serviceIds: serviceIdsToKick)
.done { _ in
Logger.info("Success.")
}.catch { error in
owsFailDebug("Error: \(error)")
}
}
private func sendInvalidGroupMessages(contactThread: TSContactThread) {
let otherUserAddress = contactThread.contactAddress
// TODO: Support PNIs.
guard let otherUserAci = otherUserAddress.serviceId as? Aci else {
owsFailDebug("Recipient is missing ACI.")
return
}
Task {
do {
// Make a real v2 group on the service.
// Local user and "other user" are members.
let groupThread = try await GroupManager.localCreateNewGroup(
members: [otherUserAddress],
name: "Real group, both users are in the group",
disappearingMessageToken: .disabledToken,
shouldSendMessage: false
)
guard let validGroupModelV2 = groupThread.groupModel as? TSGroupModelV2 else {
throw OWSAssertionError("Invalid groupModel.")
}
// Make a real v2 group on the service.
// Local user is a member but "other user" is not.
let groupThread2 = try await GroupManager.localCreateNewGroup(
members: [],
name: "Real group, recipient is not in the group",
disappearingMessageToken: .disabledToken,
shouldSendMessage: false
)
guard let missingOtherUserGroupModelV2 = groupThread2.groupModel as? TSGroupModelV2 else {
throw OWSAssertionError("Invalid groupModel.")
}
// Make a real v2 group on the service.
// "Other user" is a member but local user is not.
let groupThread3 = try await GroupManager.localCreateNewGroup(
members: [otherUserAddress],
name: "Real group, sender is not in the group",
disappearingMessageToken: .disabledToken,
shouldSendMessage: false
)
guard let groupModel = groupThread3.groupModel as? TSGroupModelV2 else {
throw OWSAssertionError("Invalid groupModel.")
}
guard groupModel.groupMembership.isFullMember(otherUserAddress) else {
throw OWSAssertionError("Other user is not a full member.")
}
// Last admin (local user) can't leave group, so first
// make the "other user" an admin.
let changeMemberThread = try await GroupManager.changeMemberRoleV2(
groupModel: groupModel,
aci: otherUserAci,
role: .administrator
).awaitable()
self.databaseStorage.write { transaction in
GroupManager.localLeaveGroupOrDeclineInvite(groupThread: changeMemberThread, tx: transaction)
}
guard let missingLocalUserGroupModelV2 = changeMemberThread.groupModel as? TSGroupModelV2 else {
throw OWSAssertionError("Invalid groupModel.")
}
sendInvalidGroupMessages(
contactThread: contactThread,
validGroupModelV2: validGroupModelV2,
missingOtherUserGroupModelV2: missingOtherUserGroupModelV2,
missingLocalUserGroupModelV2: missingLocalUserGroupModelV2
)
Logger.info("Complete.")
} catch {
owsFailDebug("Error: \(error)")
}
}
}
private func sendInvalidGroupMessages(contactThread: TSContactThread,
validGroupModelV2: TSGroupModelV2,
missingOtherUserGroupModelV2: TSGroupModelV2,
missingLocalUserGroupModelV2: TSGroupModelV2) {
var messages = [OWSDynamicOutgoingMessage]()
let groupContextInfoForGroupModel = { (groupModelV2: TSGroupModelV2) -> GroupV2ContextInfo in
let masterKey = try! GroupsV2Protos.masterKeyData(forGroupModel: groupModelV2)
return try! self.groupsV2.groupV2ContextInfo(forMasterKeyData: masterKey)
}
let validGroupContextInfo = groupContextInfoForGroupModel(validGroupModelV2)
let missingOtherUserGroupContextInfo = groupContextInfoForGroupModel(missingOtherUserGroupModelV2)
let missingLocalUserGroupContextInfo = groupContextInfoForGroupModel(missingLocalUserGroupModelV2)
let buildValidGroupContextInfo = { () -> GroupV2ContextInfo in
let groupsV2 = self.groupsV2
let groupSecretParamsData = try! groupsV2.generateGroupSecretParamsData()
let masterKeyData = try! GroupsV2Protos.masterKeyData(forGroupSecretParamsData: groupSecretParamsData)
return try! groupsV2.groupV2ContextInfo(forMasterKeyData: masterKeyData)
}
databaseStorage.read { transaction in
messages.append(OWSDynamicOutgoingMessage(thread: contactThread, transaction: transaction) {
// Real and valid group id/master key/secret params.
// Other user is not in the group.
let masterKeyData = missingOtherUserGroupContextInfo.masterKeyData
// Real revision.
let revision: UInt32 = 0
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(masterKeyData)
builder.setRevision(revision)
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("\(messages.count)")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
messages.append(OWSDynamicOutgoingMessage(thread: contactThread, transaction: transaction) {
// Real and valid group id/master key/secret params.
// Local user is not in the group.
let masterKeyData = missingLocalUserGroupContextInfo.masterKeyData
// Real revision.
let revision: UInt32 = 0
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(masterKeyData)
builder.setRevision(revision)
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("\(messages.count)")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
messages.append(OWSDynamicOutgoingMessage(thread: contactThread, transaction: transaction) {
// Real and valid group id/master key/secret params.
let masterKeyData = validGroupContextInfo.masterKeyData
// Non-existent revision.
let revision: UInt32 = 99
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(masterKeyData)
builder.setRevision(revision)
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("\(messages.count)")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
messages.append(OWSDynamicOutgoingMessage(thread: contactThread, transaction: transaction) {
// Real and valid group id/master key/secret params.
var masterKeyData = validGroupContextInfo.masterKeyData
// Truncate the master key.
masterKeyData = masterKeyData.subdata(in: Int(0)..<Int(1))
// Real revision.
let revision: UInt32 = 0
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(masterKeyData)
builder.setRevision(revision)
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("\(messages.count)")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
messages.append(OWSDynamicOutgoingMessage(thread: contactThread, transaction: transaction) {
// Real and valid group id/master key/secret params.
var masterKeyData = validGroupContextInfo.masterKeyData
// Append garbage to the master key.
masterKeyData += Randomness.generateRandomBytes(1)
// Real revision.
let revision: UInt32 = 0
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(masterKeyData)
builder.setRevision(revision)
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("\(messages.count)")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
messages.append(OWSDynamicOutgoingMessage(thread: contactThread, transaction: transaction) {
// Real and valid group id/master key/secret params.
var masterKeyData = validGroupContextInfo.masterKeyData
// Replace master key with zeroes.
masterKeyData = Data(repeating: 0, count: masterKeyData.count)
// Real revision.
let revision: UInt32 = 0
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(masterKeyData)
builder.setRevision(revision)
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("\(messages.count)")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
messages.append(OWSDynamicOutgoingMessage(thread: contactThread, transaction: transaction) {
// Real and valid group id/master key/secret params.
let masterKeyData = validGroupContextInfo.masterKeyData
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(masterKeyData)
// Don't set revision.
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("\(messages.count)")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
messages.append(OWSDynamicOutgoingMessage(thread: contactThread, transaction: transaction) {
// Real revision.
let revision: UInt32 = 0
let builder = SSKProtoGroupContextV2.builder()
// Don't set master key.
builder.setRevision(revision)
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("\(messages.count)")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
messages.append(OWSDynamicOutgoingMessage(thread: contactThread, transaction: transaction) {
// Valid-looking group id/master key/secret params, but doesn't
// correspond to an actual group on the service.
let groupV2ContextInfo: GroupV2ContextInfo = buildValidGroupContextInfo()
let masterKeyData = groupV2ContextInfo.masterKeyData
let revision: UInt32 = 0
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(masterKeyData)
builder.setRevision(revision)
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("\(messages.count)")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
messages.append(OWSDynamicOutgoingMessage(thread: contactThread, transaction: transaction) {
// Real and valid group id/master key/secret params.
let masterKeyData = validGroupContextInfo.masterKeyData
// Real revision.
let revision: UInt32 = 0
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(masterKeyData)
builder.setRevision(revision)
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("Valid gv2 message.")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
}
for message in messages {
let preparedMessage = PreparedOutgoingMessage.preprepared(transientMessageWithoutAttachments: message)
Task { try await self.messageSender.sendMessage(preparedMessage) }
}
}
private func sendPartiallyInvalidGroupMessages(groupThread: TSGroupThread) {
guard let groupModelV2 = groupThread.groupModel as? TSGroupModelV2 else {
owsFailDebug("Invalid groupModel.")
return
}
var messages = [OWSDynamicOutgoingMessage]()
let masterKey = try! GroupsV2Protos.masterKeyData(forGroupModel: groupModelV2)
let groupContextInfo = try! self.groupsV2.groupV2ContextInfo(forMasterKeyData: masterKey)
databaseStorage.read { transaction in
messages.append(OWSDynamicOutgoingMessage(thread: groupThread, transaction: transaction) {
// Real and valid group id/master key/secret params.
let masterKeyData = groupContextInfo.masterKeyData
// Real revision.
let revision: UInt32 = 0
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(masterKeyData)
builder.setRevision(revision)
// Invalid embedded change actions proto data.
let changeActionsProtoData = Randomness.generateRandomBytes(256)
builder.setGroupChange(changeActionsProtoData)
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("Invalid embedded change actions proto: \(messages.count)")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
messages.append(OWSDynamicOutgoingMessage(thread: groupThread, transaction: transaction) {
// Real and valid group id/master key/secret params.
let masterKeyData = groupContextInfo.masterKeyData
// Real revision.
let revision: UInt32 = 0
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(masterKeyData)
builder.setRevision(revision)
let dataBuilder = SSKProtoDataMessage.builder()
dataBuilder.setGroupV2(builder.buildInfallibly())
dataBuilder.setRequiredProtocolVersion(0)
dataBuilder.setBody("Valid gv2 message.")
return Self.contentProtoData(forDataBuilder: dataBuilder)
})
}
for message in messages {
let preparedMessage = PreparedOutgoingMessage.preprepared(transientMessageWithoutAttachments: message)
Task { try await self.messageSender.sendMessage(preparedMessage) }
}
}
class func contentProtoData(forDataBuilder dataBuilder: SSKProtoDataMessageBuilder) -> Data {
let dataProto = try! dataBuilder.build()
let contentBuilder = SSKProtoContent.builder()
contentBuilder.setDataMessage(dataProto)
let plaintextData = try! contentBuilder.buildSerializedData()
return plaintextData
}
private func sendGroupUpdate(groupThread: TSGroupThread) {
firstly {
GroupManager.sendGroupUpdateMessage(thread: groupThread)
}.done { _ in
Logger.info("Success.")
}.catch { error in
owsFailDebug("Error: \(error)")
}
}
private func updateV2GroupImmediately(groupThread: TSGroupThread) {
guard let groupModelV2 = groupThread.groupModel as? TSGroupModelV2 else {
owsFailDebug("Invalid groupModel.")
return
}
firstly {
self.groupV2Updates.tryToRefreshV2GroupUpToCurrentRevisionImmediately(groupId: groupModelV2.groupId,
groupSecretParamsData: groupModelV2.secretParamsData)
}.done { _ in
Logger.info("Success.")
}.catch { error in
owsFailDebug("Error: \(error)")
}
}
}
#endif