From e3dd43aaac1a7787db183491f9c061e4c05f7fbe Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Wed, 10 Feb 2021 17:23:04 -0800 Subject: [PATCH 1/8] Replace unused textResponse with used noJson, updating logging --- js/modules/loki_app_dot_net_api.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 8b2c5f312..908f4210f 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -287,7 +287,7 @@ const serverRequest = async (endpoint, options = {}) => { txtResponse = await result.text(); // cloudflare timeouts (504s) will be html... - response = options.textResponse ? txtResponse : JSON.parse(txtResponse); + response = options.noJson ? txtResponse : JSON.parse(txtResponse); // result.status will always be 200 // emulate the correct http code if available @@ -303,7 +303,7 @@ const serverRequest = async (endpoint, options = {}) => { e.message, `json: ${txtResponse}`, 'attempting connection to', - url + url.toString() ); } else { log.error( @@ -311,7 +311,7 @@ const serverRequest = async (endpoint, options = {}) => { e.code, e.message, 'attempting connection to', - url + url.toString() ); } From 3e9c9efb7efe5455ed91dfd99613af150be59d97 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Wed, 10 Feb 2021 18:04:00 -0800 Subject: [PATCH 2/8] Add default avatar back --- images/group_default.png | Bin 0 -> 555 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/group_default.png diff --git a/images/group_default.png b/images/group_default.png new file mode 100644 index 0000000000000000000000000000000000000000..6b503e9ff80f31824ea4cdcb67e8f24494638ed0 GIT binary patch literal 555 zcmeAS@N?(olHy`uVBq!ia0vp^DIm4nJ za0`PlBg3pY5H=O_6N*jg7ShvJ}R>q7#R0>x;TbZ+xGdHYDo|V!@`5|}R-29a*6(*> zKUr7p%{Itu*Ksj)6mVn_biqe(#!p)`cdC&XtIsZz6~+ZmB>OgM9O$v}F*GcyhJU_t|H-u zwg1I)e9w*uK6!dy@QAiQugmgy$1k=<)iY$bX>RiCOMNh1-tOgvMXvRWo+^s3t4!*? znT&Gjp5p5^X&&>my%=w8uY6K% z!f2keU#K+#cfS%&Roqd wt!XJazjZUWlE)R-`^rrmN*xM#iM@XqU)1F+`?}690T{Imp00i_>zopr02zYUJ^%m! literal 0 HcmV?d00001 From 78e7255cb84a4fcfbf04c747381dd89f386fbe46 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 11 Feb 2021 16:40:05 +1100 Subject: [PATCH 3/8] send the keypair to added members if we are admin and remove request encryption keypair handling --- preload.js | 2 + ts/receiver/closedGroups.ts | 131 +++++++++++++++++++++++----------- ts/receiver/contentMessage.ts | 19 +++-- ts/session/group/index.ts | 4 ++ ts/window.d.ts | 1 + 5 files changed, 107 insertions(+), 50 deletions(-) diff --git a/preload.js b/preload.js index d7f99256c..eab5aa735 100644 --- a/preload.js +++ b/preload.js @@ -445,6 +445,7 @@ window.lokiFeatureFlags = { useFileOnionRequests: true, useFileOnionRequestsV2: true, // more compact encoding of files in response onionRequestHops: 3, + useRequestEncryptionKeyPair: false, }; // eslint-disable-next-line no-extend-native,func-names @@ -478,6 +479,7 @@ if (config.environment.includes('test-integration')) { useOnionRequests: false, useFileOnionRequests: false, useOnionRequestsV2: false, + useRequestEncryptionKeyPair: false, }; /* eslint-disable global-require, import/no-extraneous-dependencies */ window.sinon = require('sinon'); diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 33325a563..449e1ac67 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -532,11 +532,18 @@ async function performIfValid( } else if (groupUpdate.type === Type.MEMBER_LEFT) { await handleClosedGroupMemberLeft(envelope, groupUpdate, convo); } else if (groupUpdate.type === Type.ENCRYPTION_KEY_PAIR_REQUEST) { - await handleClosedGroupEncryptionKeyPairRequest( - envelope, - groupUpdate, - convo - ); + if (window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + await handleClosedGroupEncryptionKeyPairRequest( + envelope, + groupUpdate, + convo + ); + } else { + window.log.warn( + 'Received ENCRYPTION_KEY_PAIR_REQUEST message but it is not enabled for now.' + ); + await removeFromCache(envelope); + } // if you add a case here, remember to add it where performIfValid is called too. } @@ -590,6 +597,15 @@ async function handleClosedGroupMembersAdded( return; } + if (await areWeAdmin(convo)) { + await sendLatestKeyPairToUsers( + envelope, + convo, + convo.id, + membersNotAlreadyPresent + ); + } + const members = [...oldMembers, ...membersNotAlreadyPresent]; // Only add update message if we have something to show @@ -604,6 +620,16 @@ async function handleClosedGroupMembersAdded( await removeFromCache(envelope); } +async function areWeAdmin(groupConvo: ConversationModel) { + if (!groupConvo) { + throw new Error('areWeAdmin needs a convo'); + } + + const groupAdmins = groupConvo.get('groupAdmins'); + const ourNumber = (await UserUtils.getCurrentDevicePubKey()) as string; + return groupAdmins?.includes(ourNumber) || false; +} + async function handleClosedGroupMembersRemoved( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, @@ -650,8 +676,7 @@ async function handleClosedGroupMembersRemoved( window.SwarmPolling.removePubkey(groupPubKey); } // Generate and distribute a new encryption key pair if needed - const isCurrentUserAdmin = firstAdmin === ourPubKey.key; - if (isCurrentUserAdmin) { + if (await areWeAdmin(convo)) { try { await ClosedGroup.generateAndSendNewEncryptionKeyPair( groupPubKey, @@ -733,58 +758,78 @@ async function handleClosedGroupMemberLeft( await removeFromCache(envelope); } -async function handleClosedGroupEncryptionKeyPairRequest( +async function sendLatestKeyPairToUsers( envelope: EnvelopePlus, - groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, - convo: ConversationModel + groupConvo: ConversationModel, + groupPubKey: string, + targetUsers: Array ) { - const sender = envelope.senderIdentity; - const groupPublicKey = envelope.source; - // Guard against self-sends - if (await UserUtils.isUs(sender)) { - window.log.info( - 'Dropping self send message of type ENCRYPTION_KEYPAIR_REQUEST' - ); - await removeFromCache(envelope); - return; - } // Get the latest encryption key pair const latestKeyPair = await getLatestClosedGroupEncryptionKeyPair( - groupPublicKey + groupPubKey ); if (!latestKeyPair) { window.log.info( 'We do not have the keypair ourself, so dropping this message.' ); - await removeFromCache(envelope); return; } - window.log.info( - `Responding to closed group encryption key pair request from: ${sender}` - ); - await ConversationController.getInstance().getOrCreateAndWait( - sender, - 'private' - ); + const expireTimer = groupConvo.get('expireTimer') || 0; - const wrappers = await ClosedGroup.buildEncryptionKeyPairWrappers( - [sender], - ECKeyPair.fromHexKeyPair(latestKeyPair) - ); - const expireTimer = convo.get('expireTimer') || 0; + await Promise.all( + targetUsers.map(async member => { + window.log.info( + `Sending latest closed group encryption key pair to: ${member}` + ); + await ConversationController.getInstance().getOrCreateAndWait( + member, + 'private' + ); - const keypairsMessage = new ClosedGroupEncryptionPairReplyMessage({ - groupId: groupPublicKey, - timestamp: Date.now(), - encryptedKeyPairs: wrappers, - expireTimer, - }); + const wrappers = await ClosedGroup.buildEncryptionKeyPairWrappers( + [member], + ECKeyPair.fromHexKeyPair(latestKeyPair) + ); - // the encryption keypair is sent using established channels - await getMessageQueue().sendToPubKey(PubKey.cast(sender), keypairsMessage); + const keypairsMessage = new ClosedGroupEncryptionPairReplyMessage({ + groupId: groupPubKey, + timestamp: Date.now(), + encryptedKeyPairs: wrappers, + expireTimer, + }); + + // the encryption keypair is sent using established channels + await getMessageQueue().sendToPubKey( + PubKey.cast(member), + keypairsMessage + ); + }) + ); +} - await removeFromCache(envelope); +async function handleClosedGroupEncryptionKeyPairRequest( + envelope: EnvelopePlus, + groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, + groupConvo: ConversationModel +) { + if (!window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + throw new Error('useRequestEncryptionKeyPair is disabled'); + } + const sender = envelope.senderIdentity; + const groupPublicKey = envelope.source; + // Guard against self-sends + if (await UserUtils.isUs(sender)) { + window.log.info( + 'Dropping self send message of type ENCRYPTION_KEYPAIR_REQUEST' + ); + await removeFromCache(envelope); + return; + } + await sendLatestKeyPairToUsers(envelope, groupConvo, groupPublicKey, [ + sender, + ]); + return removeFromCache(envelope); } export async function createClosedGroup( diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 9175c8cd6..7b7427c32 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -125,18 +125,23 @@ async function decryptForClosedGroup( 'decryptWithSessionProtocol for medium group message throw:', e ); - const keypairRequestManager = KeyPairRequestManager.getInstance(); const groupPubKey = PubKey.cast(envelope.source); - if (keypairRequestManager.canTriggerRequestWith(groupPubKey)) { - keypairRequestManager.markRequestSendFor(groupPubKey, Date.now()); - await requestEncryptionKeyPair(groupPubKey); + + // To enable back if we decide to enable encryption key pair request work again + if (window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + const keypairRequestManager = KeyPairRequestManager.getInstance(); + if (keypairRequestManager.canTriggerRequestWith(groupPubKey)) { + keypairRequestManager.markRequestSendFor(groupPubKey, Date.now()); + await requestEncryptionKeyPair(groupPubKey); + } } + // IMPORTANT do not remove the message from the cache just yet. + // We will try to decrypt it once we get the encryption keypair. + // for that to work, we need to throw an error just like here. throw new Error( `Waiting for an encryption keypair to be received for group ${groupPubKey.key}` ); - // do not remove it from the cache yet. We will try to decrypt it once we get the encryption keypair - // TODO drop it if after some time we still don't get to decrypt it - // await removeFromCache(envelope); + return null; } } diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index acd980883..4be9e19a6 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -626,6 +626,10 @@ export async function buildEncryptionKeyPairWrappers( export async function requestEncryptionKeyPair( groupPublicKey: string | PubKey ) { + if (!window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + throw new Error('useRequestEncryptionKeyPair is disabled'); + } + const groupConvo = ConversationController.getInstance().get( PubKey.cast(groupPublicKey).key ); diff --git a/ts/window.d.ts b/ts/window.d.ts index 6bd056219..2373c9c3b 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -64,6 +64,7 @@ declare global { useFileOnionRequests: boolean; useFileOnionRequestsV2: boolean; onionRequestHops: number; + useRequestEncryptionKeyPair: boolean; }; lokiFileServerAPI: LokiFileServerInstance; lokiMessageAPI: LokiMessageInterface; From 0c1343cad50b831a5d4b3fbad5b930d8ac9bac74 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 12 Feb 2021 11:00:22 +1100 Subject: [PATCH 4/8] add a way to share currently distributing keypair to added members --- ts/receiver/closedGroups.ts | 18 ++++++++++++++++-- ts/session/group/index.ts | 5 +++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 449e1ac67..eb48040c5 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -33,6 +33,11 @@ import { MessageController } from '../session/messages'; import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgoing/content/data/group'; import { queueAllCachedFromSource } from './receiver'; +export const distributingClosedGroupEncryptionKeyPairs = new Map< + string, + ECKeyPair +>(); + export async function handleClosedGroupControlMessage( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage @@ -456,6 +461,9 @@ async function handleClosedGroupEncryptionKeyPair( ); if (isKeyPairAlreadyHere) { + const existingKeyPairs = await getAllEncryptionKeyPairsForGroup( + groupPublicKey + ); window.log.info('Dropping already saved keypair for group', groupPublicKey); await removeFromCache(envelope); return; @@ -764,11 +772,17 @@ async function sendLatestKeyPairToUsers( groupPubKey: string, targetUsers: Array ) { + // use the inMemory keypair if found + + const inMemoryKeyPair = distributingClosedGroupEncryptionKeyPairs.get( + groupPubKey + ); + // Get the latest encryption key pair const latestKeyPair = await getLatestClosedGroupEncryptionKeyPair( groupPubKey ); - if (!latestKeyPair) { + if (!inMemoryKeyPair && !latestKeyPair) { window.log.info( 'We do not have the keypair ourself, so dropping this message.' ); @@ -789,7 +803,7 @@ async function sendLatestKeyPairToUsers( const wrappers = await ClosedGroup.buildEncryptionKeyPairWrappers( [member], - ECKeyPair.fromHexKeyPair(latestKeyPair) + inMemoryKeyPair || ECKeyPair.fromHexKeyPair(latestKeyPair) ); const keypairsMessage = new ClosedGroupEncryptionPairReplyMessage({ diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 4be9e19a6..d5cc8de88 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -35,6 +35,7 @@ import { ClosedGroupUpdateMessage, } from '../messages/outgoing/content/data/group'; import { MessageController } from '../messages'; +import { distributingClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups'; export interface GroupInfo { id: string; @@ -572,11 +573,15 @@ export async function generateAndSendNewEncryptionKeyPair( expireTimer, }); + distributingClosedGroupEncryptionKeyPairs.set(toHex(groupId), newKeyPair); + const messageSentCallback = async () => { window.log.info( `KeyPairMessage for ClosedGroup ${groupPublicKey} is sent. Saving the new encryptionKeyPair.` ); + distributingClosedGroupEncryptionKeyPairs.delete(toHex(groupId)); + await addClosedGroupEncryptionKeyPair( toHex(groupId), newKeyPair.toHexKeyPair() From c150fc6bb33c3ffadc95f240d8c453bc5eb964ad Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 12 Feb 2021 14:08:35 +1100 Subject: [PATCH 5/8] bump to v1.4.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97c4258b4..313f597d8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-desktop", "productName": "Session", "description": "Private messaging from your desktop", - "version": "1.4.8", + "version": "1.4.9", "license": "GPL-3.0", "author": { "name": "Loki Project", From 720922cc71307d6259eb984141580e94e0bc4124 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 15 Feb 2021 15:36:14 +1100 Subject: [PATCH 6/8] be sure to leave a group when leaving from another device --- ts/receiver/closedGroups.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index eb48040c5..e28637d99 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -744,9 +744,23 @@ async function handleClosedGroupMemberLeft( } if (didAdminLeave) { + window.SwarmPolling.removePubkey(groupPublicKey); + + await removeAllClosedGroupEncryptionKeyPairs(groupPublicKey); + // Disable typing + // if the admin was remove and we are the admin, it can only be voluntary + if (isCurrentUserAdmin) { + convo.set('left', true); + } else { + convo.set('isKickedFromGroup', true); + } + } + const didWeLeaveFromAnotherDevice = !members.includes(ourPubkey); + + if (didWeLeaveFromAnotherDevice) { await removeAllClosedGroupEncryptionKeyPairs(groupPublicKey); // Disable typing: - convo.set('isKickedFromGroup', true); + convo.set('left', true); window.SwarmPolling.removePubkey(groupPublicKey); } From 2a1d68401d61f58011b772ad884f26bac2b10368 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 15 Feb 2021 17:16:16 +1100 Subject: [PATCH 7/8] Allow allow one group creation at a time --- ts/components/MainViewController.tsx | 20 +++++++++---------- .../session/LeftPaneMessageSection.tsx | 16 +++++++++++++-- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/ts/components/MainViewController.tsx b/ts/components/MainViewController.tsx index 055f7120a..1beac675d 100644 --- a/ts/components/MainViewController.tsx +++ b/ts/components/MainViewController.tsx @@ -32,11 +32,13 @@ export class MessageView extends React.Component { // //////////// Management ///////////// // ///////////////////////////////////// +/** + * Returns true if the group was indead created + */ async function createClosedGroup( groupName: string, - groupMembers: Array, - onSuccess: any -) { + groupMembers: Array +): Promise { // Validate groupName and groupMembers length if (groupName.length === 0) { ToastUtils.pushToastError( @@ -44,13 +46,13 @@ async function createClosedGroup( window.i18n('invalidGroupNameTooShort') ); - return; + return false; } else if (groupName.length > window.CONSTANTS.MAX_GROUP_NAME_LENGTH) { ToastUtils.pushToastError( 'invalidGroupName', window.i18n('invalidGroupNameTooLong') ); - return; + return false; } // >= because we add ourself as a member AFTER this. so a 10 group is already invalid as it will be 11 with ourself @@ -61,23 +63,19 @@ async function createClosedGroup( 'pickClosedGroupMember', window.i18n('pickClosedGroupMember') ); - return; + return false; } else if (groupMembers.length >= window.CONSTANTS.CLOSED_GROUP_SIZE_LIMIT) { ToastUtils.pushToastError( 'closedGroupMaxSize', window.i18n('closedGroupMaxSize') ); - return; + return false; } const groupMemberIds = groupMembers.map(m => m.id); await createClosedGroupV2(groupName, groupMemberIds); - if (onSuccess) { - onSuccess(); - } - return true; } diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 5e890e314..e8e3b856d 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -469,8 +469,20 @@ export class LeftPaneMessageSection extends React.Component { groupName: string, groupMembers: Array ) { - await MainViewController.createClosedGroup(groupName, groupMembers, () => { - this.handleToggleOverlay(undefined); + if (this.state.loading) { + window.log.warn('Closed group creation already in progress'); + return; + } + this.setState({ loading: true }, async () => { + const groupCreated = await MainViewController.createClosedGroup( + groupName, + groupMembers + ); + + if (groupCreated) { + this.handleToggleOverlay(undefined); + } + this.setState({ loading: false }); }); } From a34720501c640ea417f066e0b9e98c8786aa7ebe Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 16 Feb 2021 14:22:22 +1100 Subject: [PATCH 8/8] update avatar on convo only if DL+decrypt is OK --- ts/receiver/dataMessage.ts | 6 +++--- .../outgoing/content/data/ChatMessage.ts | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 153b63560..e6b64d5b3 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -34,9 +34,6 @@ export async function updateProfile( !prevPointer || !_.isEqual(prevPointer, profile.profilePicture); if (needsUpdate) { - conversation.set('avatarPointer', profile.profilePicture); - conversation.set('profileKey', profileKey); - const downloaded = await downloadAttachment({ url: profile.profilePicture, isRaw: true, @@ -60,6 +57,9 @@ export async function updateProfile( ...downloaded, data: decryptedData, }); + // Only update the convo if the download and decrypt is a success + conversation.set('avatarPointer', profile.profilePicture); + conversation.set('profileKey', profileKey); ({ path } = upgraded); } catch (e) { window.log.error(`Could not decrypt profile image: ${e}`); diff --git a/ts/session/messages/outgoing/content/data/ChatMessage.ts b/ts/session/messages/outgoing/content/data/ChatMessage.ts index 147f65dff..2a661c2ed 100644 --- a/ts/session/messages/outgoing/content/data/ChatMessage.ts +++ b/ts/session/messages/outgoing/content/data/ChatMessage.ts @@ -72,9 +72,16 @@ export class ChatMessage extends DataMessage { this.quote = params.quote; this.expireTimer = params.expireTimer; if (params.lokiProfile && params.lokiProfile.profileKey) { - this.profileKey = new Uint8Array( - ByteBuffer.wrap(params.lokiProfile.profileKey).toArrayBuffer() - ); + if ( + params.lokiProfile.profileKey instanceof Uint8Array || + (params.lokiProfile.profileKey as any) instanceof ByteBuffer + ) { + this.profileKey = new Uint8Array(params.lokiProfile.profileKey); + } else { + this.profileKey = new Uint8Array( + ByteBuffer.wrap(params.lokiProfile.profileKey).toArrayBuffer() + ); + } } this.displayName = params.lokiProfile && params.lokiProfile.displayName; @@ -88,8 +95,11 @@ export class ChatMessage extends DataMessage { syncTarget: string, sentTimestamp: number ) { + // the dataMessage.profileKey is of type ByteBuffer. We need to make it a Uint8Array const lokiProfile: any = { - profileKey: dataMessage.profileKey, + profileKey: new Uint8Array( + (dataMessage.profileKey as any).toArrayBuffer() + ), }; if ((dataMessage as any)?.$type?.name !== 'DataMessage') {