diff --git a/extension/chrome/elements/compose-modules/compose-storage-module.ts b/extension/chrome/elements/compose-modules/compose-storage-module.ts index c33ca77b553..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) => { @@ -55,20 +54,24 @@ export class ComposeStorageModule extends ViewModule { } public collectAllAvailablePublicKeys = async (senderEmail: string, senderKi: KeyInfo, recipients: string[]): Promise => { - const contacts = await ContactStore.get(undefined, recipients); - const armoredPubkeys = [{ pubkey: await KeyUtil.parse(senderKi.public), email: senderEmail, isMine: true }]; + const contacts = await ContactStore.getEncryptionKeys(undefined, recipients); + 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 && contact.hasPgp && contact.pubkey) { - armoredPubkeys.push({ pubkey: contact.pubkey, email: contact.email, isMine: false }); - } else if (contact && this.ksLookupsByEmail[contact.email]) { - armoredPubkeys.push({ pubkey: this.ksLookupsByEmail[contact.email], 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 { - 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 6b558462356..4d2dc880cba 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -235,6 +235,23 @@ 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); + 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)); + } + } + 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 7d1a777e02f..dcc701161ee 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 });