From d887b6f9631045b39497203c3406f6cbed87da37 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Fri, 9 Jul 2021 13:55:04 +0000 Subject: [PATCH 1/2] Encrypt with multiple keys per recipient #3769 --- .../compose-modules/compose-storage-module.ts | 10 ++++--- .../js/common/platform/store/contact-store.ts | 21 +++++++++++++++ test/source/tests/compose.ts | 26 +++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-storage-module.ts b/extension/chrome/elements/compose-modules/compose-storage-module.ts index c33ca77b553..f7c838bfd59 100644 --- a/extension/chrome/elements/compose-modules/compose-storage-module.ts +++ b/extension/chrome/elements/compose-modules/compose-storage-module.ts @@ -55,14 +55,16 @@ export class ComposeStorageModule extends ViewModule { } public collectAllAvailablePublicKeys = async (senderEmail: string, senderKi: KeyInfo, recipients: string[]): Promise => { - const contacts = await ContactStore.get(undefined, recipients); + const contacts = await ContactStore.getEncryptionKeys(undefined, recipients); const armoredPubkeys = [{ pubkey: await KeyUtil.parse(senderKi.public), email: senderEmail, isMine: true }]; const emailsWithoutPubkeys = []; for (const i of contacts.keys()) { const contact = contacts[i]; - if (contact && contact.hasPgp && contact.pubkey) { - armoredPubkeys.push({ pubkey: contact.pubkey, email: contact.email, isMine: false }); - } else if (contact && this.ksLookupsByEmail[contact.email]) { + if (contact?.keys.length) { + for (const pubkey of contact.keys) { + armoredPubkeys.push({ pubkey, email: contact.email, isMine: false }); + } + } else if (contact && this.ksLookupsByEmail[contact.email]) { // todo: introduce lookup to key array armoredPubkeys.push({ pubkey: this.ksLookupsByEmail[contact.email], email: contact.email, isMine: false }); } else { emailsWithoutPubkeys.push(recipients[i]); diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 6b558462356..3033dee3e6f 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -235,6 +235,27 @@ export class ContactStore extends AbstractStore { } } + public static getEncryptionKeys = async (db: undefined | IDBDatabase, emails: string[]): Promise<{ email: string, keys: Key[] }[]> => { + if (!db) { // relay op through background process + return await BrowserMsg.send.bg.await.db({ f: 'getEncryptionKeys', args: [emails] }) as { email: string, keys: Key[] }[]; + } + if (emails.length === 1) { + const email = emails[0]; + const contact = await ContactStore.getOneWithAllPubkeys(db, email); + const encryptionKeys = (contact?.sortedPubkeys || []).filter(k => !k.revoked && (k.pubkey.usableForEncryption || k.pubkey.usableForEncryptionButExpired)). + map(k => k.pubkey); + // if non-expired present, return non-expired only + if (encryptionKeys.some(k => k.usableForEncryption)) { + return [{ email, keys: encryptionKeys.filter(k => k.usableForEncryption) }]; + } else { + return [{ email, keys: encryptionKeys }]; + } + } else { + return (await Promise.all(emails.map(email => ContactStore.getEncryptionKeys(db, [email])))) + .reduce((a, b) => a.concat(b)); + } + } + public static search = async (db: IDBDatabase | undefined, query: DbContactFilter): Promise => { return (await ContactStore.rawSearch(db, query)).filter(Boolean).map(ContactStore.toContactPreview); } diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index a385430e6a8..b225eda98c6 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -22,6 +22,8 @@ import { expect } from "chai"; import { BrowserRecipe } from './tooling/browser-recipe'; import { SetupPageRecipe } from './page-recipe/setup-page-recipe'; import { testConstants } from './tooling/consts'; +import { MsgUtil } from '../core/crypto/pgp/msg-util'; +import { Buf } from '../core/buf'; // tslint:disable:no-blank-lines-func // tslint:disable:no-unused-expression @@ -847,6 +849,30 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te await sendImgAndVerifyPresentInSentMsg(t, browser, 'plain'); })); + ava.default('compose - sending a message encrypted with all keys of a recipient', testWithBrowser('compatibility', async (t, browser) => { + const text = 'This message is encrypted with 2 keys of flowcrypt.compatibility'; + const subject = `Test Sending Multi-Encrypted Message With Test Text ${Util.lousyRandom()}`; + const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility'); + await ComposePageRecipe.fillMsg(composePage, { to: 'flowcrypt.compatibility@gmail.com' }, subject, { sign: true, encrypt: true }); + await composePage.waitAndType('@input-body', text, { delay: 1 }); + expect(await composePage.read('@input-body')).to.include(text); + await ComposePageRecipe.sendAndClose(composePage); + // get sent msg from mock + const sentMsg = (await GoogleData.withInitializedData('flowcrypt.compatibility@gmail.com')).getMessageBySubject(subject)!; + const message = sentMsg.payload!.body!.data!; + const encrypted = message.match(/\-\-\-\-\-BEGIN PGP MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP MESSAGE\-\-\-\-\-/s)![0]; + const encryptedData = Buf.fromUtfStr(encrypted); + const decrypted0 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData }); + // decryption without a ki should fail + expect(decrypted0.success).to.equal(false); + // decryption with ki 1 should succeed + const decrypted1 = await MsgUtil.decryptMessage({ kisWithPp: await Config.getKeyInfo(["flowcrypt.compatibility.1pp1"]), encryptedData }); + expect(decrypted1.success).to.equal(true); + // decryption with ki 2 should succeed + const decrypted2 = await MsgUtil.decryptMessage({ kisWithPp: await Config.getKeyInfo(["flowcrypt.compatibility.2pp1"]), encryptedData }); + expect(decrypted2.success).to.equal(true); + })); + ava.default('compose - sending and rendering message with U+10000 code points', testWithBrowser('compatibility', async (t, browser) => { const rainbow = '\ud83c\udf08'; await sendTextAndVerifyPresentInSentMsg(t, browser, rainbow, { sign: true, encrypt: false }); From c08fcbb3ccda699fa41d081eb78b42c8ff58e0b6 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 12 Jul 2021 15:16:07 +0000 Subject: [PATCH 2/2] moved key filtering to compose-storage-module + small edits --- .../compose-modules/compose-storage-module.ts | 23 ++++++++++--------- .../elements/compose-modules/compose-types.ts | 2 +- .../formatters/general-mail-formatter.ts | 4 ++-- .../js/common/platform/store/contact-store.ts | 12 ++++------ 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-storage-module.ts b/extension/chrome/elements/compose-modules/compose-storage-module.ts index f7c838bfd59..4316ee6c6f0 100644 --- a/extension/chrome/elements/compose-modules/compose-storage-module.ts +++ b/extension/chrome/elements/compose-modules/compose-storage-module.ts @@ -21,7 +21,6 @@ import { Ui } from '../../../js/common/browser/ui.js'; export class ComposeStorageModule extends ViewModule { private passphraseInterval: number | undefined; - private ksLookupsByEmail: { [key: string]: Key } = {}; public setHandlers = () => { BrowserMsg.addListener('passphrase_entry', async ({ entered }: Bm.PassphraseEntry) => { @@ -56,21 +55,23 @@ export class ComposeStorageModule extends ViewModule { public collectAllAvailablePublicKeys = async (senderEmail: string, senderKi: KeyInfo, recipients: string[]): Promise => { const contacts = await ContactStore.getEncryptionKeys(undefined, recipients); - const armoredPubkeys = [{ pubkey: await KeyUtil.parse(senderKi.public), email: senderEmail, isMine: true }]; + const pubkeys = [{ pubkey: await KeyUtil.parse(senderKi.public), email: senderEmail, isMine: true }]; const emailsWithoutPubkeys = []; - for (const i of contacts.keys()) { - const contact = contacts[i]; - if (contact?.keys.length) { - for (const pubkey of contact.keys) { - armoredPubkeys.push({ pubkey, email: contact.email, isMine: false }); + for (const contact of contacts) { + let keysPerEmail = contact.keys; + // if non-expired present, return non-expired only + if (keysPerEmail.some(k => k.usableForEncryption)) { + keysPerEmail = keysPerEmail.filter(k => k.usableForEncryption); + } + if (keysPerEmail.length) { + for (const pubkey of keysPerEmail) { + pubkeys.push({ pubkey, email: contact.email, isMine: false }); } - } else if (contact && this.ksLookupsByEmail[contact.email]) { // todo: introduce lookup to key array - armoredPubkeys.push({ pubkey: this.ksLookupsByEmail[contact.email], email: contact.email, isMine: false }); } else { - emailsWithoutPubkeys.push(recipients[i]); + emailsWithoutPubkeys.push(contact.email); } } - return { armoredPubkeys, emailsWithoutPubkeys }; + return { pubkeys, emailsWithoutPubkeys }; } public passphraseGet = async (senderKi?: KeyInfo) => { diff --git a/extension/chrome/elements/compose-modules/compose-types.ts b/extension/chrome/elements/compose-modules/compose-types.ts index 99005ec13c0..90032d54eac 100644 --- a/extension/chrome/elements/compose-modules/compose-types.ts +++ b/extension/chrome/elements/compose-modules/compose-types.ts @@ -37,7 +37,7 @@ export type MessageToReplyOrForward = { decryptedFiles: File[] }; -export type CollectPubkeysResult = { armoredPubkeys: PubkeyResult[], emailsWithoutPubkeys: string[] }; +export type CollectPubkeysResult = { pubkeys: PubkeyResult[], emailsWithoutPubkeys: string[] }; export type PopoverOpt = 'encrypt' | 'sign' | 'richtext'; export type PopoverChoices = { [key in PopoverOpt]: boolean }; diff --git a/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts b/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts index 290f8a59eb8..dbe432c35e0 100644 --- a/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts @@ -23,12 +23,12 @@ export class GeneralMailFormatter { return await new SignedMsgMailFormatter(view).sendableMsg(newMsgData, signingPrv!); } // encrypt (optionally sign) - const { armoredPubkeys, emailsWithoutPubkeys } = await view.storageModule.collectAllAvailablePublicKeys(newMsgData.from, senderKi, recipientsEmails); + const { pubkeys, emailsWithoutPubkeys } = await view.storageModule.collectAllAvailablePublicKeys(newMsgData.from, senderKi, recipientsEmails); if (emailsWithoutPubkeys.length) { await view.errModule.throwIfEncryptionPasswordInvalid(senderKi, newMsgData); } view.S.now('send_btn_text').text('Encrypting...'); - return await new EncryptedMsgMailFormatter(view).sendableMsg(newMsgData, armoredPubkeys, signingPrv); + return await new EncryptedMsgMailFormatter(view).sendableMsg(newMsgData, pubkeys, signingPrv); } } diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 3033dee3e6f..4d2dc880cba 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -242,14 +242,10 @@ export class ContactStore extends AbstractStore { if (emails.length === 1) { const email = emails[0]; const contact = await ContactStore.getOneWithAllPubkeys(db, email); - const encryptionKeys = (contact?.sortedPubkeys || []).filter(k => !k.revoked && (k.pubkey.usableForEncryption || k.pubkey.usableForEncryptionButExpired)). - map(k => k.pubkey); - // if non-expired present, return non-expired only - if (encryptionKeys.some(k => k.usableForEncryption)) { - return [{ email, keys: encryptionKeys.filter(k => k.usableForEncryption) }]; - } else { - return [{ email, keys: encryptionKeys }]; - } + return [{ + email, + keys: (contact?.sortedPubkeys || []).filter(k => !k.revoked && (k.pubkey.usableForEncryption || k.pubkey.usableForEncryptionButExpired)).map(k => k.pubkey) + }]; } else { return (await Promise.all(emails.map(email => ContactStore.getEncryptionKeys(db, [email])))) .reduce((a, b) => a.concat(b));