Fix medium group updates

pull/1134/head
Maxim Shishmarev 5 years ago
parent fcadcd780e
commit abde96bc1e

@ -638,23 +638,22 @@
window.doUpdateGroup = async (groupId, groupName, members, avatar) => { window.doUpdateGroup = async (groupId, groupName, members, avatar) => {
const ourKey = textsecure.storage.user.getNumber(); const ourKey = textsecure.storage.user.getNumber();
const ev = new Event('message'); const ev = {
ev.confirm = () => {}; groupDetails: {
id: groupId,
ev.data = { name: groupName,
source: ourKey, members,
timestamp: Date.now(), recipients: members,
message: { active: true,
group: { expireTimer: 0,
id: groupId, avatar: '',
type: textsecure.protobuf.GroupContext.Type.UPDATE, is_medium_group: true,
name: groupName,
members,
avatar: null, // TODO
},
}, },
confirm: () => {},
}; };
await onGroupReceived(ev);
const convo = await ConversationController.getOrCreateAndWait( const convo = await ConversationController.getOrCreateAndWait(
groupId, groupId,
'group' 'group'
@ -719,15 +718,42 @@
const recipients = _.union(convo.get('members'), members); const recipients = _.union(convo.get('members'), members);
await onMessageReceived(ev); const isMediumGroup = convo.get('is_medium_group');
convo.updateGroup({
groupId, const updateObj = {
groupName, id: groupId,
name: groupName,
avatar: nullAvatar, avatar: nullAvatar,
recipients, recipients,
members, members,
is_medium_group: isMediumGroup,
options, options,
}); };
// Send own sender keys and group secret key
if (isMediumGroup) {
const { chainKey, keyIdx } = await window.SenderKeyAPI.getSenderKeys(
groupId,
ourKey
);
updateObj.senderKey = {
chainKey: StringView.arrayBufferToHex(chainKey),
keyIdx,
};
const groupIdentity = await window.Signal.Data.getIdentityKeyById(
groupId
);
const secretKeyHex = StringView.hexToArrayBuffer(
groupIdentity.secretKey
);
updateObj.secretKey = secretKeyHex;
}
convo.updateGroup(updateObj);
}; };
window.createMediumSizeGroup = async (groupName, members) => { window.createMediumSizeGroup = async (groupName, members) => {
@ -779,6 +805,7 @@
'group' 'group'
); );
convo.updateGroupAdmins([primary]);
convo.updateGroup(ev.groupDetails); convo.updateGroup(ev.groupDetails);
convo.setFriendRequestStatus( convo.setFriendRequestStatus(

@ -2272,6 +2272,12 @@
} }
}, },
async saveChangesToDB() {
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
},
async updateGroup(providedGroupUpdate) { async updateGroup(providedGroupUpdate) {
let groupUpdate = providedGroupUpdate; let groupUpdate = providedGroupUpdate;
@ -2311,23 +2317,23 @@
mgUpdate.type = textsecure.protobuf.MediumGroupUpdate.Type.NEW_GROUP; mgUpdate.type = textsecure.protobuf.MediumGroupUpdate.Type.NEW_GROUP;
mgUpdate.groupId = id; mgUpdate.groupId = id;
mgUpdate.groupSecretKey = secretKey; mgUpdate.groupSecretKey = secretKey;
mgUpdate.senderKey = new textsecure.protobuf.SenderKey({ mgUpdate.senderKey = new textsecure.protobuf.SenderKey(senderKey);
chainKey: senderKey,
keyIdx: 0,
});
mgUpdate.members = members.map(pkHex => mgUpdate.members = members.map(pkHex =>
StringView.hexToArrayBuffer(pkHex) StringView.hexToArrayBuffer(pkHex)
); );
mgUpdate.groupName = name; mgUpdate.groupName = name;
mgUpdate.admins = this.get('groupAdmins');
proto.mediumGroupUpdate = mgUpdate; proto.mediumGroupUpdate = mgUpdate;
await textsecure.messaging.updateMediumGroup(members, proto); message.send(
this.wrapSend(textsecure.messaging.updateMediumGroup(members, proto))
);
return; return;
} }
message.send( message.send(
this.wrapSend( this.wrapSend(
textsecure.messaging.updateGroup( textsecure.messaging.sendGroupUpdate(
this.id, this.id,
this.get('name'), this.get('name'),
this.get('avatar'), this.get('avatar'),
@ -2343,7 +2349,7 @@
sendGroupInfo(recipients) { sendGroupInfo(recipients) {
if (this.isClosedGroup()) { if (this.isClosedGroup()) {
const options = this.getSendOptions(); const options = this.getSendOptions();
textsecure.messaging.updateGroup( textsecure.messaging.sendGroupUpdate(
this.id, this.id,
this.get('name'), this.get('name'),
this.get('avatar'), this.get('avatar'),
@ -2357,6 +2363,15 @@
async leaveGroup() { async leaveGroup() {
const now = Date.now(); const now = Date.now();
if (this.get('is_medium_group')) {
// NOTE: we should probably remove sender keys for groupId,
// and its secret key, but it is low priority
// TODO: need to reset everyone's sender keys
window.lokiMessageAPI.stopPollingForGroup(this.id);
}
if (this.get('type') === 'group') { if (this.get('type') === 'group') {
const groupNumbers = this.getRecipients(); const groupNumbers = this.getRecipients();
this.set({ left: true }); this.set({ left: true });

@ -74,6 +74,8 @@ class LokiMessageAPI {
this.jobQueue = new window.JobQueue(); this.jobQueue = new window.JobQueue();
this.sendingData = {}; this.sendingData = {};
this.ourKey = ourKey; this.ourKey = ourKey;
// stop polling for a group if its id is no longer found here
this.groupIdsToPoll = {};
} }
async sendMessage(pubKey, data, messageTimeStamp, ttl, options = {}) { async sendMessage(pubKey, data, messageTimeStamp, ttl, options = {}) {
@ -325,7 +327,7 @@ class LokiMessageAPI {
); );
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (this.groupIdsToPoll[groupId]) {
try { try {
let messages = await _retrieveNextMessages(node, groupId); let messages = await _retrieveNextMessages(node, groupId);
@ -384,6 +386,13 @@ class LokiMessageAPI {
async pollForGroupId(groupId, onMessages) { async pollForGroupId(groupId, onMessages) {
log.info(`Starting to poll for group id: ${groupId}`); log.info(`Starting to poll for group id: ${groupId}`);
if (this.groupIdsToPoll[groupId]) {
log.warn(`Already polling for group id: ${groupId}`);
return;
}
this.groupIdsToPoll[groupId] = true;
// Get nodes for groupId // Get nodes for groupId
const nodes = await lokiSnodeAPI.refreshSwarmNodesForPubKey(groupId); const nodes = await lokiSnodeAPI.refreshSwarmNodesForPubKey(groupId);
@ -394,6 +403,16 @@ class LokiMessageAPI {
); );
} }
async stopPollingForGroup(groupId) {
if (!this.groupIdsToPoll[groupId]) {
log.warn(`Already not polling for group id: ${groupId}`);
return;
}
log.warn(`Stop polling for group id: ${groupId}`);
delete this.groupIdsToPoll[groupId];
}
async _openRetrieveConnection(pSwarmPool, stopPollingPromise, onMessages) { async _openRetrieveConnection(pSwarmPool, stopPollingPromise, onMessages) {
const swarmPool = pSwarmPool; // lint const swarmPool = pSwarmPool; // lint
let stopPollingResult = false; let stopPollingResult = false;

@ -177,8 +177,10 @@ async function advanceRatchet(groupId, senderIdentity, idx) {
curMessageKey = messageKey; curMessageKey = messageKey;
break; break;
} else if (nextKeyIdx > idx) { } else if (nextKeyIdx > idx) {
log.error('Developer error: nextKeyIdx > idx'); log.error(
return null; `Could not decrypt for an older ratchet step: (${nextKeyIdx})nextKeyIdx > (${idx})idx`
);
throw new Error(`Cannot revert ratchet for group ${groupId}!`);
} else { } else {
// Store keys for skipped nextKeyIdx, we might need them to decrypt // Store keys for skipped nextKeyIdx, we might need them to decrypt
// messages that arrive out-of-order // messages that arrive out-of-order

@ -21,6 +21,7 @@
const debugFlags = DebugFlagsEnum.ALL; const debugFlags = DebugFlagsEnum.ALL;
const debugLogFn = (...args) => { const debugLogFn = (...args) => {
// eslint-disable-next-line no-constant-condition
if (true) { if (true) {
// process.env.NODE_ENV.includes('test-integration') || // process.env.NODE_ENV.includes('test-integration') ||
window.console.warn(...args); window.console.warn(...args);

@ -1247,72 +1247,131 @@ MessageReceiver.prototype.extend({
this.removeFromCache(envelope); this.removeFromCache(envelope);
} else if (type === textsecure.protobuf.MediumGroupUpdate.Type.NEW_GROUP) { } else if (type === textsecure.protobuf.MediumGroupUpdate.Type.NEW_GROUP) {
const maybeConvo = await window.ConversationController.get(groupId);
const groupExists = !!maybeConvo;
const { const {
members: membersBinary, members: membersBinary,
groupSecretKey, groupSecretKey,
groupName, groupName,
senderKey, senderKey,
admins,
} = groupUpdate; } = groupUpdate;
const members = membersBinary.map(pk => const members = membersBinary.map(pk =>
StringView.arrayBufferToHex(pk.toArrayBuffer()) StringView.arrayBufferToHex(pk.toArrayBuffer())
); );
const convo = await window.ConversationController.getOrCreateAndWait( const convo = groupExists
groupId, ? maybeConvo
'group' : await window.ConversationController.getOrCreateAndWait(
); groupId,
convo.set('is_medium_group', true); 'group'
convo.set('active_at', Date.now()); );
convo.set('name', groupName);
convo.setFriendRequestStatus( {
window.friends.friendRequestStatusEnum.friends // Add group update message
); const now = Date.now();
const message = convo.messageCollection.add({
conversationId: convo.id,
type: 'incoming',
sent_at: now,
received_at: now,
group_update: {
name: groupName,
members,
},
});
await window.Signal.Data.createOrUpdateIdentityKey({ const messageId = await window.Signal.Data.saveMessage(
id: groupId, message.attributes,
secretKey: StringView.arrayBufferToHex(groupSecretKey.toArrayBuffer()), {
}); Message: Whisper.Message,
}
);
message.set({ id: messageId });
}
// Save sender's key if (groupExists) {
await window.SenderKeyAPI.saveSenderKeys( // ***** Updating the group *****
groupId, log.info('Received a group update for medium group:', groupId);
envelope.source,
senderKey.chainKey,
senderKey.keyIdx
);
// TODO: Check that we are even a part of this group? // Check that the sender is admin (make sure it words with multidevice)
const ownSenderKey = await window.SenderKeyAPI.createSenderKeyForGroup( const isAdmin = convo.get('groupAdmins').includes(senderIdentity);
groupId,
ourIdentity
);
{ if (!isAdmin) {
// Send own key to every member log.warn('Rejected attempt to update a group by non-admin');
const otherMembers = _.without(members, ourIdentity); this.removeFromCache(envelope);
return;
const proto = new textsecure.protobuf.DataMessage(); }
// We reuse the same message type for sender keys convo.set('name', groupName);
const update = new textsecure.protobuf.MediumGroupUpdate(); convo.set('members', members);
update.type = textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY;
update.groupId = groupId; // TODO: check that we are still in the group (when we enable deleting members)
update.senderKey = new textsecure.protobuf.SenderKey({ convo.saveChangesToDB();
chainKey: ownSenderKey,
keyIdx: 0, // Update other fields. Add a corresponding "update" message to the conversation
} else {
// ***** Creating a new group *****
log.info('Received a new medium group:', groupId);
// TODO: Check that we are even a part of this group?
convo.set('is_medium_group', true);
convo.set('active_at', Date.now());
convo.set('name', groupName);
convo.set('groupAdmins', admins);
convo.setFriendRequestStatus(
window.friends.friendRequestStatusEnum.friends
);
const secretKeyHex = StringView.arrayBufferToHex(
groupSecretKey.toArrayBuffer()
);
await window.Signal.Data.createOrUpdateIdentityKey({
id: groupId,
secretKey: secretKeyHex,
}); });
proto.mediumGroupUpdate = update; // Save sender's key
await window.SenderKeyAPI.saveSenderKeys(
groupId,
envelope.source,
senderKey.chainKey,
senderKey.keyIdx
);
const ownSenderKey = await window.SenderKeyAPI.createSenderKeyForGroup(
groupId,
ourIdentity
);
textsecure.messaging.updateMediumGroup(otherMembers, proto); {
} // Send own key to every member
const otherMembers = _.without(members, ourIdentity);
// Subscribe to this group const proto = new textsecure.protobuf.DataMessage();
this.pollForAdditionalId(groupId);
// All further messages (maybe rather than 'control' messages) should come to this group's swarm // 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);
}
this.removeFromCache(envelope); this.removeFromCache(envelope);
} }

@ -167,6 +167,7 @@ function OutgoingMessage(
isMediumGroup, isMediumGroup,
publicSendData, publicSendData,
debugMessageType, debugMessageType,
autoSession,
} = } =
options || {}; options || {};
this.numberInfo = numberInfo; this.numberInfo = numberInfo;
@ -182,6 +183,7 @@ function OutgoingMessage(
this.online = online; this.online = online;
this.messageType = messageType || 'outgoing'; this.messageType = messageType || 'outgoing';
this.debugMessageType = debugMessageType; this.debugMessageType = debugMessageType;
this.autoSession = autoSession || false;
} }
OutgoingMessage.prototype = { OutgoingMessage.prototype = {

@ -460,7 +460,7 @@ MessageSender.prototype = {
message.dataMessage.group message.dataMessage.group
); );
// If it was a message to a group then we need to send a session request // If it was a message to a group then we need to send a session request
if (isGroupMessage) { if (isGroupMessage || options.autoSession) {
const sessionRequestMessage = textsecure.OutgoingMessage.buildSessionRequestMessage( const sessionRequestMessage = textsecure.OutgoingMessage.buildSessionRequestMessage(
number number
); );
@ -710,7 +710,11 @@ MessageSender.prototype = {
} }
// We only want to sync across closed groups that we haven't left // We only want to sync across closed groups that we haven't left
const sessionGroups = conversations.filter( const sessionGroups = conversations.filter(
c => c.isClosedGroup() && !c.get('left') && c.isFriend() && !c.get('is_medium_group') c =>
c.isClosedGroup() &&
!c.get('left') &&
c.isFriend() &&
!c.get('is_medium_group')
); );
if (sessionGroups.length === 0) { if (sessionGroups.length === 0) {
window.console.info('No closed group to sync.'); window.console.info('No closed group to sync.');
@ -1207,12 +1211,18 @@ MessageSender.prototype = {
}, },
async updateMediumGroup(members, groupUpdateProto) { async updateMediumGroup(members, groupUpdateProto) {
// Automatically request session if not found (updates use pairwise sessions)
const autoSession = true;
await this.sendGroupProto(members, groupUpdateProto, Date.now(), { await this.sendGroupProto(members, groupUpdateProto, Date.now(), {
isPublic: false, isPublic: false,
autoSession,
}); });
return true;
}, },
async updateGroup( async sendGroupUpdate(
groupId, groupId,
name, name,
avatar, avatar,
@ -1401,7 +1411,7 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
this.resetSession = sender.resetSession.bind(sender); this.resetSession = sender.resetSession.bind(sender);
this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender); this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender);
this.sendTypingMessage = sender.sendTypingMessage.bind(sender); this.sendTypingMessage = sender.sendTypingMessage.bind(sender);
this.updateGroup = sender.updateGroup.bind(sender); this.sendGroupUpdate = sender.sendGroupUpdate.bind(sender);
this.updateMediumGroup = sender.updateMediumGroup.bind(sender); this.updateMediumGroup = sender.updateMediumGroup.bind(sender);
this.addNumberToGroup = sender.addNumberToGroup.bind(sender); this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
this.setGroupName = sender.setGroupName.bind(sender); this.setGroupName = sender.setGroupName.bind(sender);

@ -70,7 +70,8 @@ message MediumGroupUpdate {
optional bytes groupSecretKey = 3; optional bytes groupSecretKey = 3;
optional SenderKey senderKey = 4; optional SenderKey senderKey = 4;
repeated bytes members = 5; repeated bytes members = 5;
optional Type type = 6; repeated string admins = 6;
optional Type type = 7;
} }
message LokiAddressMessage { message LokiAddressMessage {

@ -190,7 +190,7 @@ textarea {
&.brand { &.brand {
min-width: 165px; min-width: 165px;
height: 45px; height: 45px;
line-height: 40px; align-items: center;
padding: 0px $session-margin-lg; padding: 0px $session-margin-lg;
font-size: $session-font-md; font-size: $session-font-md;
font-family: $session-font-accent; font-family: $session-font-accent;

@ -111,6 +111,7 @@ export class InviteFriendsDialog extends React.Component<Props, State> {
return members.map((member: ContactType, index: number) => ( return members.map((member: ContactType, index: number) => (
<SessionMemberListItem <SessionMemberListItem
member={member} member={member}
key={index}
index={index} index={index}
isSelected={false} isSelected={false}
onSelect={(selectedMember: ContactType) => { onSelect={(selectedMember: ContactType) => {

Loading…
Cancel
Save