From 2a0130ff0434d44d414df3b8c851fed9c8c72b58 Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Mon, 18 May 2020 15:12:22 +1000 Subject: [PATCH] Multidevice support for medium groups --- integration_test/common.js | 44 +-- .../page-objects/conversation.page.js | 4 +- js/background.js | 27 +- js/models/conversations.js | 40 ++- js/models/messages.js | 3 +- js/modules/loki_sender_key_api.js | 17 +- libloki/api.js | 1 + libtextsecure/errors.js | 14 + libtextsecure/message_receiver.js | 298 +++++++++++------- libtextsecure/sendmessage.js | 35 +- protos/SignalService.proto | 26 +- 11 files changed, 336 insertions(+), 173 deletions(-) diff --git a/integration_test/common.js b/integration_test/common.js index db7154664..e58238daa 100644 --- a/integration_test/common.js +++ b/integration_test/common.js @@ -404,29 +404,29 @@ module.exports = { ) ).should.eventually.be.true; - await Promise.all(others.map(async app => { - - // next check that other members have been invited and have the group in their conversations - await app.client.waitForExist( - ConversationPage.rowOpenGroupConversationName( - this.VALID_CLOSED_GROUP_NAME1 - ), - 6000 + await Promise.all( + others.map(async otherApp => { + // next check that other members have been invited and have the group in their conversations + await otherApp.client.waitForExist( + ConversationPage.rowOpenGroupConversationName( + this.VALID_CLOSED_GROUP_NAME1 + ), + 6000 + ); + // open the closed group conversation on otherApp + await otherApp.client + .element(ConversationPage.conversationButtonSection) + .click(); + await this.timeout(500); + await otherApp.client + .element( + ConversationPage.rowOpenGroupConversationName( + this.VALID_CLOSED_GROUP_NAME1 + ) + ) + .click(); + }) ); - // open the closed group conversation on app2 - await app.client - .element(ConversationPage.conversationButtonSection) - .click(); - await this.timeout(500); - await app.client - .element( - ConversationPage.rowOpenGroupConversationName( - this.VALID_CLOSED_GROUP_NAME1 - ) - ) - .click(); - - })); }, async linkApp2ToApp(app1, app2) { diff --git a/integration_test/page-objects/conversation.page.js b/integration_test/page-objects/conversation.page.js index 5d45af158..64146d5c1 100644 --- a/integration_test/page-objects/conversation.page.js +++ b/integration_test/page-objects/conversation.page.js @@ -64,7 +64,9 @@ module.exports = { 'Enter a group name' ), createClosedGroupMemberItem: commonPage.divWithClass('session-member-item'), - createClosedGroupSealedSenderToggle: commonPage.divWithClass('session-toggle'), + createClosedGroupSealedSenderToggle: commonPage.divWithClass( + 'session-toggle' + ), createClosedGroupMemberItemSelected: commonPage.divWithClass( 'session-member-item selected' ), diff --git a/js/background.js b/js/background.js index bcaa5ab14..b82564f8c 100644 --- a/js/background.js +++ b/js/background.js @@ -746,17 +746,9 @@ identityKeys.privKey ); - // Constructing a "create group" message - const proto = new textsecure.protobuf.DataMessage(); + const primary = window.storage.get('primaryDevicePubKey'); - const groupUpdate = new textsecure.protobuf.MediumGroupUpdate(); - - groupUpdate.groupId = groupId; - groupUpdate.groupSecretKey = groupSecretKeyHex; - groupUpdate.senderKey = senderKey; - groupUpdate.members = [ourIdentity, ...members]; - groupUpdate.groupName = groupName; - proto.mediumGroupUpdate = groupUpdate; + const allMembers = [primary, ...members]; await window.Signal.Data.createOrUpdateIdentityKey({ id: groupId, @@ -768,11 +760,14 @@ ev.groupDetails = { id: groupId, name: groupName, - members: groupUpdate.members, - recipients: groupUpdate.members, + members: allMembers, + recipients: allMembers, active: true, expireTimer: 0, avatar: '', + secretKey: identityKeys.privKey, + senderKey, + is_medium_group: true, }; ev.confirm = () => {}; @@ -786,13 +781,14 @@ convo.updateGroup(ev.groupDetails); + convo.setFriendRequestStatus( + window.friends.friendRequestStatusEnum.friends + ); + appView.openConversation(groupId, {}); // Subscribe to this group id messageReceiver.pollForAdditionalId(groupId); - - // TODO: include ourselves so that our lined devices work as well! - await textsecure.messaging.updateMediumGroup(members, proto); }; window.doCreateGroup = async (groupName, members) => { @@ -1911,6 +1907,7 @@ members: details.members, color: details.color, type: 'group', + is_medium_group: details.is_medium_group || false, }; if (details.active) { diff --git a/js/models/conversations.js b/js/models/conversations.js index 1583d7a62..b84e84104 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -14,7 +14,8 @@ clipboard, BlockedNumberController, lokiPublicChatAPI, - JobQueue + JobQueue, + StringView */ /* eslint-disable more/no-then */ @@ -2289,12 +2290,41 @@ group_update: groupUpdate, }); - const id = await window.Signal.Data.saveMessage(message.attributes, { - Message: Whisper.Message, - }); - message.set({ id }); + const messageId = await window.Signal.Data.saveMessage( + message.attributes, + { + Message: Whisper.Message, + } + ); + message.set({ id: messageId }); const options = this.getSendOptions(); + + if (groupUpdate.is_medium_group) { + // Constructing a "create group" message + const proto = new textsecure.protobuf.DataMessage(); + + const mgUpdate = new textsecure.protobuf.MediumGroupUpdate(); + + const { id, name, secretKey, senderKey, members } = groupUpdate; + + mgUpdate.type = textsecure.protobuf.MediumGroupUpdate.Type.NEW_GROUP; + mgUpdate.groupId = id; + mgUpdate.groupSecretKey = secretKey; + mgUpdate.senderKey = new textsecure.protobuf.SenderKey({ + chainKey: senderKey, + keyIdx: 0, + }); + mgUpdate.members = members.map(pkHex => + StringView.hexToArrayBuffer(pkHex) + ); + mgUpdate.groupName = name; + proto.mediumGroupUpdate = mgUpdate; + + await textsecure.messaging.updateMediumGroup(members, proto); + return; + } + message.send( this.wrapSend( textsecure.messaging.updateGroup( diff --git a/js/models/messages.js b/js/models/messages.js index 7a6dfb207..1cd67740b 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1477,7 +1477,8 @@ if (!this.isFriendRequest()) { const c = this.getConversation(); // Don't bother sending sync messages to public chats - if (c && !c.isPublic()) { + // or groups with sender keys + if (c && !c.isPublic() && !c.get('is_medium_group')) { this.sendSyncMessage(); } } diff --git a/js/modules/loki_sender_key_api.js b/js/modules/loki_sender_key_api.js index 7e5ca5a36..df4b4f06d 100644 --- a/js/modules/loki_sender_key_api.js +++ b/js/modules/loki_sender_key_api.js @@ -5,7 +5,8 @@ dcodeIO, libloki, log, - crypto + crypto, + textsecure */ /* eslint-disable more/no-then */ @@ -39,9 +40,7 @@ async function saveSenderKeysInner( } // Save somebody else's key -async function saveSenderKeys(groupId, senderIdentity, chainKey) { - // New key, so index 0 - const keyIdx = 0; +async function saveSenderKeys(groupId, senderIdentity, chainKey, keyIdx) { const messageKeys = {}; await saveSenderKeysInner( groupId, @@ -133,7 +132,7 @@ async function advanceRatchet(groupId, senderIdentity, idx) { log.error( `Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}` ); - return null; + throw new textsecure.SenderKeyMissing(senderIdentity); } // Normally keyIdx will be 1 behind, in which case we stepRatchet one time only @@ -179,6 +178,7 @@ async function advanceRatchet(groupId, senderIdentity, idx) { break; } else if (nextKeyIdx > idx) { log.error('Developer error: nextKeyIdx > idx'); + return null; } else { // Store keys for skipped nextKeyIdx, we might need them to decrypt // messages that arrive out-of-order @@ -289,9 +289,16 @@ async function encryptWithSenderKeyInner(plaintext, groupId, ourIdentity) { return { ciphertext, keyIdx }; } +async function getSenderKeys(groupId, senderIdentity) { + const { chainKey, keyIdx } = await loadChainKey(groupId, senderIdentity); + + return { chainKey, keyIdx }; +} + module.exports = { createSenderKeyForGroup, encryptWithSenderKey, decryptWithSenderKey, saveSenderKeys, + getSenderKeys, }; diff --git a/libloki/api.js b/libloki/api.js index dabfd4bff..62e188450 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -220,6 +220,7 @@ }); return syncMessage; } + function createGroupSyncProtoMessage(sessionGroup) { // We are getting a single open group here diff --git a/libtextsecure/errors.js b/libtextsecure/errors.js index 7c5fbe050..70ec36d5b 100644 --- a/libtextsecure/errors.js +++ b/libtextsecure/errors.js @@ -263,6 +263,19 @@ } } + function SenderKeyMissing(senderIdentity) { + this.name = 'SenderKeyMissing'; + this.senderIdentity = senderIdentity; + + Error.call(this, this.name); + + // Maintains proper stack trace, where our error was thrown (only available on V8) + // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } + } + window.textsecure.UnregisteredUserError = UnregisteredUserError; window.textsecure.SendMessageNetworkError = SendMessageNetworkError; window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError; @@ -282,4 +295,5 @@ window.textsecure.TimestampError = TimestampError; window.textsecure.PublicChatError = PublicChatError; window.textsecure.PublicTokenError = PublicTokenError; + window.textsecure.SenderKeyMissing = SenderKeyMissing; })(); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 837cc3b48..569fa59df 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -849,6 +849,22 @@ MessageReceiver.prototype.extend({ return promise .then(plaintext => this.postDecrypt(envelope, plaintext)) .catch(error => { + if (error && error instanceof textsecure.SenderKeyMissing) { + const groupId = envelope.source; + const { senderIdentity } = error; + + log.info( + 'Requesting missing key for identity: ', + senderIdentity, + 'groupId: ', + groupId + ); + + textsecure.messaging.requestSenderKeys(senderIdentity, groupId); + + return; + } + let errorToThrow = error; const noSession = @@ -876,7 +892,7 @@ MessageReceiver.prototype.extend({ ev.confirm = this.removeFromCache.bind(this, envelope); const returnError = () => Promise.reject(errorToThrow); - return this.dispatchAndWait(ev).then(returnError, returnError); + this.dispatchAndWait(ev).then(returnError, returnError); }); }, async decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address) { @@ -900,7 +916,7 @@ MessageReceiver.prototype.extend({ }, // handle a SYNC message for a message // sent by another device - handleSentMessage(envelope, sentContainer, msg) { + async handleSentMessage(envelope, sentContainer, msg) { const { destination, timestamp, @@ -908,41 +924,63 @@ MessageReceiver.prototype.extend({ unidentifiedStatus, } = sentContainer; - let p = Promise.resolve(); // eslint-disable-next-line no-bitwise if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { - p = this.handleEndSession(destination); + await this.handleEndSession(destination); } - return p.then(() => - this.processDecrypted(envelope, msg).then(message => { - const primaryDevicePubKey = window.storage.get('primaryDevicePubKey'); - // handle profileKey and avatar updates - if (envelope.source === primaryDevicePubKey) { - const { profileKey, profile } = message; - const primaryConversation = ConversationController.get( - primaryDevicePubKey - ); - if (profile) { - this.updateProfile(primaryConversation, profile, profileKey); - } - } + if (msg.mediumGroupUpdate) { + await this.handleMediumGroupUpdate(envelope, msg.mediumGroupUpdate); + return; + } - const ev = new Event('sent'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.data = { - destination, - timestamp: timestamp.toNumber(), - device: envelope.sourceDevice, - unidentifiedStatus, - message, - }; - if (expirationStartTimestamp) { - ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber(); - } - return this.dispatchAndWait(ev); - }) + const message = await this.processDecrypted(envelope, msg); + + const groupId = message.group && message.group.id; + const isBlocked = this.isGroupBlocked(groupId); + const primaryDevicePubKey = window.storage.get('primaryDevicePubKey'); + const isMe = + envelope.source === textsecure.storage.user.getNumber() || + envelope.source === primaryDevicePubKey; + const isLeavingGroup = Boolean( + message.group && + message.group.type === textsecure.protobuf.GroupContext.Type.QUIT ); + + if (groupId && isBlocked && !(isMe && isLeavingGroup)) { + window.log.warn( + `Message ${this.getEnvelopeId( + envelope + )} ignored; destined for blocked group` + ); + this.removeFromCache(envelope); + return; + } + + // handle profileKey and avatar updates + if (envelope.source === primaryDevicePubKey) { + const { profileKey, profile } = message; + const primaryConversation = ConversationController.get( + primaryDevicePubKey + ); + if (profile) { + this.updateProfile(primaryConversation, profile, profileKey); + } + } + + const ev = new Event('sent'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.data = { + destination, + timestamp: timestamp.toNumber(), + device: envelope.sourceDevice, + unidentifiedStatus, + message, + }; + if (expirationStartTimestamp) { + ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber(); + } + this.dispatchAndWait(ev); }, async handleLokiAddressMessage(envelope) { window.log.warn('Ignoring a Loki address message'); @@ -1163,96 +1201,121 @@ MessageReceiver.prototype.extend({ }, async handleMediumGroupUpdate(envelope, groupUpdate) { - const { - groupId, - groupSecretKey, - senderKey, - members, - groupName, - } = groupUpdate; - - const convoExists = window.ConversationController.get(groupId, 'group'); - - if (convoExists) { - // If the group already exists, check that `members` is empty, - // and if so, it is sender key message - - // TODO: introduce TYPE into this message instead? - if (!members || !members.length) { - log.info('[sender key] got a new sender key from:', envelope.source); - - // We probably don't need to await here - await window.SenderKeyAPI.saveSenderKeys( - groupId, - envelope.source, - senderKey - ); + const { type, groupId } = groupUpdate; - this.removeFromCache(envelope); - return; - } + const ourIdentity = await textsecure.storage.user.getNumber(); + const senderIdentity = envelope.source; - log.error(`Conversation for groupId ${groupId} already exists`); - } + if ( + type === textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY_REQUEST + ) { + log.debug('[sender key] sender key request from:', senderIdentity); - const convo = await window.ConversationController.getOrCreateAndWait( - groupId, - 'group' - ); - convo.set('is_medium_group', true); - convo.set('active_at', Date.now()); - convo.set('name', groupName); + const proto = new textsecure.protobuf.DataMessage(); - await window.Signal.Data.createOrUpdateIdentityKey({ - id: groupId, - secretKey: groupSecretKey, - }); + // We reuse the same message type for sender keys + const update = new textsecure.protobuf.MediumGroupUpdate(); - // Save sender's key - await window.SenderKeyAPI.saveSenderKeys( - groupId, - envelope.source, - senderKey - ); + const { chainKey, keyIdx } = await window.SenderKeyAPI.getSenderKeys( + groupId, + ourIdentity + ); - // TODO: Check that we are even a part of this group? - const ourIdentity = await textsecure.storage.user.getNumber(); + update.type = textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY; + update.groupId = groupId; + update.senderKey = new textsecure.protobuf.SenderKey({ + chainKey: StringView.arrayBufferToHex(chainKey), + keyIdx, + }); - const ownSenderKey = await window.SenderKeyAPI.createSenderKeyForGroup( - groupId, - ourIdentity - ); + proto.mediumGroupUpdate = update; - { - // TODO: Send own key to every member + textsecure.messaging.updateMediumGroup([senderIdentity], proto); - const otherMembers = _.without(members, ourIdentity); + this.removeFromCache(envelope); + } else if (type === textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY) { + const { senderKey } = groupUpdate; - const proto = new textsecure.protobuf.DataMessage(); + log.debug('[sender key] got a new sender key from:', senderIdentity); - // We reuse the same message type for sender keys - const update = new textsecure.protobuf.MediumGroupUpdate(); - update.groupId = groupId; - update.senderKey = ownSenderKey; + await window.SenderKeyAPI.saveSenderKeys( + groupId, + senderIdentity, + senderKey.chainKey, + senderKey.keyIdx + ); - proto.mediumGroupUpdate = update; + this.removeFromCache(envelope); + } else if (type === textsecure.protobuf.MediumGroupUpdate.Type.NEW_GROUP) { + const { + members: membersBinary, + groupSecretKey, + groupName, + senderKey, + } = groupUpdate; + + const members = membersBinary.map(pk => + StringView.arrayBufferToHex(pk.toArrayBuffer()) + ); - // TODO: send to our linked devices too? + const convo = await window.ConversationController.getOrCreateAndWait( + groupId, + 'group' + ); + convo.set('is_medium_group', true); + convo.set('active_at', Date.now()); + convo.set('name', groupName); - // Don't need to await here + convo.setFriendRequestStatus( + window.friends.friendRequestStatusEnum.friends + ); - // TODO: Some of the members might not have a session with us, so - // we should send a session request + await window.Signal.Data.createOrUpdateIdentityKey({ + id: groupId, + secretKey: StringView.arrayBufferToHex(groupSecretKey.toArrayBuffer()), + }); - textsecure.messaging.updateMediumGroup(otherMembers, proto); - } + // Save sender's key + await window.SenderKeyAPI.saveSenderKeys( + groupId, + envelope.source, + senderKey.chainKey, + senderKey.keyIdx + ); + + // TODO: Check that we are even a part of this group? + const ownSenderKey = await window.SenderKeyAPI.createSenderKeyForGroup( + groupId, + ourIdentity + ); - // Subscribe to this group - this.pollForAdditionalId(groupId); + { + // Send own key to every member + const otherMembers = _.without(members, ourIdentity); - // All further messages (maybe rather than 'control' messages) should come to this group's swarm + const proto = new textsecure.protobuf.DataMessage(); - this.removeFromCache(envelope); + // We reuse the same message type for sender keys + const update = new textsecure.protobuf.MediumGroupUpdate(); + update.type = textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY; + update.groupId = groupId; + update.senderKey = new textsecure.protobuf.SenderKey({ + chainKey: ownSenderKey, + keyIdx: 0, + }); + + proto.mediumGroupUpdate = update; + + textsecure.messaging.updateMediumGroup(otherMembers, proto); + } + + // Subscribe to this group + this.pollForAdditionalId(groupId); + + // All further messages (maybe rather than 'control' messages) should come to this group's swarm + + this.removeFromCache(envelope); + } }, async handleDataMessage(envelope, msg) { window.log.info('data message from', this.getEnvelopeId(envelope)); @@ -1308,14 +1371,33 @@ MessageReceiver.prototype.extend({ !_.isEmpty(message.body) && friendRequestStatusNoneOrExpired; - // Build a 'message' event i.e. a received message event - const ev = new Event('message'); - const source = envelope.senderIdentity || senderPubKey; + const isOwnDevice = async pubkey => { + const primaryDevice = window.storage.get('primaryDevicePubKey'); + const secondaryDevices = await window.libloki.storage.getPairedDevicesFor( + primaryDevice + ); + + const allDevices = [primaryDevice, ...secondaryDevices]; + return allDevices.includes(pubkey); + }; + + const ownDevice = await isOwnDevice(source); + + let ev; + if (conversation.get('is_medium_group') && ownDevice) { + // Data messages for medium groups don't arrive as sync messages. Instead, + // linked devices poll for group messages independently, thus they need + // to recognise some of those messages at their own. + ev = new Event('sent'); + } else { + ev = new Event('message'); + } + if (envelope.senderIdentity) { message.group = { - id: envelope.source + id: envelope.source, }; } @@ -1340,6 +1422,7 @@ MessageReceiver.prototype.extend({ contact, preview, groupInvitation, + mediumGroupUpdate, }) { return ( !flags && @@ -1349,7 +1432,8 @@ MessageReceiver.prototype.extend({ _.isEmpty(quote) && _.isEmpty(contact) && _.isEmpty(preview) && - _.isEmpty(groupInvitation) + _.isEmpty(groupInvitation) && + _.isEmpty(mediumGroupUpdate) ); }, handleLegacyMessage(envelope) { diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index ecaac6676..162b36dee 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -430,7 +430,7 @@ MessageSender.prototype = { let keysFound = false; // If we don't have a session but we already have prekeys to // start communication then we should use them - if (!haveSession && !options.isPublic) { + if (!haveSession && !options.isPublic && !options.isMediumGroup) { keysFound = await hasKeys(number); } @@ -710,7 +710,7 @@ MessageSender.prototype = { } // We only want to sync across closed groups that we haven't left const sessionGroups = conversations.filter( - c => c.isClosedGroup() && !c.get('left') && c.isFriend() + c => c.isClosedGroup() && !c.get('left') && c.isFriend() && !c.get('is_medium_group') ); if (sessionGroups.length === 0) { window.console.info('No closed group to sync.'); @@ -975,7 +975,12 @@ MessageSender.prototype = { }); }, - sendGroupProto(providedNumbers, proto, timestamp = Date.now(), options = {}) { + async sendGroupProto( + providedNumbers, + proto, + timestamp = Date.now(), + options = {} + ) { // We always assume that only primary device is a member in the group const primaryDeviceKey = window.storage.get('primaryDevicePubKey') || @@ -1014,12 +1019,13 @@ MessageSender.prototype = { ); }); - return sendPromise.then(result => { - // Sync the group message to our other devices - const encoded = textsecure.protobuf.DataMessage.encode(proto); - this.sendSyncMessage(encoded, timestamp, null, null, [], [], options); - return result; - }); + const result = await sendPromise; + + // Sync the group message to our other devices + const encoded = textsecure.protobuf.DataMessage.encode(proto); + this.sendSyncMessage(encoded, timestamp, null, null, [], [], options); + + return result; }, async getMessageProto( @@ -1282,6 +1288,16 @@ MessageSender.prototype = { return this.sendGroupProto(groupNumbers, proto, Date.now(), options); }, + requestSenderKeys(sender, groupId) { + const proto = new textsecure.protobuf.DataMessage(); + const update = new textsecure.protobuf.MediumGroupUpdate(); + update.type = textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY_REQUEST; + update.groupId = groupId; + proto.mediumGroupUpdate = update; + + textsecure.messaging.updateMediumGroup([sender], proto); + }, + leaveGroup(groupId, groupNumbers, options) { const proto = new textsecure.protobuf.DataMessage(); proto.group = new textsecure.protobuf.GroupContext(); @@ -1391,6 +1407,7 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) { this.setGroupName = sender.setGroupName.bind(sender); this.setGroupAvatar = sender.setGroupAvatar.bind(sender); this.requestGroupInfo = sender.requestGroupInfo.bind(sender); + this.requestSenderKeys = sender.requestSenderKeys.bind(sender); this.leaveGroup = sender.leaveGroup.bind(sender); this.sendSyncMessage = sender.sendSyncMessage.bind(sender); this.getProfile = sender.getProfile.bind(sender); diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 018c0ce6c..3d7b4cf77 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -51,17 +51,26 @@ message MediumGroupContent { optional bytes ephemeralKey = 2; } +message SenderKey { + optional string chainKey = 1; + optional uint32 keyIdx = 2; +} + message MediumGroupUpdate { + + enum Type { + NEW_GROUP = 0; // groupId, groupName, groupSecretKey, members, senderKey + GROUP_INFO = 1; // groupId, groupName, members, senderKey + SENDER_KEY_REQUEST = 2; // groupId + SENDER_KEY = 3; // groupId, SenderKey + } + optional string groupName = 1; optional string groupId = 2; // should this be bytes? - optional string groupSecretKey = 3; - optional string senderKey = 4; - repeated string members = 5; -} - -message SenderKeyUpdate { - optional string groupId = 1; - optional string senderKey = 2; + optional bytes groupSecretKey = 3; + optional SenderKey senderKey = 4; + repeated bytes members = 5; + optional Type type = 6; } message LokiAddressMessage { @@ -424,4 +433,5 @@ message GroupDetails { optional string color = 7; optional bool blocked = 8; repeated string admins = 9; + optional bool is_medium_group = 10; }