diff --git a/js/background.js b/js/background.js index 20ec8003f..555e2ebe0 100644 --- a/js/background.js +++ b/js/background.js @@ -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 diff --git a/js/models/conversations.js b/js/models/conversations.js index d02400e72..189345aa4 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -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); diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts index bc48adc40..368b61f23 100644 --- a/js/modules/data.d.ts +++ b/js/modules/data.d.ts @@ -88,7 +88,9 @@ export function removeIndexedDBFiles(): Promise; export function getPasswordHash(): Promise; // Identity Keys -export function createOrUpdateIdentityKey(data: IdentityKey): Promise; +// 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; export function getIdentityKeyById(id: string): Promise; export function bulkAddIdentityKeys(array: Array): Promise; export function removeIdentityKeyById(id: string): Promise; diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 0dd3ed43a..07c8866c6 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -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 diff --git a/preload.js b/preload.js index 0a45db23f..ca4ccba52 100644 --- a/preload.js +++ b/preload.js @@ -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'); diff --git a/protos/SignalService.proto b/protos/SignalService.proto index bd0f937c0..479cdb73c 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -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 { diff --git a/ts/components/MainViewController.tsx b/ts/components/MainViewController.tsx index fbdce046b..6959489d3 100644 --- a/ts/components/MainViewController.tsx +++ b/ts/components/MainViewController.tsx @@ -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); } diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 0022131f2..6b349abf9 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -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 diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index ca21dd589..c371e6778 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -587,6 +587,7 @@ export async function handleMessageEvent(event: MessageEvent): Promise { return; } + // TODO: this shouldn't be called when source is not a pubkey!!! const isOurDevice = await MultiDeviceProtocol.isOurDevice(source); const shouldSendReceipt = diff --git a/ts/receiver/groups.ts b/ts/receiver/groups.ts index dedfd0ddc..a2f4254dc 100644 --- a/ts/receiver/groups.ts +++ b/ts/receiver/groups.ts @@ -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; // Primary keys + is_medium_group: boolean; + active: boolean; + avatar: any; + expireTimer: number; + secretKey: any; + color?: any; // what is this??? + blocked?: boolean; + senderKeys: Array; +} + +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(); } diff --git a/ts/receiver/mediumGroups.ts b/ts/receiver/mediumGroups.ts index edee1157a..ee1441bf7 100644 --- a/ts/receiver/mediumGroups.ts +++ b/ts/receiver/mediumGroups.ts @@ -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); } } diff --git a/ts/receiver/syncMessages.ts b/ts/receiver/syncMessages.ts index 34e3b291a..d4d574bc7 100644 --- a/ts/receiver/syncMessages.ts +++ b/ts/receiver/syncMessages.ts @@ -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); }); diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index 9898d7f92..d3afce99c 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -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); diff --git a/ts/session/index.ts b/ts/session/index.ts index f53c4d63d..6d121dfbf 100644 --- a/ts/session/index.ts +++ b/ts/session/index.ts @@ -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 }; diff --git a/ts/session/medium_group/index.ts b/ts/session/medium_group/index.ts new file mode 100644 index 000000000..3fa0d577b --- /dev/null +++ b/ts/session/medium_group/index.ts @@ -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 +): Promise> { + // 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 +) { + 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)); +} diff --git a/js/modules/loki_sender_key_api.js b/ts/session/medium_group/ratchet.ts similarity index 62% rename from js/modules/loki_sender_key_api.js rename to ts/session/medium_group/ratchet.ts index 7e895ed17..3eccdf2e7 100644 --- a/js/modules/loki_sender_key_api.js +++ b/ts/session/medium_group/ratchet.ts @@ -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 } = {}; + +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, -}; diff --git a/ts/session/medium_group/senderKeys.ts b/ts/session/medium_group/senderKeys.ts new file mode 100644 index 000000000..5c645e557 --- /dev/null +++ b/ts/session/medium_group/senderKeys.ts @@ -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 { + // 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 + ); +} diff --git a/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupChatMessage.ts b/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupChatMessage.ts index 035558975..1d5021c16 100644 --- a/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupChatMessage.ts +++ b/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupChatMessage.ts @@ -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; } diff --git a/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupCreateMessage.ts b/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupCreateMessage.ts index aed1cc1c3..8fd592f54 100644 --- a/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupCreateMessage.ts +++ b/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupCreateMessage.ts @@ -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; - admins: Array; + admins: Array; groupName: string; + senderKeys: Array; } -export abstract class MediumGroupCreateMessage extends MediumGroupResponseKeysMessage { +export abstract class MediumGroupCreateMessage extends MediumGroupMessage { public readonly groupSecretKey: Uint8Array; public readonly members: Array; - public readonly admins: Array; + public readonly admins: Array; public readonly groupName: string; + public readonly senderKeys: Array; 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; } diff --git a/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupMessage.ts b/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupMessage.ts index 2f39a0b50..fbef3daa6 100644 --- a/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupMessage.ts +++ b/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupMessage.ts @@ -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 }); } } diff --git a/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupResponseKeysMessage.ts b/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupResponseKeysMessage.ts index 3dbd8d66c..16bc8945d 100644 --- a/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupResponseKeysMessage.ts +++ b/ts/session/messages/outgoing/content/data/mediumgroup/MediumGroupResponseKeysMessage.ts @@ -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; } diff --git a/ts/session/protocols/SessionProtocol.ts b/ts/session/protocols/SessionProtocol.ts index dcb584a19..0af4690d8 100644 --- a/ts/session/protocols/SessionProtocol.ts +++ b/ts/session/protocols/SessionProtocol.ts @@ -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'; diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index d138cba81..db75d1609 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -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 { + 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, diff --git a/ts/session/snode_api/swarmPolling.ts b/ts/session/snode_api/swarmPolling.ts index a742f490a..b481bd4ed 100644 --- a/ts/session/snode_api/swarmPolling.ts +++ b/ts/session/snode_api/swarmPolling.ts @@ -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')); diff --git a/ts/window.d.ts b/ts/window.d.ts index 5ea680b4c..240c3bc3b 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -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; } }