From ee70d66cd135ed84b47a4ac7e4aa7d2dde651dea Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Mon, 30 Mar 2020 13:25:41 +0200 Subject: [PATCH 01/24] Don't spellcheck public keys --- extension/chrome/elements/add_pubkey.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/chrome/elements/add_pubkey.htm b/extension/chrome/elements/add_pubkey.htm index de97287cf06..14c285bac0d 100644 --- a/extension/chrome/elements/add_pubkey.htm +++ b/extension/chrome/elements/add_pubkey.htm @@ -35,7 +35,7 @@

Add a pubkey to email address

or paste below:
- +
From 65db1305216af17f5b017b006b2ddf29df27c701 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Mon, 30 Mar 2020 13:26:37 +0200 Subject: [PATCH 02/24] Fix typo "formated" -> "formatted" --- extension/js/common/lang.ts | 2 +- extension/js/common/ui/key-import-ui.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/js/common/lang.ts b/extension/js/common/lang.ts index d4d6914414c..b4ad6568e7f 100644 --- a/extension/js/common/lang.ts +++ b/extension/js/common/lang.ts @@ -12,7 +12,7 @@ export const Lang = { // tslint:disable-line:variable-name partiallyEncryptedKeyUnsupported: 'This Private Key seems to be only partially encrypted (some secret packets are encrypted, some not).\n\nSuch keys are not supported - please either fully encrypt or fully decrypt the key before importing it.', confirmResetAcct: (acctEmail: string) => `This will remove all your FlowCrypt settings for ${acctEmail} including your keys. It is not a recommended thing to do.\n\nMAKE SURE TO BACK UP YOUR PRIVATE KEY AND PASS PHRASE IN A SAFE PLACE FIRST OR YOU MAY LOSE IT`, confirmManualAcctEmailChange: (currentAcctEmail: string) => `Your current account email is ${currentAcctEmail}.\n\nUse this when your Google Account email address has changed and the account above is outdated.\n\nIn the following step, please sign in with your updated Google Account.\n\nContinue?`, - keyFormattedWell: (begin: string, end: string) => `Private key is not correctly formated. Please insert complete key, including "${begin}" and "${end}"\n\nEnter the private key you previously used. The corresponding public key is registered with your email, and the private key is needed to confirm this change.\n\nIf you chose to download your backup as a file, you should find it inside that file. If you backed up your key on Gmail, you will find there it by searching your inbox.`, + keyFormattedWell: (begin: string, end: string) => `Private key is not correctly formatted. Please insert complete key, including "${begin}" and "${end}"\n\nEnter the private key you previously used. The corresponding public key is registered with your email, and the private key is needed to confirm this change.\n\nIf you chose to download your backup as a file, you should find it inside that file. If you backed up your key on Gmail, you will find there it by searching your inbox.`, failedToCheckIfAcctUsesEncryption: 'Failed to check if encryption is already set up on your account. ', failedToCheckAccountBackups: 'Failed to check for account backups. ', failedToSubmitToAttester: 'Failed to submit to Attester. ', diff --git a/extension/js/common/ui/key-import-ui.ts b/extension/js/common/ui/key-import-ui.ts index eeb15dbbc43..ebce7624cc5 100644 --- a/extension/js/common/ui/key-import-ui.ts +++ b/extension/js/common/ui/key-import-ui.ts @@ -209,7 +209,7 @@ export class KeyImportUi { const headers = PgpArmor.headers(type); const { keys: [k] } = await opgp.key.readArmored(normalized); if (typeof k === 'undefined') { - throw new UserAlert(`${type === 'privateKey' ? 'Private' : 'Public'} key is not correctly formated. Please insert complete key, including "${headers.begin}" and "${headers.end}"`); + throw new UserAlert(`${type === 'privateKey' ? 'Private' : 'Public'} key is not correctly formatted. Please insert complete key, including "${headers.begin}" and "${headers.end}"`); } return k; } From 0ff8f3e26d62aa0c61995194866202e24986b0b1 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Mon, 30 Mar 2020 16:35:47 +0200 Subject: [PATCH 03/24] Add sending S/MIME encrypted e-mails to X.509 keys --- .../compose-modules/compose-draft-module.ts | 10 ++-- .../encrypted-mail-msg-formatter.ts | 6 +-- extension/chrome/elements/compose.htm | 1 + .../common/api/email-provider/gmail/gmail.ts | 2 +- .../common/api/email-provider/sendable-msg.ts | 8 ++-- extension/js/common/core/mime.ts | 13 ++++++ extension/js/common/core/pgp-key.ts | 4 ++ extension/js/common/core/pgp-msg.ts | 46 ++++++++++++++++++- .../js/common/platform/store/contact-store.ts | 22 +++++++++ extension/js/common/ui/key-import-ui.ts | 4 ++ 10 files changed, 104 insertions(+), 12 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-draft-module.ts b/extension/chrome/elements/compose-modules/compose-draft-module.ts index a6cf3987fcb..fa9a9198ac1 100644 --- a/extension/chrome/elements/compose-modules/compose-draft-module.ts +++ b/extension/chrome/elements/compose-modules/compose-draft-module.ts @@ -104,10 +104,12 @@ export class ComposeDraftModule extends ViewModule { msgData.pwd = undefined; // not needed for drafts const sendable = await new EncryptedMsgMailFormatter(this.view, true).sendableMsg(msgData, pubkeys); this.view.S.cached('send_btn_note').text('Saving'); - if (this.view.threadId) { // reply draft - sendable.body['text/plain'] = `[cryptup:link:draft_reply:${this.view.threadId}]\n\n${sendable.body['text/plain'] || ''}`; - } else if (this.view.draftId) { // new message compose draft with known draftid - sendable.body['text/plain'] = `[cryptup:link:draft_compose:${this.view.draftId}]\n\n${sendable.body['text/plain'] || ''}`; + if (!(sendable.body instanceof Uint8Array)) { + if (this.view.threadId) { // reply draft + sendable.body['text/plain'] = `[cryptup:link:draft_reply:${this.view.threadId}]\n\n${sendable.body['text/plain'] || ''}`; + } else if (this.view.draftId) { // new message compose draft with known draftid + sendable.body['text/plain'] = `[cryptup:link:draft_compose:${this.view.draftId}]\n\n${sendable.body['text/plain'] || ''}`; + } } const mimeMsg = await sendable.toMime(); if (!this.view.draftId) { diff --git a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts index 88a791a72e7..dc17725e1eb 100644 --- a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts @@ -66,7 +66,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { private sendableSimpleTextMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key) => { const atts = this.isDraft ? [] : await this.view.attsModule.attach.collectEncryptAtts(pubs.map(p => p.pubkey)); const encrypted = await this.encryptDataArmor(Buf.fromUtfStr(newMsg.plaintext), undefined, pubs, signingPrv); - const encryptedBody = { 'text/plain': encrypted.toString() }; + const encryptedBody = encrypted instanceof Buf ? { 'text/plain': encrypted.toString() } : encrypted; return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: encryptedBody, atts, isDraft: this.isDraft }); } @@ -86,9 +86,9 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { } private encryptDataArmor = async (data: Buf, pwd: string | undefined, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise => { - const encryptAsOfDate = await this.encryptMsgAsOfDateIfSomeAreExpiredAndUserConfirmedModal(pubs); + const encryptAsOfDate = await this.encryptMsgAsOfDateIfSomeAreExpiredAndUserConfirmedModal([]); const r = await PgpMsg.encrypt({ pubkeys: pubs.map(p => p.pubkey), signingPrv, pwd, data, armor: true, date: encryptAsOfDate }) as OpenPGP.EncryptArmorResult; - return Buf.fromUtfStr(r.data); + return typeof r.data === 'string' ? Buf.fromUtfStr(r.data) : r.data; } private getPwdMsgSendableBodyWithOnlineReplyMsgToken = async (authInfo: FcUuidAuth, newMsgData: NewMsgData): Promise => { diff --git a/extension/chrome/elements/compose.htm b/extension/chrome/elements/compose.htm index f780af17ad9..aeef5f3940e 100644 --- a/extension/chrome/elements/compose.htm +++ b/extension/chrome/elements/compose.htm @@ -185,6 +185,7 @@

New Secure Message

+ diff --git a/extension/js/common/api/email-provider/gmail/gmail.ts b/extension/js/common/api/email-provider/gmail/gmail.ts index 254b7ca6858..9a552113201 100644 --- a/extension/js/common/api/email-provider/gmail/gmail.ts +++ b/extension/js/common/api/email-provider/gmail/gmail.ts @@ -279,7 +279,7 @@ export class Gmail extends EmailProviderApi implements EmailProviderInterface { if (format === 'full') { const bodies = GmailParser.findBodies(gmailMsg); const atts = GmailParser.findAtts(gmailMsg); - const fromTextBody = PgpArmor.clip(Buf.fromBase64UrlStr(bodies['text/plain'] || '').toUtfStr()); + const fromTextBody = PgpArmor.clip(Buf.fromBase64UrlStr(bodies['text/plain']?.toString() || '').toUtfStr()); if (fromTextBody) { return { armored: fromTextBody, subject, isPwdMsg }; } diff --git a/extension/js/common/api/email-provider/sendable-msg.ts b/extension/js/common/api/email-provider/sendable-msg.ts index 7400664ceb3..d3de8da5acc 100644 --- a/extension/js/common/api/email-provider/sendable-msg.ts +++ b/extension/js/common/api/email-provider/sendable-msg.ts @@ -16,7 +16,7 @@ type SendableMsgDefinition = { from: string; recipients: Recipients; subject: string; - body: SendableMsgBody; + body: SendableMsgBody | Uint8Array; atts: Att[]; thread?: string; type?: MimeEncodeType, @@ -51,7 +51,7 @@ export class SendableMsg { public from: string, public recipients: Recipients, public subject: string, - public body: SendableMsgBody, + public body: SendableMsgBody | Uint8Array, public atts: Att[], public thread: string | undefined, public type: MimeEncodeType, @@ -83,7 +83,9 @@ export class SendableMsg { } } this.headers.Subject = this.subject; - if (this.type === 'pgpMimeSigned' && this.sign) { + if (this.body instanceof Uint8Array) { + return await Mime.encodePlain(this.body, this.headers); + } else if (this.type === 'pgpMimeSigned' && this.sign) { return await Mime.encodePgpMimeSigned(this.body, this.headers, this.atts, this.sign); } else { return await Mime.encode(this.body, this.headers, this.atts, this.type); diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index 6d7acc1dfa4..7bed9776049 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -214,6 +214,19 @@ export class Mime { return rootNode.build(); // tslint:disable-line:no-unsafe-any } + public static encodePlain = async (body: Uint8Array, headers: RichHeaders): Promise => { + const rootContentType = 'application/pkcs7-mime; name="smime.p7m"; smime-type=enveloped-data'; + const rootNode = new MimeBuilder(rootContentType, { includeBccInHeader: true }); // tslint:disable-line:no-unsafe-any + for (const key of Object.keys(headers)) { + rootNode.addHeader(key, headers[key]); // tslint:disable-line:no-unsafe-any + } + rootNode.setContent(body); + rootNode.addHeader('Content-Transfer-Encoding', 'base64'); // tslint:disable-line:no-unsafe-any + rootNode.addHeader('Content-Disposition', 'attachment; filename="smime.p7m"'); + rootNode.addHeader('Content-Description', 'S/MIME Encrypted Message'); + return rootNode.build(); // tslint:disable-line:no-unsafe-any + } + public static subjectWithoutPrefixes = (subject: string): string => { return subject.replace(/^((Re|Fwd): ?)+/g, '').trim(); } diff --git a/extension/js/common/core/pgp-key.ts b/extension/js/common/core/pgp-key.ts index d797e615bd9..75e73f753da 100644 --- a/extension/js/common/core/pgp-key.ts +++ b/extension/js/common/core/pgp-key.ts @@ -352,6 +352,10 @@ export class PgpKey { * This is used to figure out how recently was key updated, and if one key is newer than other. */ public static lastSig = async (key: OpenPGP.key.Key): Promise => { + // key is undefined only for X.509 keys + if (!key) { + return Date.now(); + } await key.getExpirationTime(); // will force all sigs to be verified const allSignatures: OpenPGP.packet.Signature[] = []; for (const user of key.users) { diff --git a/extension/js/common/core/pgp-msg.ts b/extension/js/common/core/pgp-msg.ts index f80cdff8d25..309df346e8d 100644 --- a/extension/js/common/core/pgp-msg.ts +++ b/extension/js/common/core/pgp-msg.ts @@ -2,6 +2,7 @@ 'use strict'; +import * as forge from 'node-forge'; import { Contact, KeyInfo, PgpKey, PrvKeyInfo } from './pgp-key.js'; import { MsgBlockType, ReplaceableMsgBlockType } from './msg-block.js'; import { Value } from './common.js'; @@ -25,7 +26,8 @@ export namespace PgpMsgMethod { export type VerifyDetached = (arg: Arg.VerifyDetached) => Promise; export type Decrypt = (arg: Arg.Decrypt) => Promise; export type Type = (arg: Arg.Type) => Promise; - export type Encrypt = (arg: Arg.Encrypt) => Promise; + export type X509EncryptResult = { data: Uint8Array }; + export type Encrypt = (arg: Arg.Encrypt) => Promise; } type SortedKeysForDecrypt = { @@ -211,6 +213,38 @@ export class PgpMsg { } public static encrypt: PgpMsgMethod.Encrypt = async ({ pubkeys, signingPrv, pwd, data, filename, armor, date }) => { + // slice(1) filters first key that is always own OpenPGP key + // in result if X.509 encryption is selected the e-mail author + // cannot decrypt e-mails that they have sent + const keyTypes = new Set(pubkeys.slice(1).map(PgpMsg.getKeyType)); + if (keyTypes.size > 1) { + throw new Error('Mixed key types are not allowed: ' + [...keyTypes]); + } + const keyType = keyTypes.keys().next().value; + if (keyType === 'x509') { + const p7 = forge.pkcs7.createEnvelopedData(); + + for (const pubkey of pubkeys.slice(1)) { + p7.addRecipient(forge.pki.certificateFromPem(pubkey)); + } + + const headers = `Subject: test`; + + p7.content = forge.util.createBuffer(headers + '\r\n\r\n' + data); + + p7.encrypt(); + + const derBuffer = forge.asn1.toDer(p7.toAsn1()).getBytes(); + + const arr = []; + for (let i = 0, j = derBuffer.length; i < j; ++i) { + arr.push(derBuffer.charCodeAt(i)); + } + + return { + data: new Uint8Array(arr) + }; + } const message = opgp.message.fromBinary(data, filename, date); const options: OpenPGP.EncryptOptions = { armor, message, date }; let usedChallenge = false; @@ -253,6 +287,16 @@ export class PgpMsg { return diagnosis; } + private static getKeyType(pubkey: string): 'openpgp' | 'x509' { + if (pubkey.startsWith('-----BEGIN CERTIFICATE-----')) { + return 'x509'; + } else if (pubkey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')) { + return 'openpgp'; + } else { + throw new Error('Unknown key type: ' + pubkey); + } + } + private static cryptoMsgGetSignedBy = async (msg: OpenpgpMsgOrCleartext, keys: SortedKeysForDecrypt) => { keys.signedBy = Value.arr.unique(await PgpKey.longids(msg.getSigningKeyIds ? msg.getSigningKeyIds() : [])); if (keys.signedBy.length && typeof ContactStore.get === 'function') { diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 135c86fc2e6..8bce5581141 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -103,6 +103,28 @@ export class ContactStore extends AbstractStore { expiresOn: null }; } + // X.509 certificate + if (pubkey.startsWith('-----BEGIN CERTIFICATE-----')) { + // FIXME: For now we return random data. + // Later we'll return serial ID from the certificate. + const longid = Math.random() + ''; + return { + email: validEmail, + name: name || null, + pubkey, + has_pgp: 1, // number because we use it for sorting + searchable: ContactStore.dbCreateSearchIndexList(validEmail, name || null, true), + client: ContactStore.storablePgpClient(client || 'pgp'), + fingerprint: Math.random() + '', + longid, + longids: [longid], + pending_lookup: 0, + last_use: lastUse || null, + pubkey_last_sig: lastSig || null, + pubkey_last_check: lastCheck || null, + expiresOn: null + }; + } const k = await PgpKey.read(pubkey); if (!k) { throw new Error(`Could not read pubkey as valid OpenPGP key for: ${validEmail}`); diff --git a/extension/js/common/ui/key-import-ui.ts b/extension/js/common/ui/key-import-ui.ts index ebce7624cc5..cfcada513ee 100644 --- a/extension/js/common/ui/key-import-ui.ts +++ b/extension/js/common/ui/key-import-ui.ts @@ -146,6 +146,10 @@ export class KeyImportUi { } public checkPub = async (armored: string): Promise => { + // For X.509 keys assume they're good if they start with the marker string + if (armored.startsWith('-----BEGIN CERTIFICATE-----')) { + return armored; + } const { normalized } = await this.normalize('publicKey', armored); const parsed = await this.read('publicKey', normalized); await this.longid(parsed); From c4842e10be54bf4858b52e2926b993570f148f78 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Thu, 2 Apr 2020 16:04:54 +0200 Subject: [PATCH 04/24] Add node-forge.d.ts to test files --- conf/tsconfig.test.json | 1 + 1 file changed, 1 insertion(+) diff --git a/conf/tsconfig.test.json b/conf/tsconfig.test.json index 3ca7dabbe10..3b2bebf4fd8 100644 --- a/conf/tsconfig.test.json +++ b/conf/tsconfig.test.json @@ -13,6 +13,7 @@ "outDir": "../build/test" }, "files": [ + "../extension/types/node-forge.d.ts", "../test/source/test.ts", "../test/source/patterns.ts", "../test/source/async-stack.ts", From b7b54e3970de31832d94563f46391592edb4a29f Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Tue, 7 Apr 2020 09:11:06 +0200 Subject: [PATCH 05/24] Move getKeyType to PgpKey --- extension/js/common/core/pgp-key.ts | 10 ++++++++++ extension/js/common/core/pgp-msg.ts | 12 +----------- extension/js/common/platform/store/contact-store.ts | 2 +- extension/js/common/ui/key-import-ui.ts | 3 +-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/extension/js/common/core/pgp-key.ts b/extension/js/common/core/pgp-key.ts index 75e73f753da..d59e1f97df6 100644 --- a/extension/js/common/core/pgp-key.ts +++ b/extension/js/common/core/pgp-key.ts @@ -385,4 +385,14 @@ export class PgpKey { return await opgp.stream.readToEnd(certificate); } } + + public static getKeyType(pubkey: string): 'openpgp' | 'x509' | 'unknown' { + if (pubkey.startsWith('-----BEGIN CERTIFICATE-----')) { + return 'x509'; + } else if (pubkey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')) { + return 'openpgp'; + } else { + return 'unknown'; + } + } } diff --git a/extension/js/common/core/pgp-msg.ts b/extension/js/common/core/pgp-msg.ts index 309df346e8d..4c04771d580 100644 --- a/extension/js/common/core/pgp-msg.ts +++ b/extension/js/common/core/pgp-msg.ts @@ -216,7 +216,7 @@ export class PgpMsg { // slice(1) filters first key that is always own OpenPGP key // in result if X.509 encryption is selected the e-mail author // cannot decrypt e-mails that they have sent - const keyTypes = new Set(pubkeys.slice(1).map(PgpMsg.getKeyType)); + const keyTypes = new Set(pubkeys.slice(1).map(PgpKey.getKeyType)); if (keyTypes.size > 1) { throw new Error('Mixed key types are not allowed: ' + [...keyTypes]); } @@ -287,16 +287,6 @@ export class PgpMsg { return diagnosis; } - private static getKeyType(pubkey: string): 'openpgp' | 'x509' { - if (pubkey.startsWith('-----BEGIN CERTIFICATE-----')) { - return 'x509'; - } else if (pubkey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')) { - return 'openpgp'; - } else { - throw new Error('Unknown key type: ' + pubkey); - } - } - private static cryptoMsgGetSignedBy = async (msg: OpenpgpMsgOrCleartext, keys: SortedKeysForDecrypt) => { keys.signedBy = Value.arr.unique(await PgpKey.longids(msg.getSigningKeyIds ? msg.getSigningKeyIds() : [])); if (keys.signedBy.length && typeof ContactStore.get === 'function') { diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 8bce5581141..c7b8b73031a 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -104,7 +104,7 @@ export class ContactStore extends AbstractStore { }; } // X.509 certificate - if (pubkey.startsWith('-----BEGIN CERTIFICATE-----')) { + if (PgpKey.getKeyType(pubkey) === 'x509') { // FIXME: For now we return random data. // Later we'll return serial ID from the certificate. const longid = Math.random() + ''; diff --git a/extension/js/common/ui/key-import-ui.ts b/extension/js/common/ui/key-import-ui.ts index cfcada513ee..c0a38f16b5e 100644 --- a/extension/js/common/ui/key-import-ui.ts +++ b/extension/js/common/ui/key-import-ui.ts @@ -146,8 +146,7 @@ export class KeyImportUi { } public checkPub = async (armored: string): Promise => { - // For X.509 keys assume they're good if they start with the marker string - if (armored.startsWith('-----BEGIN CERTIFICATE-----')) { + if (PgpKey.getKeyType(armored) === 'x509') { return armored; } const { normalized } = await this.normalize('publicKey', armored); From d2040d8c31c5cbb544aa518163598d76f4514edc Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Tue, 7 Apr 2020 11:38:18 +0200 Subject: [PATCH 06/24] Extract S/MIME function into a separate module --- .../encrypted-mail-msg-formatter.ts | 24 +++++----- .../common/api/email-provider/sendable-msg.ts | 13 ++++-- extension/js/common/core/mime.ts | 13 ++++-- extension/js/common/core/pgp-msg.ts | 46 ++++++++----------- extension/js/common/core/smime.ts | 18 ++++++++ 5 files changed, 67 insertions(+), 47 deletions(-) create mode 100644 extension/js/common/core/smime.ts diff --git a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts index dc17725e1eb..1b086e548e7 100644 --- a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts @@ -14,7 +14,7 @@ import { Buf } from '../../../../js/common/core/buf.js'; import { Catch } from '../../../../js/common/platform/catch.js'; import { Lang } from '../../../../js/common/lang.js'; import { PgpKey } from '../../../../js/common/core/pgp-key.js'; -import { PgpMsg } from '../../../../js/common/core/pgp-msg.js'; +import { PgpMsg, PgpMsgMethod } from '../../../../js/common/core/pgp-msg.js'; import { SendableMsg } from '../../../../js/common/api/email-provider/sendable-msg.js'; import { Settings } from '../../../../js/common/settings.js'; import { Ui } from '../../../../js/common/browser/ui.js'; @@ -23,6 +23,8 @@ import { opgp } from '../../../../js/common/core/pgp.js'; import { ContactStore } from '../../../../js/common/platform/store/contact-store.js'; import { AcctStore } from '../../../../js/common/platform/store/acct-store.js'; +type DataArmorResult = PgpMsgMethod.OpenPGPEncryptArmorResult | PgpMsgMethod.X509EncryptResult; + export class EncryptedMsgMailFormatter extends BaseMailFormatter { public sendableMsg = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise => { @@ -43,7 +45,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { const authInfo = await AcctStore.authInfo(this.acctEmail); const msgBodyWithReplyToken = await this.getPwdMsgSendableBodyWithOnlineReplyMsgToken(authInfo, newMsg); const pgpMimeWithAtts = await Mime.encode(msgBodyWithReplyToken, { Subject: newMsg.subject }, await this.view.attsModule.attach.collectAtts()); - const pwdEncryptedWithAtts = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAtts), newMsg.pwd, []); // encrypted only for pwd, not signed + const { data: pwdEncryptedWithAtts } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAtts), newMsg.pwd, []); // encrypted only for pwd, not signed const { short, admin_code } = await Backend.messageUpload( authInfo.uuid ? authInfo : undefined, pwdEncryptedWithAtts, @@ -57,7 +59,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { // encoded as: PGP/MIME-like structure but with attachments as external files due to email size limit (encrypted for pubkeys only) const msgBody = this.richtext ? { 'text/plain': newMsg.plaintext, 'text/html': newMsg.plainhtml } : { 'text/plain': newMsg.plaintext }; const pgpMimeNoAtts = await Mime.encode(msgBody, { Subject: newMsg.subject }, []); // no atts, attached to email separately - const pubEncryptedNoAtts = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeNoAtts), undefined, pubs, signingPrv); // encrypted only for pubs + const { data: pubEncryptedNoAtts } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeNoAtts), undefined, pubs, signingPrv); // encrypted only for pubs const atts = this.createPgpMimeAtts(pubEncryptedNoAtts).concat(await this.view.attsModule.attach.collectEncryptAtts(pubs.map(p => p.pubkey))); // encrypted only for pubs const emailIntroAndLinkBody = await this.formatPwdEncryptedMsgBodyLink(short); return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: emailIntroAndLinkBody, atts, isDraft: this.isDraft }); @@ -65,15 +67,15 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { private sendableSimpleTextMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key) => { const atts = this.isDraft ? [] : await this.view.attsModule.attach.collectEncryptAtts(pubs.map(p => p.pubkey)); - const encrypted = await this.encryptDataArmor(Buf.fromUtfStr(newMsg.plaintext), undefined, pubs, signingPrv); - const encryptedBody = encrypted instanceof Buf ? { 'text/plain': encrypted.toString() } : encrypted; - return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: encryptedBody, atts, isDraft: this.isDraft }); + const { data: encryptedBody, type } = await this.encryptDataArmor(Buf.fromUtfStr(newMsg.plaintext), undefined, pubs, signingPrv); + const mimeType = type === 'smime' ? 'smimePlain' : undefined; + return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: { "encrypted/buf": encryptedBody }, type: mimeType, atts, isDraft: this.isDraft }); } private sendableRichTextMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key) => { const plainAtts = this.isDraft ? [] : await this.view.attsModule.attach.collectAtts(); const pgpMimeToEncrypt = await Mime.encode({ 'text/plain': newMsg.plaintext, 'text/html': newMsg.plainhtml }, { Subject: newMsg.subject }, plainAtts); - const encrypted = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeToEncrypt), undefined, pubs, signingPrv); + const { data: encrypted } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeToEncrypt), undefined, pubs, signingPrv); const atts = this.createPgpMimeAtts(encrypted); return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: {}, atts, type: 'pgpMimeEncrypted', isDraft: this.isDraft }); } @@ -85,10 +87,10 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { return atts; } - private encryptDataArmor = async (data: Buf, pwd: string | undefined, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise => { - const encryptAsOfDate = await this.encryptMsgAsOfDateIfSomeAreExpiredAndUserConfirmedModal([]); - const r = await PgpMsg.encrypt({ pubkeys: pubs.map(p => p.pubkey), signingPrv, pwd, data, armor: true, date: encryptAsOfDate }) as OpenPGP.EncryptArmorResult; - return typeof r.data === 'string' ? Buf.fromUtfStr(r.data) : r.data; + private encryptDataArmor = async (data: Buf, pwd: string | undefined, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise => { + const pgpPubs = pubs.filter(pub => PgpKey.getKeyType(pub.pubkey) === 'openpgp'); + const encryptAsOfDate = await this.encryptMsgAsOfDateIfSomeAreExpiredAndUserConfirmedModal(pgpPubs); + return (await PgpMsg.encrypt({ pubkeys: pubs.map(p => p.pubkey), signingPrv, pwd, data, armor: true, date: encryptAsOfDate }) as DataArmorResult); } private getPwdMsgSendableBodyWithOnlineReplyMsgToken = async (authInfo: FcUuidAuth, newMsgData: NewMsgData): Promise => { diff --git a/extension/js/common/api/email-provider/sendable-msg.ts b/extension/js/common/api/email-provider/sendable-msg.ts index d3de8da5acc..a67fa03520e 100644 --- a/extension/js/common/api/email-provider/sendable-msg.ts +++ b/extension/js/common/api/email-provider/sendable-msg.ts @@ -16,7 +16,7 @@ type SendableMsgDefinition = { from: string; recipients: Recipients; subject: string; - body: SendableMsgBody | Uint8Array; + body: SendableMsgBody; atts: Att[]; thread?: string; type?: MimeEncodeType, @@ -51,7 +51,7 @@ export class SendableMsg { public from: string, public recipients: Recipients, public subject: string, - public body: SendableMsgBody | Uint8Array, + public body: SendableMsgBody, public atts: Att[], public thread: string | undefined, public type: MimeEncodeType, @@ -83,11 +83,16 @@ export class SendableMsg { } } this.headers.Subject = this.subject; - if (this.body instanceof Uint8Array) { - return await Mime.encodePlain(this.body, this.headers); + if (this.type === 'smimePlain' && this.body['encrypted/buf']) { + return await Mime.encodePlain(this.body['encrypted/buf'], this.headers); } else if (this.type === 'pgpMimeSigned' && this.sign) { return await Mime.encodePgpMimeSigned(this.body, this.headers, this.atts, this.sign); } else { + // encrypted/buf is a Buf instance that is converted to one part text/plain + // message + if (this.body['encrypted/buf']) { + this.body = { 'text/plain': this.body['encrypted/buf'].toString() }; + } return await Mime.encode(this.body, this.headers, this.atts, this.type); } } diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index 7bed9776049..d665c611acb 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -34,9 +34,14 @@ export type MimeContent = { bcc: string[]; }; -export type MimeEncodeType = 'pgpMimeEncrypted' | 'pgpMimeSigned' | undefined; +export type MimeEncodeType = 'pgpMimeEncrypted' | 'pgpMimeSigned' | 'smimePlain' | undefined; export type RichHeaders = Dict; -export type SendableMsgBody = { [key: string]: string | undefined; 'text/plain'?: string; 'text/html'?: string; }; +export type SendableMsgBody = { + [key: string]: string | Uint8Array | undefined; + 'text/plain'?: string; + 'text/html'?: string; + 'encrypted/buf'?: Uint8Array; +}; export type MimeProccesedMsg = { rawSignedContent: string | undefined, headers: Dict, @@ -203,7 +208,7 @@ export class Mime { } else { contentNode = new MimeBuilder('multipart/alternative'); // tslint:disable-line:no-unsafe-any for (const type of Object.keys(body)) { - contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, body[type]!)); // already present, that's why part of for loop + contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, String(body[type]!))); // already present, that's why part of for loop } } rootNode.appendChild(contentNode); // tslint:disable-line:no-unsafe-any @@ -239,7 +244,7 @@ export class Mime { } const bodyNodes = new MimeBuilder('multipart/alternative'); // tslint:disable-line:no-unsafe-any for (const type of Object.keys(body)) { - bodyNodes.appendChild(Mime.newContentNode(MimeBuilder, type, body[type]!)); // tslint:disable-line:no-unsafe-any + bodyNodes.appendChild(Mime.newContentNode(MimeBuilder, type, String(body[type]!))); // tslint:disable-line:no-unsafe-any } const signedContentNode = new MimeBuilder('multipart/mixed'); // tslint:disable-line:no-unsafe-any signedContentNode.appendChild(bodyNodes); // tslint:disable-line:no-unsafe-any diff --git a/extension/js/common/core/pgp-msg.ts b/extension/js/common/core/pgp-msg.ts index 4c04771d580..cf59dbe4e85 100644 --- a/extension/js/common/core/pgp-msg.ts +++ b/extension/js/common/core/pgp-msg.ts @@ -1,8 +1,6 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ 'use strict'; - -import * as forge from 'node-forge'; import { Contact, KeyInfo, PgpKey, PrvKeyInfo } from './pgp-key.js'; import { MsgBlockType, ReplaceableMsgBlockType } from './msg-block.js'; import { Value } from './common.js'; @@ -13,6 +11,7 @@ import { PgpHash } from './pgp-hash.js'; import { opgp } from './pgp.js'; import { KeyCache } from '../platform/key-cache.js'; import { ContactStore } from '../platform/store/contact-store.js'; +import { encrypt as smimeEncrypt } from './smime.js'; export namespace PgpMsgMethod { export namespace Arg { @@ -26,8 +25,17 @@ export namespace PgpMsgMethod { export type VerifyDetached = (arg: Arg.VerifyDetached) => Promise; export type Decrypt = (arg: Arg.Decrypt) => Promise; export type Type = (arg: Arg.Type) => Promise; - export type X509EncryptResult = { data: Uint8Array }; - export type Encrypt = (arg: Arg.Encrypt) => Promise; + export type Encrypt = (arg: Arg.Encrypt) => Promise; + export type OpenPGPEncryptResult = OpenPGPEncryptArmorResult | OpenPGP.EncryptBinaryResult; + export interface OpenPGPEncryptArmorResult { + data: Uint8Array; + signature?: string; + type: 'openpgp'; + } + export type X509EncryptResult = { + data: Uint8Array; + type: 'smime'; + }; } type SortedKeysForDecrypt = { @@ -216,34 +224,15 @@ export class PgpMsg { // slice(1) filters first key that is always own OpenPGP key // in result if X.509 encryption is selected the e-mail author // cannot decrypt e-mails that they have sent - const keyTypes = new Set(pubkeys.slice(1).map(PgpKey.getKeyType)); + const otherKeys = pubkeys.slice(1); + // tslint:disable-next-line: no-unbound-method + const keyTypes = new Set(otherKeys.map(PgpKey.getKeyType)); if (keyTypes.size > 1) { throw new Error('Mixed key types are not allowed: ' + [...keyTypes]); } const keyType = keyTypes.keys().next().value; if (keyType === 'x509') { - const p7 = forge.pkcs7.createEnvelopedData(); - - for (const pubkey of pubkeys.slice(1)) { - p7.addRecipient(forge.pki.certificateFromPem(pubkey)); - } - - const headers = `Subject: test`; - - p7.content = forge.util.createBuffer(headers + '\r\n\r\n' + data); - - p7.encrypt(); - - const derBuffer = forge.asn1.toDer(p7.toAsn1()).getBytes(); - - const arr = []; - for (let i = 0, j = derBuffer.length; i < j; ++i) { - arr.push(derBuffer.charCodeAt(i)); - } - - return { - data: new Uint8Array(arr) - }; + return smimeEncrypt(otherKeys, data); } const message = opgp.message.fromBinary(data, filename, date); const options: OpenPGP.EncryptOptions = { armor, message, date }; @@ -265,7 +254,8 @@ export class PgpMsg { if (signingPrv && typeof signingPrv.isPrivate !== 'undefined' && signingPrv.isPrivate()) { // tslint:disable-line:no-unbound-method - only testing if exists options.privateKeys = [signingPrv]; } - return await opgp.encrypt(options); + const result = await opgp.encrypt(options); + return { data: Buf.fromUtfStr(result.data), signature: result.signature, type: 'openpgp' }; } public static diagnosePubkeys: PgpMsgMethod.DiagnosePubkeys = async ({ privateKis, message }) => { diff --git a/extension/js/common/core/smime.ts b/extension/js/common/core/smime.ts new file mode 100644 index 00000000000..c36c6b50313 --- /dev/null +++ b/extension/js/common/core/smime.ts @@ -0,0 +1,18 @@ +/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ +import * as forge from 'node-forge'; + +export const encrypt = (pubkeys: string[], data: Uint8Array): { data: Uint8Array, type: 'smime' } => { + const p7 = forge.pkcs7.createEnvelopedData(); + for (const pubkey of pubkeys) { + p7.addRecipient(forge.pki.certificateFromPem(pubkey)); + } + const headers = `Content-Type: text/plain`; + p7.content = forge.util.createBuffer(headers + '\r\n\r\n' + data); + p7.encrypt(); + const derBuffer = forge.asn1.toDer(p7.toAsn1()).getBytes(); + const arr = []; + for (let i = 0, j = derBuffer.length; i < j; ++i) { + arr.push(derBuffer.charCodeAt(i)); + } + return { data: new Uint8Array(arr), type: 'smime' }; +}; From 240d1be773f6a2234168bd173ae901f70eef08a5 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Tue, 7 Apr 2020 12:09:27 +0200 Subject: [PATCH 07/24] Suppress tslint errors --- extension/js/common/core/mime.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index d665c611acb..292149a7eee 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -225,10 +225,10 @@ export class Mime { for (const key of Object.keys(headers)) { rootNode.addHeader(key, headers[key]); // tslint:disable-line:no-unsafe-any } - rootNode.setContent(body); + rootNode.setContent(body); // tslint:disable-line:no-unsafe-any rootNode.addHeader('Content-Transfer-Encoding', 'base64'); // tslint:disable-line:no-unsafe-any - rootNode.addHeader('Content-Disposition', 'attachment; filename="smime.p7m"'); - rootNode.addHeader('Content-Description', 'S/MIME Encrypted Message'); + rootNode.addHeader('Content-Disposition', 'attachment; filename="smime.p7m"'); // tslint:disable-line:no-unsafe-any + rootNode.addHeader('Content-Description', 'S/MIME Encrypted Message'); // tslint:disable-line:no-unsafe-any return rootNode.build(); // tslint:disable-line:no-unsafe-any } From e621b60570fe7775bf73e02c1729cc1772984d4f Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Tue, 7 Apr 2020 12:45:15 +0200 Subject: [PATCH 08/24] Convert to arrow function --- extension/js/common/core/pgp-key.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/js/common/core/pgp-key.ts b/extension/js/common/core/pgp-key.ts index d59e1f97df6..c887417662d 100644 --- a/extension/js/common/core/pgp-key.ts +++ b/extension/js/common/core/pgp-key.ts @@ -386,7 +386,7 @@ export class PgpKey { } } - public static getKeyType(pubkey: string): 'openpgp' | 'x509' | 'unknown' { + public static getKeyType = (pubkey: string): 'openpgp' | 'x509' | 'unknown' => { if (pubkey.startsWith('-----BEGIN CERTIFICATE-----')) { return 'x509'; } else if (pubkey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')) { From 1d99df748eea0f64897c16b37ecfd293ad4cc7f0 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Tue, 7 Apr 2020 13:30:49 +0200 Subject: [PATCH 09/24] Wrap in buf only for armored output --- extension/js/common/core/pgp-msg.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extension/js/common/core/pgp-msg.ts b/extension/js/common/core/pgp-msg.ts index cf59dbe4e85..402810f1fdd 100644 --- a/extension/js/common/core/pgp-msg.ts +++ b/extension/js/common/core/pgp-msg.ts @@ -255,7 +255,10 @@ export class PgpMsg { options.privateKeys = [signingPrv]; } const result = await opgp.encrypt(options); - return { data: Buf.fromUtfStr(result.data), signature: result.signature, type: 'openpgp' }; + if (typeof result.data === 'string') { + return { data: Buf.fromUtfStr(result.data), signature: result.signature, type: 'openpgp' }; + } + return result; } public static diagnosePubkeys: PgpMsgMethod.DiagnosePubkeys = async ({ privateKis, message }) => { From 3d2c56659848c14bf8db1994c41bbe6ace0712d4 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Tue, 7 Apr 2020 13:53:05 +0200 Subject: [PATCH 10/24] Return EncryptBinaryResult when `data` is not a string --- extension/js/common/core/pgp-msg.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extension/js/common/core/pgp-msg.ts b/extension/js/common/core/pgp-msg.ts index 402810f1fdd..05dbbde1074 100644 --- a/extension/js/common/core/pgp-msg.ts +++ b/extension/js/common/core/pgp-msg.ts @@ -254,11 +254,12 @@ export class PgpMsg { if (signingPrv && typeof signingPrv.isPrivate !== 'undefined' && signingPrv.isPrivate()) { // tslint:disable-line:no-unbound-method - only testing if exists options.privateKeys = [signingPrv]; } - const result = await opgp.encrypt(options); + const result: OpenPGP.EncryptBinaryResult | OpenPGP.EncryptArmorResult = await opgp.encrypt(options); if (typeof result.data === 'string') { return { data: Buf.fromUtfStr(result.data), signature: result.signature, type: 'openpgp' }; + } else { + return result as unknown as OpenPGP.EncryptBinaryResult; } - return result; } public static diagnosePubkeys: PgpMsgMethod.DiagnosePubkeys = async ({ privateKis, message }) => { From e44b66c2627b1802b0a9446c1080cc3cde46ab36 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 03:55:03 +0000 Subject: [PATCH 11/24] rm unnecessary check --- .../elements/compose-modules/compose-draft-module.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-draft-module.ts b/extension/chrome/elements/compose-modules/compose-draft-module.ts index fa9a9198ac1..a6cf3987fcb 100644 --- a/extension/chrome/elements/compose-modules/compose-draft-module.ts +++ b/extension/chrome/elements/compose-modules/compose-draft-module.ts @@ -104,12 +104,10 @@ export class ComposeDraftModule extends ViewModule { msgData.pwd = undefined; // not needed for drafts const sendable = await new EncryptedMsgMailFormatter(this.view, true).sendableMsg(msgData, pubkeys); this.view.S.cached('send_btn_note').text('Saving'); - if (!(sendable.body instanceof Uint8Array)) { - if (this.view.threadId) { // reply draft - sendable.body['text/plain'] = `[cryptup:link:draft_reply:${this.view.threadId}]\n\n${sendable.body['text/plain'] || ''}`; - } else if (this.view.draftId) { // new message compose draft with known draftid - sendable.body['text/plain'] = `[cryptup:link:draft_compose:${this.view.draftId}]\n\n${sendable.body['text/plain'] || ''}`; - } + if (this.view.threadId) { // reply draft + sendable.body['text/plain'] = `[cryptup:link:draft_reply:${this.view.threadId}]\n\n${sendable.body['text/plain'] || ''}`; + } else if (this.view.draftId) { // new message compose draft with known draftid + sendable.body['text/plain'] = `[cryptup:link:draft_compose:${this.view.draftId}]\n\n${sendable.body['text/plain'] || ''}`; } const mimeMsg = await sendable.toMime(); if (!this.view.draftId) { From 0969e213283b66f32bb37f3dc50389c3807721e2 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 04:00:30 +0000 Subject: [PATCH 12/24] move type def --- .../formatters/encrypted-mail-msg-formatter.ts | 6 ++---- extension/js/common/core/pgp-msg.ts | 9 +++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts index 1b086e548e7..98b17fb84b5 100644 --- a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts @@ -23,8 +23,6 @@ import { opgp } from '../../../../js/common/core/pgp.js'; import { ContactStore } from '../../../../js/common/platform/store/contact-store.js'; import { AcctStore } from '../../../../js/common/platform/store/acct-store.js'; -type DataArmorResult = PgpMsgMethod.OpenPGPEncryptArmorResult | PgpMsgMethod.X509EncryptResult; - export class EncryptedMsgMailFormatter extends BaseMailFormatter { public sendableMsg = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise => { @@ -87,10 +85,10 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { return atts; } - private encryptDataArmor = async (data: Buf, pwd: string | undefined, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise => { + private encryptDataArmor = async (data: Buf, pwd: string | undefined, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise => { const pgpPubs = pubs.filter(pub => PgpKey.getKeyType(pub.pubkey) === 'openpgp'); const encryptAsOfDate = await this.encryptMsgAsOfDateIfSomeAreExpiredAndUserConfirmedModal(pgpPubs); - return (await PgpMsg.encrypt({ pubkeys: pubs.map(p => p.pubkey), signingPrv, pwd, data, armor: true, date: encryptAsOfDate }) as DataArmorResult); + return await PgpMsg.encrypt({ pubkeys: pubs.map(p => p.pubkey), signingPrv, pwd, data, armor: true, date: encryptAsOfDate }) as PgpMsgMethod.EncryptAnyArmorResult; } private getPwdMsgSendableBodyWithOnlineReplyMsgToken = async (authInfo: FcUuidAuth, newMsgData: NewMsgData): Promise => { diff --git a/extension/js/common/core/pgp-msg.ts b/extension/js/common/core/pgp-msg.ts index 05dbbde1074..866b7c54a5b 100644 --- a/extension/js/common/core/pgp-msg.ts +++ b/extension/js/common/core/pgp-msg.ts @@ -25,14 +25,15 @@ export namespace PgpMsgMethod { export type VerifyDetached = (arg: Arg.VerifyDetached) => Promise; export type Decrypt = (arg: Arg.Decrypt) => Promise; export type Type = (arg: Arg.Type) => Promise; - export type Encrypt = (arg: Arg.Encrypt) => Promise; - export type OpenPGPEncryptResult = OpenPGPEncryptArmorResult | OpenPGP.EncryptBinaryResult; - export interface OpenPGPEncryptArmorResult { + export type Encrypt = (arg: Arg.Encrypt) => Promise; + export type EncryptPgpResult = EncryptPgpArmorResult | OpenPGP.EncryptBinaryResult; + export type EncryptAnyArmorResult = PgpMsgMethod.EncryptPgpArmorResult | EncryptX509Result; + export interface EncryptPgpArmorResult { data: Uint8Array; signature?: string; type: 'openpgp'; } - export type X509EncryptResult = { + export type EncryptX509Result = { data: Uint8Array; type: 'smime'; }; From 4035d00c2d6c4834fc0fdc5126b1ea68aab3b7d4 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 04:01:29 +0000 Subject: [PATCH 13/24] rm unneeded toString --- extension/js/common/api/email-provider/gmail/gmail.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/js/common/api/email-provider/gmail/gmail.ts b/extension/js/common/api/email-provider/gmail/gmail.ts index 9a552113201..254b7ca6858 100644 --- a/extension/js/common/api/email-provider/gmail/gmail.ts +++ b/extension/js/common/api/email-provider/gmail/gmail.ts @@ -279,7 +279,7 @@ export class Gmail extends EmailProviderApi implements EmailProviderInterface { if (format === 'full') { const bodies = GmailParser.findBodies(gmailMsg); const atts = GmailParser.findAtts(gmailMsg); - const fromTextBody = PgpArmor.clip(Buf.fromBase64UrlStr(bodies['text/plain']?.toString() || '').toUtfStr()); + const fromTextBody = PgpArmor.clip(Buf.fromBase64UrlStr(bodies['text/plain'] || '').toUtfStr()); if (fromTextBody) { return { armored: fromTextBody, subject, isPwdMsg }; } From cd558fab6b410e80a486454eee6bae1a9c8c0a97 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 04:03:34 +0000 Subject: [PATCH 14/24] rename smimePlain to smimeEncrypted --- .../formatters/encrypted-mail-msg-formatter.ts | 2 +- extension/js/common/api/email-provider/sendable-msg.ts | 4 ++-- extension/js/common/core/mime.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts index 98b17fb84b5..3e522f3ab84 100644 --- a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts @@ -66,7 +66,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { private sendableSimpleTextMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key) => { const atts = this.isDraft ? [] : await this.view.attsModule.attach.collectEncryptAtts(pubs.map(p => p.pubkey)); const { data: encryptedBody, type } = await this.encryptDataArmor(Buf.fromUtfStr(newMsg.plaintext), undefined, pubs, signingPrv); - const mimeType = type === 'smime' ? 'smimePlain' : undefined; + const mimeType = type === 'smime' ? 'smimeEncrypted' : undefined; return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: { "encrypted/buf": encryptedBody }, type: mimeType, atts, isDraft: this.isDraft }); } diff --git a/extension/js/common/api/email-provider/sendable-msg.ts b/extension/js/common/api/email-provider/sendable-msg.ts index a67fa03520e..89c98216e9b 100644 --- a/extension/js/common/api/email-provider/sendable-msg.ts +++ b/extension/js/common/api/email-provider/sendable-msg.ts @@ -83,8 +83,8 @@ export class SendableMsg { } } this.headers.Subject = this.subject; - if (this.type === 'smimePlain' && this.body['encrypted/buf']) { - return await Mime.encodePlain(this.body['encrypted/buf'], this.headers); + if (this.type === 'smimeEncrypted' && this.body['encrypted/buf']) { + return await Mime.encodeSmime(this.body['encrypted/buf'], this.headers); } else if (this.type === 'pgpMimeSigned' && this.sign) { return await Mime.encodePgpMimeSigned(this.body, this.headers, this.atts, this.sign); } else { diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index 292149a7eee..28175b9a901 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -34,7 +34,7 @@ export type MimeContent = { bcc: string[]; }; -export type MimeEncodeType = 'pgpMimeEncrypted' | 'pgpMimeSigned' | 'smimePlain' | undefined; +export type MimeEncodeType = 'pgpMimeEncrypted' | 'pgpMimeSigned' | 'smimeEncrypted' | undefined; export type RichHeaders = Dict; export type SendableMsgBody = { [key: string]: string | Uint8Array | undefined; @@ -219,7 +219,7 @@ export class Mime { return rootNode.build(); // tslint:disable-line:no-unsafe-any } - public static encodePlain = async (body: Uint8Array, headers: RichHeaders): Promise => { + public static encodeSmime = async (body: Uint8Array, headers: RichHeaders): Promise => { const rootContentType = 'application/pkcs7-mime; name="smime.p7m"; smime-type=enveloped-data'; const rootNode = new MimeBuilder(rootContentType, { includeBccInHeader: true }); // tslint:disable-line:no-unsafe-any for (const key of Object.keys(headers)) { From 99fa101f00e96660f415605ecf925d17183563fc Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 04:21:22 +0000 Subject: [PATCH 15/24] use Buf in SendableMsgBody --- .../formatters/encrypted-mail-msg-formatter.ts | 2 +- extension/js/common/core/mime.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts index 3e522f3ab84..d53d5dedf2d 100644 --- a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts @@ -67,7 +67,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { const atts = this.isDraft ? [] : await this.view.attsModule.attach.collectEncryptAtts(pubs.map(p => p.pubkey)); const { data: encryptedBody, type } = await this.encryptDataArmor(Buf.fromUtfStr(newMsg.plaintext), undefined, pubs, signingPrv); const mimeType = type === 'smime' ? 'smimeEncrypted' : undefined; - return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: { "encrypted/buf": encryptedBody }, type: mimeType, atts, isDraft: this.isDraft }); + return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: { "encrypted/buf": Buf.fromUint8(encryptedBody) }, type: mimeType, atts, isDraft: this.isDraft }); } private sendableRichTextMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key) => { diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index 28175b9a901..450a580a93a 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -37,10 +37,10 @@ export type MimeContent = { export type MimeEncodeType = 'pgpMimeEncrypted' | 'pgpMimeSigned' | 'smimeEncrypted' | undefined; export type RichHeaders = Dict; export type SendableMsgBody = { - [key: string]: string | Uint8Array | undefined; + [key: string]: string | Buf | undefined; 'text/plain'?: string; 'text/html'?: string; - 'encrypted/buf'?: Uint8Array; + 'encrypted/buf'?: Buf; }; export type MimeProccesedMsg = { rawSignedContent: string | undefined, @@ -244,7 +244,7 @@ export class Mime { } const bodyNodes = new MimeBuilder('multipart/alternative'); // tslint:disable-line:no-unsafe-any for (const type of Object.keys(body)) { - bodyNodes.appendChild(Mime.newContentNode(MimeBuilder, type, String(body[type]!))); // tslint:disable-line:no-unsafe-any + bodyNodes.appendChild(Mime.newContentNode(MimeBuilder, type, body[type]!.toString())); // tslint:disable-line:no-unsafe-any } const signedContentNode = new MimeBuilder('multipart/mixed'); // tslint:disable-line:no-unsafe-any signedContentNode.appendChild(bodyNodes); // tslint:disable-line:no-unsafe-any From bd4cd6e817fe669babc1018476967a558eb3a55b Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 04:23:32 +0000 Subject: [PATCH 16/24] remove unnecesary explicit type --- extension/js/common/core/pgp-msg.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/js/common/core/pgp-msg.ts b/extension/js/common/core/pgp-msg.ts index 866b7c54a5b..58c2f13a272 100644 --- a/extension/js/common/core/pgp-msg.ts +++ b/extension/js/common/core/pgp-msg.ts @@ -255,7 +255,7 @@ export class PgpMsg { if (signingPrv && typeof signingPrv.isPrivate !== 'undefined' && signingPrv.isPrivate()) { // tslint:disable-line:no-unbound-method - only testing if exists options.privateKeys = [signingPrv]; } - const result: OpenPGP.EncryptBinaryResult | OpenPGP.EncryptArmorResult = await opgp.encrypt(options); + const result = await opgp.encrypt(options); if (typeof result.data === 'string') { return { data: Buf.fromUtfStr(result.data), signature: result.signature, type: 'openpgp' }; } else { From 900592cbfc662daf4e0e3b614f8ce7525e79f7b8 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 05:54:49 +0000 Subject: [PATCH 17/24] todos --- extension/js/common/core/pgp-key.ts | 5 ++--- extension/js/common/ui/key-import-ui.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/extension/js/common/core/pgp-key.ts b/extension/js/common/core/pgp-key.ts index c887417662d..b4cf3c02244 100644 --- a/extension/js/common/core/pgp-key.ts +++ b/extension/js/common/core/pgp-key.ts @@ -352,9 +352,8 @@ export class PgpKey { * This is used to figure out how recently was key updated, and if one key is newer than other. */ public static lastSig = async (key: OpenPGP.key.Key): Promise => { - // key is undefined only for X.509 keys - if (!key) { - return Date.now(); + if (!key) { // key is undefined only for X.509 keys + return Date.now(); // todo - this definitely needs to be refactored soon #2731 } await key.getExpirationTime(); // will force all sigs to be verified const allSignatures: OpenPGP.packet.Signature[] = []; diff --git a/extension/js/common/ui/key-import-ui.ts b/extension/js/common/ui/key-import-ui.ts index c0a38f16b5e..f62b872fb96 100644 --- a/extension/js/common/ui/key-import-ui.ts +++ b/extension/js/common/ui/key-import-ui.ts @@ -147,7 +147,7 @@ export class KeyImportUi { public checkPub = async (armored: string): Promise => { if (PgpKey.getKeyType(armored) === 'x509') { - return armored; + return armored; // todo - check the key parameters, else it may throw later or cause other trouble } const { normalized } = await this.normalize('publicKey', armored); const parsed = await this.read('publicKey', normalized); From 58a9bc2010f36cd9a42db9a03bd09f5346c97161 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 06:26:55 +0000 Subject: [PATCH 18/24] more explicit recipient pub choosing --- .../encrypted-mail-msg-formatter.ts | 7 +++--- extension/js/common/core/pgp-key.ts | 22 ++++++++++++++++++- extension/js/common/core/pgp-msg.ts | 15 +++++-------- extension/js/common/ui/att-ui.ts | 7 ++++-- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts index d53d5dedf2d..1cf7eda642e 100644 --- a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts @@ -58,13 +58,13 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { const msgBody = this.richtext ? { 'text/plain': newMsg.plaintext, 'text/html': newMsg.plainhtml } : { 'text/plain': newMsg.plaintext }; const pgpMimeNoAtts = await Mime.encode(msgBody, { Subject: newMsg.subject }, []); // no atts, attached to email separately const { data: pubEncryptedNoAtts } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeNoAtts), undefined, pubs, signingPrv); // encrypted only for pubs - const atts = this.createPgpMimeAtts(pubEncryptedNoAtts).concat(await this.view.attsModule.attach.collectEncryptAtts(pubs.map(p => p.pubkey))); // encrypted only for pubs + const atts = this.createPgpMimeAtts(pubEncryptedNoAtts).concat(await this.view.attsModule.attach.collectEncryptAtts(pubs)); // encrypted only for pubs const emailIntroAndLinkBody = await this.formatPwdEncryptedMsgBodyLink(short); return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: emailIntroAndLinkBody, atts, isDraft: this.isDraft }); } private sendableSimpleTextMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key) => { - const atts = this.isDraft ? [] : await this.view.attsModule.attach.collectEncryptAtts(pubs.map(p => p.pubkey)); + const atts = this.isDraft ? [] : await this.view.attsModule.attach.collectEncryptAtts(pubs); const { data: encryptedBody, type } = await this.encryptDataArmor(Buf.fromUtfStr(newMsg.plaintext), undefined, pubs, signingPrv); const mimeType = type === 'smime' ? 'smimeEncrypted' : undefined; return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: { "encrypted/buf": Buf.fromUint8(encryptedBody) }, type: mimeType, atts, isDraft: this.isDraft }); @@ -88,7 +88,8 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { private encryptDataArmor = async (data: Buf, pwd: string | undefined, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise => { const pgpPubs = pubs.filter(pub => PgpKey.getKeyType(pub.pubkey) === 'openpgp'); const encryptAsOfDate = await this.encryptMsgAsOfDateIfSomeAreExpiredAndUserConfirmedModal(pgpPubs); - return await PgpMsg.encrypt({ pubkeys: pubs.map(p => p.pubkey), signingPrv, pwd, data, armor: true, date: encryptAsOfDate }) as PgpMsgMethod.EncryptAnyArmorResult; + const pubsForEncryption = PgpKey.choosePubsBasedOnKeyTypeCombinationForPartialSmimeSupport(pubs); + return await PgpMsg.encrypt({ pubkeys: pubsForEncryption, signingPrv, pwd, data, armor: true, date: encryptAsOfDate }) as PgpMsgMethod.EncryptAnyArmorResult; } private getPwdMsgSendableBodyWithOnlineReplyMsgToken = async (authInfo: FcUuidAuth, newMsgData: NewMsgData): Promise => { diff --git a/extension/js/common/core/pgp-key.ts b/extension/js/common/core/pgp-key.ts index b4cf3c02244..b6a87274a68 100644 --- a/extension/js/common/core/pgp-key.ts +++ b/extension/js/common/core/pgp-key.ts @@ -3,11 +3,12 @@ 'use strict'; import { Buf } from './buf.js'; -import { Catch } from '../platform/catch.js'; +import { Catch, UnreportableError } from '../platform/catch.js'; import { MsgBlockParser } from './msg-block-parser.js'; import { PgpArmor } from './pgp-armor.js'; import { opgp } from './pgp.js'; import { KeyCache } from '../platform/key-cache.js'; +import { PubkeyResult } from '../../../chrome/elements/compose-modules/compose-types.js'; export type Contact = { email: string; @@ -394,4 +395,23 @@ export class PgpKey { return 'unknown'; } } + + public static choosePubsBasedOnKeyTypeCombinationForPartialSmimeSupport = (pubs: PubkeyResult[]): string[] => { + const myPubs = pubs.filter(pub => pub.isMine); // currently this must be openpgp pub + const otherPgpPubs = pubs.filter(pub => !pub.isMine && PgpKey.getKeyType(pub.pubkey) === 'openpgp'); + const otherSmimePubs = pubs.filter(pub => !pub.isMine && PgpKey.getKeyType(pub.pubkey) === 'x509'); + if (otherPgpPubs.length && otherSmimePubs.length) { + let err = `Cannot use mixed OpenPGP (${otherPgpPubs.map(p => p.email).join(', ')}) and S/MIME (${otherSmimePubs.map(p => p.email).join(', ')}) public keys yet.`; + err += 'If you need to email S/MIME recipient, do not add any OpenPGP recipient at the same time.'; + throw new UnreportableError(err); + } + if (otherPgpPubs.length) { + return myPubs.concat(...otherPgpPubs).map(p => p.pubkey); + } + if (otherSmimePubs.length) { // todo - currently skipping my own pgp keys when encrypting message for S/MIME + return otherSmimePubs.map(pub => pub.pubkey); + } + return myPubs.map(p => p.pubkey); + } + } diff --git a/extension/js/common/core/pgp-msg.ts b/extension/js/common/core/pgp-msg.ts index 58c2f13a272..53e27a877e0 100644 --- a/extension/js/common/core/pgp-msg.ts +++ b/extension/js/common/core/pgp-msg.ts @@ -222,19 +222,14 @@ export class PgpMsg { } public static encrypt: PgpMsgMethod.Encrypt = async ({ pubkeys, signingPrv, pwd, data, filename, armor, date }) => { - // slice(1) filters first key that is always own OpenPGP key - // in result if X.509 encryption is selected the e-mail author - // cannot decrypt e-mails that they have sent - const otherKeys = pubkeys.slice(1); - // tslint:disable-next-line: no-unbound-method - const keyTypes = new Set(otherKeys.map(PgpKey.getKeyType)); - if (keyTypes.size > 1) { + const keyTypes = new Set(pubkeys.map(k => PgpKey.getKeyType(k))); + if (keyTypes.has('openpgp') && keyTypes.has('x509')) { throw new Error('Mixed key types are not allowed: ' + [...keyTypes]); } - const keyType = keyTypes.keys().next().value; - if (keyType === 'x509') { - return smimeEncrypt(otherKeys, data); + if (keyTypes.has('x509')) { + return smimeEncrypt(pubkeys, data); } + // todo - move above lines to an abstract method const message = opgp.message.fromBinary(data, filename, date); const options: OpenPGP.EncryptOptions = { armor, message, date }; let usedChallenge = false; diff --git a/extension/js/common/ui/att-ui.ts b/extension/js/common/ui/att-ui.ts index 187b92f2852..bcb1ddb11e0 100644 --- a/extension/js/common/ui/att-ui.ts +++ b/extension/js/common/ui/att-ui.ts @@ -7,6 +7,8 @@ import { Catch } from '../platform/catch.js'; import { Dict } from '../core/common.js'; import { PgpMsg } from '../core/pgp-msg.js'; import { Ui } from '../browser/ui.js'; +import { PubkeyResult } from '../../../chrome/elements/compose-modules/compose-types.js'; +import { PgpKey } from '../core/pgp-key.js'; declare const qq: any; @@ -80,12 +82,13 @@ export class AttUI { return atts; } - public collectEncryptAtts = async (pubkeys: string[]): Promise => { + public collectEncryptAtts = async (pubs: PubkeyResult[]): Promise => { const atts: Att[] = []; for (const uploadFileId of Object.keys(this.attachedFiles)) { const file = this.attachedFiles[uploadFileId]; const data = await this.readAttDataAsUint8(uploadFileId); - const encrypted = await PgpMsg.encrypt({ pubkeys, data, filename: file.name, armor: false }) as OpenPGP.EncryptBinaryResult; + const pubsForEncryption = PgpKey.choosePubsBasedOnKeyTypeCombinationForPartialSmimeSupport(pubs); + const encrypted = await PgpMsg.encrypt({ pubkeys: pubsForEncryption, data, filename: file.name, armor: false }) as OpenPGP.EncryptBinaryResult; atts.push(new Att({ name: file.name.replace(/[^a-zA-Z\-_.0-9]/g, '_').replace(/__+/g, '_') + '.pgp', type: file.type, data: encrypted.message.packets.write() })); } return atts; From 68b7ab09f1ae38ce99776d8c290c89f65fb9126d Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 06:58:03 +0000 Subject: [PATCH 19/24] move PubkeyResult to pgp-key --- extension/chrome/elements/compose-modules/compose-types.ts | 2 +- .../formatters/encrypted-mail-msg-formatter.ts | 4 ++-- extension/js/common/core/pgp-key.ts | 3 ++- extension/js/common/ui/att-ui.ts | 3 +-- test/source/platform/catch.ts | 1 + 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-types.ts b/extension/chrome/elements/compose-modules/compose-types.ts index 876988672c4..202b8f22cc5 100644 --- a/extension/chrome/elements/compose-modules/compose-types.ts +++ b/extension/chrome/elements/compose-modules/compose-types.ts @@ -4,6 +4,7 @@ import { RecipientType } from '../../../js/common/api/api.js'; import { Recipients } from '../../../js/common/api/email-provider/email-provider-api.js'; +import { PubkeyResult } from '../../../js/common/core/pgp-key.js'; export type RecipientStatus = 0 | 1 | 2 | 3 | 4 | 5; @@ -37,7 +38,6 @@ export type MessageToReplyOrForward = { decryptedFiles: File[] }; -export type PubkeyResult = { pubkey: string, email: string, isMine: boolean }; export type CollectPubkeysResult = { armoredPubkeys: PubkeyResult[], emailsWithoutPubkeys: string[] }; export type PopoverOpt = 'encrypt' | 'sign' | 'richtext'; diff --git a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts index 1cf7eda642e..0784c87eb23 100644 --- a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts @@ -6,14 +6,14 @@ import { Backend, FcUuidAuth } from '../../../../js/common/api/backend.js'; import { BaseMailFormatter } from './base-mail-formatter.js'; import { ComposerResetBtnTrigger } from '../compose-err-module.js'; import { Mime, SendableMsgBody } from '../../../../js/common/core/mime.js'; -import { NewMsgData, PubkeyResult } from '../compose-types.js'; +import { NewMsgData } from '../compose-types.js'; import { Str, Value } from '../../../../js/common/core/common.js'; import { ApiErr } from '../../../../js/common/api/error/api-error.js'; import { Att } from '../../../../js/common/core/att.js'; import { Buf } from '../../../../js/common/core/buf.js'; import { Catch } from '../../../../js/common/platform/catch.js'; import { Lang } from '../../../../js/common/lang.js'; -import { PgpKey } from '../../../../js/common/core/pgp-key.js'; +import { PgpKey, PubkeyResult } from '../../../../js/common/core/pgp-key.js'; import { PgpMsg, PgpMsgMethod } from '../../../../js/common/core/pgp-msg.js'; import { SendableMsg } from '../../../../js/common/api/email-provider/sendable-msg.js'; import { Settings } from '../../../../js/common/settings.js'; diff --git a/extension/js/common/core/pgp-key.ts b/extension/js/common/core/pgp-key.ts index b6a87274a68..73cd73fd888 100644 --- a/extension/js/common/core/pgp-key.ts +++ b/extension/js/common/core/pgp-key.ts @@ -8,7 +8,8 @@ import { MsgBlockParser } from './msg-block-parser.js'; import { PgpArmor } from './pgp-armor.js'; import { opgp } from './pgp.js'; import { KeyCache } from '../platform/key-cache.js'; -import { PubkeyResult } from '../../../chrome/elements/compose-modules/compose-types.js'; + +export type PubkeyResult = { pubkey: string, email: string, isMine: boolean }; export type Contact = { email: string; diff --git a/extension/js/common/ui/att-ui.ts b/extension/js/common/ui/att-ui.ts index bcb1ddb11e0..1d4fdb6e359 100644 --- a/extension/js/common/ui/att-ui.ts +++ b/extension/js/common/ui/att-ui.ts @@ -7,8 +7,7 @@ import { Catch } from '../platform/catch.js'; import { Dict } from '../core/common.js'; import { PgpMsg } from '../core/pgp-msg.js'; import { Ui } from '../browser/ui.js'; -import { PubkeyResult } from '../../../chrome/elements/compose-modules/compose-types.js'; -import { PgpKey } from '../core/pgp-key.js'; +import { PgpKey, PubkeyResult } from '../core/pgp-key.js'; declare const qq: any; diff --git a/test/source/platform/catch.ts b/test/source/platform/catch.ts index ad7330f897b..c52c2278f98 100644 --- a/test/source/platform/catch.ts +++ b/test/source/platform/catch.ts @@ -5,6 +5,7 @@ const VERSION = 'B.1.0'; export type ObjWithStack = { stack: string }; +export class UnreportableError extends Error { } export class Catch { From 71195ad8aa33726af17ea03690d0af28f2e356d6 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 09:04:27 +0000 Subject: [PATCH 20/24] add s/mime test cases --- extension/chrome/dev/smime.htm | 30 +----- extension/chrome/elements/add_pubkey.htm | 2 +- .../compose-modules/compose-err-module.ts | 4 +- extension/js/common/platform/catch.ts | 4 + extension/js/common/ui/att-ui.ts | 5 +- test/source/mock/google/google-endpoints.ts | 5 +- .../strategies/send-message-strategy.ts | 27 +++++- test/source/test.ts | 9 +- .../tests/page-recipe/compose-page-recipe.ts | 7 +- test/source/tests/tests/compose.ts | 96 +++++++++++++++++-- 10 files changed, 140 insertions(+), 49 deletions(-) diff --git a/extension/chrome/dev/smime.htm b/extension/chrome/dev/smime.htm index 986ef2f014b..3cbf41d3ed4 100644 --- a/extension/chrome/dev/smime.htm +++ b/extension/chrome/dev/smime.htm @@ -9,35 +9,7 @@

S/MIME

Cert

Get a free testing cert here

-

Headers

+
diff --git a/extension/chrome/elements/compose-modules/compose-err-module.ts b/extension/chrome/elements/compose-modules/compose-err-module.ts index 018fc2dbebd..1bab08df1f8 100644 --- a/extension/chrome/elements/compose-modules/compose-err-module.ts +++ b/extension/chrome/elements/compose-modules/compose-err-module.ts @@ -3,7 +3,7 @@ 'use strict'; import { BrowserEventErrHandler, Ui } from '../../../js/common/browser/ui.js'; -import { Catch, UnreportableError } from '../../../js/common/platform/catch.js'; +import { Catch } from '../../../js/common/platform/catch.js'; import { NewMsgData, SendBtnTexts } from './compose-types.js'; import { ApiErr } from '../../../js/common/api/error/api-error.js'; import { BrowserExtension } from '../../../js/common/browser/browser-extension.js'; @@ -87,7 +87,7 @@ export class ComposeErrModule extends ViewModule { } else if (e instanceof ComposerUserError) { await Ui.modal.error(e.message); } else { - if (!(e instanceof ComposerResetBtnTrigger || e instanceof UnreportableError || e instanceof ComposerNotReadyError)) { + if (!(e instanceof ComposerResetBtnTrigger || e instanceof ComposerNotReadyError)) { Catch.reportErr(e); await Ui.modal.error(`Failed to send message due to: ${String(e)}`); } diff --git a/extension/js/common/platform/catch.ts b/extension/js/common/platform/catch.ts index 271edf713ba..753ef43ed8a 100644 --- a/extension/js/common/platform/catch.ts +++ b/extension/js/common/platform/catch.ts @@ -136,6 +136,10 @@ export class Catch { if (e.hasOwnProperty('workerStack')) { // https://github.com/openpgpjs/openpgpjs/issues/656#event-1498323188 e.stack += Catch.formattedStackBlock('openpgp.js worker stack', String((e as any).workerStack)); } + if (e instanceof UnreportableError) { + console.error('Not reporting UnreportableError:', e); + return; + } } Catch.onErrorInternalHandler(e instanceof Error ? e.message : String(e), window.location.href, line, col, e, true); } diff --git a/extension/js/common/ui/att-ui.ts b/extension/js/common/ui/att-ui.ts index 1d4fdb6e359..6d7ec019945 100644 --- a/extension/js/common/ui/att-ui.ts +++ b/extension/js/common/ui/att-ui.ts @@ -3,7 +3,7 @@ 'use strict'; import { Att } from '../core/att.js'; -import { Catch } from '../platform/catch.js'; +import { Catch, UnreportableError } from '../platform/catch.js'; import { Dict } from '../core/common.js'; import { PgpMsg } from '../core/pgp-msg.js'; import { Ui } from '../browser/ui.js'; @@ -87,6 +87,9 @@ export class AttUI { const file = this.attachedFiles[uploadFileId]; const data = await this.readAttDataAsUint8(uploadFileId); const pubsForEncryption = PgpKey.choosePubsBasedOnKeyTypeCombinationForPartialSmimeSupport(pubs); + if (pubs.find(pub => PgpKey.getKeyType(pub.pubkey) === 'x509')) { + throw new UnreportableError('Attachments are not yet supported when sending to recipients using S/MIME x509 certificates.'); + } const encrypted = await PgpMsg.encrypt({ pubkeys: pubsForEncryption, data, filename: file.name, armor: false }) as OpenPGP.EncryptBinaryResult; atts.push(new Att({ name: file.name.replace(/[^a-zA-Z\-_.0-9]/g, '_').replace(/__+/g, '_') + '.pgp', type: file.type, data: encrypted.message.packets.write() })); } diff --git a/test/source/mock/google/google-endpoints.ts b/test/source/mock/google/google-endpoints.ts index 321e39faa5e..0a9f7a5ad16 100644 --- a/test/source/mock/google/google-endpoints.ts +++ b/test/source/mock/google/google-endpoints.ts @@ -14,7 +14,7 @@ type DraftSaveModel = { message: { raw: string, threadId: string } }; const allowedRecipients: Array = ['flowcrypt.compatibility@gmail.com', 'human+manualcopypgp@flowcrypt.com', 'censored@email.com', 'test@email.com', 'human@flowcrypt.com', 'human+nopgp@flowcrypt.com', 'expired.on.attester@domain.com', - 'test.ci.compose@org.flowcrypt.com']; + 'test.ci.compose@org.flowcrypt.com', 'smime1@recipient.com', 'smime2@recipient.com', 'smime@recipient.com', 'smime.att@recipient.com']; export const mockGoogleEndpoints: HandlersDefinition = { '/o/oauth2/auth': async ({ query: { client_id, response_type, access_type, state, redirect_uri, scope, login_hint, result } }, req) => { @@ -164,7 +164,8 @@ export const mockGoogleEndpoints: HandlersDefinition = { await testingStrategyContext.test(parseResult.mimeMsg, parseResult.base64); } catch (e) { if (!(e instanceof UnsuportableStrategyError)) { // No such strategy for test - throw e; + throw e; // todo - should start throwing unsupported test strategies too, else changing subject will cause incomplete testing + // todo - should stop calling it "strategy", better just "SentMessageTest" or similar } } return { id: 'fakesendid', labelIds: ['SENT'], threadId: parseResult.threadId }; diff --git a/test/source/mock/google/strategies/send-message-strategy.ts b/test/source/mock/google/strategies/send-message-strategy.ts index 8d73a0fd717..2b94b5bd639 100644 --- a/test/source/mock/google/strategies/send-message-strategy.ts +++ b/test/source/mock/google/strategies/send-message-strategy.ts @@ -1,10 +1,10 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ -import { AddressObject, ParsedMail } from 'mailparser'; +import { AddressObject, ParsedMail, StructuredHeader } from 'mailparser'; import { ITestMsgStrategy, UnsuportableStrategyError } from './strategy-base.js'; - import { Buf } from '../../../core/buf'; import { Config } from '../../../util'; +import { expect } from 'chai'; import { GoogleData } from '../google-data'; import { HttpClientErr } from '../../lib/api'; import { PgpMsg } from '../../../core/pgp-msg'; @@ -119,6 +119,23 @@ class NewMessageCCAndBCCTestStrategy implements ITestMsgStrategy { } } +class SmimeEncryptedMessageStrategy implements ITestMsgStrategy { + public test = async (mimeMsg: ParsedMail) => { + expect((mimeMsg.headers.get('content-type') as StructuredHeader).value).to.equal('application/pkcs7-mime'); + expect((mimeMsg.headers.get('content-type') as StructuredHeader).params['name']).to.equal('smime.p7m'); + expect((mimeMsg.headers.get('content-type') as StructuredHeader).params['smime-type']).to.equal('enveloped-data'); + expect(mimeMsg.headers.get('content-transfer-encoding')).to.equal('base64'); + expect((mimeMsg.headers.get('content-disposition') as StructuredHeader).value).to.equal('attachment'); + expect((mimeMsg.headers.get('content-disposition') as StructuredHeader).params['filename']).to.equal('smime.p7m'); + expect(mimeMsg.headers.get('content-description')).to.equal('S/MIME Encrypted Message'); + expect(mimeMsg.attachments).to.exist; + expect(mimeMsg.attachments!.length).to.equal(1); + expect(mimeMsg.attachments![0].contentType).to.equal('application/pkcs7-mime'); + expect(mimeMsg.attachments![0].filename).to.equal('smime.p7m'); + expect(mimeMsg.attachments![0].size).to.be.greaterThan(300); + } +} + export class TestBySubjectStrategyContext { private strategy: ITestMsgStrategy; @@ -137,6 +154,12 @@ export class TestBySubjectStrategyContext { this.strategy = new PwdEncryptedMessageTestStrategy(); } else if (subject.includes('Message With Image')) { this.strategy = new SaveMessageInStorageStrategy(); + } else if (subject.includes('send with single S/MIME cert')) { + this.strategy = new SmimeEncryptedMessageStrategy(); + } else if (subject.includes('send with several S/MIME certs')) { + this.strategy = new SmimeEncryptedMessageStrategy(); + } else if (subject.includes('send with S/MIME attachment')) { + this.strategy = new SmimeEncryptedMessageStrategy(); } else { throw new UnsuportableStrategyError(`There isn't any strategy for this subject: ${subject}`); } diff --git a/test/source/test.ts b/test/source/test.ts index b2d86218595..56642b1b119 100644 --- a/test/source/test.ts +++ b/test/source/test.ts @@ -88,8 +88,13 @@ ava.after.always('evaluate Catch.reportErr errors', async t => { t.pass(); return; } - const foundExpectedErr = mockBackendData.reportedErrors.find(re => re.message === `intentional error for debugging`); - const foundUnwantedErrs = mockBackendData.reportedErrors.filter(re => re.message !== `intentional error for debugging` && !re.message.includes('traversal forbidden')); + // todo - here we filter out an error that would otherwise be useful + // in one test we are testing an error scenario + // our S/MIME implementation is still early so it throws "reportable" errors like this during tests + const usefulErrors = mockBackendData.reportedErrors.filter(e => e.message !== 'Too few bytes to read ASN.1 value.'); + // end of todo + const foundExpectedErr = usefulErrors.find(re => re.message === `intentional error for debugging`); + const foundUnwantedErrs = usefulErrors.filter(re => re.message !== `intentional error for debugging` && !re.message.includes('traversal forbidden')); if (!foundExpectedErr && internalTestState.expectiIntentionalErrReport) { t.fail(`Catch.reportErr errors: missing intentional error`); } else if (foundUnwantedErrs.length) { diff --git a/test/source/tests/page-recipe/compose-page-recipe.ts b/test/source/tests/page-recipe/compose-page-recipe.ts index 6fc5b2c01ae..e2a61cf0385 100644 --- a/test/source/tests/page-recipe/compose-page-recipe.ts +++ b/test/source/tests/page-recipe/compose-page-recipe.ts @@ -69,6 +69,7 @@ export class ComposePageRecipe extends PageRecipe { } const body = subject?.match(/RTL/) ? 'مرحبا' : `This is an automated puppeteer test: ${subject || '(no-subject)'}`; await composePageOrFrame.type('@input-body', body); + return { subject, body }; } @@ -100,10 +101,8 @@ export class ComposePageRecipe extends PageRecipe { await Util.sleep(1); } } - if (composePageOrFrame instanceof ControllablePage) { - await composePageOrFrame.page.evaluate(() => { $('#input_text').focus(); }); - await Util.sleep(1); - } + await composePageOrFrame.target.evaluate(() => { $('#input_text').focus(); }); + await Util.sleep(1); } public static waitWhenDraftIsSaved = async (composePageOrFrame: Controllable) => { diff --git a/test/source/tests/tests/compose.ts b/test/source/tests/tests/compose.ts index 693a91c310c..c5130d22807 100644 --- a/test/source/tests/tests/compose.ts +++ b/test/source/tests/tests/compose.ts @@ -2,7 +2,7 @@ import * as ava from 'ava'; -import { BrowserHandle, Controllable, ControllablePage } from '../../browser'; +import { BrowserHandle, Controllable, ControllablePage, ControllableFrame } from '../../browser'; import { Config, Util } from '../../util'; import { AvaContext } from '..'; @@ -22,6 +22,10 @@ import { SetupPageRecipe } from '../page-recipe/setup-page-recipe'; // tslint:disable:no-blank-lines-func // tslint:disable:no-unused-expression +/* eslint-disable max-len */ + +// get s/mime cert for testing: https://extrassl.actalis.it/portal/uapub/freemail?lang=en +const smimeCert = "-----BEGIN CERTIFICATE-----\nMIIE9DCCA9ygAwIBAgIQY/cCXnAPOUUwH7L7pWdPhDANBgkqhkiG9w0BAQsFADCB\njTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRl\nIFNhbiBQaWV0cm8xIzAhBgNVBAoMGkFjdGFsaXMgUy5wLkEuLzAzMzU4NTIwOTY3\nMSwwKgYDVQQDDCNBY3RhbGlzIENsaWVudCBBdXRoZW50aWNhdGlvbiBDQSBHMjAe\nFw0yMDAzMjMxMzU2NDZaFw0yMTAzMjMxMzU2NDZaMCIxIDAeBgNVBAMMF2FjdGFs\naXNAbWV0YS4zM21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEArVVpXBkzGvcqib8rDwqHCaKm2EiPslQ8I0G1ZDxrs6Ke2QXNm3yGVwOzkVvK\neEnuzE5M4BBeh+GwcfvoyS/xI6m44WWnqj65cJoSLA1ypE4D4urv/pzG783y2Vdy\nQ96izBdFyevsil89Z2AxZxrFh1RC2XvgXad4yyD4yvVpHskfPexnhLliHl7cpXjw\n5D2n1hBGR8CSDbQAgO58PB7Y2ldrTi+rWBu2Akuk/YyWOOiGA8pdfLBIkOFJTeQc\nm7+vWP2JTN6Xp+JkGvXQBRaqwyGVg8fSc4e7uGCXZaH5/Na2FXY2OL+tYDDb27zS\n3cBrzEbGVjA6raYxcrFWV4PkdwIDAQABo4IBuDCCAbQwDAYDVR0TAQH/BAIwADAf\nBgNVHSMEGDAWgBRr8o2eaMElBB9RNFf2FlyU6k1pGjB+BggrBgEFBQcBAQRyMHAw\nOwYIKwYBBQUHMAKGL2h0dHA6Ly9jYWNlcnQuYWN0YWxpcy5pdC9jZXJ0cy9hY3Rh\nbGlzLWF1dGNsaWcyMDEGCCsGAQUFBzABhiVodHRwOi8vb2NzcDA5LmFjdGFsaXMu\naXQvVkEvQVVUSENMLUcyMCIGA1UdEQQbMBmBF2FjdGFsaXNAbWV0YS4zM21haWwu\nY29tMEcGA1UdIARAMD4wPAYGK4EfARgBMDIwMAYIKwYBBQUHAgEWJGh0dHBzOi8v\nd3d3LmFjdGFsaXMuaXQvYXJlYS1kb3dubG9hZDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwQwSAYDVR0fBEEwPzA9oDugOYY3aHR0cDovL2NybDA5LmFjdGFs\naXMuaXQvUmVwb3NpdG9yeS9BVVRIQ0wtRzIvZ2V0TGFzdENSTDAdBgNVHQ4EFgQU\nFrtAdAOjrcVeHg5K+T7sj7GHySMwDgYDVR0PAQH/BAQDAgWgMA0GCSqGSIb3DQEB\nCwUAA4IBAQAa9lXKDmV9874ojmIZEBL1S8mKaSNBWP+n0vp5FO0Yh5oL9lspYTPs\n8s6alWUSpVHV8if4uZ2EfcNpNkm9dAajj2n/F/Jyfkp8URu4uvBfm1QColl/zM/D\nx4B7FaD2dw0jTF/k5ulDmzUOc4k+j3LtZNbDOZMF/2g05hSKde/he1njlY3oKa9g\nVW8ftc2NwiSMthxyEIM+ALbNQVML2oN50gArBn5GeI22/aIBZxjtbEdmSTZIf82H\nsOwAnhJ+pD5iIPaF2oa0yN3PvI6IGxLpEv16tQO1N6e5bdP6ZDwqTQJyK+oNTNda\nyPLCqVTFJQWaCR5ZTekRQPTDZkjxjxbs\n-----END CERTIFICATE-----"; export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: TestWithBrowser) => { @@ -407,8 +411,8 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te })); ava.default('compose - key-mismatch - standalone - key mismatch loading', testWithBrowser('compatibility', async (t, browser) => { - const params = 'threadId=15f7f5630573be2d&skipClickPrompt=___cu_true___&ignoreDraft=___cu_true___&replyMsgId=15f7f5630573be2d&disableDraftSaving=___cu_true___&replyPubkeyMismatch=___cu_true___'; // eslint-disable-line max-len - const replyMismatchPage = await browser.newPage(t, 'chrome/elements/compose.htm?account_email=flowcrypt.compatibility%40gmail.com&parent_tab_id=0&debug=___cu_true___&frameId=none&' + params); // eslint-disable-line max-len + const params = 'threadId=15f7f5630573be2d&skipClickPrompt=___cu_true___&ignoreDraft=___cu_true___&replyMsgId=15f7f5630573be2d&disableDraftSaving=___cu_true___&replyPubkeyMismatch=___cu_true___'; + const replyMismatchPage = await browser.newPage(t, 'chrome/elements/compose.htm?account_email=flowcrypt.compatibility%40gmail.com&parent_tab_id=0&debug=___cu_true___&frameId=none&' + params); await replyMismatchPage.waitForSelTestState('ready'); await Util.sleep(3); await expectRecipientElements(replyMismatchPage, { to: ['censored@email.com'], cc: [], bcc: [] }); @@ -544,7 +548,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te })); ava.default('compose - saving and rendering a draft with image', testWithBrowser('compatibility', async (t, browser) => { - const imgBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAnElEQVR42u3RAQ0AAAgDIE1u9FvDOahAVzLFGS1ECEKEIEQIQoQgRIgQIQgRghAhCBGCECEIQYgQhAhBiBCECEEIQoQgRAhChCBECEIQIgQhQhAiBCFCEIIQIQgRghAhCBGCEIQIQYgQhAhBiBCEIEQIQoQgRAhChCAEIUIQIgQhQhAiBCEIEYIQIQgRghAhCBEiRAhChCBECEK+W3uw+TnWoJc/AAAAAElFTkSuQmCC'; // eslint-disable-line max-len + const imgBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAnElEQVR42u3RAQ0AAAgDIE1u9FvDOahAVzLFGS1ECEKEIEQIQoQgRIgQIQgRghAhCBGCECEIQYgQhAhBiBCECEEIQoQgRAhChCBECEIQIgQhQhAiBCFCEIIQIQgRghAhCBGCEIQIQYgQhAhBiBCEIEQIQoQgRAhChCAEIUIQIgQhQhAiBCEIEYIQIQgRghAhCBEiRAhChCBECEK+W3uw+TnWoJc/AAAAAElFTkSuQmCC'; let composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility'); const subject = `saving and rendering a draft with image ${Util.lousyRandom()}`; await ComposePageRecipe.fillMsg(composePage, { to: 'human@flowcrypt.com' }, subject, { 'richtext': true }); @@ -637,13 +641,93 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te ava.todo('compose - reply - skip click prompt'); + ava.default('send with single S/MIME cert', testWithBrowser('compose', async (t, browser) => { + const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('test.ci.compose@org.flowcrypt.com')); + let composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); + await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime@recipient.com' }, t.title); + await pastePublicKeyManually(composeFrame, inboxPage, 'smime@recipient.com', smimeCert); + await composeFrame.waitAndClick('@action-send', { delay: 2 }); + await inboxPage.waitTillGone('@container-new-message'); + })); + + ava.default('send with several S/MIME certs', testWithBrowser('compose', async (t, browser) => { + const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('test.ci.compose@org.flowcrypt.com')); + let composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); + await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime1@recipient.com', cc: 'smime2@recipient.com' }, t.title); + await pastePublicKeyManually(composeFrame, inboxPage, 'smime1@recipient.com', smimeCert); + await pastePublicKeyManually(composeFrame, inboxPage, 'smime2@recipient.com', smimeCert); + await composeFrame.waitAndClick('@action-send', { delay: 2 }); + await inboxPage.waitTillGone('@container-new-message'); + })); + + ava.default('send with S/MIME attachment', testWithBrowser('compose', async (t, browser) => { + // todo - this is not yet looking for actual attachment in the result, just checks that it's s/mime message + const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('test.ci.compose@org.flowcrypt.com')); + let composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); + await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime.att@recipient.com' }, t.title); + await pastePublicKeyManually(composeFrame, inboxPage, 'smime.att@recipient.com', smimeCert); + const fileInput = await composeFrame.target.$('input[type=file]'); + await fileInput!.uploadFile('test/samples/small.txt', 'test/samples/small.png', 'test/samples/small.pdf'); + await composeFrame.waitAndClick('@action-send', { delay: 2 }); + await PageRecipe.waitForModalAndRespond(composeFrame, 'error', { + contentToCheck: 'Attachments are not yet supported when sending to recipients using S/MIME x509 certificates.', + timeout: 40 + }); + })); + + ava.default('send with mixed S/MIME and PGP recipients - should show err', testWithBrowser('compose', async (t, browser) => { + const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('test.ci.compose@org.flowcrypt.com')); + let composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); + await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime@recipient.com', cc: 'human@flowcrypt.com' }, t.title); + await pastePublicKeyManually(composeFrame, inboxPage, 'smime@recipient.com', smimeCert); + await composeFrame.waitAndClick('@action-send', { delay: 2 }); + await PageRecipe.waitForModalAndRespond(composeFrame, 'error', { + contentToCheck: 'Failed to send message due to: Error: Cannot use mixed OpenPGP (human@flowcrypt.com) and S/MIME (smime@recipient.com) public keys yet.If you need to email S/MIME recipient, do not add any OpenPGP recipient at the same time.', + timeout: 40 + }); + })); + + ava.default('send with broken S/MIME cert - err', testWithBrowser('compose', async (t, browser) => { + const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('test.ci.compose@org.flowcrypt.com')); + let composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); + await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime@recipient.com' }, t.title); + const brokenCert = smimeCert.split('\n'); + brokenCert.splice(5, 5); // remove 5th to 10th line from cert - make it useless + await pastePublicKeyManually(composeFrame, inboxPage, 'smime@recipient.com', brokenCert.join('\n')); + await composeFrame.waitAndClick('@action-send', { delay: 2 }); + await PageRecipe.waitForModalAndRespond(composeFrame, 'error', { contentToCheck: 'Too few bytes to read ASN.1 value.', timeout: 40 }); + })); + + // todo - unexpectedly works + // ava.default.only('send non-S/MIME cert - err', testWithBrowser('compose', async (t, browser) => { + // const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('test.ci.compose@org.flowcrypt.com')); + // let composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); + // await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime@recipient.com' }, t.title); + // const httpsCert = '-----BEGIN CERTIFICATE-----\nMIIFZTCCBE2gAwIBAgISA/LOLnFAcrNSDjMi+PvkSbX1MA0GCSqGSIb3DQEBCwUA\nMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD\nExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDAzMTQxNTQ0NTVaFw0y\nMDA2MTIxNTQ0NTVaMBgxFjAUBgNVBAMTDWZsb3djcnlwdC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBYeT+zyJK4VrAtpBoxnzNrgPMkeJ3WBw3\nlZrO7GXsPUUQL/2uL3NfMwQ4qWqsiJStShaTQ0UX1MQCBgdOY/Ajr5xgyCz4aE0+\nQeReGy+qFyoGE9okVdF+/uJhFTOkK8goA4rDRN3MrSuWsivc/5/8Htd/M01JFAcU\nEblrPkSBtJp8IAtr+QD8etmMd05N0oQFNFT/T7QNrEdItCKSS6jMpprR4phr792K\niQh9MzhZ3O+QEM+UKpsL0dM9C6PD9jNFjFz3EDch/VFPbBlcBfWGvYnjBlqKjhYA\nLPUVPgIF4CVQ60EoOHk1ewyoAyydYyFXppUz1eDvemUhLMWuBJ2tAgMBAAGjggJ1\nMIICcTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF\nBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFMr4ERxBRtKNI67oIkJHN2QSBptE\nMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMw\nYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9y\nZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9y\nZy8wKQYDVR0RBCIwIIIPKi5mbG93Y3J5cHQuY29tgg1mbG93Y3J5cHQuY29tMEwG\nA1UdIARFMEMwCAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEW\nGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIIBBgYKKwYBBAHWeQIEAgSB9wSB\n9ADyAHcAb1N2rDHwMRnYmQCkURX/dxUcEdkCwQApBo2yCJo32RMAAAFw2e8sLwAA\nBAMASDBGAiEA7Omcf4+uFphcbEq19r4GoWi7E1qvsJTykvgH342x1d4CIQDSCJZK\n3zsVSw8I1GVfnIr/drVhgn4TJgacXx6+gBzfXQB3ALIeBcyLos2KIE6HZvkruYol\nIGdr2vpw57JJUy3vi5BeAAABcNnvK/kAAAQDAEgwRgIhAP7BbIkG/mNclZAVqgA0\nomAB/6xMwbu1ZUsHNBMkZG+QAiEAmZWCVdUfmFs3b+zDEaAF7eFDnz7qbDa5q6M0\n98r8In0wDQYJKoZIhvcNAQELBQADggEBAFaUhUkxGkHc3lxozCbozM7ffAOcK5De\nJGoTtsXw/XmMACBIIqn2Aan+zvQdK/cWV9+dYu5tA/PHZwVbfKAU2x+Fizs7uDgs\nslg16un1/DP7bmi4Ih3KDVyznzgTwWPq9CmPMIeCXBSGvGN4xdfyIf7mKPSmsEB3\ngkM8HyE27e2u8B4f/R4W+sbqx0h5Y/Kv6NFqgQlatEY2HdAQDYYL21xO1ZjaUozP\nyfHQSJwGHp3/1Xdq5mIkV7w9xxhOn64FXp4S0spVCxT3er1EEUurq+lXjyeX4Dog\n1gy3r417NPqQWuBJcA/InSaS/GUyGghp+kuGfIDqVYfQqU1297nThEA=\n-----END CERTIFICATE-----\n'; + // await pastePublicKeyManually(composeFrame, inboxPage, 'smime@recipient.com', httpsCert); + // await composeFrame.waitAndClick('@action-send', { delay: 2 }); + // await PageRecipe.waitForModalAndRespond(composeFrame, 'error', { contentToCheck: 'dunno', timeout: 40 }); + // })); + } }; +const pastePublicKeyManually = async (composeFrame: ControllableFrame, inboxPage: ControllablePage, recipient: string, pub: string) => { + await Util.sleep(1); // todo: should wait until recipient actually loaded + await composeFrame.waitForContent('.email_address.no_pgp', recipient); + await composeFrame.waitAndClick('@action-open-add-pubkey-dialog', { delay: 1 }); + await inboxPage.waitAll('@dialog-add-pubkey'); + const addPubkeyDialog = await inboxPage.getFrame(['add_pubkey.htm']); + await addPubkeyDialog.waitAndType('@input-pubkey', pub); + await Util.sleep(1); + await addPubkeyDialog.waitAndClick('@action-add-pubkey'); + await inboxPage.waitTillGone('@dialog-add-pubkey'); +}; + const sendImgAndVerifyPresentInSentMsg = async (t: AvaContext, browser: BrowserHandle, sendingType: 'encrypt' | 'sign') => { // send a message with image in it - const imgBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAnElEQVR42u3RAQ0AAAgDIE1u9FvDOahAVzLFGS1ECEKEIEQIQoQgRIgQIQgRghAhCBGCECEIQYgQhAhBiBCECEEIQoQgRAhChCBECEIQIgQhQhAiBCFCEIIQIQgRghAhCBGCEIQIQYgQhAhBiBCEIEQIQoQgRAhChCAEIUIQIgQhQhAiBCEIEYIQIQgRghAhCBEiRAhChCBECEK+W3uw+TnWoJc/AAAAAElFTkSuQmCC'; // eslint-disable-line max-len + const imgBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAnElEQVR42u3RAQ0AAAgDIE1u9FvDOahAVzLFGS1ECEKEIEQIQoQgRIgQIQgRghAhCBGCECEIQYgQhAhBiBCECEEIQoQgRAhChCBECEIQIgQhQhAiBCFCEIIQIQgRghAhCBGCEIQIQYgQhAhBiBCEIEQIQoQgRAhChCAEIUIQIgQhQhAiBCEIEYIQIQgRghAhCBEiRAhChCBECEK+W3uw+TnWoJc/AAAAAElFTkSuQmCC'; const subject = `Test Sending ${sendingType === 'sign' ? 'Signed' : 'Encrypted'} Message With Image ${Util.lousyRandom()}`; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility'); await ComposePageRecipe.fillMsg(composePage, { to: 'human@flowcrypt.com' }, subject, { richtext: true, sign: sendingType === 'sign', encrypt: sendingType === 'encrypt' }); @@ -653,7 +737,7 @@ const sendImgAndVerifyPresentInSentMsg = async (t: AvaContext, browser: BrowserH await ComposePageRecipe.sendAndClose(composePage); // get sent msg id from mock const sentMsg = new GoogleData('flowcrypt.compatibility@gmail.com').getMessageBySubject(subject)!; - let url = `chrome/dev/ci_pgp_host_page.htm?frameId=none&msgId=${encodeURIComponent(sentMsg.id)}&senderEmail=flowcrypt.compatibility%40gmail.com&isOutgoing=___cu_false___&acctEmail=flowcrypt.compatibility%40gmail.com`; // eslint-disable-line max-len + let url = `chrome/dev/ci_pgp_host_page.htm?frameId=none&msgId=${encodeURIComponent(sentMsg.id)}&senderEmail=flowcrypt.compatibility%40gmail.com&isOutgoing=___cu_false___&acctEmail=flowcrypt.compatibility%40gmail.com`; if (sendingType === 'sign') { url += '&signature=___cu_true___'; } From 5c88bbd8279b058b9341be132edebe4c6e897a9a Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 09:08:04 +0000 Subject: [PATCH 21/24] try disabling (possibly) unused branch --- extension/js/common/api/email-provider/sendable-msg.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extension/js/common/api/email-provider/sendable-msg.ts b/extension/js/common/api/email-provider/sendable-msg.ts index 89c98216e9b..2d04ca5ae4d 100644 --- a/extension/js/common/api/email-provider/sendable-msg.ts +++ b/extension/js/common/api/email-provider/sendable-msg.ts @@ -87,11 +87,11 @@ export class SendableMsg { return await Mime.encodeSmime(this.body['encrypted/buf'], this.headers); } else if (this.type === 'pgpMimeSigned' && this.sign) { return await Mime.encodePgpMimeSigned(this.body, this.headers, this.atts, this.sign); - } else { - // encrypted/buf is a Buf instance that is converted to one part text/plain - // message + } else { // encrypted/buf is a Buf instance that is converted to single-part message if (this.body['encrypted/buf']) { - this.body = { 'text/plain': this.body['encrypted/buf'].toString() }; + // currently no code is using this path, smimeEncrypted handled above + // this.body = { 'text/plain': this.body['encrypted/buf'].toString() }; + throw new Error('Unexpected encrypted/buf on a non-s/mime message'); } return await Mime.encode(this.body, this.headers, this.atts, this.type); } From ae2379ab365e2e4e8a4d263e3519f2bbab465454 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 09:09:29 +0000 Subject: [PATCH 22/24] remove s/mime dev page --- extension/chrome/dev/smime.htm | 29 ------------------------ extension/chrome/dev/smime.ts | 41 ---------------------------------- 2 files changed, 70 deletions(-) delete mode 100644 extension/chrome/dev/smime.htm delete mode 100644 extension/chrome/dev/smime.ts diff --git a/extension/chrome/dev/smime.htm b/extension/chrome/dev/smime.htm deleted file mode 100644 index 3cbf41d3ed4..00000000000 --- a/extension/chrome/dev/smime.htm +++ /dev/null @@ -1,29 +0,0 @@ - - - -S/MIME demo - - - - -

S/MIME

-

Cert

-

Get a free testing cert here

- -

Headers

- -

Content

- -
- -

Output

- -

You can paste this into a file and send it via command line. I use `git send-email` but probably anything else would work.

- diff --git a/extension/chrome/dev/smime.ts b/extension/chrome/dev/smime.ts deleted file mode 100644 index 52e60434e35..00000000000 --- a/extension/chrome/dev/smime.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ - -'use strict'; - -import { Catch } from '../../js/common/platform/catch.js'; - -import * as forge from 'node-forge'; - -import { BrowserWindow } from '../../js/common/browser/browser-window'; - -const HEADERS = `MIME-Version: 1.0 -Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type=enveloped-data -Content-Transfer-Encoding: base64 -Content-Disposition: attachment; filename="smime.p7m" -Content-Description: S/MIME Encrypted Message`; - -Catch.try(async () => { - const mimeCodec: { - foldLines(text: string, maxLength: number, afterSpace: boolean): string; - } = (window as unknown as BrowserWindow)['emailjs-mime-codec']; // tslint:disable-line:no-unsafe-any - const wrap = (text: string) => mimeCodec.foldLines(text, 76, true); - - const encrypt = () => { - const p7 = forge.pkcs7.createEnvelopedData(); - const cert = forge.pki.certificateFromPem(String($('#cert').val())); - p7.addRecipient(cert); - - const headers = $('#headers').val(); - - p7.content = forge.util.createBuffer(headers + '\r\n\r\n' + $('#content').val()); - - p7.encrypt(); - - const derBuffer = forge.asn1.toDer(p7.toAsn1()).getBytes(); - - $('#email').val(headers + '\r\n' + HEADERS + '\r\n\r\n' + wrap(btoa(derBuffer))); - }; - - $("#encrypt").click(encrypt); - -})(); From f2eeeec72970cd653f984c1503a144d07d44a08d Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 09:51:49 +0000 Subject: [PATCH 23/24] fix tests --- extension/js/common/api/email-provider/sendable-msg.ts | 6 ++---- .../mock/google/strategies/send-message-strategy.ts | 5 ++--- test/source/tests/page-recipe/compose-page-recipe.ts | 1 - test/source/tests/tests/compose.ts | 10 +++++----- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/extension/js/common/api/email-provider/sendable-msg.ts b/extension/js/common/api/email-provider/sendable-msg.ts index 2d04ca5ae4d..09317f8b421 100644 --- a/extension/js/common/api/email-provider/sendable-msg.ts +++ b/extension/js/common/api/email-provider/sendable-msg.ts @@ -87,11 +87,9 @@ export class SendableMsg { return await Mime.encodeSmime(this.body['encrypted/buf'], this.headers); } else if (this.type === 'pgpMimeSigned' && this.sign) { return await Mime.encodePgpMimeSigned(this.body, this.headers, this.atts, this.sign); - } else { // encrypted/buf is a Buf instance that is converted to single-part message + } else { // encrypted/buf is a Buf instance that is converted to single-part plain/text message if (this.body['encrypted/buf']) { - // currently no code is using this path, smimeEncrypted handled above - // this.body = { 'text/plain': this.body['encrypted/buf'].toString() }; - throw new Error('Unexpected encrypted/buf on a non-s/mime message'); + this.body = { 'text/plain': this.body['encrypted/buf'].toString() }; } return await Mime.encode(this.body, this.headers, this.atts, this.type); } diff --git a/test/source/mock/google/strategies/send-message-strategy.ts b/test/source/mock/google/strategies/send-message-strategy.ts index 2b94b5bd639..10f2ad88a66 100644 --- a/test/source/mock/google/strategies/send-message-strategy.ts +++ b/test/source/mock/google/strategies/send-message-strategy.ts @@ -122,13 +122,12 @@ class NewMessageCCAndBCCTestStrategy implements ITestMsgStrategy { class SmimeEncryptedMessageStrategy implements ITestMsgStrategy { public test = async (mimeMsg: ParsedMail) => { expect((mimeMsg.headers.get('content-type') as StructuredHeader).value).to.equal('application/pkcs7-mime'); - expect((mimeMsg.headers.get('content-type') as StructuredHeader).params['name']).to.equal('smime.p7m'); + expect((mimeMsg.headers.get('content-type') as StructuredHeader).params.name).to.equal('smime.p7m'); expect((mimeMsg.headers.get('content-type') as StructuredHeader).params['smime-type']).to.equal('enveloped-data'); expect(mimeMsg.headers.get('content-transfer-encoding')).to.equal('base64'); expect((mimeMsg.headers.get('content-disposition') as StructuredHeader).value).to.equal('attachment'); - expect((mimeMsg.headers.get('content-disposition') as StructuredHeader).params['filename']).to.equal('smime.p7m'); + expect((mimeMsg.headers.get('content-disposition') as StructuredHeader).params.filename).to.equal('smime.p7m'); expect(mimeMsg.headers.get('content-description')).to.equal('S/MIME Encrypted Message'); - expect(mimeMsg.attachments).to.exist; expect(mimeMsg.attachments!.length).to.equal(1); expect(mimeMsg.attachments![0].contentType).to.equal('application/pkcs7-mime'); expect(mimeMsg.attachments![0].filename).to.equal('smime.p7m'); diff --git a/test/source/tests/page-recipe/compose-page-recipe.ts b/test/source/tests/page-recipe/compose-page-recipe.ts index e2a61cf0385..9c019856eb1 100644 --- a/test/source/tests/page-recipe/compose-page-recipe.ts +++ b/test/source/tests/page-recipe/compose-page-recipe.ts @@ -69,7 +69,6 @@ export class ComposePageRecipe extends PageRecipe { } const body = subject?.match(/RTL/) ? 'مرحبا' : `This is an automated puppeteer test: ${subject || '(no-subject)'}`; await composePageOrFrame.type('@input-body', body); - return { subject, body }; } diff --git a/test/source/tests/tests/compose.ts b/test/source/tests/tests/compose.ts index c5130d22807..c12e587834d 100644 --- a/test/source/tests/tests/compose.ts +++ b/test/source/tests/tests/compose.ts @@ -643,7 +643,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te ava.default('send with single S/MIME cert', testWithBrowser('compose', async (t, browser) => { const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('test.ci.compose@org.flowcrypt.com')); - let composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); + const composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime@recipient.com' }, t.title); await pastePublicKeyManually(composeFrame, inboxPage, 'smime@recipient.com', smimeCert); await composeFrame.waitAndClick('@action-send', { delay: 2 }); @@ -652,7 +652,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te ava.default('send with several S/MIME certs', testWithBrowser('compose', async (t, browser) => { const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('test.ci.compose@org.flowcrypt.com')); - let composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); + const composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime1@recipient.com', cc: 'smime2@recipient.com' }, t.title); await pastePublicKeyManually(composeFrame, inboxPage, 'smime1@recipient.com', smimeCert); await pastePublicKeyManually(composeFrame, inboxPage, 'smime2@recipient.com', smimeCert); @@ -663,7 +663,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te ava.default('send with S/MIME attachment', testWithBrowser('compose', async (t, browser) => { // todo - this is not yet looking for actual attachment in the result, just checks that it's s/mime message const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('test.ci.compose@org.flowcrypt.com')); - let composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); + const composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime.att@recipient.com' }, t.title); await pastePublicKeyManually(composeFrame, inboxPage, 'smime.att@recipient.com', smimeCert); const fileInput = await composeFrame.target.$('input[type=file]'); @@ -677,7 +677,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te ava.default('send with mixed S/MIME and PGP recipients - should show err', testWithBrowser('compose', async (t, browser) => { const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('test.ci.compose@org.flowcrypt.com')); - let composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); + const composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime@recipient.com', cc: 'human@flowcrypt.com' }, t.title); await pastePublicKeyManually(composeFrame, inboxPage, 'smime@recipient.com', smimeCert); await composeFrame.waitAndClick('@action-send', { delay: 2 }); @@ -689,7 +689,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te ava.default('send with broken S/MIME cert - err', testWithBrowser('compose', async (t, browser) => { const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('test.ci.compose@org.flowcrypt.com')); - let composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); + const composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime@recipient.com' }, t.title); const brokenCert = smimeCert.split('\n'); brokenCert.splice(5, 5); // remove 5th to 10th line from cert - make it useless From e2a1060cbe0c8c714bccd8156e5acbbdf778a980 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 16 Apr 2020 09:55:13 +0000 Subject: [PATCH 24/24] use buf.toString() rather than String(buf) --- extension/js/common/core/mime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index 450a580a93a..c9443d2fa64 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -208,7 +208,7 @@ export class Mime { } else { contentNode = new MimeBuilder('multipart/alternative'); // tslint:disable-line:no-unsafe-any for (const type of Object.keys(body)) { - contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, String(body[type]!))); // already present, that's why part of for loop + contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, body[type]!.toString())); // already present, that's why part of for loop } } rootNode.appendChild(contentNode); // tslint:disable-line:no-unsafe-any