Merge pull request #1373 from loki-project/clearnet

pull/1390/head v1.4.0
Audric Ackermann 5 years ago committed by GitHub
commit 2f5072c317
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -171,6 +171,7 @@ module.exports = {
getSenderKeys,
createOrUpdateSenderKeys,
removeAllClosedGroupRatchets,
};
function generateUUID() {
@ -923,6 +924,12 @@ async function createOrUpdateSenderKeys(data) {
);
}
async function removeAllClosedGroupRatchets(groupId) {
await db.run(`DELETE FROM ${SENDER_KEYS_TABLE} WHERE groupId = $groupId;`, {
$groupId: groupId,
});
}
async function updateToLokiSchemaVersion4(currentVersion, instance) {
if (currentVersion >= 4) {
return;

@ -1403,7 +1403,7 @@
await libsession
.getMessageQueue()
.send(destinationPubkey, mediumGroupChatMessage);
.sendToGroup(mediumGroupChatMessage);
} else {
const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage(
{

@ -1238,7 +1238,7 @@
const isOurDevice = await window.libsession.Protocols.MultiDeviceProtocol.isOurDevice(
sentMessage.device
);
// FIXME this is not correct and will cause issues with syncing
// At this point the only way to check for medium
// group is by comparing the encryption type
const isMediumGroupMessage =
@ -1272,6 +1272,46 @@
sentMessage.device
);
if (isMediumGroupMessage) {
// Delete all ratchets (it's important that this happens * after * sending out the update)
const shouldTriggerRatchetReset =
dataMessage &&
dataMessage.mediumGroupUpdate &&
dataMessage.mediumGroupUpdate.senderKeys &&
dataMessage.mediumGroupUpdate.senderKeys.length === 0;
if (shouldTriggerRatchetReset) {
const { groupPublicKey } = dataMessage.mediumGroupUpdate;
const groupPubKeyUint = new Uint8Array(
groupPublicKey.toArrayBuffer()
);
const groupId = libsession.Utils.StringUtils.decode(
groupPubKeyUint,
'hex'
);
await window.Signal.Data.removeAllClosedGroupRatchets(groupId);
// Send out the user's new ratchet to all members (minus the removed ones) using established channels
const ourPrimary = await window.Signal.Util.UserUtil.getPrimary();
const userSenderKey = await window.libsession.MediumGroup.createSenderKeyForGroup(
groupId,
ourPrimary
);
const currentMembers = this.getConversation().get('members');
window.log.warn(
'Sharing our new senderKey with remainingMembers via established channels with',
currentMembers
);
await window.libsession.MediumGroup.shareSenderKeys(
groupId,
currentMembers,
userSenderKey
);
}
}
/**
* We should hit the notify endpoint for push notification only if:
* It's a one-to-one chat or a closed group

@ -412,3 +412,4 @@ export function getMessagesWithFileAttachments(
// Sender Keys
export function getSenderKeys(groupId: any, senderIdentity: any): Promise<any>;
export function createOrUpdateSenderKeys(data: any): Promise<void>;
export function removeAllClosedGroupRatchets(groupId: string): Promise<void>;

@ -196,6 +196,7 @@ module.exports = {
getSenderKeys,
createOrUpdateSenderKeys,
removeAllClosedGroupRatchets,
};
function init() {
@ -704,6 +705,10 @@ async function createOrUpdateSenderKeys(data) {
await channels.createOrUpdateSenderKeys(data);
}
async function removeAllClosedGroupRatchets(groupId) {
await channels.removeAllClosedGroupRatchets(groupId);
}
// Sessions
async function createOrUpdateSession(data) {

@ -271,6 +271,10 @@
};
const getGroupSettingsProps = () => {
const members = this.model.get('members') || [];
const ourPK = window.textsecure.storage.user.getNumber();
const isAdmin = this.model.isMediumGroup()
? true
: this.model.get('groupAdmins').includes(ourPK);
return {
id: this.model.id,
@ -280,7 +284,7 @@
avatarPath: this.model.getAvatarPath(),
isGroup: !this.model.isPrivate(),
isPublic: this.model.isPublic(),
isAdmin: true, // allow closed group edits from anyone this.model.get('groupAdmins').includes(ourPK),
isAdmin,
isRss: this.model.isRss(),
memberCount: members.length,
amMod: this.model.isModerator(
@ -701,9 +705,13 @@
const { sender } = mostRecent;
const contact = ConversationController.getOrCreate(sender, 'private');
// we need the opposite theme
const color =
window.Events.getThemeSetting() === 'light' ? 'dark' : 'light';
const props = {
...contact.format(),
conversationType: this.model.isPrivate() ? 'direct' : 'group',
color,
};
if (this.typingBubbleView) {

@ -98,7 +98,10 @@
} else {
this.titleText = i18n('updateGroupDialogTitle', this.groupName);
// anybody can edit a closed group name or members
this.isAdmin = true;
const ourPK = window.textsecure.storage.user.getNumber();
this.isAdmin = groupConvo.isMediumGroup()
? true
: groupConvo.get('groupAdmins').includes(ourPK);
const convos = window.getConversations().models.filter(d => !!d);
this.existingMembers = groupConvo.get('members') || [];

@ -55,9 +55,8 @@ message MediumGroupUpdate {
enum Type {
NEW = 0; // groupPublicKey, name, senderKeys, members, admins, groupPrivateKey
INFO = 1; // groupPublicKey, name, senderKeys, members, admins
SENDER_KEY = 2; // groupPublicKey, senderKeys
SENDER_KEY_REQUEST = 3; // groupPublicKey
QUIT = 4; // groupPublicKey
SENDER_KEY_REQUEST = 2; // groupPublicKey
SENDER_KEY = 3; // groupPublicKey, senderKeys
}
message SenderKey {

@ -100,9 +100,8 @@
padding-inline-start: 3px;
padding-inline-end: 3px;
position: absolute;
left: 50%;
margin-inline-start: 30px;
position: static;
margin-inline-start: 5px;
top: 2px;
font-weight: 300;

@ -108,28 +108,21 @@ class ConversationListItem extends React.PureComponent<Props> {
);
}
public renderUnread() {
const { unreadCount, mentionedUs } = this.props;
public renderHeader() {
const { unreadCount, mentionedUs, i18n, isMe, lastUpdated } = this.props;
const {} = this.props;
let atSymbol = null;
let unreadCountDiv = null;
if (unreadCount > 0) {
const atSymbol = mentionedUs ? <p className="at-symbol">@</p> : null;
return (
<div>
<p className="module-conversation-list-item__unread-count">
{unreadCount}
</p>
{atSymbol}
</div>
atSymbol = mentionedUs ? <p className="at-symbol">@</p> : null;
unreadCountDiv = (
<p className="module-conversation-list-item__unread-count">
{unreadCount}
</p>
);
}
return null;
}
public renderHeader() {
const { unreadCount, i18n, isMe, lastUpdated } = this.props;
return (
<div className="module-conversation-list-item__header">
<div
@ -142,7 +135,8 @@ class ConversationListItem extends React.PureComponent<Props> {
>
{this.renderUser()}
</div>
{this.renderUnread()}
{unreadCountDiv}
{atSymbol}
{
<div
className={classNames(

@ -50,7 +50,6 @@ export class MessageView extends React.Component {
async function createClosedGroup(
groupName: string,
groupMembers: Array<ContactType>,
senderKeys: boolean,
onSuccess: any
) {
// Validate groupName and groupMembers length
@ -95,7 +94,7 @@ async function createClosedGroup(
const groupMemberIds = groupMembers.map(m => m.id);
if (senderKeys) {
if (window.lokiFeatureFlags.enableSenderKeys) {
await createMediumGroup(groupName, groupMemberIds);
} else {
await createLegacyGroup(groupName, groupMemberIds);

@ -58,7 +58,7 @@ export class TypingBubble extends React.Component<Props> {
)}
>
<div className="module-message__typing-container">
<TypingAnimation color="light" i18n={i18n} />
<TypingAnimation color={color} i18n={i18n} />
</div>
{this.renderAvatar()}
</div>

@ -306,9 +306,8 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
}}
onButtonClick={async (
groupName: string,
groupMembers: Array<ContactType>,
senderKeys: boolean
) => this.onCreateClosedGroup(groupName, groupMembers, senderKeys)}
groupMembers: Array<ContactType>
) => this.onCreateClosedGroup(groupName, groupMembers)}
searchTerm={searchTerm}
updateSearch={this.updateSearchBound}
showSpinner={loading}
@ -491,17 +490,11 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
private async onCreateClosedGroup(
groupName: string,
groupMembers: Array<ContactType>,
senderKeys: boolean
groupMembers: Array<ContactType>
) {
await MainViewController.createClosedGroup(
groupName,
groupMembers,
senderKeys,
() => {
this.handleToggleOverlay(undefined);
}
);
await MainViewController.createClosedGroup(groupName, groupMembers, () => {
this.handleToggleOverlay(undefined);
});
}
private handleNewSessionButtonClick() {

@ -89,8 +89,7 @@ async function decryptForMediumGroup(
groupId,
sourceAsStr
);
return unpad(plaintext);
return plaintext ? unpad(plaintext) : null;
}
function unpad(paddedData: ArrayBuffer): ArrayBuffer {
@ -291,25 +290,25 @@ async function decrypt(
if (error && error instanceof textsecure.SenderKeyMissing) {
const groupId = envelope.source;
const { senderIdentity } = error;
if (senderIdentity) {
log.info(
'Requesting missing key for identity: ',
senderIdentity,
'groupId: ',
groupId
);
log.info(
'Requesting missing key for identity: ',
senderIdentity,
'groupId: ',
groupId
);
const params = {
timestamp: Date.now(),
groupId,
};
const params = {
timestamp: Date.now(),
groupId,
};
const requestKeysMessage = new MediumGroupRequestKeysMessage(params);
const sender = new PubKey(senderIdentity);
// tslint:disable-next-line no-floating-promises
libsession.getMessageQueue().send(sender, requestKeysMessage);
const requestKeysMessage = new MediumGroupRequestKeysMessage(params);
const sender = new PubKey(senderIdentity);
void libsession.getMessageQueue().send(sender, requestKeysMessage);
return;
return;
}
}
let errorToThrow = error;

@ -85,8 +85,9 @@ export async function preprocessGroupMessage(
if (newGroup) {
conversation.updateGroupAdmins(group.admins);
} else {
// group members and names can be changed from any member
const fromAdmin = true;
// be sure to drop a message from a non admin if it tries to change group members
// or change the group name
const fromAdmin = conversation.get('groupAdmins').includes(primarySource);
if (!fromAdmin) {
// Make sure the message is not removing members / renaming the group

@ -10,10 +10,13 @@ import * as SenderKeyAPI from '../session/medium_group';
import { getChainKey } from '../session/medium_group/ratchet';
import { StringUtils } from '../session/utils';
import { BufferType } from '../session/utils/String';
import { MultiDeviceProtocol } from '../session/protocols';
import { ConversationModel } from '../../js/models/conversations';
import { UserUtil } from '../util';
import { RatchetState } from '../session/medium_group/senderKeys';
import {
createSenderKeyForGroup,
RatchetState,
shareSenderKeys,
} from '../session/medium_group/senderKeys';
const toHex = (d: BufferType) => StringUtils.decode(d, 'hex');
const fromHex = (d: string) => StringUtils.encode(d, 'hex');
@ -32,13 +35,19 @@ async function handleSenderKeyRequest(
log.debug('[sender key] sender key request from:', senderIdentity);
const maybeKey = await getChainKey(groupId, ourIdentity);
let maybeKey = await getChainKey(groupId, ourIdentity);
if (!maybeKey) {
// Regenerate? This should never happen though
log.error('Could not find own sender key');
await removeFromCache(envelope);
return;
log.error('Could not find own sender key. Generating new one.');
maybeKey = await SenderKeyAPI.createSenderKeyForGroup(
groupId,
PubKey.cast(ourIdentity)
);
if (!maybeKey) {
log.error('Could not find own sender key after regenerate');
await removeFromCache(envelope);
return;
}
}
// We reuse the same message type for sender keys
@ -67,23 +76,6 @@ async function handleSenderKeyRequest(
await removeFromCache(envelope);
}
async function shareSenderKeys(
groupId: string,
recipientsPrimary: Array<string>,
senderKey: RatchetState
) {
const message = new MediumGroupResponseKeysMessage({
timestamp: Date.now(),
groupId,
senderKey,
});
const recipients = recipientsPrimary.map(pk => PubKey.cast(pk));
await Promise.all(
recipients.map(pk => getMessageQueue().sendUsingMultiDevice(pk, message))
);
}
async function handleSenderKey(
envelope: EnvelopePlus,
groupUpdate: SignalService.MediumGroupUpdate
@ -243,10 +235,10 @@ function sanityCheckMediumGroupUpdate(
const joining = diff.joiningMembers || [];
const leaving = diff.leavingMembers || [];
// 1. When there are no member changes, we don't expect any sender keys
// 1. When there are no member changes, we expect all sender keys
if (!joining.length && !leaving.length) {
if (groupUpdate.senderKeys.length) {
window.log.error('Unexpected sender keys in group update');
if (groupUpdate.senderKeys.length !== groupUpdate.members.length) {
window.log.error('Incorrect number of sender keys in group update');
}
}
@ -270,13 +262,10 @@ async function handleMediumGroupChange(
envelope: EnvelopePlus,
groupUpdate: SignalService.MediumGroupUpdate
) {
const senderIdentity = envelope.source;
const {
name,
groupPublicKey,
members: membersBinary,
admins: adminsBinary,
senderKeys,
} = groupUpdate;
const { log } = window;
@ -307,7 +296,7 @@ async function handleMediumGroupChange(
return;
}
// // Check that the sender is admin (make sure it words with multidevice)
// Check that the sender is admin (make sure it words with multidevice)
const isAdmin = true;
if (!isAdmin) {
@ -326,7 +315,7 @@ async function handleMediumGroupChange(
const primary = await UserUtil.getPrimary();
sanityCheckMediumGroupUpdate(primary, diff, groupUpdate);
// console.log(`Got group update`, groupUpdate);
await saveIncomingRatchetKeys(groupId, senderKeys);
// Only add update message if we have something to show
@ -342,73 +331,31 @@ async function handleMediumGroupChange(
convo.set('isKickedFromGroup', true);
// Disable typing:
convo.updateTextInputState();
window.SwarmPolling.removePubkey(groupId);
} else {
if (maybeConvo.get('isKickedFromGroup')) {
// Enable typing:
maybeConvo.set('isKickedFromGroup', false);
maybeConvo.set('left', false);
// Subscribe to this group id
window.SwarmPolling.addGroupId(new PubKey(groupId));
maybeConvo.updateTextInputState();
}
}
await convo.commit();
await removeFromCache(envelope);
}
async function handleQuit(
envelope: EnvelopePlus,
groupUpdate: SignalService.MediumGroupUpdate
) {
const quitter = envelope.source;
const groupId = toHex(groupUpdate.groupPublicKey);
const quitterPrimary = await MultiDeviceProtocol.getPrimaryDevice(quitter);
const maybeConvo = await window.ConversationController.get(groupId);
if (!maybeConvo) {
window.log.warn('Received QUIT for a non-existing medium group');
await removeFromCache(envelope);
return;
}
const convo = maybeConvo;
// 1. Remove primary device from members:
const members = convo.get('members');
const membersUpdated = _.without(members, quitterPrimary.key);
convo.set({ members: membersUpdated });
convo.commit();
// 2. Show message (device left the group);
await SenderKeyAPI.addUpdateMessage(
convo,
{ leavingMembers: [quitterPrimary.key] },
'incoming'
);
const ourNumber = (await UserUtil.getCurrentDevicePubKey()) as string;
const primary = await UserUtil.getPrimary();
if (quitterPrimary.key === primary.key) {
convo.set('isKickedFromGroup', true);
// Disable typing:
convo.updateTextInputState();
await convo.commit();
if (diff.leavingMembers && diff.leavingMembers.length > 0 && !areWeKicked) {
// Send out the user's new ratchet to all members (minus the removed ones) using established channels
const userSenderKey = await createSenderKeyForGroup(groupId, primary);
window.log.warn(
'Sharing our new senderKey with remainingMembers via message',
members
);
await removeFromCache(envelope);
return;
await shareSenderKeys(groupId, members, userSenderKey);
}
// 3. update your own sender key
const senderKey = await SenderKeyAPI.createSenderKeyForGroup(
groupId,
PubKey.cast(ourNumber)
);
// Send keys in the background
// tslint:disable-next-line no-floating-promises
shareSenderKeys(groupId, membersUpdated, senderKey);
await removeFromCache(envelope);
}
@ -427,8 +374,6 @@ export async function handleMediumGroupUpdate(
await handleNewGroup(envelope, groupUpdate);
} else if (type === Type.INFO) {
await handleMediumGroupChange(envelope, groupUpdate);
} else if (type === Type.QUIT) {
await handleQuit(envelope, groupUpdate);
} else {
window.log.error('Unknown group update type: ', type);
}

@ -4,7 +4,8 @@ import * as Types from './types';
import * as Utils from './utils';
import * as Sending from './sending';
import * as Constants from './constants';
import * as MediumGroup from './medium_group';
export * from './instance';
export { Messages, Utils, Protocols, Types, Sending, Constants };
export { Messages, Utils, Protocols, Types, Sending, Constants, MediumGroup };

@ -8,12 +8,12 @@ import {
RatchetState,
saveSenderKeys,
saveSenderKeysInner,
shareSenderKeys,
} from './senderKeys';
import { getChainKey } from './ratchet';
import { MultiDeviceProtocol } from '../protocols';
import { BufferType } from '../utils/String';
import { UserUtil } from '../../util';
import { MediumGroupQuitMessage } from '../messages/outgoing/content/data/mediumgroup/MediumGroupQuitMessage';
import {
ClosedGroupChatMessage,
ClosedGroupMessage,
@ -21,7 +21,6 @@ import {
ExpirationTimerUpdateMessage,
MediumGroupCreateMessage,
MediumGroupMessage,
Message,
} from '../messages/outgoing';
import { MessageModel, MessageModelType } from '../../../js/models/messages';
import { getMessageQueue } from '../../session';
@ -35,6 +34,7 @@ export {
saveSenderKeys,
saveSenderKeysInner,
getChainKey,
shareSenderKeys,
};
const toHex = (d: BufferType) => StringUtils.decode(d, 'hex');
@ -118,12 +118,14 @@ export async function createMediumGroup(
const dbMessage = await addUpdateMessage(convo, groupDiff, 'outgoing');
// be sure to call this before sending the message.
// the sending pipeline needs to know from GroupUtils when a message is for a medium group
await updateOrCreateGroup(groupDetails);
await sendGroupUpdate(convo, groupDiff, groupDetails, dbMessage.id);
// ***** 3. Add update message to the conversation *****
await updateOrCreateGroup(groupDetails);
convo.updateGroupAdmins(admins);
window.owsDesktopApp.appView.openConversation(groupId, {});
@ -185,6 +187,7 @@ export async function leaveMediumGroup(groupId: string) {
// TODO: need to reset everyone's sender keys
window.SwarmPolling.removePubkey(groupId);
// TODO audric: we just left a group, we have to regenerate our senderkey
const maybeConvo = await ConversationController.get(groupId);
@ -207,16 +210,23 @@ export async function leaveMediumGroup(groupId: string) {
sent_at: now,
received_at: now,
});
const ourPrimary = await UserUtil.getPrimary();
const updateParams = {
timestamp: Date.now(),
identifier: dbMessage.id,
groupId,
const members = convo.get('members').filter(m => m !== ourPrimary.key);
// do not include senderkey as everyone needs to generate new one
const groupUpdate: GroupInfo = {
id: convo.get('id'),
name: convo.get('name'),
members,
is_medium_group: true,
admins: convo.get('groupAdmins'),
};
const message = new MediumGroupQuitMessage(updateParams);
await sendToMembers(groupId, message, dbMessage);
await sendGroupUpdateForMedium(
{ leavingMembers: [ourPrimary.key] },
groupUpdate,
dbMessage.id
);
}
// Just a container to store two named list of keys
@ -251,48 +261,22 @@ async function getExistingSenderKeysForGroup(
return maybeKeys.filter(d => d !== null).map(d => d as RatchetState);
}
// Create all sender keys based on the changes in
// the group's composition
async function getOrCreateSenderKeysForUpdate(
groupId: string,
members: Array<string>,
changes: MemberChanges
): Promise<SenderKeysContainer> {
// 1. Create sender keys for every joining member
const joining = changes.joiningMembers || [];
const leaving = changes.leavingMembers || [];
let newKeys = await createSenderKeysForMembers(groupId, joining);
// 2. Get ratchet states for existing members
// Get a list of senderKeys we have to send to joining members
// Basically this is the senderkey of all members who joined concatenated with
// the one of members currently in the group.
const existingMembers = _.difference(members, joining);
// Also, the list of senderkeys for existing member must be empty if there is any leaving members,
// as they each member need to regenerate a new senderkey
async function getOrUpdateSenderKeysForJoiningMembers(
groupId: string,
members: Array<string>
): Promise<Array<RatchetState>> {
// get all devices for members
const allDevices = _.flatten(
await Promise.all(
existingMembers.map(m => MultiDeviceProtocol.getAllDevices(m))
)
await Promise.all(members.map(m => MultiDeviceProtocol.getAllDevices(m)))
);
let existingKeys: Array<RatchetState> = [];
if (leaving.length > 0) {
// If we have leaving members, we have to re-generate ratchet
// keys for existing members
const otherKeys = await Promise.all(
allDevices.map(async device => {
return createSenderKeyForGroup(groupId, PubKey.cast(device));
})
);
newKeys = _.union(newKeys, otherKeys);
} else {
// We can reuse existing keys
existingKeys = await getExistingSenderKeysForGroup(groupId, allDevices);
}
return { existingKeys, newKeys };
return getExistingSenderKeysForGroup(groupId, allDevices);
}
async function getGroupSecretKey(groupId: string): Promise<Uint8Array> {
@ -313,6 +297,9 @@ async function getGroupSecretKey(groupId: string): Promise<Uint8Array> {
}
async function syncMediumGroup(group: ConversationModel) {
throw new Error(
'Medium group syncing must be done once multi device is enabled back'
);
const ourPrimary = await UserUtil.getPrimary();
const groupId = group.get('id');
@ -343,7 +330,6 @@ async function syncMediumGroup(group: ConversationModel) {
members: group.get('members'),
is_medium_group: true,
admins: group.get('groupAdmins'),
senderKeysContainer,
secretKey,
};
@ -409,12 +395,7 @@ export async function initiateGroupUpdate(
};
if (isMediumGroup) {
// Send sender keys and group secret key
updateObj.senderKeysContainer = await getOrCreateSenderKeysForUpdate(
groupId,
members,
diff
);
// Send group secret key
const secretKey = await getGroupSecretKey(groupId);
updateObj.secretKey = secretKey;
@ -522,7 +503,6 @@ interface GroupInfo {
blocked?: boolean;
admins?: Array<string>;
secretKey?: Uint8Array;
senderKeysContainer?: SenderKeysContainer;
}
interface UpdatableGroupState {
@ -596,29 +576,28 @@ export function calculateGroupDiff(
return groupDiff;
}
async function sendGroupUpdateForExistingMembers(
leavingMembers: Array<string>,
remainingMembers: Array<string>,
async function sendGroupUpdateForMedium(
diff: MemberChanges,
groupUpdate: GroupInfo,
messageId?: string
) {
const { id: groupId, members, name: groupName } = groupUpdate;
const ourPrimary = await UserUtil.getPrimary();
const leavingMembers = diff.leavingMembers || [];
const joiningMembers = diff.joiningMembers || [];
const wasAnyUserRemoved = leavingMembers.length > 0;
const isUserLeaving = leavingMembers.includes(ourPrimary.key);
const membersBin = members.map(
(pkHex: string) => new Uint8Array(fromHex(pkHex))
);
const admins = groupUpdate.admins || [];
const adminsBin = admins.map(
(pkHex: string) => new Uint8Array(fromHex(pkHex))
);
// Existing members only receive new sender keys
const senderKeys = groupUpdate.senderKeysContainer
? groupUpdate.senderKeysContainer.newKeys
: [];
const params = {
timestamp: Date.now(),
identifier: messageId || uuid(),
@ -626,106 +605,91 @@ async function sendGroupUpdateForExistingMembers(
members: membersBin,
groupName,
admins: adminsBin,
senderKeys,
};
const message = new MediumGroupUpdateMessage(params);
remainingMembers.forEach(async member => {
const memberPubKey = new PubKey(member);
await getMessageQueue().sendUsingMultiDevice(memberPubKey, message);
});
// Remove sender keys from the params to send to leaving memebers
params.senderKeys = [];
const strippedMessage = new MediumGroupUpdateMessage(params);
leavingMembers.forEach(async member => {
const memberPubKey = new PubKey(member);
await getMessageQueue().sendUsingMultiDevice(memberPubKey, strippedMessage);
});
}
async function sendGroupUpdateForJoiningMembers(
recipients: Array<string>,
groupUpdate: GroupInfo,
messageId?: string
) {
const { id: groupId, name, members } = groupUpdate;
const now = Date.now();
const { secretKey, senderKeysContainer } = groupUpdate;
if (!secretKey) {
window.log.error('Group secret key not specified, aborting...');
return;
}
if (wasAnyUserRemoved) {
if (isUserLeaving && leavingMembers.length !== 1) {
window.log.warn("Can't remove self and others simultaneously.");
return;
}
// Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually)
const paramsWithoutSenderKeys = {
...params,
senderKeys: [],
};
let senderKeys: Array<RatchetState> = [];
if (!senderKeysContainer) {
window.log.warn('Sender keys for joining members not found');
} else {
// Joining members should receive all known sender keys
senderKeys = _.union(
senderKeysContainer.existingKeys,
senderKeysContainer.newKeys
const messageStripped = new MediumGroupUpdateMessage(
paramsWithoutSenderKeys
);
}
const membersBin = members.map(
(pkHex: string) => new Uint8Array(fromHex(pkHex))
);
const admins = groupUpdate.admins || [];
const adminsBin = admins.map(
(pkHex: string) => new Uint8Array(fromHex(pkHex))
);
const createParams = {
timestamp: now,
groupId,
identifier: messageId || uuid(),
groupSecretKey: secretKey,
members: membersBin,
groupName: name,
admins: adminsBin,
senderKeys,
};
const mediumGroupCreateMessage = new MediumGroupCreateMessage(createParams);
recipients.forEach(async member => {
const memberPubKey = new PubKey(member);
await getMessageQueue().sendUsingMultiDevice(
memberPubKey,
mediumGroupCreateMessage
window.log.warn('Sending to groupUpdateMessage without senderKeys');
await getMessageQueue().sendToGroup(messageStripped);
} else {
let senderKeys: Array<RatchetState>;
if (joiningMembers.length > 0) {
// Generate ratchets for any new members
senderKeys = await createSenderKeysForMembers(groupId, joiningMembers);
} else {
// It's not a member change, maybe an name change. So just reuse all senderkeys
senderKeys = await getOrUpdateSenderKeysForJoiningMembers(
groupId,
members
);
}
const paramsWithSenderKeys = {
...params,
senderKeys,
};
// Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
const message = new MediumGroupUpdateMessage(paramsWithSenderKeys);
window.log.warn(
'Sending to groupUpdateMessage with joining members senderKeys to groupAddress',
senderKeys
);
});
}
async function sendGroupUpdateForMedium(
diff: MemberChanges,
groupUpdate: GroupInfo,
messageId?: string
) {
const joining = diff.joiningMembers || [];
const leaving = diff.leavingMembers || [];
await getMessageQueue().sendToGroup(message);
// 1. create group for all joining members (send timeout timer if necessary)
if (joining.length) {
await sendGroupUpdateForJoiningMembers(joining, groupUpdate, messageId);
}
// now send a CREATE group message with all senderkeys no matter what to all joining members, using established channels
if (joiningMembers.length) {
const { secretKey } = groupUpdate;
// 2. send group update to all other members
const others = _.difference(groupUpdate.members, joining);
if (others.length) {
await sendGroupUpdateForExistingMembers(
leaving,
others,
groupUpdate,
messageId
);
if (!secretKey) {
window.log.error('Group secret key not specified, aborting...');
return;
}
const allSenderKeys = await getOrUpdateSenderKeysForJoiningMembers(
groupId,
members
);
const createParams = {
timestamp: Date.now(),
identifier: messageId || uuid(),
groupSecretKey: secretKey,
groupId,
members: membersBin,
groupName,
admins: adminsBin,
senderKeys: allSenderKeys,
};
const mediumGroupCreateMessage = new MediumGroupCreateMessage(
createParams
);
// console.warn(
// 'sending group create to',
// joiningMembers,
// ' obj: ',
// mediumGroupCreateMessage
// );
joiningMembers.forEach(async member => {
const memberPubKey = new PubKey(member);
await getMessageQueue().sendUsingMultiDevice(
memberPubKey,
mediumGroupCreateMessage
);
});
}
}
}

@ -2,6 +2,8 @@ import { PubKey } from '../types';
import * as Data from '../../../js/modules/data';
import { saveSenderKeysInner } from './index';
import { StringUtils } from '../utils';
import { MediumGroupRequestKeysMessage } from '../messages/outgoing';
import { getMessageQueue } from '..';
const toHex = (buffer: ArrayBuffer) => StringUtils.decode(buffer, 'hex');
const fromHex = (hex: string) => StringUtils.encode(hex, 'hex');
@ -14,12 +16,16 @@ async function queueJobForNumber(number: string, runJob: any) {
const runCurrent = runPrevious.then(runJob, runJob);
jobQueue[number] = runCurrent;
// tslint:disable-next-line no-floating-promises
runCurrent.then(() => {
if (jobQueue[number] === runCurrent) {
// tslint:disable-next-line no-dynamic-delete
delete jobQueue[number];
}
});
runCurrent
.then(() => {
if (jobQueue[number] === runCurrent) {
// tslint:disable-next-line no-dynamic-delete
delete jobQueue[number];
}
})
.catch((e: any) => {
window.log.error('queueJobForNumber() Caught error', e);
});
return runCurrent;
}
@ -198,6 +204,11 @@ async function advanceRatchet(
log.error('[idx] not found key for idx: ', idx);
// I probably want a better error handling than this
return null;
} else if (idx === ratchet.keyIdx) {
log.error(
`advanceRatchet() called with idx:${idx}, current ratchetIdx:${ratchet.keyIdx}. We already burnt that keyIdx before.`
);
return null;
}
const { messageKeys } = ratchet;
@ -265,11 +276,19 @@ async function decryptWithSenderKeyInner(
return null;
}
// TODO: this might fail, handle this
const plaintext = await window.libloki.crypto.DecryptGCM(
messageKey,
ciphertext
);
return plaintext;
try {
const plaintext = await window.libloki.crypto.DecryptGCM(
messageKey,
ciphertext
);
return plaintext;
} catch (e) {
window.log.error('Got error during DecryptGCM()', e);
if (e instanceof DOMException) {
window.log.error(
'Got DOMException during DecryptGCM(). Rethrowing as SenderKeyMissing '
);
throw new window.textsecure.SenderKeyMissing(senderIdentity);
}
}
}

@ -1,6 +1,8 @@
import { PubKey } from '../types';
import { StringUtils } from '../utils';
import * as Data from '../../../js/modules/data';
import { MediumGroupResponseKeysMessage } from '../messages/outgoing';
import { getMessageQueue } from '..';
const toHex = (buffer: ArrayBuffer) => StringUtils.decode(buffer, 'hex');
const fromHex = (hex: string) => StringUtils.encode(hex, 'hex');
@ -86,3 +88,20 @@ export async function saveSenderKeys(
messageKeys
);
}
export async function shareSenderKeys(
groupId: string,
recipientsPrimary: Array<string>,
senderKey: RatchetState
) {
const message = new MediumGroupResponseKeysMessage({
timestamp: Date.now(),
groupId,
senderKey,
});
const recipients = recipientsPrimary.map(pk => PubKey.cast(pk));
await Promise.all(
recipients.map(pk => getMessageQueue().sendUsingMultiDevice(pk, message))
);
}

@ -1,12 +0,0 @@
import { SignalService } from '../../../../../../protobuf';
import { MediumGroupMessage } from '.';
export class MediumGroupQuitMessage extends MediumGroupMessage {
protected mediumGroupContext(): SignalService.MediumGroupUpdate {
const mediumGroupContext = super.mediumGroupContext();
mediumGroupContext.type = SignalService.MediumGroupUpdate.Type.QUIT;
return mediumGroupContext;
}
}

@ -7,6 +7,8 @@ import {
ClosedGroupMessage,
ContentMessage,
ExpirationTimerUpdateMessage,
MediumGroupChatMessage,
MediumGroupMessage,
OpenGroupMessage,
SessionRequestMessage,
SyncMessage,
@ -51,7 +53,7 @@ export class MessageQueue implements MessageQueueInterface {
}
public async sendToGroup(
message: OpenGroupMessage | ContentMessage
message: OpenGroupMessage | ContentMessage | MediumGroupMessage
): Promise<void> {
// Open groups
if (message instanceof OpenGroupMessage) {
@ -93,11 +95,20 @@ export class MessageQueue implements MessageQueueInterface {
groupId = message.groupId;
} else if (message instanceof ExpirationTimerUpdateMessage) {
groupId = message.groupId;
} else if (message instanceof MediumGroupMessage) {
groupId = message.groupId;
}
if (!groupId) {
throw new Error('Invalid group message passed in sendToGroup.');
}
// if this is a medium group message. We just need to send to the group pubkey
if (
message instanceof MediumGroupMessage ||
message instanceof MediumGroupChatMessage
) {
return this.send(PubKey.cast(groupId), message);
}
// Get devices in group
let recipients = await GroupUtils.getGroupMembers(groupId);

@ -1,6 +1,7 @@
import {
ClosedGroupMessage,
ContentMessage,
MediumGroupMessage,
OpenGroupMessage,
SyncMessage,
} from '../messages/outgoing';
@ -8,7 +9,10 @@ import { RawMessage } from '../types/RawMessage';
import { TypedEventEmitter } from '../utils';
import { PubKey } from '../types';
type GroupMessageType = OpenGroupMessage | ClosedGroupMessage;
type GroupMessageType =
| OpenGroupMessage
| ClosedGroupMessage
| MediumGroupMessage;
export interface MessageQueueInterfaceEvents {
success: (

@ -45,6 +45,7 @@ export async function send(
timestamp,
cipherText
);
// console.warn('sending', envelope, ' to ', device.key);
const data = wrapEnvelope(envelope);
return pRetry(

@ -51,12 +51,16 @@ export class SwarmPolling {
}
public addGroupId(pubkey: PubKey) {
this.groupPubkeys.push(pubkey);
if (this.groupPubkeys.findIndex(m => m.key === pubkey.key) === -1) {
this.groupPubkeys.push(pubkey);
}
}
public addPubkey(pk: PubKey | string) {
const pubkey = PubKey.cast(pk);
this.pubkeys.push(pubkey);
if (this.pubkeys.findIndex(m => m.key === pubkey.key) === -1) {
this.pubkeys.push(pubkey);
}
}
public removePubkey(pk: PubKey | string) {

@ -6,6 +6,7 @@ import {
} from '../messages/outgoing';
import { EncryptionType, PubKey } from '../types';
import { SessionProtocol } from '../protocols';
import { MediumGroupUpdateMessage } from '../messages/outgoing/content/data/mediumgroup/MediumGroupUpdateMessage';
export async function toRawMessage(
device: PubKey,
@ -16,7 +17,10 @@ export async function toRawMessage(
const plainTextBuffer = message.plainTextBuffer();
let encryption: EncryptionType;
if (message instanceof MediumGroupChatMessage) {
if (
message instanceof MediumGroupChatMessage ||
message instanceof MediumGroupUpdateMessage
) {
encryption = EncryptionType.MediumGroup;
} else if (message instanceof SessionRequestMessage) {
encryption = EncryptionType.Fallback;

Loading…
Cancel
Save