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) => {
const ourKey = textsecure.storage.user.getNumber();
const ev = new Event('message');
ev.confirm = () => {};
ev.data = {
source: ourKey,
timestamp: Date.now(),
message: {
group: {
id: groupId,
type: textsecure.protobuf.GroupContext.Type.UPDATE,
name: groupName,
members,
avatar: null, // TODO
},
const ev = {
groupDetails: {
id: groupId,
name: groupName,
members,
recipients: members,
active: true,
expireTimer: 0,
avatar: '',
is_medium_group: true,
},
confirm: () => {},
};
await onGroupReceived(ev);
const convo = await ConversationController.getOrCreateAndWait(
groupId,
'group'
@ -719,15 +718,42 @@
const recipients = _.union(convo.get('members'), members);
await onMessageReceived(ev);
convo.updateGroup({
groupId,
groupName,
const isMediumGroup = convo.get('is_medium_group');
const updateObj = {
id: groupId,
name: groupName,
avatar: nullAvatar,
recipients,
members,
is_medium_group: isMediumGroup,
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) => {
@ -779,6 +805,7 @@
'group'
);
convo.updateGroupAdmins([primary]);
convo.updateGroup(ev.groupDetails);
convo.setFriendRequestStatus(

@ -2272,6 +2272,12 @@
}
},
async saveChangesToDB() {
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
},
async updateGroup(providedGroupUpdate) {
let groupUpdate = providedGroupUpdate;
@ -2311,23 +2317,23 @@
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.senderKey = new textsecure.protobuf.SenderKey(senderKey);
mgUpdate.members = members.map(pkHex =>
StringView.hexToArrayBuffer(pkHex)
);
mgUpdate.groupName = name;
mgUpdate.admins = this.get('groupAdmins');
proto.mediumGroupUpdate = mgUpdate;
await textsecure.messaging.updateMediumGroup(members, proto);
message.send(
this.wrapSend(textsecure.messaging.updateMediumGroup(members, proto))
);
return;
}
message.send(
this.wrapSend(
textsecure.messaging.updateGroup(
textsecure.messaging.sendGroupUpdate(
this.id,
this.get('name'),
this.get('avatar'),
@ -2343,7 +2349,7 @@
sendGroupInfo(recipients) {
if (this.isClosedGroup()) {
const options = this.getSendOptions();
textsecure.messaging.updateGroup(
textsecure.messaging.sendGroupUpdate(
this.id,
this.get('name'),
this.get('avatar'),
@ -2357,6 +2363,15 @@
async leaveGroup() {
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') {
const groupNumbers = this.getRecipients();
this.set({ left: true });

@ -74,6 +74,8 @@ class LokiMessageAPI {
this.jobQueue = new window.JobQueue();
this.sendingData = {};
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 = {}) {
@ -325,7 +327,7 @@ class LokiMessageAPI {
);
// eslint-disable-next-line no-constant-condition
while (true) {
while (this.groupIdsToPoll[groupId]) {
try {
let messages = await _retrieveNextMessages(node, groupId);
@ -384,6 +386,13 @@ class LokiMessageAPI {
async pollForGroupId(groupId, onMessages) {
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
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) {
const swarmPool = pSwarmPool; // lint
let stopPollingResult = false;

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

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

@ -1247,72 +1247,131 @@ MessageReceiver.prototype.extend({
this.removeFromCache(envelope);
} else if (type === textsecure.protobuf.MediumGroupUpdate.Type.NEW_GROUP) {
const maybeConvo = await window.ConversationController.get(groupId);
const groupExists = !!maybeConvo;
const {
members: membersBinary,
groupSecretKey,
groupName,
senderKey,
admins,
} = groupUpdate;
const members = membersBinary.map(pk =>
StringView.arrayBufferToHex(pk.toArrayBuffer())
);
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 convo = groupExists
? maybeConvo
: await window.ConversationController.getOrCreateAndWait(
groupId,
'group'
);
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({
id: groupId,
secretKey: StringView.arrayBufferToHex(groupSecretKey.toArrayBuffer()),
});
const messageId = await window.Signal.Data.saveMessage(
message.attributes,
{
Message: Whisper.Message,
}
);
message.set({ id: messageId });
}
// Save sender's key
await window.SenderKeyAPI.saveSenderKeys(
groupId,
envelope.source,
senderKey.chainKey,
senderKey.keyIdx
);
if (groupExists) {
// ***** Updating the group *****
log.info('Received a group update for medium group:', groupId);
// TODO: Check that we are even a part of this group?
const ownSenderKey = await window.SenderKeyAPI.createSenderKeyForGroup(
groupId,
ourIdentity
);
// Check that the sender is admin (make sure it words with multidevice)
const isAdmin = convo.get('groupAdmins').includes(senderIdentity);
{
// Send own key to every member
const otherMembers = _.without(members, ourIdentity);
const proto = new textsecure.protobuf.DataMessage();
// 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,
if (!isAdmin) {
log.warn('Rejected attempt to update a group by non-admin');
this.removeFromCache(envelope);
return;
}
convo.set('name', groupName);
convo.set('members', members);
// TODO: check that we are still in the group (when we enable deleting members)
convo.saveChangesToDB();
// 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
this.pollForAdditionalId(groupId);
const proto = new textsecure.protobuf.DataMessage();
// 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);
}

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

@ -460,7 +460,7 @@ MessageSender.prototype = {
message.dataMessage.group
);
// 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(
number
);
@ -710,7 +710,11 @@ 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.get('is_medium_group')
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.');
@ -1207,12 +1211,18 @@ MessageSender.prototype = {
},
async updateMediumGroup(members, groupUpdateProto) {
// Automatically request session if not found (updates use pairwise sessions)
const autoSession = true;
await this.sendGroupProto(members, groupUpdateProto, Date.now(), {
isPublic: false,
autoSession,
});
return true;
},
async updateGroup(
async sendGroupUpdate(
groupId,
name,
avatar,
@ -1401,7 +1411,7 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
this.resetSession = sender.resetSession.bind(sender);
this.sendMessageToGroup = sender.sendMessageToGroup.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.addNumberToGroup = sender.addNumberToGroup.bind(sender);
this.setGroupName = sender.setGroupName.bind(sender);

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

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

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

Loading…
Cancel
Save