Fix medium group sending for single device users; protocol changes

pull/1238/head
Maxim Shishmarev 5 years ago
parent 9be0dcabd9
commit 237bd84b35

@ -623,6 +623,7 @@
}
});
// TODO: make sure updating still works
window.doUpdateGroup = async (groupId, groupName, members, avatar) => {
const ourKey = textsecure.storage.user.getNumber();
@ -633,22 +634,19 @@
const oldMembers = convo.get('members');
const oldName = convo.getName();
const ev = {
groupDetails: {
id: groupId,
name: groupName,
members,
active: true,
expireTimer: convo.get('expireTimer'),
avatar,
is_medium_group: false,
},
confirm: () => {},
const groupDetails = {
id: groupId,
name: groupName,
members,
active: true,
expireTimer: convo.get('expireTimer'),
avatar,
is_medium_group: false,
};
const recipients = _.union(convo.get('members'), members);
await window.NewReceiver.onGroupReceived(ev);
await window.NewReceiver.onGroupReceived(groupDetails);
if (convo.isPublic()) {
const API = await convo.getPublicSendData();
@ -733,7 +731,7 @@
}
// Send own sender keys and group secret key
if (isMediumGroup) {
const { chainKey, keyIdx } = await window.SenderKeyAPI.getSenderKeys(
const { chainKey, keyIdx } = await window.MediumGroups.getSenderKeys(
groupId,
ourKey
);
@ -757,63 +755,6 @@
convo.updateGroup(updateObj);
};
window.createMediumSizeGroup = async (groupName, members) => {
// Create Group Identity
const identityKeys = await libsignal.KeyHelper.generateIdentityKeyPair();
const groupId = StringView.arrayBufferToHex(identityKeys.pubKey);
const ourIdentity = await textsecure.storage.user.getNumber();
const senderKey = await window.SenderKeyAPI.createSenderKeyForGroup(
groupId,
ourIdentity
);
const groupSecretKeyHex = StringView.arrayBufferToHex(
identityKeys.privKey
);
const primary = window.storage.get('primaryDevicePubKey');
const allMembers = [primary, ...members];
await window.Signal.Data.createOrUpdateIdentityKey({
id: groupId,
secretKey: groupSecretKeyHex,
});
const ev = {
groupDetails: {
id: groupId,
name: groupName,
members: allMembers,
recipients: allMembers,
active: true,
expireTimer: 0,
avatar: '',
secretKey: identityKeys.privKey,
senderKey,
is_medium_group: true,
},
confirm: () => {},
};
await window.NewReceiver.onGroupReceived(ev);
const convo = await ConversationController.getOrCreateAndWait(
groupId,
'group'
);
convo.updateGroupAdmins([primary]);
convo.updateGroup(ev.groupDetails);
appView.openConversation(groupId, {});
// Subscribe to this group id
window.SwarmPolling.addGroupId(new libsession.Types.PubKey(groupId));
};
window.doCreateGroup = async (groupName, members) => {
const keypair = await libsignal.KeyHelper.generateIdentityKeyPair();
const groupId = StringView.arrayBufferToHex(keypair.pubKey);
@ -823,20 +764,17 @@
textsecure.storage.user.getNumber();
const allMembers = [primaryDeviceKey, ...members];
const ev = {
groupDetails: {
id: groupId,
name: groupName,
members: allMembers,
recipients: allMembers,
active: true,
expireTimer: 0,
avatar: '',
},
confirm: () => {},
const groupDetails = {
id: groupId,
name: groupName,
members: allMembers,
recipients: allMembers,
active: true,
expireTimer: 0,
avatar: '',
};
await window.NewReceiver.onGroupReceived(ev);
await window.NewReceiver.onGroupReceived(groupDetails);
const convo = await ConversationController.getOrCreateAndWait(
groupId,
@ -844,7 +782,7 @@
);
convo.updateGroupAdmins([primaryDeviceKey]);
convo.updateGroup(ev.groupDetails);
convo.updateGroup(groupDetails);
textsecure.messaging.sendGroupSyncMessage([convo]);
appView.openConversation(groupId, {});
@ -1611,10 +1549,6 @@
'message',
window.NewReceiver.handleMessageEvent
);
messageReceiver.addEventListener(
'group',
window.NewReceiver.onGroupReceived
);
messageReceiver.addEventListener(
'sent',
window.NewReceiver.handleMessageEvent

@ -1409,13 +1409,14 @@
groupId: destination,
}
);
await Promise.all(
members.map(async m => {
const memberPubKey = new libsession.Types.PubKey(m);
await libsession
.getMessageQueue()
.sendUsingMultiDevice(memberPubKey, mediumGroupChatMessage);
})
const rawMessage = libsession.Utils.MessageUtils.toRawMessage(
destinationPubkey,
mediumGroupChatMessage
);
await libsession.Sending.MessageSender.sendToMediumGroup(
rawMessage,
this.get('id')
);
} else {
const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage(
@ -1837,12 +1838,18 @@
groupUpdate = this.pick(['name', 'avatar', 'members']);
}
const now = Date.now();
const message = this.messageCollection.add({
conversationId: this.id,
type: 'outgoing',
sent_at: now,
received_at: now,
group_update: groupUpdate,
group_update: _.pick(groupUpdate, [
'name',
'members',
'avatar',
'admins',
]),
});
const messageId = await window.Signal.Data.saveMessage(
@ -1853,24 +1860,30 @@
);
message.set({ id: messageId });
// TODO: if I added members, it is my responsibility to generate ratchet keys for them
// Difference between `recipients` and `members` is that `recipients` includes the members which were removed in this update
const { id, name, members, avatar, recipients } = groupUpdate;
if (groupUpdate.is_medium_group) {
const { secretKey, senderKey } = groupUpdate;
// Constructing a "create group" message
const { chainKey, keyIdx } = senderKey;
const { secretKey, senderKeys } = groupUpdate;
const membersBin = members.map(
pkHex => new Uint8Array(StringView.hexToArrayBuffer(pkHex))
);
const adminsBin = this.get('groupAdmins').map(
pkHex => new Uint8Array(StringView.hexToArrayBuffer(pkHex))
);
const createParams = {
timestamp: now,
groupId: id,
identifier: messageId,
groupSecretKey: secretKey,
members: members.map(pkHex => StringView.hexToArrayBuffer(pkHex)),
members: membersBin,
groupName: name,
admins: this.get('groupAdmins'),
chainKey,
keyIdx,
admins: adminsBin,
senderKeys,
};
const mediumGroupCreateMessage = new libsession.Messages.Outgoing.MediumGroupCreateMessage(
@ -1879,7 +1892,6 @@
members.forEach(async member => {
const memberPubKey = new libsession.Types.PubKey(member);
await ConversationController.getOrCreateAndWait(member, 'private');
libsession
.getMessageQueue()
.sendUsingMultiDevice(memberPubKey, mediumGroupCreateMessage);

@ -88,7 +88,9 @@ export function removeIndexedDBFiles(): Promise<void>;
export function getPasswordHash(): Promise<string | null>;
// Identity Keys
export function createOrUpdateIdentityKey(data: IdentityKey): Promise<void>;
// TODO: identity key has different shape depending on how it is called,
// so we need to come up with a way to make TS work with all of them
export function createOrUpdateIdentityKey(data: any): Promise<void>;
export function getIdentityKeyById(id: string): Promise<IdentityKey | null>;
export function bulkAddIdentityKeys(array: Array<IdentityKey>): Promise<void>;
export function removeIdentityKeyById(id: string): Promise<void>;

@ -5,7 +5,6 @@
libloki,
StringView,
lokiMessageAPI,
log
*/
/* eslint-disable more/no-then */
@ -454,76 +453,6 @@ OutgoingMessage.prototype = {
this.successfulNumbers[this.successfulNumbers.length] = number;
this.numberCompleted();
},
async sendMediumGroupMessage(groupId) {
const ttl = getTTLForType(this.messageType);
const plaintext = this.message.toArrayBuffer();
const ourIdentity = textsecure.storage.user.getNumber();
const {
ciphertext,
keyIdx,
} = await window.SenderKeyAPI.encryptWithSenderKey(
plaintext,
groupId,
ourIdentity
);
if (!ciphertext) {
log.error('could not encrypt for medium group');
return;
}
const source = ourIdentity;
// We should include ciphertext idx in the message
const content = new textsecure.protobuf.MediumGroupCiphertext({
ciphertext,
source,
keyIdx,
});
// Encrypt for the group's identity key to hide source and key idx:
const {
ciphertext: ciphertextOuter,
ephemeralKey,
} = await libloki.crypto.encryptForPubkey(
groupId,
content.encode().toArrayBuffer()
);
const contentOuter = new textsecure.protobuf.MediumGroupContent({
ciphertext: ciphertextOuter,
ephemeralKey,
});
log.debug(
'Group ciphertext: ',
window.Signal.Crypto.arrayBufferToBase64(ciphertext)
);
const outgoingObject = {
type: textsecure.protobuf.Envelope.Type.MEDIUM_GROUP_CIPHERTEXT,
ttl,
ourKey: ourIdentity,
sourceDevice: 1,
content: contentOuter.encode().toArrayBuffer(),
};
// TODO: Rather than using sealed sender, we just generate a key pair, perform an ECDH against
// the group's public key and encrypt using the derived key
const socketMessage = wrapInWebsocketMessage(
outgoingObject,
this.timestamp
);
await this.transmitMessage(groupId, socketMessage, this.timestamp, ttl);
this.successfulNumbers[this.successfulNumbers.length] = groupId;
this.numberCompleted();
},
// Send a message to a private group member or a session chat (one to one)
async sendSessionMessage(outgoingObjects) {
// TODO: handle multiple devices/messages per transmit

@ -336,8 +336,6 @@ window.WebAPI = initializeWebAPI();
window.seedNodeList = JSON.parse(config.seedNodeList);
window.SenderKeyAPI = require('./js/modules/loki_sender_key_api');
const { OnionAPI } = require('./ts/session/onions');
window.OnionAPI = OnionAPI;
@ -431,6 +429,8 @@ window.NewReceiver = require('./ts/receiver/receiver');
window.NewSnodeAPI = require('./ts/session/snode_api/serviceNodeAPI');
window.SnodePool = require('./ts/session/snode_api/snodePool');
window.MediumGroups = require('./ts/session/medium_group');
const { SwarmPolling } = require('./ts/session/snode_api/swarmPolling');
const { SwarmPollingStub } = require('./ts/session/snode_api/swarmPollingStub');

@ -51,27 +51,27 @@ message MediumGroupContent {
optional bytes ephemeralKey = 2;
}
message SenderKey {
optional string chainKey = 1;
optional uint32 keyIdx = 2;
}
message MediumGroupUpdate {
enum Type {
NEW = 0; // groupPublicKey, name, groupPrivateKey, senderKeys, members, admins
INFO = 1; // groupPublicKey, name, senderKeys, members, admins
SENDER_KEY = 2; // groupPublicKey, senderKeys
SENDER_KEY_REQUEST = 3; // groupId
}
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
}
message SenderKey {
optional bytes chainKey = 1;
optional uint32 keyIndex = 2;
optional bytes publicKey = 3;
}
optional string groupName = 1;
optional string groupId = 2; // should this be bytes?
optional bytes groupSecretKey = 3;
optional SenderKey senderKey = 4;
repeated bytes members = 5;
repeated string admins = 6;
optional Type type = 7;
optional string name = 1;
optional bytes groupPublicKey = 2;
optional bytes groupPrivateKey = 3;
repeated SenderKey senderKeys = 4;
repeated bytes members = 5;
repeated bytes admins = 6;
optional Type type = 7;
}
message LokiAddressMessage {

@ -6,6 +6,8 @@ import {
SettingsView,
} from './session/settings/SessionSettings';
import { createMediumSizeGroup } from '../session/medium_group';
export const MainViewController = {
createClosedGroup,
renderMessageView,
@ -82,7 +84,7 @@ async function createClosedGroup(
const groupMemberIds = groupMembers.map(m => m.id);
if (senderKeys) {
await window.createMediumSizeGroup(groupName, groupMemberIds);
await createMediumSizeGroup(groupName, groupMemberIds);
} else {
await window.doCreateGroup(groupName, groupMemberIds);
}

@ -19,6 +19,7 @@ import { handleSyncMessage } from './syncMessages';
import { onError } from './errors';
import ByteBuffer from 'bytebuffer';
import { BlockedNumberController } from '../util/blockedNumberController';
import { decryptWithSenderKey } from '../session/medium_group/ratchet';
export async function handleContentMessage(envelope: EnvelopePlus) {
const plaintext = await decrypt(envelope, envelope.content);
@ -54,7 +55,6 @@ async function decryptForMediumGroup(
ephemeralKey,
} = SignalService.MediumGroupContent.decode(new Uint8Array(ciphertextObj));
const ephemKey = ephemeralKey.buffer;
const secretKey = dcodeIO.ByteBuffer.wrap(
secretKeyHex,
'hex'
@ -62,16 +62,16 @@ async function decryptForMediumGroup(
const mediumGroupCiphertext = await libloki.crypto.decryptForPubkey(
secretKey,
ephemKey,
outerCiphertext.buffer
ephemeralKey,
outerCiphertext
);
const { ciphertext, keyIdx } = SignalService.MediumGroupCiphertext.decode(
mediumGroupCiphertext
new Uint8Array(mediumGroupCiphertext)
);
const plaintext = await window.SenderKeyAPI.decryptWithSenderKey(
ciphertext.buffer,
const plaintext = await decryptWithSenderKey(
ciphertext,
keyIdx,
groupId,
senderIdentity

@ -587,6 +587,7 @@ export async function handleMessageEvent(event: MessageEvent): Promise<void> {
return;
}
// TODO: this shouldn't be called when source is not a pubkey!!!
const isOurDevice = await MultiDeviceProtocol.isOurDevice(source);
const shouldSendReceipt =

@ -4,6 +4,7 @@ import { getMessageQueue } from '../session';
import { PubKey } from '../session/types';
import _ from 'lodash';
import { BlockedNumberController } from '../util/blockedNumberController';
import { RatchetKey } from '../session/messages/outgoing/content/data/mediumgroup/MediumGroupMessage';
function isGroupBlocked(groupId: string) {
return BlockedNumberController.isGroupBlocked(groupId);
@ -130,7 +131,21 @@ export async function preprocessGroupMessage(
return false;
}
export async function onGroupReceived(ev: any) {
interface GroupInfo {
id: string;
name: string;
members: Array<string>; // Primary keys
is_medium_group: boolean;
active: boolean;
avatar: any;
expireTimer: number;
secretKey: any;
color?: any; // what is this???
blocked?: boolean;
senderKeys: Array<RatchetKey>;
}
export async function onGroupReceived(details: GroupInfo) {
const {
ConversationController,
libloki,
@ -139,7 +154,6 @@ export async function onGroupReceived(ev: any) {
Whisper,
} = window;
const details = ev.groupDetails;
const { id } = details;
libloki.api.debug.logGroupSync(
@ -201,9 +215,6 @@ export async function onGroupReceived(ev: any) {
Conversation: Whisper.Conversation,
});
// send a session request for all the members we do not have a session with
await window.libloki.api.sendSessionRequestsToMembers(updates.members);
const { expireTimer } = details;
const isValidExpireTimer = typeof expireTimer === 'number';
if (!isValidExpireTimer) {
@ -215,6 +226,4 @@ export async function onGroupReceived(ev: any) {
await conversation.updateExpirationTimer(expireTimer, source, receivedAt, {
fromSync: true,
});
ev.confirm();
}

@ -6,11 +6,14 @@ import { getMessageQueue } from '../session';
import { PubKey } from '../session/types';
import _ from 'lodash';
import * as SenderKeyAPI from '../session/medium_group';
import { StringUtils } from '../session/utils';
async function handleSenderKeyRequest(
envelope: EnvelopePlus,
groupUpdate: any
) {
const { SenderKeyAPI, StringView, textsecure, log } = window;
const { StringView, textsecure, log } = window;
const senderIdentity = envelope.source;
const ourIdentity = await textsecure.storage.user.getNumber();
@ -19,7 +22,7 @@ async function handleSenderKeyRequest(
log.debug('[sender key] sender key request from:', senderIdentity);
// We reuse the same message type for sender keys
const { chainKey, keyIdx } = await SenderKeyAPI.getSenderKeys(
const { chainKey, keyIdx } = await SenderKeyAPI.getChainKey(
groupId,
ourIdentity
);
@ -28,8 +31,11 @@ async function handleSenderKeyRequest(
const responseParams = {
timestamp: Date.now(),
groupId,
chainKey: chainKeyHex,
keyIdx,
senderKey: {
chainKey: chainKeyHex,
keyIdx,
pubKey: ourIdentity,
},
};
const keysResponseMessage = new MediumGroupResponseKeysMessage(
@ -43,7 +49,7 @@ async function handleSenderKeyRequest(
}
async function handleSenderKey(envelope: EnvelopePlus, groupUpdate: any) {
const { SenderKeyAPI, log } = window;
const { log } = window;
const { groupId, senderKey } = groupUpdate;
const senderIdentity = envelope.source;
@ -51,7 +57,7 @@ async function handleSenderKey(envelope: EnvelopePlus, groupUpdate: any) {
await SenderKeyAPI.saveSenderKeys(
groupId,
senderIdentity,
PubKey.cast(senderIdentity),
senderKey.chainKey,
senderKey.keyIdx
);
@ -59,29 +65,35 @@ async function handleSenderKey(envelope: EnvelopePlus, groupUpdate: any) {
await removeFromCache(envelope);
}
async function handleNewGroup(envelope: EnvelopePlus, groupUpdate: any) {
const { SenderKeyAPI, StringView, Whisper, log, textsecure } = window;
async function handleNewGroup(
envelope: EnvelopePlus,
groupUpdate: SignalService.MediumGroupUpdate
) {
const { Whisper, log } = window;
const senderIdentity = envelope.source;
const ourIdentity = await textsecure.storage.user.getNumber();
const {
groupId,
name,
groupPublicKey,
groupPrivateKey,
members: membersBinary,
groupSecretKey,
groupName,
senderKey,
admins,
admins: adminsBinary,
senderKeys,
} = groupUpdate;
const groupId = StringUtils.decode(groupPublicKey, 'hex');
const maybeConvo = await window.ConversationController.get(groupId);
const groupExists = !!maybeConvo;
const members = membersBinary.map((pk: any) =>
StringView.arrayBufferToHex(pk.toArrayBuffer())
const members = membersBinary.map((pk: Uint8Array) =>
StringUtils.decode(pk, 'hex')
);
const admins = adminsBinary.map((pk: Uint8Array) =>
StringUtils.decode(pk, 'hex')
);
const groupExists = !!maybeConvo;
const convo = groupExists
? maybeConvo
: await window.ConversationController.getOrCreateAndWait(groupId, 'group');
@ -95,7 +107,7 @@ async function handleNewGroup(envelope: EnvelopePlus, groupUpdate: any) {
sent_at: now,
received_at: now,
group_update: {
name: groupName,
name,
members,
},
});
@ -119,7 +131,7 @@ async function handleNewGroup(envelope: EnvelopePlus, groupUpdate: any) {
return;
}
convo.set('name', groupName);
convo.set('name', name);
convo.set('members', members);
// TODO: check that we are still in the group (when we enable deleting members)
@ -134,56 +146,41 @@ async function handleNewGroup(envelope: EnvelopePlus, groupUpdate: any) {
convo.set('is_medium_group', true);
convo.set('active_at', Date.now());
convo.set('name', groupName);
convo.set('name', name);
convo.set('groupAdmins', admins);
const secretKeyHex = StringView.arrayBufferToHex(
groupSecretKey.toArrayBuffer()
);
const secretKeyHex = StringUtils.decode(groupPrivateKey, 'hex');
await window.Signal.Data.createOrUpdateIdentityKey({
id: groupId,
secretKey: secretKeyHex,
});
// Save sender's key
await SenderKeyAPI.saveSenderKeys(
groupId,
envelope.source,
senderKey.chainKey,
senderKey.keyIdx
// Save everyone's ratchet key
await Promise.all(
senderKeys.map(async senderKey => {
// Note that keyIndex is a number and 0 is considered a valid value:
if (
senderKey.chainKey &&
senderKey.keyIndex !== undefined &&
senderKey.publicKey
) {
const pubKey = StringUtils.decode(senderKey.publicKey, 'hex');
const chainKey = StringUtils.decode(senderKey.chainKey, 'hex');
const keyIndex = senderKey.keyIndex as number;
await SenderKeyAPI.saveSenderKeys(
groupId,
PubKey.cast(pubKey),
chainKey,
keyIndex
);
} else {
log.error('Received invalid sender key');
}
})
);
const ownSenderKeyHex = await SenderKeyAPI.createSenderKeyForGroup(
groupId,
ourIdentity
);
{
// Send own key to every member
const otherMembers = _.without(members, ourIdentity);
// We reuse the same message type for sender keys
const responseParams = {
timestamp: Date.now(),
groupId,
chainKey: ownSenderKeyHex,
keyIdx: 0,
};
const keysResponseMessage = new MediumGroupResponseKeysMessage(
responseParams
);
// send our senderKey to every other member
otherMembers.forEach((member: string) => {
const memberPubKey = new PubKey(member);
getMessageQueue()
.sendUsingMultiDevice(memberPubKey, keysResponseMessage)
.ignore();
});
}
window.SwarmPolling.addGroupId(groupId);
window.SwarmPolling.addGroupId(PubKey.cast(groupId));
}
await removeFromCache(envelope);
@ -200,7 +197,7 @@ export async function handleMediumGroupUpdate(
await handleSenderKeyRequest(envelope, groupUpdate);
} else if (type === Type.SENDER_KEY) {
await handleSenderKey(envelope, groupUpdate);
} else if (type === Type.NEW_GROUP) {
} else if (type === Type.NEW) {
await handleNewGroup(envelope, groupUpdate);
}
}

@ -332,10 +332,8 @@ async function handleGroups(
const promises = [];
while (groupDetails !== undefined) {
groupDetails.id = groupDetails.id.toBinary();
const ev: any = new Event('group');
ev.groupDetails = groupDetails;
const promise = onGroupReceived(ev).catch((e: any) => {
const promise = onGroupReceived(groupDetails).catch((e: any) => {
window.log.error('error processing group', e);
});

@ -46,13 +46,13 @@ export async function encrypt(
cipherText: Uint8Array;
}> {
const plainText = padPlainTextBuffer(plainTextBuffer);
const address = new window.libsignal.SignalProtocolAddress(device.key, 1);
if (encryptionType === EncryptionType.MediumGroup) {
// TODO: Do medium group stuff here
throw new Error('Encryption is not yet supported');
throw new Error('MediumGroup should not be encypted here');
}
const address = new window.libsignal.SignalProtocolAddress(device.key, 1);
let innerCipherText: CipherTextObject;
if (encryptionType === EncryptionType.Fallback) {
const cipher = new window.libloki.crypto.FallBackSessionCipher(address);

@ -2,7 +2,8 @@ import * as Messages from './messages';
import * as Protocols from './protocols';
import * as Types from './types';
import * as Utils from './utils';
import * as Sending from './sending';
export * from './instance';
export { Messages, Utils, Protocols, Types };
export { Messages, Utils, Protocols, Types, Sending };

@ -0,0 +1,85 @@
import { PubKey } from '../types';
import { onGroupReceived } from '../../receiver/receiver';
import { StringUtils } from '../utils';
import * as Data from '../../../js/modules/data';
import {
createSenderKeyForGroup,
RatchetState,
saveSenderKeys,
saveSenderKeysInner,
} from './senderKeys';
import { getChainKey } from './ratchet';
export {
createSenderKeyForGroup,
saveSenderKeys,
saveSenderKeysInner,
getChainKey,
};
async function createSenderKeysForMembers(
groupId: string,
members: Array<string>
): Promise<Array<RatchetState>> {
// TODO: generate for secondary devices too
return Promise.all(
members.map(async pk => {
return createSenderKeyForGroup(groupId, PubKey.cast(pk));
})
);
}
export async function createMediumSizeGroup(
groupName: string,
members: Array<string>
) {
const { ConversationController, libsignal } = window;
// Create Group Identity
const identityKeys = await libsignal.KeyHelper.generateIdentityKeyPair();
const groupId = StringUtils.decode(identityKeys.pubKey, 'hex');
const groupSecretKeyHex = StringUtils.decode(identityKeys.privKey, 'hex');
const primary = window.storage.get('primaryDevicePubKey');
const allMembers = [primary, ...members];
const senderKeys = await createSenderKeysForMembers(groupId, allMembers);
// TODO: make this strongly typed!
await Data.createOrUpdateIdentityKey({
id: groupId,
secretKey: groupSecretKeyHex,
});
const groupDetails = {
id: groupId,
name: groupName,
members: allMembers,
recipients: allMembers,
active: true,
expireTimer: 0,
avatar: '',
secretKey: new Uint8Array(identityKeys.privKey),
senderKeys,
is_medium_group: true,
};
await onGroupReceived(groupDetails);
const convo = await ConversationController.getOrCreateAndWait(
groupId,
'group'
);
convo.updateGroupAdmins([primary]);
convo.updateGroup(groupDetails);
window.owsDesktopApp.appView.openConversation(groupId, {});
// Subscribe to this group id
window.SwarmPolling.addGroupId(new PubKey(groupId));
}

@ -1,76 +1,94 @@
/* global
Signal,
libsignal,
StringView,
dcodeIO,
libloki,
log,
crypto,
textsecure
*/
/* eslint-disable more/no-then */
const toHex = buffer => StringView.arrayBufferToHex(buffer);
const fromHex = hex => dcodeIO.ByteBuffer.wrap(hex, 'hex').toArrayBuffer();
async function saveSenderKeysInner(
groupId,
senderIdentity,
chainKey,
keyIdx,
messageKeys
) {
const ratchet = {
chainKey,
messageKeys,
idx: keyIdx,
};
import { PubKey } from '../types';
import * as Data from '../../../js/modules/data';
import { saveSenderKeysInner } from './index';
import { StringUtils } from '../utils';
await Signal.Data.createOrUpdateSenderKeys({
groupId,
senderIdentity,
ratchet,
const toHex = (buffer: ArrayBuffer) => StringUtils.decode(buffer, 'hex');
const fromHex = (hex: string) => StringUtils.encode(hex, 'hex');
const jobQueue: { [key: string]: Promise<any> } = {};
async function queueJobForNumber(number: string, runJob: any) {
// tslint:disable-next-line no-promise-as-boolean
const runPrevious = jobQueue[number] || Promise.resolve();
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];
}
});
return runCurrent;
}
log.debug(
`Saving sender keys for groupId ${groupId}, sender ${senderIdentity}`
);
// This is different from the other ratchet type!
interface Ratchet {
chainKey: any;
keyIdx: number;
messageKeys: any;
}
// Save somebody else's key
async function saveSenderKeys(groupId, senderIdentity, chainKey, keyIdx) {
const messageKeys = {};
await saveSenderKeysInner(
groupId,
senderIdentity,
chainKey,
keyIdx,
messageKeys
);
async function loadChainKey(groupId: string, senderIdentity: string) {
const senderKeyEntry = await Data.getSenderKeys(groupId, senderIdentity);
if (!senderKeyEntry) {
// TODO: we should try to request the key from the sender in this case
throw Error(
`Sender key not found for group ${groupId} sender ${senderIdentity}`
);
}
const { chainKeyHex, idx: keyIdx, messageKeys } = senderKeyEntry.ratchet;
if (!chainKeyHex) {
throw Error('Chain key not found');
}
// TODO: This could fail if the data is not hex, handle
// this case
const chainKey = fromHex(chainKeyHex);
return { chainKey, keyIdx, messageKeys };
}
async function createSenderKeyForGroup(groupId, senderIdentity) {
// Generate Chain Key (32 random bytes)
const rootChainKey = await libsignal.crypto.getRandomBytes(32);
const rootChainKeyHex = toHex(rootChainKey);
export async function getChainKey(groupId: string, senderIdentity: string) {
const { chainKey, keyIdx } = await loadChainKey(groupId, senderIdentity);
const keyIdx = 0;
const messageKeys = {};
return { chainKey, keyIdx };
}
await saveSenderKeysInner(
groupId,
senderIdentity,
rootChainKeyHex,
keyIdx,
messageKeys
export async function encryptWithSenderKey(
plaintext: Uint8Array,
groupId: string,
ourIdentity: string
) {
// We only want to serialize jobs with the same pair (groupId, ourIdentity)
const id = groupId + ourIdentity;
return queueJobForNumber(id, () =>
encryptWithSenderKeyInner(plaintext, groupId, ourIdentity)
);
}
async function encryptWithSenderKeyInner(
plaintext: Uint8Array,
groupId: string,
ourIdentity: string
) {
const { messageKey, keyIdx } = await stepRatchetOnce(groupId, ourIdentity);
const ciphertext = await window.libloki.crypto.EncryptGCM(
messageKey,
plaintext
);
return rootChainKeyHex;
return { ciphertext, keyIdx };
}
async function hmacSHA256(keybuf, data) {
async function hmacSHA256(keybuf: any, data: any) {
// NOTE: importKey returns a 'PromiseLike'
// tslint:disable-next-line await-promise
const key = await crypto.subtle.importKey(
'raw',
keybuf,
@ -82,7 +100,7 @@ async function hmacSHA256(keybuf, data) {
return crypto.subtle.sign({ name: 'HMAC', hash: 'SHA-256' }, key, data);
}
async function stepRatchet(ratchet) {
async function stepRatchet(ratchet: Ratchet) {
const { chainKey, keyIdx, messageKeys } = ratchet;
const byteArray = new Uint8Array(1);
@ -97,14 +115,17 @@ async function stepRatchet(ratchet) {
return { nextChainKey, messageKey, nextKeyIdx, messageKeys };
}
async function stepRatchetOnce(groupId, senderIdentity) {
async function stepRatchetOnce(
groupId: string,
senderIdentity: string
): Promise<{ messageKey: any; keyIdx: any }> {
const ratchet = await loadChainKey(groupId, senderIdentity);
if (!ratchet) {
log.error(
window.log.error(
`Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
);
return null;
throw {};
}
const { nextChainKey, messageKey, nextKeyIdx } = await stepRatchet(ratchet);
@ -115,7 +136,7 @@ async function stepRatchetOnce(groupId, senderIdentity) {
await saveSenderKeysInner(
groupId,
senderIdentity,
PubKey.cast(senderIdentity),
nextChainKeyHex,
nextKeyIdx,
messageKeys
@ -125,14 +146,20 @@ async function stepRatchetOnce(groupId, senderIdentity) {
}
// Advance the ratchet until idx
async function advanceRatchet(groupId, senderIdentity, idx) {
async function advanceRatchet(
groupId: string,
senderIdentity: string,
idx: number
) {
const { log } = window;
const ratchet = await loadChainKey(groupId, senderIdentity);
if (!ratchet) {
log.error(
`Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
);
throw new textsecure.SenderKeyMissing(senderIdentity);
throw new window.textsecure.SenderKeyMissing(senderIdentity);
}
// Normally keyIdx will be 1 behind, in which case we stepRatchet one time only
@ -142,12 +169,14 @@ async function advanceRatchet(groupId, senderIdentity, idx) {
// remove it from the database (there is no need to advance the ratchet)
const messageKey = ratchet.messageKeys[idx];
if (messageKey) {
// tslint:disable-next-line no-dynamic-delete
delete ratchet.messageKeys[idx];
// TODO: just pass in the ratchet?
// tslint:disable-next-line no-shadowed-variable
const chainKeyHex = toHex(ratchet.chainKey);
await saveSenderKeysInner(
groupId,
senderIdentity,
PubKey.cast(senderIdentity),
chainKeyHex,
ratchet.keyIdx,
ratchet.messageKeys
@ -165,7 +194,7 @@ async function advanceRatchet(groupId, senderIdentity, idx) {
let curMessageKey;
// eslint-disable-next-line no-constant-condition
// tslint:disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
const { nextKeyIdx, nextChainKey, messageKey } = await stepRatchet(ratchet);
@ -192,7 +221,7 @@ async function advanceRatchet(groupId, senderIdentity, idx) {
await saveSenderKeysInner(
groupId,
senderIdentity,
PubKey.cast(senderIdentity),
chainKeyHex,
idx,
messageKeys
@ -201,58 +230,11 @@ async function advanceRatchet(groupId, senderIdentity, idx) {
return curMessageKey;
}
async function loadChainKey(groupId, senderIdentity) {
const senderKeyEntry = await Signal.Data.getSenderKeys(
groupId,
senderIdentity
);
if (!senderKeyEntry) {
// TODO: we should try to request the key from the sender in this case
log.error(
`Sender key not found for group ${groupId} sender ${senderIdentity}`
);
// TODO: throw instead?
return null;
}
const {
chainKey: chainKeyHex,
idx: keyIdx,
messageKeys,
} = senderKeyEntry.ratchet;
if (!chainKeyHex) {
log.error('Chain key not found');
return null;
}
// TODO: This could fail if the data is not hex, handle
// this case
const chainKey = fromHex(chainKeyHex);
return { chainKey, keyIdx, messageKeys };
}
const jobQueue = {};
function queueJobForNumber(number, runJob) {
const runPrevious = jobQueue[number] || Promise.resolve();
const runCurrent = runPrevious.then(runJob, runJob);
jobQueue[number] = runCurrent;
runCurrent.then(() => {
if (jobQueue[number] === runCurrent) {
delete jobQueue[number];
}
});
return runCurrent;
}
async function decryptWithSenderKey(
ciphertext,
curKeyIdx,
groupId,
senderIdentity
export async function decryptWithSenderKey(
ciphertext: Uint8Array,
curKeyIdx: number,
groupId: string,
senderIdentity: string
) {
// We only want to serialize jobs with the same pair (groupId, senderIdentity)
const id = groupId + senderIdentity;
@ -262,45 +244,18 @@ async function decryptWithSenderKey(
}
async function decryptWithSenderKeyInner(
ciphertext,
curKeyIdx,
groupId,
senderIdentity
ciphertext: Uint8Array,
curKeyIdx: number,
groupId: string,
senderIdentity: string
) {
const messageKey = await advanceRatchet(groupId, senderIdentity, curKeyIdx);
// TODO: this might fail, handle this
const plaintext = await libloki.crypto.DecryptGCM(messageKey, ciphertext);
return plaintext;
}
async function encryptWithSenderKey(plaintext, groupId, ourIdentity) {
// We only want to serialize jobs with the same pair (groupId, ourIdentity)
const id = groupId + ourIdentity;
return queueJobForNumber(id, () =>
encryptWithSenderKeyInner(plaintext, groupId, ourIdentity)
const plaintext = await window.libloki.crypto.DecryptGCM(
messageKey,
ciphertext
);
}
async function encryptWithSenderKeyInner(plaintext, groupId, ourIdentity) {
const { messageKey, keyIdx } = await stepRatchetOnce(groupId, ourIdentity);
const ciphertext = await libloki.crypto.EncryptGCM(messageKey, plaintext);
return { ciphertext, keyIdx };
}
async function getSenderKeys(groupId, senderIdentity) {
const { chainKey, keyIdx } = await loadChainKey(groupId, senderIdentity);
return { chainKey, keyIdx };
return plaintext;
}
module.exports = {
createSenderKeyForGroup,
encryptWithSenderKey,
decryptWithSenderKey,
saveSenderKeys,
getSenderKeys,
};

@ -0,0 +1,88 @@
import { PubKey } from '../types';
import { StringUtils } from '../utils';
import * as Data from '../../../js/modules/data';
const toHex = (buffer: ArrayBuffer) => StringUtils.decode(buffer, 'hex');
const fromHex = (hex: string) => StringUtils.encode(hex, 'hex');
export interface RatchetState {
chainKey: Uint8Array;
keyIdx: number;
pubKey: Uint8Array;
}
// TODO: make this private when no longer needed by JS
export async function saveSenderKeysInner(
groupId: string,
senderIdentity: PubKey,
chainKeyHex: string,
keyIdx: number,
messageKeys: any
) {
const { log } = window;
const ratchet = {
chainKeyHex,
messageKeys,
idx: keyIdx,
};
log.debug(
'saving ratchet keys for group ',
groupId,
'sender',
senderIdentity.key
);
await Data.createOrUpdateSenderKeys({
groupId,
senderIdentity: senderIdentity.key,
ratchet,
});
log.debug(
`Saving sender keys for groupId ${groupId}, sender ${senderIdentity.key}`
);
}
export async function createSenderKeyForGroup(
groupId: string,
senderIdentity: PubKey
): Promise<RatchetState> {
// Generate Chain Key (32 random bytes)
const rootChainKey = await window.libsignal.crypto.getRandomBytes(32);
const rootChainKeyHex = toHex(rootChainKey);
const keyIdx = 0;
const messageKeys = {};
await saveSenderKeysInner(
groupId,
senderIdentity,
rootChainKeyHex,
keyIdx,
messageKeys
);
const pubKey = new Uint8Array(fromHex(senderIdentity.key));
const chainKey = new Uint8Array(rootChainKey);
return { pubKey, chainKey, keyIdx: 0 };
}
// Save somebody else's key
export async function saveSenderKeys(
groupId: string,
senderIdentity: PubKey,
chainKeyHex: string,
keyIdx: number
) {
const messageKeys = {};
await saveSenderKeysInner(
groupId,
senderIdentity,
chainKeyHex,
keyIdx,
messageKeys
);
}

@ -23,7 +23,6 @@ export class MediumGroupChatMessage extends MediumGroupMessage {
public dataProto(): SignalService.DataMessage {
const messageProto = this.chatMessage.dataProto();
messageProto.mediumGroupUpdate = super.dataProto().mediumGroupUpdate;
return messageProto;
}

@ -1,48 +1,60 @@
import { SignalService } from '../../../../../../protobuf';
import {
MediumGroupResponseKeysMessage,
MediumGroupResponseKeysParams,
} from './MediumGroupResponseKeysMessage';
MediumGroupMessage,
MediumGroupMessageParams,
RatchetKey,
} from './MediumGroupMessage';
interface MediumGroupCreateParams extends MediumGroupResponseKeysParams {
interface MediumGroupCreateParams extends MediumGroupMessageParams {
groupSecretKey: Uint8Array;
members: Array<Uint8Array>;
admins: Array<string>;
admins: Array<Uint8Array>;
groupName: string;
senderKeys: Array<RatchetKey>;
}
export abstract class MediumGroupCreateMessage extends MediumGroupResponseKeysMessage {
export abstract class MediumGroupCreateMessage extends MediumGroupMessage {
public readonly groupSecretKey: Uint8Array;
public readonly members: Array<Uint8Array>;
public readonly admins: Array<string>;
public readonly admins: Array<Uint8Array>;
public readonly groupName: string;
public readonly senderKeys: Array<RatchetKey>;
constructor({
timestamp,
identifier,
chainKey,
keyIdx,
groupId,
groupSecretKey,
members,
admins,
groupName,
senderKeys,
}: MediumGroupCreateParams) {
super({ timestamp, identifier, groupId, chainKey, keyIdx });
super({ timestamp, identifier, groupId });
this.groupSecretKey = groupSecretKey;
this.members = members;
this.admins = admins;
this.groupName = groupName;
this.senderKeys = senderKeys;
}
protected mediumGroupContext(): SignalService.MediumGroupUpdate {
const mediumGroupContext = super.mediumGroupContext();
mediumGroupContext.type = SignalService.MediumGroupUpdate.Type.NEW_GROUP;
mediumGroupContext.groupSecretKey = this.groupSecretKey;
const senderKeys = this.senderKeys.map(sk => {
return {
chainKey: sk.chainKey,
keyIndex: sk.keyIdx,
publicKey: sk.pubKey,
};
});
mediumGroupContext.type = SignalService.MediumGroupUpdate.Type.NEW;
mediumGroupContext.groupPrivateKey = this.groupSecretKey;
mediumGroupContext.members = this.members;
mediumGroupContext.admins = this.admins;
mediumGroupContext.groupName = this.groupName;
mediumGroupContext.name = this.groupName;
mediumGroupContext.senderKeys = senderKeys;
return mediumGroupContext;
}

@ -4,6 +4,12 @@ import { MessageParams } from '../../../Message';
import { PubKey } from '../../../../../types';
import { StringUtils } from '../../../../../utils';
export interface RatchetKey {
chainKey: Uint8Array;
keyIdx: number;
pubKey: Uint8Array;
}
export interface MediumGroupMessageParams extends MessageParams {
groupId: string | PubKey;
}
@ -31,6 +37,9 @@ export abstract class MediumGroupMessage extends DataMessage {
}
protected mediumGroupContext(): SignalService.MediumGroupUpdate {
return new SignalService.MediumGroupUpdate({ groupId: this.groupId.key });
const groupPublicKey = new Uint8Array(
StringUtils.encode(this.groupId.key, 'hex')
);
return new SignalService.MediumGroupUpdate({ groupPublicKey });
}
}

@ -1,36 +1,34 @@
import { SignalService } from '../../../../../../protobuf';
import { MediumGroupMessage, MediumGroupMessageParams } from '.';
import { MediumGroupMessage, MediumGroupMessageParams, RatchetKey } from '.';
export interface MediumGroupResponseKeysParams
extends MediumGroupMessageParams {
chainKey: string;
keyIdx: number;
senderKey: RatchetKey;
}
export class MediumGroupResponseKeysMessage extends MediumGroupMessage {
public readonly chainKey: string;
public readonly keyIdx: number;
public readonly senderKey: RatchetKey;
constructor({
timestamp,
identifier,
groupId,
chainKey,
keyIdx,
senderKey,
}: MediumGroupResponseKeysParams) {
super({ timestamp, identifier, groupId });
this.chainKey = chainKey;
this.keyIdx = keyIdx;
this.senderKey = senderKey;
}
protected mediumGroupContext(): SignalService.MediumGroupUpdate {
const mediumGroupContext = super.mediumGroupContext();
mediumGroupContext.type = SignalService.MediumGroupUpdate.Type.SENDER_KEY;
mediumGroupContext.senderKey = new SignalService.SenderKey({
chainKey: this.chainKey,
keyIdx: this.keyIdx,
const senderKey = new SignalService.MediumGroupUpdate.SenderKey({
chainKey: this.senderKey.chainKey,
keyIndex: this.senderKey.keyIdx,
publicKey: this.senderKey.pubKey,
});
mediumGroupContext.senderKeys = [senderKey];
return mediumGroupContext;
}

@ -1,5 +1,4 @@
import { SessionRequestMessage } from '../messages/outgoing';
// import { MessageSender } from '../sending';
import { createOrUpdateItem, getItemById } from '../../../js/modules/data';
import { MessageSender } from '../sending';
import { MessageUtils } from '../utils';

@ -5,6 +5,7 @@ import { OpenGroupMessage } from '../messages/outgoing';
import { SignalService } from '../../protobuf';
import { UserUtil } from '../../util';
import { MessageEncrypter } from '../crypto';
import { encryptWithSenderKey } from '../../session/medium_group/ratchet';
import pRetry from 'p-retry';
import { PubKey } from '../types';
@ -52,6 +53,57 @@ export async function send(
);
}
export async function sendToMediumGroup(
message: RawMessage,
groupId: string,
attempts: number = 3
): Promise<void> {
if (!canSendToSnode()) {
throw new Error('lokiMessageAPI is not initialized.');
}
const { plainTextBuffer, timestamp, ttl } = message;
const ourKey = window.textsecure.storage.user.getNumber();
const { ciphertext, keyIdx } = await encryptWithSenderKey(
plainTextBuffer,
groupId,
ourKey
);
const envelopeType = SignalService.Envelope.Type.MEDIUM_GROUP_CIPHERTEXT;
// We should include ciphertext idx in the message
const content = SignalService.MediumGroupCiphertext.encode({
ciphertext,
source: ourKey,
keyIdx,
}).finish();
// Encrypt for the group's identity key to hide source and key idx:
const {
ciphertext: ciphertextOuter,
ephemeralKey,
} = await window.libloki.crypto.encryptForPubkey(groupId, content);
const contentOuter = SignalService.MediumGroupContent.encode({
ciphertext: ciphertextOuter,
ephemeralKey: new Uint8Array(ephemeralKey),
}).finish();
const envelope = await buildEnvelope(envelopeType, timestamp, contentOuter);
const data = wrapEnvelope(envelope);
return pRetry(
async () =>
window.lokiMessageAPI.sendMessage(groupId, data, timestamp, ttl),
{
retries: Math.max(attempts - 1, 0),
factor: 1,
}
);
}
async function buildEnvelope(
type: SignalService.Envelope.Type,
timestamp: number,

@ -16,7 +16,7 @@ interface Message {
data: string;
}
// Some websocket nonsence
// Some websocket nonsense
export function processMessage(message: string, options: any = {}) {
try {
const dataPlaintext = new Uint8Array(StringUtils.encode(message, 'base64'));

2
ts/window.d.ts vendored

@ -28,7 +28,6 @@ declare global {
LokiRssAPI: any;
LokiSnodeAPI: any;
MessageController: any;
SenderKeyAPI: any;
Session: any;
Signal: SignalInterface;
StringView: any;
@ -88,5 +87,6 @@ declare global {
ContactBuffer: any;
GroupBuffer: any;
SwarmPolling: SwarmPolling;
owsDesktopApp: any;
}
}

Loading…
Cancel
Save