Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { Ui } from '../../../js/common/browser/ui.js';
export class ComposeStorageModule extends ViewModule<ComposeView> {

private passphraseInterval: number | undefined;
private ksLookupsByEmail: { [key: string]: Key } = {};

public setHandlers = () => {
BrowserMsg.addListener('passphrase_entry', async ({ entered }: Bm.PassphraseEntry) => {
Expand Down Expand Up @@ -55,20 +54,24 @@ export class ComposeStorageModule extends ViewModule<ComposeView> {
}

public collectAllAvailablePublicKeys = async (senderEmail: string, senderKi: KeyInfo, recipients: string[]): Promise<CollectPubkeysResult> => {
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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
17 changes: 17 additions & 0 deletions extension/js/common/platform/store/contact-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContactPreview[]> => {
return (await ContactStore.rawSearch(db, query)).filter(Boolean).map(ContactStore.toContactPreview);
}
Expand Down
26 changes: 26 additions & 0 deletions test/source/tests/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 });
Expand Down