Merge pull request #341 from oxen-io/closed-groups

Explicit Closed Group Updates
pull/349/head
Niels Andriesse 5 years ago committed by GitHub
commit d30d3d026e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,7 +12,7 @@ target 'Session' do
pod 'PromiseKit', :inhibit_warnings => true
pod 'PureLayout', '~> 3.1.4', :inhibit_warnings => true
pod 'Reachability', :inhibit_warnings => true
pod 'Sodium', :inhibit_warnings => true
pod 'Sodium', '~> 0.8.0', :inhibit_warnings => true
pod 'SSZipArchive', :inhibit_warnings => true
pod 'Starscream', git: 'https://github.com/signalapp/Starscream.git', branch: 'signal-release', :inhibit_warnings => true
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true
@ -71,7 +71,7 @@ target 'SessionMessagingKit' do
pod 'Reachability', :inhibit_warnings => true
pod 'SAMKeychain', :inhibit_warnings => true
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
pod 'Sodium', :inhibit_warnings => true
pod 'Sodium', '~> 0.8.0', :inhibit_warnings => true
pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true
end

@ -140,7 +140,7 @@ DEPENDENCIES:
- Reachability
- SAMKeychain
- SignalCoreKit (from `https://github.com/signalapp/SignalCoreKit.git`)
- Sodium
- Sodium (~> 0.8.0)
- SSZipArchive
- Starscream (from `https://github.com/signalapp/Starscream.git`, branch `signal-release`)
- SwiftProtobuf (~> 1.5.0)
@ -230,6 +230,6 @@ SPEC CHECKSUMS:
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 3263ab95f60e220882ca53cca4c6bdc2e7a80381
PODFILE CHECKSUM: e639c728c4ea2a89f3e904f3a07d380c7459bdd9
COCOAPODS: 1.10.0.rc.1

@ -1,12 +0,0 @@
PROTOC=protoc \
--proto_path='./'
WRAPPER_SCRIPT=../Scripts/ProtoWrappers.py \
--proto-dir='./' --verbose
all: webrtc_data_proto
webrtc_data_proto: OWSWebRTCDataProtos.proto
$(PROTOC) --swift_out=../Signal/src/Generated \
OWSWebRTCDataProtos.proto
$(WRAPPER_SCRIPT) --dst-dir=../Signal/src/Generated \
--wrapper-prefix=WebRTCProto --proto-prefix=WebRTCProtos --proto-file=OWSWebRTCDataProtos.proto

@ -1,37 +0,0 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
// iOS - since we use a modern proto-compiler, we must specify
// the legacy proto format.
syntax = "proto2";
// iOS - package name determines class prefix
package WebRTCProtos;
option java_package = "org.thoughtcrime.securesms.webrtc";
option java_outer_classname = "WebRtcDataProtos";
message Connected {
// @required
optional uint64 id = 1;
}
message Hangup {
// @required
optional uint64 id = 1;
}
message VideoStreamingStatus {
// @required
optional uint64 id = 1;
optional bool enabled = 2;
}
message Data {
optional Connected connected = 1;
optional Hangup hangup = 2;
optional VideoStreamingStatus videoStreamingStatus = 3;
}

@ -526,7 +526,7 @@
C3471ED42555386B00297E91 /* AESGCM.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D72553860B00C340D1 /* AESGCM.swift */; };
C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3471F4B25553AB000297E91 /* MessageReceiver+Decryption.swift */; };
C3471FA42555439E00297E91 /* Notification+MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3471FA32555439E00297E91 /* Notification+MessageSender.swift */; };
C34A977425A3E34A00852C71 /* ClosedGroupUpdateV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C34A977325A3E34A00852C71 /* ClosedGroupUpdateV2.swift */; };
C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C34A977325A3E34A00852C71 /* ClosedGroupControlMessage.swift */; };
C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; };
C352A2F525574B4700338F3E /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A2F425574B4700338F3E /* Job.swift */; };
C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A2FE25574B6300338F3E /* MessageSendJob.swift */; };
@ -1569,7 +1569,7 @@
C3471ECA2555356A00297E91 /* MessageSender+Encryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSender+Encryption.swift"; sourceTree = "<group>"; };
C3471F4B25553AB000297E91 /* MessageReceiver+Decryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+Decryption.swift"; sourceTree = "<group>"; };
C3471FA32555439E00297E91 /* Notification+MessageSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+MessageSender.swift"; sourceTree = "<group>"; };
C34A977325A3E34A00852C71 /* ClosedGroupUpdateV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedGroupUpdateV2.swift; sourceTree = "<group>"; };
C34A977325A3E34A00852C71 /* ClosedGroupControlMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedGroupControlMessage.swift; sourceTree = "<group>"; };
C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = "<group>"; };
C352A2F425574B4700338F3E /* Job.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Job.swift; sourceTree = "<group>"; };
C352A2FE25574B6300338F3E /* MessageSendJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSendJob.swift; sourceTree = "<group>"; };
@ -2411,7 +2411,7 @@
C3C2A7702553A41E00C340D1 /* ControlMessage.swift */,
C300A5BC2554B00D00555489 /* ReadReceipt.swift */,
C300A5D22554B05A00555489 /* TypingIndicator.swift */,
C34A977325A3E34A00852C71 /* ClosedGroupUpdateV2.swift */,
C34A977325A3E34A00852C71 /* ClosedGroupControlMessage.swift */,
C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */,
);
path = "Control Messages";
@ -4936,7 +4936,7 @@
C3471FA42555439E00297E91 /* Notification+MessageSender.swift in Sources */,
C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */,
C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */,
C34A977425A3E34A00852C71 /* ClosedGroupUpdateV2.swift in Sources */,
C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */,
C32C5E97256DE0CB003C73A2 /* OWSPrimaryStorage.m in Sources */,
C32C5EB9256DE130003C73A2 /* OWSQuotedReplyModel+Conversion.swift in Sources */,
C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */,

@ -3,8 +3,9 @@
final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate {
private let thread: TSGroupThread
private var name = ""
private var members: [String] = [] { didSet { tableView.reloadData() } }
private var members: [String] = [] { didSet { handleMembersChanged() } }
private var isEditingGroupName = false { didSet { handleIsEditingGroupNameChanged() } }
private var tableViewHeightConstraint: NSLayoutConstraint!
// MARK: Components
private lazy var groupNameLabel: UILabel = {
@ -67,8 +68,8 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
func getDisplayName(for publicKey: String) -> String {
return UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) ?? publicKey
}
members = GroupUtilities.getClosedGroupMembers(thread).sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
setUpViewHierarchy()
members = GroupUtilities.getClosedGroupMembers(thread).sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
updateNavigationBarButtons()
name = thread.groupModel.groupName!
}
@ -109,6 +110,9 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
middleStackView.alignment = .center
middleStackView.layoutMargins = UIEdgeInsets(top: Values.smallSpacing, leading: Values.mediumSpacing, bottom: Values.smallSpacing, trailing: Values.mediumSpacing)
middleStackView.isLayoutMarginsRelativeArrangement = true
middleStackView.set(.height, to: Values.largeButtonHeight + Values.smallSpacing * 2)
// Table view
tableViewHeightConstraint = tableView.set(.height, to: 0)
// Main stack view
let mainStackView = UIStackView(arrangedSubviews: [
UIView.vSpacer(Values.veryLargeSpacing),
@ -129,7 +133,6 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
mainStackView.pin(to: scrollView)
view.addSubview(scrollView)
scrollView.pin(to: view)
mainStackView.pin(.bottom, to: .bottom, of: view)
}
// MARK: Table View Data Source / Delegate
@ -175,6 +178,11 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
navigationItem.rightBarButtonItem = doneButton
}
private func handleMembersChanged() {
tableViewHeightConstraint.constant = CGFloat(members.count) * 67
tableView.reloadData()
}
private func handleIsEditingGroupNameChanged() {
updateNavigationBarButtons()
UIView.animate(withDuration: 0.25) {
@ -263,9 +271,9 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
Storage.write(with: { [weak self] transaction in
do {
if !members.contains(getUserHexEncodedPublicKey()) {
try MessageSender.leaveV2(groupPublicKey, using: transaction)
try MessageSender.v2_leave(groupPublicKey, using: transaction)
} else {
try MessageSender.updateV2(groupPublicKey, with: members, name: name, transaction: transaction)
try MessageSender.v2_update(groupPublicKey, with: members, name: name, transaction: transaction)
}
} catch {
DispatchQueue.main.async {

@ -171,7 +171,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self] _ in
var promise: Promise<TSGroupThread>!
Storage.writeSync { transaction in
promise = MessageSender.createV2ClosedGroup(name: name, members: selectedContacts, transaction: transaction)
promise = MessageSender.createClosedGroup(name: name, members: selectedContacts, transaction: transaction)
}
let _ = promise.done(on: DispatchQueue.main) { thread in
self?.presentingViewController?.dismiss(animated: true, completion: nil)

@ -939,7 +939,7 @@ static CGRect oldframe;
if (gThread.isClosedGroup) {
NSString *groupPublicKey = [LKGroupUtilities getDecodedGroupID:gThread.groupModel.groupId];
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[SNMessageSender leaveClosedGroupWithPublicKey:groupPublicKey using:transaction error:nil];
[SNMessageSender v2_leaveClosedGroupWithPublicKey:groupPublicKey using:transaction error:nil];
}];
}

@ -379,7 +379,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIViewC
let groupID = thread.groupModel.groupId
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
do {
try MessageSender.leaveV2(groupPublicKey, using: transaction)
try MessageSender.v2_leave(groupPublicKey, using: transaction)
} catch {
// TODO: Handle
}

@ -1,7 +1,7 @@
import SessionProtocolKit
import SessionUtilitiesKit
public final class ClosedGroupUpdateV2 : ControlMessage {
public final class ClosedGroupControlMessage : ControlMessage {
public var kind: Kind?
public override var ttl: UInt64 {
@ -14,14 +14,23 @@ public final class ClosedGroupUpdateV2 : ControlMessage {
// MARK: Kind
public enum Kind : CustomStringConvertible {
case new(publicKey: Data, name: String, encryptionKeyPair: ECKeyPair, members: [Data], admins: [Data])
/// - Note: Deprecated in favor of more explicit group updates.
case update(name: String, members: [Data])
case encryptionKeyPair([KeyPairWrapper]) // The new encryption key pair encrypted for each member individually
case nameChange(name: String)
case membersAdded(members: [Data])
case membersRemoved(members: [Data])
case memberLeft
public var description: String {
switch self {
case .new: return "new"
case .update: return "update"
case .encryptionKeyPair: return "encryptionKeyPair"
case .nameChange: return "nameChange"
case .membersAdded: return "membersAdded"
case .membersRemoved: return "membersRemoved"
case .memberLeft: return "memberLeft"
}
}
}
@ -49,13 +58,13 @@ public final class ClosedGroupUpdateV2 : ControlMessage {
coder.encode(encryptedKeyPair, forKey: "encryptedKeyPair")
}
public static func fromProto(_ proto: SNProtoDataMessageClosedGroupUpdateV2KeyPairWrapper) -> KeyPairWrapper? {
public static func fromProto(_ proto: SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper) -> KeyPairWrapper? {
return KeyPairWrapper(publicKey: proto.publicKey.toHexString(), encryptedKeyPair: proto.encryptedKeyPair)
}
public func toProto() -> SNProtoDataMessageClosedGroupUpdateV2KeyPairWrapper? {
public func toProto() -> SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper? {
guard let publicKey = publicKey, let encryptedKeyPair = encryptedKeyPair else { return nil }
let result = SNProtoDataMessageClosedGroupUpdateV2KeyPairWrapper.builder(publicKey: Data(hex: publicKey), encryptedKeyPair: encryptedKeyPair)
let result = SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper.builder(publicKey: Data(hex: publicKey), encryptedKeyPair: encryptedKeyPair)
do {
return try result.build()
} catch {
@ -83,6 +92,10 @@ public final class ClosedGroupUpdateV2 : ControlMessage {
case .update(let name, _):
return !name.isEmpty
case .encryptionKeyPair: return true
case .nameChange(let name): return !name.isEmpty
case .membersAdded(let members): return !members.isEmpty
case .membersRemoved(let members): return !members.isEmpty
case .memberLeft: return true
}
}
@ -105,6 +118,17 @@ public final class ClosedGroupUpdateV2 : ControlMessage {
case "encryptionKeyPair":
guard let wrappers = coder.decodeObject(forKey: "wrappers") as? [KeyPairWrapper] else { return nil }
self.kind = .encryptionKeyPair(wrappers)
case "nameChange":
guard let name = coder.decodeObject(forKey: "name") as? String else { return nil }
self.kind = .nameChange(name: name)
case "membersAdded":
guard let members = coder.decodeObject(forKey: "members") as? [Data] else { return nil }
self.kind = .membersAdded(members: members)
case "membersRemoved":
guard let members = coder.decodeObject(forKey: "members") as? [Data] else { return nil }
self.kind = .membersRemoved(members: members)
case "memberLeft":
self.kind = .memberLeft
default: return nil
}
}
@ -127,33 +151,53 @@ public final class ClosedGroupUpdateV2 : ControlMessage {
case .encryptionKeyPair(let wrappers):
coder.encode("encryptionKeyPair", forKey: "kind")
coder.encode(wrappers, forKey: "wrappers")
case .nameChange(let name):
coder.encode("nameChange", forKey: "kind")
coder.encode(name, forKey: "name")
case .membersAdded(let members):
coder.encode("membersAdded", forKey: "kind")
coder.encode(members, forKey: "members")
case .membersRemoved(let members):
coder.encode("membersRemoved", forKey: "kind")
coder.encode(members, forKey: "members")
case .memberLeft:
coder.encode("memberLeft", forKey: "kind")
}
}
// MARK: Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> ClosedGroupUpdateV2? {
guard let closedGroupUpdateProto = proto.dataMessage?.closedGroupUpdateV2 else { return nil }
public override class func fromProto(_ proto: SNProtoContent) -> ClosedGroupControlMessage? {
guard let closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage else { return nil }
let kind: Kind
switch closedGroupUpdateProto.type {
switch closedGroupControlMessageProto.type {
case .new:
guard let publicKey = closedGroupUpdateProto.publicKey, let name = closedGroupUpdateProto.name,
let encryptionKeyPairAsProto = closedGroupUpdateProto.encryptionKeyPair else { return nil }
guard let publicKey = closedGroupControlMessageProto.publicKey, let name = closedGroupControlMessageProto.name,
let encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair else { return nil }
do {
let encryptionKeyPair = try ECKeyPair(publicKeyData: encryptionKeyPairAsProto.publicKey.removing05PrefixIfNeeded(), privateKeyData: encryptionKeyPairAsProto.privateKey)
kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair,
members: closedGroupUpdateProto.members, admins: closedGroupUpdateProto.admins)
members: closedGroupControlMessageProto.members, admins: closedGroupControlMessageProto.admins)
} catch {
SNLog("Couldn't parse key pair.")
return nil
}
case .update:
guard let name = closedGroupUpdateProto.name else { return nil }
kind = .update(name: name, members: closedGroupUpdateProto.members)
guard let name = closedGroupControlMessageProto.name else { return nil }
kind = .update(name: name, members: closedGroupControlMessageProto.members)
case .encryptionKeyPair:
let wrappers = closedGroupUpdateProto.wrappers.compactMap { KeyPairWrapper.fromProto($0) }
let wrappers = closedGroupControlMessageProto.wrappers.compactMap { KeyPairWrapper.fromProto($0) }
kind = .encryptionKeyPair(wrappers)
case .nameChange:
guard let name = closedGroupControlMessageProto.name else { return nil }
kind = .nameChange(name: name)
case .membersAdded:
kind = .membersAdded(members: closedGroupControlMessageProto.members)
case .membersRemoved:
kind = .membersRemoved(members: closedGroupControlMessageProto.members)
case .memberLeft:
kind = .memberLeft
}
return ClosedGroupUpdateV2(kind: kind)
return ClosedGroupControlMessage(kind: kind)
}
public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? {
@ -162,32 +206,43 @@ public final class ClosedGroupUpdateV2 : ControlMessage {
return nil
}
do {
let closedGroupUpdate: SNProtoDataMessageClosedGroupUpdateV2.SNProtoDataMessageClosedGroupUpdateV2Builder
let closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGroupControlMessageBuilder
switch kind {
case .new(let publicKey, let name, let encryptionKeyPair, let members, let admins):
closedGroupUpdate = SNProtoDataMessageClosedGroupUpdateV2.builder(type: .new)
closedGroupUpdate.setPublicKey(publicKey)
closedGroupUpdate.setName(name)
let encryptionKeyPairAsProto = SNProtoDataMessageClosedGroupUpdateV2KeyPair.builder(publicKey: encryptionKeyPair.publicKey, privateKey: encryptionKeyPair.privateKey)
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .new)
closedGroupControlMessage.setPublicKey(publicKey)
closedGroupControlMessage.setName(name)
let encryptionKeyPairAsProto = SNProtoDataMessageClosedGroupControlMessageKeyPair.builder(publicKey: encryptionKeyPair.publicKey, privateKey: encryptionKeyPair.privateKey)
do {
closedGroupUpdate.setEncryptionKeyPair(try encryptionKeyPairAsProto.build())
closedGroupControlMessage.setEncryptionKeyPair(try encryptionKeyPairAsProto.build())
} catch {
SNLog("Couldn't construct closed group update proto from: \(self).")
return nil
}
closedGroupUpdate.setMembers(members)
closedGroupUpdate.setAdmins(admins)
closedGroupControlMessage.setMembers(members)
closedGroupControlMessage.setAdmins(admins)
case .update(let name, let members):
closedGroupUpdate = SNProtoDataMessageClosedGroupUpdateV2.builder(type: .update)
closedGroupUpdate.setName(name)
closedGroupUpdate.setMembers(members)
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .update)
closedGroupControlMessage.setName(name)
closedGroupControlMessage.setMembers(members)
case .encryptionKeyPair(let wrappers):
closedGroupUpdate = SNProtoDataMessageClosedGroupUpdateV2.builder(type: .encryptionKeyPair)
closedGroupUpdate.setWrappers(wrappers.compactMap { $0.toProto() })
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .encryptionKeyPair)
closedGroupControlMessage.setWrappers(wrappers.compactMap { $0.toProto() })
case .nameChange(let name):
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .nameChange)
closedGroupControlMessage.setName(name)
case .membersAdded(let members):
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .membersAdded)
closedGroupControlMessage.setMembers(members)
case .membersRemoved(let members):
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .membersRemoved)
closedGroupControlMessage.setMembers(members)
case .memberLeft:
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .memberLeft)
}
let contentProto = SNProtoContent.builder()
let dataMessageProto = SNProtoDataMessage.builder()
dataMessageProto.setClosedGroupUpdateV2(try closedGroupUpdate.build())
dataMessageProto.setClosedGroupControlMessage(try closedGroupControlMessage.build())
// Group context
try setGroupContextIfNeeded(on: dataMessageProto, using: transaction)
// Expiration timer
@ -209,7 +264,7 @@ public final class ClosedGroupUpdateV2 : ControlMessage {
// MARK: Description
public override var description: String {
"""
ClosedGroupUpdate(
ClosedGroupControlMessage(
kind: \(kind?.description ?? "null")
)
"""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,10 +1,9 @@
// DO NOT EDIT.
// swift-format-ignore-file
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: WebSocketResources.proto
//
// For information on using the generated types, please see the documentation:
// For information on using the generated types, please see the documenation:
// https://github.com/apple/swift-protobuf/
//*
@ -21,7 +20,7 @@ import SwiftProtobuf
// If the compiler emits an error on this type, it is because this file
// was generated by a version of the `protoc` Swift plug-in that is
// incompatible with the version of SwiftProtobuf to which you are linking.
// Please ensure that you are building against the same version of the API
// Please ensure that your are building against the same version of the API
// that was used to generate this file.
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
@ -146,31 +145,31 @@ struct WebSocketProtos_WebSocketMessage {
/// @required
var type: WebSocketProtos_WebSocketMessage.TypeEnum {
get {return _type ?? .unknown}
set {_type = newValue}
get {return _storage._type ?? .unknown}
set {_uniqueStorage()._type = newValue}
}
/// Returns true if `type` has been explicitly set.
var hasType: Bool {return self._type != nil}
var hasType: Bool {return _storage._type != nil}
/// Clears the value of `type`. Subsequent reads from it will return its default value.
mutating func clearType() {self._type = nil}
mutating func clearType() {_uniqueStorage()._type = nil}
var request: WebSocketProtos_WebSocketRequestMessage {
get {return _request ?? WebSocketProtos_WebSocketRequestMessage()}
set {_request = newValue}
get {return _storage._request ?? WebSocketProtos_WebSocketRequestMessage()}
set {_uniqueStorage()._request = newValue}
}
/// Returns true if `request` has been explicitly set.
var hasRequest: Bool {return self._request != nil}
var hasRequest: Bool {return _storage._request != nil}
/// Clears the value of `request`. Subsequent reads from it will return its default value.
mutating func clearRequest() {self._request = nil}
mutating func clearRequest() {_uniqueStorage()._request = nil}
var response: WebSocketProtos_WebSocketResponseMessage {
get {return _response ?? WebSocketProtos_WebSocketResponseMessage()}
set {_response = newValue}
get {return _storage._response ?? WebSocketProtos_WebSocketResponseMessage()}
set {_uniqueStorage()._response = newValue}
}
/// Returns true if `response` has been explicitly set.
var hasResponse: Bool {return self._response != nil}
var hasResponse: Bool {return _storage._response != nil}
/// Clears the value of `response`. Subsequent reads from it will return its default value.
mutating func clearResponse() {self._response = nil}
mutating func clearResponse() {_uniqueStorage()._response = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
@ -205,9 +204,7 @@ struct WebSocketProtos_WebSocketMessage {
init() {}
fileprivate var _type: WebSocketProtos_WebSocketMessage.TypeEnum? = nil
fileprivate var _request: WebSocketProtos_WebSocketRequestMessage? = nil
fileprivate var _response: WebSocketProtos_WebSocketResponseMessage? = nil
fileprivate var _storage = _StorageClass.defaultInstance
}
#if swift(>=4.2)
@ -336,34 +333,70 @@ extension WebSocketProtos_WebSocketMessage: SwiftProtobuf.Message, SwiftProtobuf
3: .same(proto: "response"),
]
fileprivate class _StorageClass {
var _type: WebSocketProtos_WebSocketMessage.TypeEnum? = nil
var _request: WebSocketProtos_WebSocketRequestMessage? = nil
var _response: WebSocketProtos_WebSocketResponseMessage? = nil
static let defaultInstance = _StorageClass()
private init() {}
init(copying source: _StorageClass) {
_type = source._type
_request = source._request
_response = source._response
}
}
fileprivate mutating func _uniqueStorage() -> _StorageClass {
if !isKnownUniquelyReferenced(&_storage) {
_storage = _StorageClass(copying: _storage)
}
return _storage
}
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
_ = _uniqueStorage()
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
while let fieldNumber = try decoder.nextFieldNumber() {
switch fieldNumber {
case 1: try decoder.decodeSingularEnumField(value: &self._type)
case 2: try decoder.decodeSingularMessageField(value: &self._request)
case 3: try decoder.decodeSingularMessageField(value: &self._response)
case 1: try decoder.decodeSingularEnumField(value: &_storage._type)
case 2: try decoder.decodeSingularMessageField(value: &_storage._request)
case 3: try decoder.decodeSingularMessageField(value: &_storage._response)
default: break
}
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._type {
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
if let v = _storage._type {
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
}
if let v = self._request {
if let v = _storage._request {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
}
if let v = self._response {
if let v = _storage._response {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
}
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: WebSocketProtos_WebSocketMessage, rhs: WebSocketProtos_WebSocketMessage) -> Bool {
if lhs._type != rhs._type {return false}
if lhs._request != rhs._request {return false}
if lhs._response != rhs._response {return false}
if lhs._storage !== rhs._storage {
let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
let _storage = _args.0
let rhs_storage = _args.1
if _storage._type != rhs_storage._type {return false}
if _storage._request != rhs_storage._request {return false}
if _storage._response != rhs_storage._response {return false}
return true
}
if !storagesAreEqual {return false}
}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

@ -14,9 +14,11 @@ message Envelope {
// @required
required Type type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
// @required
optional uint64 timestamp = 5;
optional bytes content = 8;
optional uint64 serverTimestamp = 10;
}
message TypingMessage {
@ -34,6 +36,7 @@ message TypingMessage {
message Content {
optional DataMessage dataMessage = 1;
optional SyncMessage syncMessage = 2;
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
}
@ -156,12 +159,16 @@ message DataMessage {
optional string profilePicture = 2;
}
message ClosedGroupUpdateV2 {
message ClosedGroupControlMessage {
enum Type {
NEW = 1; // publicKey, name, encryptionKeyPair, members, admins
UPDATE = 2; // name, members
ENCRYPTION_KEY_PAIR = 3; // wrappers
NAME_CHANGE = 4; // name
MEMBERS_ADDED = 5; // members
MEMBERS_REMOVED = 6; // members
MEMBER_LEFT = 7;
}
message KeyPair {
@ -199,7 +206,7 @@ message DataMessage {
repeated Contact contact = 9;
repeated Preview preview = 10;
optional LokiProfile profile = 101;
optional ClosedGroupUpdateV2 closedGroupUpdateV2 = 104;
optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional PublicChatInfo publicChatInfo = 999;
}
@ -215,6 +222,20 @@ message ReceiptMessage {
repeated uint64 timestamp = 2;
}
message SyncMessage {
message Sent {
optional string destination = 1;
optional uint64 timestamp = 2;
optional DataMessage message = 3;
optional uint64 expirationStartTimestamp = 4;
optional bool isRecipientUpdate = 6 [default = false];
}
optional Sent sent = 1;
optional bytes padding = 8;
}
message AttachmentPointer {
enum Flags {

@ -11,7 +11,7 @@ extension MessageReceiver {
switch message {
case let message as ReadReceipt: handleReadReceipt(message, using: transaction)
case let message as TypingIndicator: handleTypingIndicator(message, using: transaction)
case let message as ClosedGroupUpdateV2: handleClosedGroupUpdateV2(message, using: transaction)
case let message as ClosedGroupControlMessage: handleClosedGroupControlMessage(message, using: transaction)
case let message as ExpirationTimerUpdate: handleExpirationTimerUpdate(message, using: transaction)
case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
default: fatalError()
@ -231,15 +231,19 @@ extension MessageReceiver {
return tsIncomingMessageID
}
private static func handleClosedGroupUpdateV2(_ message: ClosedGroupUpdateV2, using transaction: Any) {
private static func handleClosedGroupControlMessage(_ message: ClosedGroupControlMessage, using transaction: Any) {
switch message.kind! {
case .new: handleNewGroupV2(message, using: transaction)
case .update: handleGroupUpdateV2(message, using: transaction)
case .encryptionKeyPair: handleGroupEncryptionKeyPair(message, using: transaction)
case .new: handleNewClosedGroup(message, using: transaction)
case .update: handleClosedGroupUpdated(message, using: transaction) // Deprecated
case .encryptionKeyPair: handleClosedGroupEncryptionKeyPair(message, using: transaction)
case .nameChange: handleClosedGroupNameChanged(message, using: transaction)
case .membersAdded: handleClosedGroupMembersAdded(message, using: transaction)
case .membersRemoved: handleClosedGroupMembersRemoved(message, using: transaction)
case .memberLeft: handleClosedGroupMemberLeft(message, using: transaction)
}
}
private static func handleNewGroupV2(_ message: ClosedGroupUpdateV2, using transaction: Any) {
private static func handleNewClosedGroup(_ message: ClosedGroupControlMessage, using transaction: Any) {
// Prepare
guard case let .new(publicKeyAsData, name, encryptionKeyPair, membersAsData, adminsAsData) = message.kind else { return }
let transaction = transaction as! YapDatabaseReadWriteTransaction
@ -269,7 +273,181 @@ extension MessageReceiver {
infoMessage.save(with: transaction)
}
private static func handleGroupUpdateV2(_ message: ClosedGroupUpdateV2, using transaction: Any) {
private static func handleClosedGroupEncryptionKeyPair(_ message: ClosedGroupControlMessage, using transaction: Any) {
// Prepare
guard case let .encryptionKeyPair(wrappers) = message.kind, let groupPublicKey = message.groupPublicKey else { return }
let transaction = transaction as! YapDatabaseReadWriteTransaction
let userPublicKey = getUserHexEncodedPublicKey()
guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else {
return SNLog("Couldn't find user X25519 key pair.")
}
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
return SNLog("Ignoring closed group encryption key pair for nonexistent group.")
}
guard thread.groupModel.groupAdminIds.contains(message.sender!) else {
return SNLog("Ignoring closed group encryption key pair from non-admin.")
}
// Find our wrapper and decrypt it if possible
guard let wrapper = wrappers.first(where: { $0.publicKey == userPublicKey }), let encryptedKeyPair = wrapper.encryptedKeyPair else { return }
let plaintext: Data
do {
plaintext = try MessageReceiver.decryptWithSessionProtocol(ciphertext: encryptedKeyPair, using: userKeyPair).plaintext
} catch {
return SNLog("Couldn't decrypt closed group encryption key pair.")
}
// Parse it
let proto: SNProtoDataMessageClosedGroupControlMessageKeyPair
do {
proto = try SNProtoDataMessageClosedGroupControlMessageKeyPair.parseData(plaintext)
} catch {
return SNLog("Couldn't parse closed group encryption key pair.")
}
let keyPair: ECKeyPair
do {
keyPair = try ECKeyPair(publicKeyData: proto.publicKey.removing05PrefixIfNeeded(), privateKeyData: proto.privateKey)
} catch {
return SNLog("Couldn't parse closed group encryption key pair.")
}
// Store it
Storage.shared.addClosedGroupEncryptionKeyPair(keyPair, for: groupPublicKey, using: transaction)
SNLog("Received a new closed group encryption key pair.")
}
private static func handleClosedGroupNameChanged(_ message: ClosedGroupControlMessage, using transaction: Any) {
guard case let .nameChange(name) = message.kind else { return }
let transaction = transaction as! YapDatabaseReadWriteTransaction
performIfValid(for: message, using: transaction) { groupID, thread, group in
// Update the group
let newGroupModel = TSGroupModel(title: name, memberIds: group.groupMemberIds, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds)
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user if needed
guard name != group.groupName else { return }
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
}
private static func handleClosedGroupMembersAdded(_ message: ClosedGroupControlMessage, using transaction: Any) {
guard case let .membersAdded(membersAsData) = message.kind else { return }
let transaction = transaction as! YapDatabaseReadWriteTransaction
performIfValid(for: message, using: transaction) { groupID, thread, group in
// Update the group
let members = Set(group.groupMemberIds).union(membersAsData.map { $0.toHexString() })
let newGroupModel = TSGroupModel(title: group.groupName, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds)
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user if needed
guard members != Set(group.groupMemberIds) else { return }
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
}
private static func handleClosedGroupMembersRemoved(_ message: ClosedGroupControlMessage, using transaction: Any) {
guard case let .membersRemoved(membersAsData) = message.kind else { return }
let transaction = transaction as! YapDatabaseReadWriteTransaction
guard let groupPublicKey = message.groupPublicKey else { return }
performIfValid(for: message, using: transaction) { groupID, thread, group in
// Check that the admin wasn't removed
let members = Set(group.groupMemberIds).subtracting(membersAsData.map { $0.toHexString() })
guard members.contains(group.groupAdminIds.first!) else {
return SNLog("Ignoring invalid closed group update.")
}
// If the current user was removed:
// Stop polling for the group
// Remove the key pairs associated with the group
// Notify the PN server
let userPublicKey = getUserHexEncodedPublicKey()
let wasCurrentUserRemoved = !members.contains(userPublicKey)
if wasCurrentUserRemoved {
Storage.shared.removeClosedGroupPublicKey(groupPublicKey, using: transaction)
Storage.shared.removeAllClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction)
let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey)
}
// Generate and distribute a new encryption key pair if needed
let isCurrentUserAdmin = group.groupAdminIds.contains(getUserHexEncodedPublicKey())
if isCurrentUserAdmin {
do {
try MessageSender.generateAndSendNewEncryptionKeyPair(for: groupPublicKey, to: Set(members), using: transaction)
} catch {
SNLog("Couldn't distribute new encryption key pair.")
}
}
// Update the group
let newGroupModel = TSGroupModel(title: group.groupName, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds)
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user if needed
guard members != Set(group.groupMemberIds) else { return }
let infoMessageType: TSInfoMessageType = wasCurrentUserRemoved ? .typeGroupQuit : .typeGroupUpdate
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: infoMessageType, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
}
private static func handleClosedGroupMemberLeft(_ message: ClosedGroupControlMessage, using transaction: Any) {
guard case .memberLeft = message.kind else { return }
let transaction = transaction as! YapDatabaseReadWriteTransaction
guard let groupPublicKey = message.groupPublicKey else { return }
performIfValid(for: message, using: transaction) { groupID, thread, group in
let didAdminLeave = group.groupAdminIds.contains(message.sender!)
let members: Set<String> = didAdminLeave ? [] : Set(group.groupMemberIds).subtracting([ message.sender! ]) // If the admin leaves the group is disbanded
// Guard against self-sends
guard message.sender != getUserHexEncodedPublicKey() else {
return SNLog("Ignoring invalid closed group update.")
}
// Generate and distribute a new encryption key pair if needed
let isCurrentUserAdmin = group.groupAdminIds.contains(getUserHexEncodedPublicKey())
if isCurrentUserAdmin {
do {
try MessageSender.generateAndSendNewEncryptionKeyPair(for: groupPublicKey, to: members, using: transaction)
} catch {
SNLog("Couldn't distribute new encryption key pair.")
}
}
// Update the group
let newGroupModel = TSGroupModel(title: group.groupName, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds)
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user if needed
guard members != Set(group.groupMemberIds) else { return }
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
}
private static func performIfValid(for message: ClosedGroupControlMessage, using transaction: Any, _ update: (Data, TSGroupThread, TSGroupModel) -> Void) {
// Prepare
let transaction = transaction as! YapDatabaseReadWriteTransaction
// Get the group
guard let groupPublicKey = message.groupPublicKey else { return }
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
return SNLog("Ignoring closed group update for nonexistent group.")
}
let group = thread.groupModel
// Check that the message isn't from before the group was created
guard Double(message.sentTimestamp!) > thread.creationDate.timeIntervalSince1970 * 1000 else {
return SNLog("Ignoring closed group update from before thread was created.")
}
// Check that the sender is a member of the group
guard Set(group.groupMemberIds).contains(message.sender!) else {
return SNLog("Ignoring closed group update from non-member.")
}
// Perform the update
update(groupID, thread, group)
}
// MARK: - Deprecated
/// - Note: Deprecated.
private static func handleClosedGroupUpdated(_ message: ClosedGroupControlMessage, using transaction: Any) {
// Prepare
guard case let .update(name, membersAsData) = message.kind else { return }
let transaction = transaction as! YapDatabaseReadWriteTransaction
@ -327,46 +505,4 @@ extension MessageReceiver {
infoMessage.save(with: transaction)
}
}
private static func handleGroupEncryptionKeyPair(_ message: ClosedGroupUpdateV2, using transaction: Any) {
// Prepare
guard case let .encryptionKeyPair(wrappers) = message.kind, let groupPublicKey = message.groupPublicKey else { return }
let transaction = transaction as! YapDatabaseReadWriteTransaction
let userPublicKey = getUserHexEncodedPublicKey()
guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else {
return SNLog("Couldn't find user X25519 key pair.")
}
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
return SNLog("Ignoring closed group encryption key pair for nonexistent group.")
}
guard thread.groupModel.groupAdminIds.contains(message.sender!) else {
return SNLog("Ignoring closed group encryption key pair from non-admin.")
}
// Find our wrapper and decrypt it if possible
guard let wrapper = wrappers.first(where: { $0.publicKey == userPublicKey }), let encryptedKeyPair = wrapper.encryptedKeyPair else { return }
let plaintext: Data
do {
plaintext = try MessageReceiver.decryptWithSessionProtocol(ciphertext: encryptedKeyPair, using: userKeyPair).plaintext
} catch {
return SNLog("Couldn't decrypt closed group encryption key pair.")
}
// Parse it
let proto: SNProtoDataMessageClosedGroupUpdateV2KeyPair
do {
proto = try SNProtoDataMessageClosedGroupUpdateV2KeyPair.parseData(plaintext)
} catch {
return SNLog("Couldn't parse closed group encryption key pair.")
}
let keyPair: ECKeyPair
do {
keyPair = try ECKeyPair(publicKeyData: proto.publicKey.removing05PrefixIfNeeded(), privateKeyData: proto.privateKey)
} catch {
return SNLog("Couldn't parse closed group encryption key pair.")
}
// Store it
Storage.shared.addClosedGroupEncryptionKeyPair(keyPair, for: groupPublicKey, using: transaction)
SNLog("Received a new closed group encryption key pair.")
}
}

@ -109,7 +109,7 @@ public enum MessageReceiver {
let message: Message? = {
if let readReceipt = ReadReceipt.fromProto(proto) { return readReceipt }
if let typingIndicator = TypingIndicator.fromProto(proto) { return typingIndicator }
if let closedGroupUpdate = ClosedGroupUpdateV2.fromProto(proto) { return closedGroupUpdate }
if let closedGroupControlMessage = ClosedGroupControlMessage.fromProto(proto) { return closedGroupControlMessage }
if let expirationTimerUpdate = ExpirationTimerUpdate.fromProto(proto) { return expirationTimerUpdate }
if let visibleMessage = VisibleMessage.fromProto(proto) { return visibleMessage }
return nil

@ -3,7 +3,7 @@ import SessionProtocolKit
extension MessageSender {
public static func createV2ClosedGroup(name: String, members: Set<String>, transaction: YapDatabaseReadWriteTransaction) -> Promise<TSGroupThread> {
public static func createClosedGroup(name: String, members: Set<String>, transaction: YapDatabaseReadWriteTransaction) -> Promise<TSGroupThread> {
// Prepare
var members = members
let userPublicKey = getUserHexEncodedPublicKey()
@ -27,10 +27,10 @@ extension MessageSender {
guard member != userPublicKey else { continue }
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction)
let closedGroupUpdateKind = ClosedGroupUpdateV2.Kind.new(publicKey: Data(hex: groupPublicKey), name: name,
let closedGroupControlMessageKind = ClosedGroupControlMessage.Kind.new(publicKey: Data(hex: groupPublicKey), name: name,
encryptionKeyPair: encryptionKeyPair, members: membersAsData, admins: adminsAsData)
let closedGroupUpdate = ClosedGroupUpdateV2(kind: closedGroupUpdateKind)
let promise = MessageSender.sendNonDurably(closedGroupUpdate, in: thread, using: transaction)
let closedGroupControlMessage = ClosedGroupControlMessage(kind: closedGroupControlMessageKind)
let promise = MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction)
promises.append(promise)
}
// Add the group to the user's set of public keys to poll for
@ -46,7 +46,195 @@ extension MessageSender {
return when(fulfilled: promises).map2 { thread }
}
public static func updateV2(_ groupPublicKey: String, with members: Set<String>, name: String, transaction: YapDatabaseReadWriteTransaction) throws {
public static func generateAndSendNewEncryptionKeyPair(for groupPublicKey: String, to targetMembers: Set<String>, using transaction: Any) throws {
// Prepare
let transaction = transaction as! YapDatabaseReadWriteTransaction
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
SNLog("Can't distribute new encryption key pair for nonexistent closed group.")
throw Error.noThread
}
guard thread.groupModel.groupAdminIds.contains(getUserHexEncodedPublicKey()) else {
SNLog("Can't distribute new encryption key pair as a non-admin.")
throw Error.invalidClosedGroupUpdate
}
// Generate the new encryption key pair
let newKeyPair = Curve25519.generateKeyPair()
// Distribute it
let proto = try SNProtoDataMessageClosedGroupControlMessageKeyPair.builder(publicKey: newKeyPair.publicKey,
privateKey: newKeyPair.privateKey).build()
let plaintext = try proto.serializedData()
let wrappers = try targetMembers.compactMap { publicKey -> ClosedGroupControlMessage.KeyPairWrapper in
let ciphertext = try MessageSender.encryptWithSessionProtocol(plaintext, for: publicKey)
return ClosedGroupControlMessage.KeyPairWrapper(publicKey: publicKey, encryptedKeyPair: ciphertext)
}
let closedGroupControlMessage = ClosedGroupControlMessage(kind: .encryptionKeyPair(wrappers))
let _ = MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction).done { // FIXME: It'd be great if we could make this a durable operation
// Store it * after * having sent out the message to the group
SNMessagingKitConfiguration.shared.storage.write { transaction in
Storage.shared.addClosedGroupEncryptionKeyPair(newKeyPair, for: groupPublicKey, using: transaction)
}
}
}
public static func v2_update(_ groupPublicKey: String, with members: Set<String>, name: String, transaction: YapDatabaseReadWriteTransaction) throws {
// Get the group, check preconditions & prepare
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
SNLog("Can't update nonexistent closed group.")
throw Error.noThread
}
let group = thread.groupModel
// Update name if needed
if name != group.groupName { try setName(to: name, for: groupPublicKey, using: transaction) }
// Add members if needed
let addedMembers = members.subtracting(group.groupMemberIds)
if !addedMembers.isEmpty { try addMembers(addedMembers, to: groupPublicKey, using: transaction) }
// Remove members if needed
let removedMembers = Set(group.groupMemberIds).subtracting(members)
if !removedMembers.isEmpty { try removeMembers(removedMembers, to: groupPublicKey, using: transaction) }
}
public static func setName(to name: String, for groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws {
// Get the group, check preconditions & prepare
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
SNLog("Can't leave nonexistent closed group.")
throw Error.noThread
}
guard !name.isEmpty else {
SNLog("Can't set closed group name to an empty value.")
throw Error.invalidClosedGroupUpdate
}
let group = thread.groupModel
// Send the update to the group
let closedGroupControlMessage = ClosedGroupControlMessage(kind: .nameChange(name: name))
MessageSender.send(closedGroupControlMessage, in: thread, using: transaction)
// Update the group
let newGroupModel = TSGroupModel(title: name, memberIds: group.groupMemberIds, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds)
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
public static func addMembers(_ newMembers: Set<String>, to groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws {
// Get the group, check preconditions & prepare
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
SNLog("Can't leave nonexistent closed group.")
throw Error.noThread
}
guard !newMembers.isEmpty else {
SNLog("Invalid closed group update.")
throw Error.invalidClosedGroupUpdate
}
let group = thread.groupModel
let members = [String](Set(group.groupMemberIds).union(newMembers))
let membersAsData = members.map { Data(hex: $0) }
let adminsAsData = group.groupAdminIds.map { Data(hex: $0) }
guard let encryptionKeyPair = Storage.shared.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else {
SNLog("Couldn't find encryption key pair for closed group: \(groupPublicKey).")
throw Error.noKeyPair
}
// Send the update to the group
let closedGroupControlMessage = ClosedGroupControlMessage(kind: .membersAdded(members: newMembers.map { Data(hex: $0) }))
MessageSender.send(closedGroupControlMessage, in: thread, using: transaction)
// Send updates to the new members individually
for member in newMembers {
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction)
let closedGroupControlMessageKind = ClosedGroupControlMessage.Kind.new(publicKey: Data(hex: groupPublicKey), name: group.groupName!,
encryptionKeyPair: encryptionKeyPair, members: membersAsData, admins: adminsAsData)
let closedGroupControlMessage = ClosedGroupControlMessage(kind: closedGroupControlMessageKind)
MessageSender.send(closedGroupControlMessage, in: thread, using: transaction)
}
// Update the group
let newGroupModel = TSGroupModel(title: group.groupName, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds)
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
public static func removeMembers(_ membersToRemove: Set<String>, to groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws {
// Get the group, check preconditions & prepare
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
SNLog("Can't leave nonexistent closed group.")
throw Error.noThread
}
guard !membersToRemove.isEmpty else {
SNLog("Invalid closed group update.")
throw Error.invalidClosedGroupUpdate
}
let group = thread.groupModel
let members = Set(group.groupMemberIds).subtracting(membersToRemove)
let isCurrentUserAdmin = group.groupAdminIds.contains(getUserHexEncodedPublicKey())
// Send the update to the group and generate + distribute a new encryption key pair if needed
let closedGroupControlMessage = ClosedGroupControlMessage(kind: .membersRemoved(members: membersToRemove.map { Data(hex: $0) }))
if isCurrentUserAdmin {
let _ = MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction).done {
try generateAndSendNewEncryptionKeyPair(for: groupPublicKey, to: members, using: transaction)
}
} else {
MessageSender.send(closedGroupControlMessage, in: thread, using: transaction)
}
// Update the group
let newGroupModel = TSGroupModel(title: group.groupName, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds)
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
@objc(v2_leaveClosedGroupWithPublicKey:using:error:)
public static func v2_leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws {
// Get the group, check preconditions & prepare
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
SNLog("Can't leave nonexistent closed group.")
throw Error.noThread
}
let group = thread.groupModel
let userPublicKey = getUserHexEncodedPublicKey()
let isCurrentUserAdmin = group.groupAdminIds.contains(userPublicKey)
let members: Set<String> = isCurrentUserAdmin ? [] : Set(group.groupMemberIds).subtracting([ userPublicKey ]) // If the admin leaves the group is disbanded
let admins: Set<String> = isCurrentUserAdmin ? [] : Set(group.groupAdminIds)
// Send the update to the group
let closedGroupControlMessage = ClosedGroupControlMessage(kind: .memberLeft)
let _ = MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction).done {
SNMessagingKitConfiguration.shared.storage.write { transaction in
// Remove the group from the database and unsubscribe from PNs
Storage.shared.removeAllClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction)
Storage.shared.removeClosedGroupPublicKey(groupPublicKey, using: transaction)
let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey)
}
}
// Update the group
let newGroupModel = TSGroupModel(title: group.groupName, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: [String](admins))
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
// MARK: - Deprecated
/// - Note: Deprecated.
public static func update(_ groupPublicKey: String, with members: Set<String>, name: String, transaction: YapDatabaseReadWriteTransaction) throws {
// Prepare
let userPublicKey = getUserHexEncodedPublicKey()
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
@ -81,9 +269,9 @@ extension MessageSender {
}
}
// Send the update to the group
let mainClosedGroupUpdate = ClosedGroupUpdateV2(kind: .update(name: name, members: membersAsData))
let mainClosedGroupControlMessage = ClosedGroupControlMessage(kind: .update(name: name, members: membersAsData))
if isUserLeaving {
let _ = MessageSender.sendNonDurably(mainClosedGroupUpdate, in: thread, using: transaction).done {
let _ = MessageSender.sendNonDurably(mainClosedGroupControlMessage, in: thread, using: transaction).done {
SNMessagingKitConfiguration.shared.storage.write { transaction in
// Remove the group from the database and unsubscribe from PNs
Storage.shared.removeAllClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction)
@ -92,7 +280,7 @@ extension MessageSender {
}
}
} else {
MessageSender.send(mainClosedGroupUpdate, in: thread, using: transaction)
MessageSender.send(mainClosedGroupControlMessage, in: thread, using: transaction)
// Generate and distribute a new encryption key pair if needed
if wasAnyUserRemoved && isCurrentUserAdmin {
try generateAndSendNewEncryptionKeyPair(for: groupPublicKey, to: members.subtracting(newMembers), using: transaction)
@ -101,10 +289,10 @@ extension MessageSender {
for member in newMembers {
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction)
let closedGroupUpdateKind = ClosedGroupUpdateV2.Kind.new(publicKey: Data(hex: groupPublicKey), name: name,
let closedGroupControlMessageKind = ClosedGroupControlMessage.Kind.new(publicKey: Data(hex: groupPublicKey), name: name,
encryptionKeyPair: encryptionKeyPair, members: membersAsData, admins: adminsAsData)
let closedGroupUpdate = ClosedGroupUpdateV2(kind: closedGroupUpdateKind)
MessageSender.send(closedGroupUpdate, in: thread, using: transaction)
let closedGroupControlMessage = ClosedGroupControlMessage(kind: closedGroupControlMessageKind)
MessageSender.send(closedGroupControlMessage, in: thread, using: transaction)
}
}
// Update the group
@ -116,8 +304,9 @@ extension MessageSender {
infoMessage.save(with: transaction)
}
/// - Note: Deprecated.
@objc(leaveClosedGroupWithPublicKey:using:error:)
public static func leaveV2(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws {
public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws {
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
@ -133,38 +322,6 @@ extension MessageSender {
} else {
newMembers = [] // If the admin leaves the group is destroyed
}
return try updateV2(groupPublicKey, with: newMembers, name: group.groupName!, transaction: transaction)
}
public static func generateAndSendNewEncryptionKeyPair(for groupPublicKey: String, to targetMembers: Set<String>, using transaction: Any) throws {
// Prepare
let transaction = transaction as! YapDatabaseReadWriteTransaction
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
SNLog("Can't distribute new encryption key pair for nonexistent closed group.")
throw Error.noThread
}
guard thread.groupModel.groupAdminIds.contains(getUserHexEncodedPublicKey()) else {
SNLog("Can't distribute new encryption key pair as a non-admin.")
throw Error.invalidClosedGroupUpdate
}
// Generate the new encryption key pair
let newKeyPair = Curve25519.generateKeyPair()
// Distribute it
let proto = try SNProtoDataMessageClosedGroupUpdateV2KeyPair.builder(publicKey: newKeyPair.publicKey,
privateKey: newKeyPair.privateKey).build()
let plaintext = try proto.serializedData()
let wrappers = try targetMembers.compactMap { publicKey -> ClosedGroupUpdateV2.KeyPairWrapper in
let ciphertext = try MessageSender.encryptWithSessionProtocol(plaintext, for: publicKey)
return ClosedGroupUpdateV2.KeyPairWrapper(publicKey: publicKey, encryptedKeyPair: ciphertext)
}
let closedGroupUpdate = ClosedGroupUpdateV2(kind: .encryptionKeyPair(wrappers))
let _ = MessageSender.sendNonDurably(closedGroupUpdate, in: thread, using: transaction).done { // FIXME: It'd be great if we could make this a durable operation
// Store it * after * having sent out the message to the group
SNMessagingKitConfiguration.shared.storage.write { transaction in
Storage.shared.addClosedGroupEncryptionKeyPair(newKeyPair, for: groupPublicKey, using: transaction)
}
}
return try update(groupPublicKey, with: newMembers, name: group.groupName!, transaction: transaction)
}
}

@ -242,7 +242,7 @@ public final class MessageSender : NSObject {
storage.write(with: { transaction in
MessageSender.handleSuccessfulMessageSend(message, to: destination, using: transaction)
var shouldNotify = (message is VisibleMessage)
if let closedGroupUpdate = message as? ClosedGroupUpdateV2, case .new = closedGroupUpdate.kind {
if let closedGroupControlMessage = message as? ClosedGroupControlMessage, case .new = closedGroupControlMessage.kind {
shouldNotify = true
}
if shouldNotify {

@ -135,18 +135,22 @@ const int32_t kGroupIdLength = 16;
}
if (membersWhoJoined.count > 0) {
updatedGroupInfoString = @"New members joined";
NSArray *newMembersNames = [[membersWhoJoined allObjects] map:^NSString*(NSString* item) {
return [LKUserDisplayNameUtilities getPrivateChatDisplayNameAvoidWriteTransaction:item] ?: item;
}];
updatedGroupInfoString = [updatedGroupInfoString
stringByAppendingString:[NSString
stringWithFormat:NSLocalizedString(@"GROUP_MEMBER_JOINED", @""),
[newMembersNames componentsJoinedByString:@", "]]];
}
if (newModel.removedMembers.count > 0) {
NSString *masterDeviceHexEncodedPublicKey = [NSUserDefaults.standardUserDefaults stringForKey:@"masterDeviceHexEncodedPublicKey"];
NSString *hexEncodedPublicKey = masterDeviceHexEncodedPublicKey != nil ? masterDeviceHexEncodedPublicKey : TSAccountManager.localNumber;
if ([newModel.removedMembers containsObject:hexEncodedPublicKey]) {
if ([newModel.removedMembers containsObject:[SNGeneralUtilities getUserPublicKey]]) {
updatedGroupInfoString = [updatedGroupInfoString
stringByAppendingString:NSLocalizedString(@"YOU_WERE_REMOVED", @"")];
} else {
NSArray *removedMemberNames = [newModel.removedMembers.allObjects map:^NSString*(NSString* publicKey) {
return [LKUserDisplayNameUtilities getPrivateChatDisplayNameFor:publicKey];
return [LKUserDisplayNameUtilities getPrivateChatDisplayNameAvoidWriteTransaction:publicKey] ?: publicKey;
}];
if ([removedMemberNames count] > 1) {
updatedGroupInfoString = [updatedGroupInfoString

@ -15,10 +15,6 @@ typedef NS_ENUM(NSUInteger, OWSVerificationState) {
@class SNProtoVerified;
NSString *OWSVerificationStateToString(OWSVerificationState verificationState);
SNProtoVerified *_Nullable BuildVerifiedProtoWithRecipientId(NSString *destinationRecipientId,
NSData *identityKey,
OWSVerificationState verificationState,
NSUInteger paddingBytesLength);
@interface OWSRecipientIdentity : TSYapDatabaseObject

@ -23,45 +23,6 @@ NSString *OWSVerificationStateToString(OWSVerificationState verificationState)
}
}
SNProtoVerifiedState OWSVerificationStateToProtoState(OWSVerificationState verificationState)
{
switch (verificationState) {
case OWSVerificationStateDefault:
return SNProtoVerifiedStateDefault;
case OWSVerificationStateVerified:
return SNProtoVerifiedStateVerified;
case OWSVerificationStateNoLongerVerified:
return SNProtoVerifiedStateUnverified;
}
}
SNProtoVerified *_Nullable BuildVerifiedProtoWithRecipientId(NSString *destinationRecipientId,
NSData *identityKey,
OWSVerificationState verificationState,
NSUInteger paddingBytesLength)
{
SNProtoVerifiedBuilder *verifiedBuilder = [SNProtoVerified builderWithDestination:destinationRecipientId];
verifiedBuilder.identityKey = identityKey;
verifiedBuilder.state = OWSVerificationStateToProtoState(verificationState);
if (paddingBytesLength > 0) {
// We add the same amount of padding in the VerificationStateSync message and it's coresponding NullMessage so
// that the sync message is indistinguishable from an outgoing Sent transcript corresponding to the NullMessage.
// We pad the NullMessage so as to obscure it's content. The sync message (like all sync messages) will be
// *additionally* padded by the superclass while being sent. The end result is we send a NullMessage of a
// non-distinct size, and a verification sync which is ~1-512 bytes larger then that.
verifiedBuilder.nullMessage = [Cryptography generateRandomBytes:paddingBytesLength];
}
NSError *error;
SNProtoVerified *_Nullable verifiedProto = [verifiedBuilder buildAndReturnError:&error];
if (error || !verifiedProto) {
return nil;
}
return verifiedProto;
}
@interface OWSRecipientIdentity ()
@property (atomic) OWSVerificationState verificationState;

@ -45,15 +45,6 @@ NS_ASSUME_NONNULL_BEGIN
[dataMessageBuilder setProfileKey:self.localProfileKey.keyData];
}
+ (void)addLocalProfileKeyIfNecessary:(TSThread *)thread
recipientId:(NSString *)recipientId
callMessageBuilder:(SNProtoCallMessageBuilder *)callMessageBuilder
{
if ([self shouldMessageHaveLocalProfileKey:thread recipientId:recipientId]) {
[callMessageBuilder setProfileKey:self.localProfileKey.keyData];
}
}
@end
NS_ASSUME_NONNULL_END

@ -53,10 +53,10 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
group.groupModel.groupType == .closedGroup { // Should always be true because we don't get PNs for open groups
senderDisplayName = String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderDisplayName, group.groupModel.groupName ?? MessageStrings.newGroupDefaultTitle)
}
case let closedGroupUpdate as ClosedGroupUpdateV2:
case let closedGroupControlMessage as ClosedGroupControlMessage:
// TODO: We could consider actually handling the update here. Not sure if there's enough time though, seeing as though
// in some cases we need to send messages (e.g. our sender key) to a number of other users.
switch closedGroupUpdate.kind {
switch closedGroupControlMessage.kind {
case .new(_, let name, _, _, _): snippet = "\(senderDisplayName) added you to \(name)"
default: return self.handleFailure(for: notificationContent)
}

Loading…
Cancel
Save