From 8f250eb0c682cd1fffd12b9b093d1374c357f65c Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 25 Nov 2021 07:14:41 +0000 Subject: [PATCH 01/31] refactor decryption functions to use verificationPubs as parameter --- extension/chrome/elements/attachment.ts | 12 +- .../chrome/elements/attachment_preview.ts | 6 +- .../compose-modules/compose-draft-module.ts | 6 +- .../compose-modules/compose-quote-module.ts | 8 +- .../compose-recipients-module.ts | 4 +- .../compose-modules/compose-storage-module.ts | 4 +- extension/chrome/elements/pgp_block.ts | 4 + .../pgp-block-attachmens-module.ts | 3 +- .../pgp-block-decrypt-module.ts | 44 +++---- .../pgp-block-render-module.ts | 5 +- .../pgp-block-signature-module.ts | 73 ++++------- extension/chrome/settings/modules/decrypt.ts | 6 +- extension/js/common/core/crypto/key.ts | 14 ++ .../js/common/core/crypto/pgp/msg-util.ts | 122 ++++-------------- .../js/common/core/crypto/pgp/openpgp-key.ts | 60 ++++++++- .../js/common/platform/store/contact-store.ts | 59 ++------- .../strategies/send-message-strategy.ts | 6 +- test/source/platform/store/contact-store.ts | 6 - test/source/tests/compose.ts | 6 +- test/source/tests/unit-node.ts | 82 +++++++++--- 20 files changed, 268 insertions(+), 262 deletions(-) diff --git a/extension/chrome/elements/attachment.ts b/extension/chrome/elements/attachment.ts index 3db31bff88e..b1a1bec737e 100644 --- a/extension/chrome/elements/attachment.ts +++ b/extension/chrome/elements/attachment.ts @@ -205,7 +205,11 @@ export class AttachmentDownloadView extends View { private processAsPublicKeyAndHideAttachmentIfAppropriate = async () => { if (this.attachment.msgId && this.attachment.id && this.attachment.treatAs() === 'publicKey') { // this is encrypted public key - download && decrypt & parse & render const { data } = await this.gmail.attachmentGet(this.attachment.msgId, this.attachment.id); - const decrRes = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), encryptedData: data }); + const decrRes = await MsgUtil.decryptMessage({ + kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), + encryptedData: data, + verificationPubs: [] // todo: signature? + }); if (decrRes.success && decrRes.content) { const openpgpType = await MsgUtil.type({ data: decrRes.content }); if (openpgpType && openpgpType.type === 'publicKey' && openpgpType.armored) { // 'openpgpType.armored': could potentially process unarmored pubkey files, maybe later @@ -254,7 +258,11 @@ export class AttachmentDownloadView extends View { }; private decryptAndSaveAttachmentToDownloads = async () => { - const result = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), encryptedData: this.attachment.getData() }); + const result = await MsgUtil.decryptMessage({ + kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), + encryptedData: this.attachment.getData(), + verificationPubs: [] // todo: signature? + }); Xss.sanitizeRender(this.downloadButton, this.originalButtonHTML || ''); if (result.success) { if (!result.filename || ['msg.txt', 'null'].includes(result.filename)) { diff --git a/extension/chrome/elements/attachment_preview.ts b/extension/chrome/elements/attachment_preview.ts index 0ea40612cb0..f358b5e337a 100644 --- a/extension/chrome/elements/attachment_preview.ts +++ b/extension/chrome/elements/attachment_preview.ts @@ -87,7 +87,11 @@ View.run(class AttachmentPreviewView extends AttachmentDownloadView { }; private decrypt = async () => { - const result = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), encryptedData: this.attachment.getData() }); + const result = await MsgUtil.decryptMessage({ + kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), + encryptedData: this.attachment.getData(), + verificationPubs: [] // todo: signature? + }); if ((result as DecryptSuccess).content) { return result.content; } else if ((result as DecryptError).error.type === DecryptErrTypes.needPassphrase) { diff --git a/extension/chrome/elements/compose-modules/compose-draft-module.ts b/extension/chrome/elements/compose-modules/compose-draft-module.ts index 3100a11cb8a..8da5408ab19 100644 --- a/extension/chrome/elements/compose-modules/compose-draft-module.ts +++ b/extension/chrome/elements/compose-modules/compose-draft-module.ts @@ -289,7 +289,11 @@ export class ComposeDraftModule extends ViewModule { return await this.abortAndRenderReplyMsgComposeTableIfIsReplyBox('!rawBlock'); } const encryptedData = rawBlock.content instanceof Buf ? rawBlock.content : Buf.fromUtfStr(rawBlock.content); - const decrypted = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail), encryptedData }); + const decrypted = await MsgUtil.decryptMessage({ + kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail), + encryptedData, + verificationPubs: [] + }); if (!decrypted.success) { if (decrypted.error.type === DecryptErrTypes.needPassphrase) { // "close" button will wipe this frame out, so no need to exit the recursion diff --git a/extension/chrome/elements/compose-modules/compose-quote-module.ts b/extension/chrome/elements/compose-modules/compose-quote-module.ts index 20d89796cda..2052951628d 100644 --- a/extension/chrome/elements/compose-modules/compose-quote-module.ts +++ b/extension/chrome/elements/compose-modules/compose-quote-module.ts @@ -123,7 +123,11 @@ export class ComposeQuoteModule extends ViewModule { let attachmentMeta: { content: Buf, filename?: string } | undefined; if (block.type === 'encryptedAttachment') { this.setQuoteLoaderProgress('decrypting...'); - const result = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail), encryptedData: block.attachmentMeta.data }); + const result = await MsgUtil.decryptMessage({ + kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail), + encryptedData: block.attachmentMeta.data, + verificationPubs: [] // todo: signature? + }); if (result.success) { attachmentMeta = { content: result.content, filename: result.filename }; } @@ -160,7 +164,7 @@ export class ComposeQuoteModule extends ViewModule { }; private decryptMessage = async (encryptedData: Buf): Promise => { - const decryptRes = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail), encryptedData }); + const decryptRes = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail), encryptedData, verificationPubs: [] }); if (decryptRes.success) { return decryptRes.content.toUtfStr(); } else if (decryptRes.error && decryptRes.error.type === DecryptErrTypes.needPassphrase) { diff --git a/extension/chrome/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index 2ca61fae8d6..961cc5bd91a 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -3,7 +3,7 @@ 'use strict'; import { ChunkedCb, EmailProviderContact, RecipientType } from '../../../js/common/api/shared/api.js'; -import { Contact, KeyUtil } from '../../../js/common/core/crypto/key.js'; +import { Contact, KeyUtil, PubkeyInfo } from '../../../js/common/core/crypto/key.js'; import { PUBKEY_LOOKUP_RESULT_FAIL, PUBKEY_LOOKUP_RESULT_WRONG } from './compose-err-module.js'; import { ProviderContactsQuery, Recipients } from '../../../js/common/api/email-provider/email-provider-api.js'; import { RecipientElement, RecipientStatus } from './compose-types.js'; @@ -20,7 +20,7 @@ import { moveElementInArray } from '../../../js/common/platform/util.js'; import { ViewModule } from '../../../js/common/view-module.js'; import { ComposeView } from '../compose.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; -import { ContactPreview, ContactStore, ContactUpdate, PubkeyInfo } from '../../../js/common/platform/store/contact-store.js'; +import { ContactPreview, ContactStore, ContactUpdate } from '../../../js/common/platform/store/contact-store.js'; /** * todo - this class is getting too big diff --git a/extension/chrome/elements/compose-modules/compose-storage-module.ts b/extension/chrome/elements/compose-modules/compose-storage-module.ts index ded7e951b96..703b8d35736 100644 --- a/extension/chrome/elements/compose-modules/compose-storage-module.ts +++ b/extension/chrome/elements/compose-modules/compose-storage-module.ts @@ -3,7 +3,7 @@ 'use strict'; import { BrowserMsg } from '../../../js/common/browser/browser-msg.js'; -import { KeyInfo, KeyUtil, Key, PubkeyResult } from '../../../js/common/core/crypto/key.js'; +import { KeyInfo, KeyUtil, Key, PubkeyInfo, PubkeyResult } from '../../../js/common/core/crypto/key.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { Assert } from '../../../js/common/assert.js'; import { Catch, UnreportableError } from '../../../js/common/platform/catch.js'; @@ -13,7 +13,7 @@ import { ViewModule } from '../../../js/common/view-module.js'; import { ComposeView } from '../compose.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; -import { ContactStore, PubkeyInfo } from '../../../js/common/platform/store/contact-store.js'; +import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; import { Settings } from '../../../js/common/settings.js'; diff --git a/extension/chrome/elements/pgp_block.ts b/extension/chrome/elements/pgp_block.ts index 561be9a5338..1b17014187d 100644 --- a/extension/chrome/elements/pgp_block.ts +++ b/extension/chrome/elements/pgp_block.ts @@ -67,6 +67,10 @@ export class PgpBlockView extends View { this.decryptModule = new PgpBlockViewDecryptModule(this); } + public getSigner = () => { + return this.senderEmail; + }; + public render = async () => { const storage = await AcctStore.get(this.acctEmail, ['setup_done', 'google_token_scopes']); this.orgRules = await OrgRules.newInstance(this.acctEmail); diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts index 2a44a7b15b0..b988e9eae8b 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts @@ -69,7 +69,8 @@ export class PgpBlockViewAttachmentsModule { private decryptAndSaveAttachmentToDownloads = async (encrypted: Attachment) => { const kisWithPp = await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail); - const decrypted = await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData: encrypted.getData() }); + // todo: signature? + const decrypted = await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData: encrypted.getData(), verificationPubs: [] }); if (decrypted.success) { const attachment = new Attachment({ name: encrypted.name.replace(/\.(pgp|gpg)$/, ''), type: encrypted.type, data: decrypted.content }); Browser.saveToDownloads(attachment); diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts index 377b333c1f9..84056344757 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts @@ -13,6 +13,8 @@ import { Ui } from '../../../js/common/browser/ui.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; +import { PubkeyInfo } from '../../../js/common/core/crypto/key.js'; +import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; export class PgpBlockViewDecryptModule { @@ -25,6 +27,7 @@ export class PgpBlockViewDecryptModule { } public initialize = async (forcePullMsgFromApi = false) => { + const verificationPubs = (await ContactStore.getOneWithAllPubkeys(undefined, this.view.getSigner()))?.sortedPubkeys ?? []; try { if (this.canReadEmails && this.view.signature === true && this.view.msgId) { this.view.renderModule.renderText('Loading signed message...'); @@ -34,13 +37,13 @@ export class PgpBlockViewDecryptModule { const parsed = await Mime.decode(mimeMsg); if (parsed && typeof parsed.rawSignedContent === 'string' && parsed.signature) { this.view.signature = parsed.signature; - await this.decryptAndRender(Buf.fromUtfStr(parsed.rawSignedContent)); + await this.decryptAndRender(Buf.fromUtfStr(parsed.rawSignedContent), verificationPubs); } else { await this.view.errorModule.renderErr('Error: could not properly parse signed message', parsed.rawSignedContent || parsed.text || parsed.html || mimeMsg.toUtfStr()); } } else if (this.view.encryptedMsgUrlParam && !forcePullMsgFromApi) { // ascii armored message supplied this.view.renderModule.renderText(this.view.signature ? 'Verifying..' : 'Decrypting...'); - await this.decryptAndRender(this.view.encryptedMsgUrlParam); + await this.decryptAndRender(this.view.encryptedMsgUrlParam, verificationPubs); } else { // need to fetch the inline signed + armored or encrypted +armored message block from gmail api if (!this.view.msgId) { Xss.sanitizeRender('#pgp_block', `Missing msgId to fetch message in pgp_block. If this happens repeatedly, please report the issue to human@flowcrypt.com`); @@ -54,7 +57,7 @@ export class PgpBlockViewDecryptModule { this.isPwdMsgBasedOnMsgSnippet = isPwdMsg; this.view.renderModule.renderText('Decrypting...'); this.msgFetchedFromApi = format; - await this.decryptAndRender(Buf.fromUtfStr(armored), undefined, subject); + await this.decryptAndRender(Buf.fromUtfStr(armored), verificationPubs, undefined, subject); } } } catch (e) { @@ -62,27 +65,22 @@ export class PgpBlockViewDecryptModule { } }; - private decryptAndRender = async (encryptedData: Buf, optionalPwd?: string, plainSubject?: string) => { + private decryptAndRender = async (encryptedData: Buf, verificationPubs: PubkeyInfo[], optionalPwd?: string, plainSubject?: string) => { if (typeof this.view.signature !== 'string') { const kisWithPp = await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail); - const result = await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData }); + const result = await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData, verificationPubs }); if (typeof result === 'undefined') { await this.view.errorModule.renderErr(Lang.general.restartBrowserAndTryAgain, undefined); } else if (result.success) { - if (result.signature?.contact && !result.signature.match && this.canReadEmails && this.msgFetchedFromApi !== 'raw' && !result.signature.isErrFatal) { - console.info(`re-fetching message ${this.view.msgId} from api because failed signature check: ${!this.msgFetchedFromApi ? 'full' : 'raw'}`); - await this.initialize(true); - } else { - await this.view.renderModule.decideDecryptedContentFormattingAndRender(result.content, Boolean(result.isEncrypted), result.signature, - async () => { - const decryptResult = await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData }); - if (!decryptResult.success) { - return undefined; - } else { - return decryptResult.signature; - } - }, plainSubject); - } + await this.view.renderModule.decideDecryptedContentFormattingAndRender(result.content, Boolean(result.isEncrypted), result.signature, + /* todo: retry? async () => { + const decryptResult = await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData, verificationPubs }); + if (!decryptResult.success) { + return undefined; + } else { + return decryptResult.signature; + } + }*/ plainSubject); } else if (result.error.type === DecryptErrTypes.format) { if (this.canReadEmails && this.msgFetchedFromApi !== 'raw') { console.info(`re-fetching message ${this.view.msgId} from api because looks like bad formatting: ${!this.msgFetchedFromApi ? 'full' : 'raw'}`); @@ -99,7 +97,7 @@ export class PgpBlockViewDecryptModule { })); await PassphraseStore.waitUntilPassphraseChanged(this.view.acctEmail, result.longids.needPassphrase); this.view.renderModule.renderText('Decrypting...'); - await this.decryptAndRender(encryptedData, optionalPwd); + await this.decryptAndRender(encryptedData, verificationPubs, optionalPwd); } else { const primaryKi = await KeyStore.getFirstOptional(this.view.acctEmail); if (!result.longids.chosen && !primaryKi) { @@ -119,9 +117,9 @@ export class PgpBlockViewDecryptModule { } else { // this.view.signature is string // sometimes signatures come wrongly percent-encoded. Here we check for typical "=3Dabcd" at the end const sigText = Buf.fromUtfStr(this.view.signature.replace('\n=3D', '\n=')); - const signatureResult = await BrowserMsg.send.bg.await.pgpMsgVerifyDetached({ plaintext: encryptedData, sigText }); - await this.view.renderModule.decideDecryptedContentFormattingAndRender(encryptedData, false, signatureResult, - async () => { return await BrowserMsg.send.bg.await.pgpMsgVerifyDetached({ plaintext: encryptedData, sigText }); }); + const signatureResult = await BrowserMsg.send.bg.await.pgpMsgVerifyDetached({ plaintext: encryptedData, sigText, verificationPubs }); + await this.view.renderModule.decideDecryptedContentFormattingAndRender(encryptedData, false, signatureResult); + // todo: retry? } }; diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts index 9737066f1df..e3ea0b911da 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts @@ -76,10 +76,9 @@ export class PgpBlockViewRenderModule { } }; - public decideDecryptedContentFormattingAndRender = async (decryptedBytes: Buf, isEncrypted: boolean, sigResult: VerifyRes | undefined, - retryVerification: () => Promise, plainSubject?: string) => { + public decideDecryptedContentFormattingAndRender = async (decryptedBytes: Buf, isEncrypted: boolean, sigResult: VerifyRes | undefined, plainSubject?: string) => { this.setFrameColor(isEncrypted ? 'green' : 'gray'); - this.view.signatureModule.renderPgpSignatureCheckResult(sigResult, retryVerification); + this.view.signatureModule.renderPgpSignatureCheckResult(sigResult); const publicKeys: string[] = []; let renderableAttachments: Attachment[] = []; let decryptedContent = decryptedBytes.toUtfStr(); diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index 552cdc3f8e6..6c3e10114f0 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -2,67 +2,50 @@ 'use strict'; -import { ApiErr } from '../../../js/common/api/shared/api-error.js'; -import { BrowserMsg } from '../../../js/common/browser/browser-msg.js'; -import { Catch } from '../../../js/common/platform/catch.js'; import { PgpBlockView } from '../pgp_block'; import { Ui } from '../../../js/common/browser/ui.js'; import { VerifyRes } from '../../../js/common/core/crypto/pgp/msg-util.js'; -import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; -import { Str } from '../../../js/common/core/common.js'; export class PgpBlockViewSignatureModule { - private static setSigner = (signature: VerifyRes): void => { - const signerEmail = signature.signer?.primaryUserId ? Str.parseEmail(signature.signer.primaryUserId).email : undefined; - $('#pgp_signature > .cursive > span').text(signerEmail || 'Unknown Signer'); - }; - constructor(private view: PgpBlockView) { } - public renderPgpSignatureCheckResult = (signature: VerifyRes | undefined, retryVerification?: () => Promise) => { + public renderPgpSignatureCheckResult = (signature: VerifyRes | undefined) => { this.view.renderModule.doNotSetStateAsReadyYet = true; // so that body state is not marked as ready too soon - automated tests need to know when to check results - if (signature?.signer && !signature.contact) { - this.renderPgpSignatureCheckMissingPubkeyOptions(signature.signer.longid, this.view.senderEmail, retryVerification).then((newSignature) => { // async so that it doesn't block rendering - if (newSignature) { - return this.renderPgpSignatureCheckResult(newSignature, undefined); - } - PgpBlockViewSignatureModule.setSigner(signature); - this.view.renderModule.doNotSetStateAsReadyYet = false; - Ui.setTestState('ready'); - $('#pgp_block').css('min-height', '100px'); // signature fail can have a lot of text in it to render - this.view.renderModule.resizePgpBlockFrame(); - }).catch(Catch.reportErr); + if (!signature) { + $('#pgp_signature').addClass('bad'); + $('#pgp_signature > .cursive').remove(); + $('#pgp_signature > .result').text('Message Not Signed'); + } else if (signature.error) { + $('#pgp_signature').addClass('bad'); + $('#pgp_signature > .result').text(signature.error); + this.view.renderModule.setFrameColor('red'); + } else if (signature.match) { + $('#pgp_signature').addClass('good'); + $('#pgp_signature > .result').text('matching signature'); } else { - if (!signature) { - $('#pgp_signature').addClass('bad'); - $('#pgp_signature > .cursive').remove(); - $('#pgp_signature > .result').text('Message Not Signed'); - } else if (signature.error) { - $('#pgp_signature').addClass('bad'); - $('#pgp_signature > .result').text(signature.error); - this.view.renderModule.setFrameColor('red'); - } else if (signature.match && signature.signer && signature.contact) { - $('#pgp_signature').addClass('good'); - $('#pgp_signature > .result').text('matching signature'); - } else { - $('#pgp_signature').addClass('bad'); - $('#pgp_signature > .result').text('signature does not match'); - this.view.renderModule.setFrameColor('red'); - } - if (signature) { - PgpBlockViewSignatureModule.setSigner(signature); - } - this.view.renderModule.doNotSetStateAsReadyYet = false; - Ui.setTestState('ready'); + $('#pgp_signature').addClass('bad'); + $('#pgp_signature > .result').text('signature does not match'); + this.view.renderModule.setFrameColor('red'); + } + if (signature) { + this.setSigner(signature); } - // $('#pgp_signature').css('block'); + this.view.renderModule.doNotSetStateAsReadyYet = false; + Ui.setTestState('ready'); + }; + + private setSigner = (signature: VerifyRes): void => { + const signerEmail = signature.match ? this.view.getSigner() : undefined; + $('#pgp_signature > .cursive > span').text(signerEmail || 'Unknown Signer'); }; /** * don't have appropriate pubkey by longid in contacts + * */ + /* private renderPgpSignatureCheckMissingPubkeyOptions = async (signerLongid: string, senderEmail: string, retryVerification?: () => Promise): Promise => { const render = (note: string, action: () => void) => $('#pgp_signature').addClass('neutral').find('.result').text(note).click(this.view.setHandler(action)); @@ -109,5 +92,5 @@ export class PgpBlockViewSignatureModule { } return undefined; }; - +*/ } diff --git a/extension/chrome/settings/modules/decrypt.ts b/extension/chrome/settings/modules/decrypt.ts index 74cc3de6062..4283a97bfab 100644 --- a/extension/chrome/settings/modules/decrypt.ts +++ b/extension/chrome/settings/modules/decrypt.ts @@ -55,7 +55,11 @@ View.run(class ManualDecryptView extends View { }; private decryptAndDownload = async (encrypted: Attachment) => { // todo - this is more or less copy-pasted from attachment.js, should use common function - const result = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), encryptedData: encrypted.getData() }); + const result = await MsgUtil.decryptMessage({ + kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), + encryptedData: encrypted.getData(), + verificationPubs: [] // todo: signature? + }); if (result.success) { const attachment = new Attachment({ name: encrypted.name.replace(/\.(pgp|gpg|asc)$/i, ''), type: encrypted.type, data: result.content }); Browser.saveToDownloads(attachment); diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index bf6912edcdd..fcb4a6d047b 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -67,6 +67,20 @@ export interface KeyInfo { emails?: string[]; // todo - used to be missing - but migration was supposed to add it? setting back to optional for now } +export type PubkeyInfo = { + pubkey: Key, + // IMPORTANT NOTE: + // It might look like we can format PubkeyInfo[] out of Key[], but that's not good, + // because in the storage we have the table Revocations that stores fingerprints + // of revoked keys that may not exist in the database (Pubkeys table), + // that is pre-emptive external revocation. So (in a rare case) the lookup method + // receives a valid key, saves it to the storage, and after re-querying the storage, + // this key maybe returned as revoked. This is why PubkeyInfo has revoked property + // regardless of the fact that Key itself also has it. + revoked: boolean, + lastCheck?: number | undefined +}; + export interface KeyIdentity { id: string, // a fingerprint of the primary key in OpenPGP, and similarly a fingerprint of the actual cryptographic key (eg RSA fingerprint) in S/MIME type: 'openpgp' | 'x509' diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index e67c69c0337..c0eac1d5c96 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -1,15 +1,12 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ 'use strict'; -import { Contact, Key, KeyInfo, ExtendedKeyInfo, KeyUtil } from '../key.js'; +import { Key, KeyInfo, ExtendedKeyInfo, KeyUtil, PubkeyInfo } from '../key.js'; import { MsgBlockType, ReplaceableMsgBlockType } from '../../msg-block.js'; -import { Value } from '../../common.js'; import { Buf } from '../../buf.js'; -import { Catch } from '../../../platform/catch.js'; import { PgpArmor, PreparedForDecrypt } from './pgp-armor.js'; import { opgp } from './openpgpjs-custom.js'; import { KeyCache } from '../../../platform/key-cache.js'; -import { ContactStore } from '../../../platform/store/contact-store.js'; import { SmimeKey, SmimeMsg } from '../smime/smime-key.js'; import { OpenPGPKey } from './openpgp-key.js'; @@ -26,9 +23,9 @@ export namespace PgpMsgMethod { export namespace Arg { export type Encrypt = { pubkeys: Key[], signingPrv?: Key, pwd?: string, data: Uint8Array, filename?: string, armor: boolean, date?: Date }; export type Type = { data: Uint8Array | string }; - export type Decrypt = { kisWithPp: ExtendedKeyInfo[], encryptedData: Uint8Array, msgPwd?: string }; + export type Decrypt = { kisWithPp: ExtendedKeyInfo[], encryptedData: Uint8Array, msgPwd?: string, verificationPubs: PubkeyInfo[] }; export type DiagnosePubkeys = { armoredPubs: string[], message: Uint8Array }; - export type VerifyDetached = { plaintext: Uint8Array, sigText: Uint8Array }; + export type VerifyDetached = { plaintext: Uint8Array, sigText: Uint8Array, verificationPubs: PubkeyInfo[] }; } export type DiagnosePubkeys = (arg: Arg.DiagnosePubkeys) => Promise; export type VerifyDetached = (arg: Arg.VerifyDetached) => Promise; @@ -49,8 +46,6 @@ export namespace PgpMsgMethod { } type SortedKeysForDecrypt = { - verificationContacts: Contact[]; - forVerification: OpenPGP.key.Key[]; encryptedFor: string[]; signedBy: string[]; prvMatching: ExtendedKeyInfo[]; @@ -64,12 +59,12 @@ type DecryptError$error = { type: DecryptErrTypes; message: string; }; type DecryptError$longids = { message: string[]; matching: string[]; chosen: string[]; needPassphrase: string[]; }; export type DecryptError = { success: false; error: DecryptError$error; longids: DecryptError$longids; content?: Buf; isEncrypted?: boolean; }; -type OpenpgpMsgOrCleartext = OpenPGP.message.Message | OpenPGP.cleartext.CleartextMessage; - export type VerifyRes = { + // longid is set up even if the signature isn't verified + // primaryUserId is set up from the found key + // todo: make `match` a structure and move `primaryUserId` inside it or remove at all (#2147 is no longer appropriate) signer?: { primaryUserId: string | undefined, longid: string }; - contact?: Contact; - match: boolean | null; + match: boolean | null; // we can return some pubkey information here error?: string; isErrFatal?: boolean, content?: Buf @@ -148,61 +143,13 @@ export class MsgUtil { return await OpenPGPKey.sign(signingPrivate, data, detached); }; - public static verify = async (msg: OpenpgpMsgOrCleartext, pubs: OpenPGP.key.Key[], contact?: Contact): Promise => { - const verifyRes: VerifyRes = { contact, match: null }; // tslint:disable-line:no-null-keyword - try { - // this is here to ensure execution order when 1) verify, 2) read data, 3) processing signatures - // Else it will hang trying to read a stream: https://github.com/openpgpjs/openpgpjs/issues/916#issuecomment-510620625 - const verifications = await msg.verify(pubs); // first step - const stream = msg instanceof opgp.message.Message ? msg.getLiteralData() : msg.getText(); - if (stream) { // encrypted message - const data = await opgp.stream.readToEnd(stream); // second step - verifyRes.content = data instanceof Uint8Array ? new Buf(data) : Buf.fromUtfStr(data); - } - // third step below - for (const verification of verifications) { - // todo - a valid signature is a valid signature, and should be surfaced. Currently, if any of the signatures are not valid, it's showing all as invalid - // .. as it is now this could allow an attacker to append bogus signatures to validly signed messages, making otherwise correct messages seem incorrect - // .. which is not really an issue - an attacker that can append signatures could have also just slightly changed the message, causing the same experience - // .. so for now #wontfix unless a reasonable usecase surfaces - verifyRes.match = (verifyRes.match === true || verifyRes.match === null) && await verification.verified; - if (!verifyRes.signer) { - // todo - currently only the first signer will be reported. Should we be showing all signers? How common is that? - verifyRes.signer = { - longid: OpenPGPKey.bytesToLongid(verification.keyid.bytes), - primaryUserId: await OpenPGPKey.getPrimaryUserId(pubs, verification.keyid) - }; - } - } - } catch (verifyErr) { - verifyRes.match = null; // tslint:disable-line:no-null-keyword - if (verifyErr instanceof Error && verifyErr.message === 'Can only verify message with one literal data packet.') { - verifyRes.error = 'FlowCrypt is not equipped to verify this message'; - verifyRes.isErrFatal = true; // don't try to re-fetch the message from API - } else if (verifyErr instanceof Error && verifyErr.message.startsWith('Insecure message hash algorithm:')) { - verifyRes.error = `Could not verify message: ${verifyErr.message}. Sender is using old, insecure OpenPGP software.`; - verifyRes.isErrFatal = true; // don't try to re-fetch the message from API - } else if (verifyErr instanceof Error && verifyErr.message === 'Signature is expired') { - verifyRes.error = verifyErr.message; - verifyRes.isErrFatal = true; // don't try to re-fetch the message from API - } else if (verifyErr instanceof Error && verifyErr.message === 'Message digest did not match') { - verifyRes.error = verifyErr.message; - } else { - verifyRes.error = `Error verifying this message: ${String(verifyErr)}`; - Catch.reportErr(verifyErr); - } - } - return verifyRes; - }; - - public static verifyDetached: PgpMsgMethod.VerifyDetached = async ({ plaintext, sigText }) => { + public static verifyDetached: PgpMsgMethod.VerifyDetached = async ({ plaintext, sigText, verificationPubs }) => { const message = opgp.message.fromText(Buf.fromUint8(plaintext).toUtfStr()); await message.appendSignature(Buf.fromUint8(sigText).toUtfStr()); - const keys = await MsgUtil.getSortedKeys([], message); - return await MsgUtil.verify(message, keys.forVerification, keys.verificationContacts[0]); + return await OpenPGPKey.verify(message, verificationPubs); }; - public static decryptMessage: PgpMsgMethod.Decrypt = async ({ kisWithPp, encryptedData, msgPwd }) => { + public static decryptMessage: PgpMsgMethod.Decrypt = async ({ kisWithPp, encryptedData, msgPwd, verificationPubs }) => { const longids: DecryptError$longids = { message: [], matching: [], chosen: [], needPassphrase: [] }; let prepared: PreparedForDecrypt; try { @@ -210,18 +157,19 @@ export class MsgUtil { } catch (formatErr) { return { success: false, error: { type: DecryptErrTypes.format, message: String(formatErr) }, longids }; } + if (prepared.isCleartext) { + // todo: error if no verificationPubs? + const signature = await OpenPGPKey.verify(prepared.message, verificationPubs); + const content = signature.content || Buf.fromUtfStr('no content'); + signature.content = undefined; // no need to duplicate data + return { success: true, content, isEncrypted: false, signature }; + } + const isEncrypted = true; const keys = prepared.isPkcs7 ? await MsgUtil.getSmimeKeys(kisWithPp, prepared.message) : await MsgUtil.getSortedKeys(kisWithPp, prepared.message); longids.message = keys.encryptedFor; longids.matching = keys.prvForDecrypt.map(ki => ki.longid); longids.chosen = keys.prvForDecryptDecrypted.map(decrypted => decrypted.ki.longid); longids.needPassphrase = keys.prvForDecryptWithoutPassphrases.map(ki => ki.longid); - const isEncrypted = !prepared.isCleartext; - if (!isEncrypted && !prepared.isPkcs7) { - const signature = await MsgUtil.verify(prepared.message, keys.forVerification, keys.verificationContacts[0]); - const content = signature.content || Buf.fromUtfStr('no content'); - signature.content = undefined; // no need to duplicate data - return { success: true, content, isEncrypted, signature }; - } if (!keys.prvForDecryptDecrypted.length && (!msgPwd || prepared.isPkcs7)) { return { success: false, error: { type: DecryptErrTypes.needPassphrase, message: 'Missing pass phrase' }, longids, isEncrypted }; } @@ -230,7 +178,9 @@ export class MsgUtil { const decrypted = SmimeKey.decryptMessage(prepared.message, keys.prvForDecryptDecrypted[0].decrypted); return { success: true, content: new Buf(decrypted), isEncrypted }; } - const packets = (prepared.message as OpenPGP.message.Message).packets; + // cleartext and PKCS#7 are gone by this line + const msg = prepared.message as OpenPGP.message.Message; + const packets = msg.packets; const isSymEncrypted = packets.filter(p => p.tag === opgp.enums.packet.symEncryptedSessionKey).length > 0; const isPubEncrypted = packets.filter(p => p.tag === opgp.enums.packet.publicKeyEncryptedSessionKey).length > 0; if (isSymEncrypted && !isPubEncrypted && !msgPwd) { @@ -238,14 +188,14 @@ export class MsgUtil { } const passwords = msgPwd ? [msgPwd] : undefined; const privateKeys = keys.prvForDecryptDecrypted.map(decrypted => decrypted.decrypted); - const decrypted = await OpenPGPKey.decryptMessage(prepared.message as OpenPGP.message.Message, privateKeys, passwords); - await MsgUtil.cryptoMsgGetSignedBy(decrypted, keys); // we can only figure out who signed the msg once it's decrypted - const signature = keys.signedBy.length ? await MsgUtil.verify(decrypted, keys.forVerification, keys.verificationContacts[0]) : undefined; + const decrypted = await OpenPGPKey.decryptMessage(msg, privateKeys, passwords); + // todo: test when not signed at all + const signature = await OpenPGPKey.verify(decrypted, verificationPubs); const content = signature?.content || new Buf(await opgp.stream.readToEnd(decrypted.getLiteralData()!)); if (signature?.content) { signature.content = undefined; // already passed as "content" on the response object, don't need it duplicated } - if (!prepared.isCleartext && (prepared.message as OpenPGP.message.Message).packets.filterByTag(opgp.enums.packet.symmetricallyEncrypted).length) { + if (msg.packets.filterByTag(opgp.enums.packet.symmetricallyEncrypted).length) { const noMdc = 'Security threat!\n\nMessage is missing integrity checks (MDC). ' + ' The sender should update their outdated software.\n\nDisplay the message at your own risk.'; return { success: false, content, error: { type: DecryptErrTypes.noMdc, message: noMdc }, longids, isEncrypted }; @@ -287,23 +237,8 @@ export class MsgUtil { return diagnosis; }; - private static cryptoMsgGetSignedBy = async (msg: OpenpgpMsgOrCleartext, keys: SortedKeysForDecrypt) => { - keys.signedBy = Value.arr.unique(msg.getSigningKeyIds ? msg.getSigningKeyIds().map(kid => OpenPGPKey.bytesToLongid(kid.bytes)) : []); - if (keys.signedBy.length && typeof ContactStore.get === 'function') { - const verificationContacts = await ContactStore.get(undefined, keys.signedBy); - keys.verificationContacts = verificationContacts.filter(contact => contact && contact.pubkey) as Contact[]; - keys.forVerification = []; - for (const contact of keys.verificationContacts) { - const { keys: keysForVerification } = await opgp.key.readArmored(KeyUtil.armor(contact.pubkey!)); - keys.forVerification.push(...keysForVerification); - } - } - }; - - private static getSortedKeys = async (kiWithPp: ExtendedKeyInfo[], msg: OpenpgpMsgOrCleartext): Promise => { + private static getSortedKeys = async (kiWithPp: ExtendedKeyInfo[], msg: OpenPGP.message.Message): Promise => { const keys: SortedKeysForDecrypt = { - verificationContacts: [], - forVerification: [], encryptedFor: [], signedBy: [], prvMatching: [], @@ -311,9 +246,8 @@ export class MsgUtil { prvForDecryptDecrypted: [], prvForDecryptWithoutPassphrases: [], }; - const encryptionKeyids = msg instanceof opgp.message.Message ? (msg as OpenPGP.message.Message).getEncryptionKeyIds() : []; + const encryptionKeyids = msg.getEncryptionKeyIds(); keys.encryptedFor = encryptionKeyids.map(kid => OpenPGPKey.bytesToLongid(kid.bytes)); - await MsgUtil.cryptoMsgGetSignedBy(msg, keys); if (keys.encryptedFor.length) { keys.prvMatching = kiWithPp.filter(ki => KeyUtil.getKeyInfoLongids(ki).some( longid => keys.encryptedFor.includes(longid))); @@ -344,8 +278,6 @@ export class MsgUtil { private static getSmimeKeys = async (kiWithPp: ExtendedKeyInfo[], msg: SmimeMsg): Promise => { const keys: SortedKeysForDecrypt = { - verificationContacts: [], - forVerification: [], encryptedFor: [], signedBy: [], prvMatching: [], diff --git a/extension/js/common/core/crypto/pgp/openpgp-key.ts b/extension/js/common/core/crypto/pgp/openpgp-key.ts index 2f2c2873d95..fdf8532ef0d 100644 --- a/extension/js/common/core/crypto/pgp/openpgp-key.ts +++ b/extension/js/common/core/crypto/pgp/openpgp-key.ts @@ -1,13 +1,15 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ -import { Key, PrvPacket, KeyAlgo, KeyUtil, UnexpectedKeyTypeError } from '../key.js'; +import { Key, PrvPacket, KeyAlgo, KeyUtil, UnexpectedKeyTypeError, PubkeyInfo } from '../key.js'; import { opgp } from './openpgpjs-custom.js'; import { Catch } from '../../../platform/catch.js'; import { Str } from '../../common.js'; import { Buf } from '../../buf.js'; -import { PgpMsgMethod, MsgUtil } from './msg-util.js'; +import { PgpMsgMethod, VerifyRes } from './msg-util.js'; const internal = Symbol('internal openpgpjs library format key'); +type OpenpgpMsgOrCleartext = OpenPGP.message.Message | OpenPGP.cleartext.CleartextMessage; + export class OpenPGPKey { private static readonly encryptionText = 'This is the text we are encrypting!'; @@ -433,6 +435,57 @@ export class OpenPGPKey { return undefined; }; + public static verify = async (msg: OpenpgpMsgOrCleartext, pubs: PubkeyInfo[]): Promise => { + const verifyRes: VerifyRes = { match: null }; // tslint:disable-line:no-null-keyword + // msg.getSigningKeyIds ? msg.getSigningKeyIds() + // todo: double-check if S/MIME ever gets here + const opgpKeys = pubs.filter(x => !x.revoked && x.pubkey.type === 'openpgp').map(x => OpenPGPKey.extractExternalLibraryObjFromKey(x.pubkey)); + // todo: expired? + try { + // this is here to ensure execution order when 1) verify, 2) read data, 3) processing signatures + // Else it will hang trying to read a stream: https://github.com/openpgpjs/openpgpjs/issues/916#issuecomment-510620625 + const verifications = await msg.verify(opgpKeys); // first step + const stream = msg instanceof opgp.message.Message ? msg.getLiteralData() : msg.getText(); + if (stream) { // encrypted message + const data = await opgp.stream.readToEnd(stream); // second step + verifyRes.content = data instanceof Uint8Array ? new Buf(data) : Buf.fromUtfStr(data); + } + // third step below + for (const verification of verifications) { + // todo - a valid signature is a valid signature, and should be surfaced. Currently, if any of the signatures are not valid, it's showing all as invalid + // .. as it is now this could allow an attacker to append bogus signatures to validly signed messages, making otherwise correct messages seem incorrect + // .. which is not really an issue - an attacker that can append signatures could have also just slightly changed the message, causing the same experience + // .. so for now #wontfix unless a reasonable usecase surfaces + verifyRes.match = (verifyRes.match === true || verifyRes.match === null) && await verification.verified; + if (!verifyRes.signer) { + // todo - currently only the first signer will be reported. Should we be showing all signers? How common is that? + verifyRes.signer = { + longid: OpenPGPKey.bytesToLongid(verification.keyid.bytes), + primaryUserId: await OpenPGPKey.getPrimaryUserId(opgpKeys, verification.keyid) + }; + } + } + } catch (verifyErr) { + verifyRes.match = null; // tslint:disable-line:no-null-keyword + if (verifyErr instanceof Error && verifyErr.message === 'Can only verify message with one literal data packet.') { + verifyRes.error = 'FlowCrypt is not equipped to verify this message'; + verifyRes.isErrFatal = true; // don't try to re-fetch the message from API + } else if (verifyErr instanceof Error && verifyErr.message.startsWith('Insecure message hash algorithm:')) { + verifyRes.error = `Could not verify message: ${verifyErr.message}. Sender is using old, insecure OpenPGP software.`; + verifyRes.isErrFatal = true; // don't try to re-fetch the message from API + } else if (verifyErr instanceof Error && verifyErr.message === 'Signature is expired') { + verifyRes.error = verifyErr.message; + verifyRes.isErrFatal = true; // don't try to re-fetch the message from API + } else if (verifyErr instanceof Error && verifyErr.message === 'Message digest did not match') { + verifyRes.error = verifyErr.message; + } else { + verifyRes.error = `Error verifying this message: ${String(verifyErr)}`; + Catch.reportErr(verifyErr); + } + } + return verifyRes; + }; + private static getSortedUserids = async (key: OpenPGP.key.Key): Promise => { const data = (await Promise.all(key.users.filter(Boolean).map(async (user) => { const primaryKey = key.primaryKey; @@ -696,7 +749,8 @@ export class OpenPGPKey { } const signedMessage = await opgp.message.fromText(OpenPGPKey.encryptionText).sign([key]); output.push('sign msg ok'); - const verifyResult = await MsgUtil.verify(signedMessage, [key]); + const verifyResult = await OpenPGPKey.verify(signedMessage, + [{ pubkey: await OpenPGPKey.convertExternalLibraryObjToKey(key), revoked: false }]); if (verifyResult.error !== null && typeof verifyResult.error !== 'undefined') { output.push(`verify failed: ${verifyResult.error}`); } else { diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 1c84b71d4e2..74552c5f523 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -5,7 +5,7 @@ import { Catch } from '../catch.js'; import { opgp } from '../../core/crypto/pgp/openpgpjs-custom.js'; import { BrowserMsg } from '../../browser/browser-msg.js'; import { DateUtility, Str } from '../../core/common.js'; -import { Key, Contact, KeyUtil } from '../../core/crypto/key.js'; +import { Key, Contact, KeyUtil, PubkeyInfo } from '../../core/crypto/key.js'; // tslint:disable:no-null-keyword @@ -64,22 +64,8 @@ export type ContactUpdate = { type DbContactFilter = { hasPgp?: boolean, substring?: string, limit?: number }; -export type PubkeyInfo = { - pubkey: Key, - // IMPORTANT NOTE: - // It might look like we can format PubkeyInfo[] out of Key[], but that's not good, - // because in the storage we have the table Revocations that stores fingerprints - // of revoked keys that may not exist in the database (Pubkeys table), - // that is pre-emptive external revocation. So (in a rare case) the lookup method - // receives a valid key, saves it to the storage, and after re-querying the storage, - // this key maybe returned as revoked. This is why PubkeyInfo has revoked property - // regardless of the fact that Key itself also has it. - revoked: boolean, - lastCheck?: number | undefined -}; - -export type EmailWithSortedPubkeys = { - info: Email, +type EmailWithSortedPubkeys = { + info: Email, // todo: convert to a model class, exclude unnecessary fields like searchable sortedPubkeys: PubkeyInfo[] }; @@ -337,6 +323,7 @@ export class ContactStore extends AbstractStore { return emailEntity ? { info: emailEntity, sortedPubkeys: await ContactStore.sortKeys(pubkeys, revocations) } : undefined; }; + // todo: return parsed and with applied revocation public static getPubkey = async (db: IDBDatabase | undefined, { id, type }: { id: string, type: string }): Promise => { if (!db) { // relay op through background process @@ -686,40 +673,16 @@ export class ContactStore extends AbstractStore { return ContactStore.toContactFromKey(contactWithAllPubkeys.info, selected?.pubkey, selected?.lastCheck, Boolean(selected?.revoked)); } // search all longids - const tx = db.transaction(['emails', 'pubkeys'], 'readonly'); - return await new Promise((resolve, reject) => { - const req = tx.objectStore('pubkeys').index('index_longids').get(emailOrLongid); - ContactStore.setReqPipe(req, - (pubkey: Pubkey) => { - if (!pubkey) { - resolve(undefined); - return; - } - const req2 = tx.objectStore('emails').index('index_fingerprints').get(pubkey.fingerprint!); - ContactStore.setReqPipe(req2, - (email: Email) => { - if (!email) { - resolve(undefined); - } else { - resolve(ContactStore.toContact(db, email, pubkey)); - } - }, - reject); - }, - reject); - }); + throw new Error('longid search is deprecated'); // should not get here }; private static getKeyAttributes = (key: Key | undefined): PubkeyAttributes => { return { fingerprint: key?.id ?? null, expiresOn: DateUtility.asNumber(key?.expiration) }; }; - private static toContact = async (db: IDBDatabase, email: Email, pubkey: Pubkey | undefined): Promise => { - if (!email) { - return; - } + /* + private static isRevoked = async (db: IDBDatabase, pubkey: Pubkey | undefined): Promise => { const parsed = pubkey ? await KeyUtil.parse(pubkey.armoredKey) : undefined; - let revokedExternally = false; if (parsed && !parsed.revoked) { const revocations: Revocation[] = await new Promise((resolve, reject) => { const tx = db.transaction(['revocations'], 'readonly'); @@ -728,13 +691,13 @@ export class ContactStore extends AbstractStore { ContactStore.setReqPipe(req, resolve, reject); }); if (revocations.length) { - revokedExternally = true; + return true; // revoked externally } } - return ContactStore.toContactFromKey(email, parsed, parsed ? pubkey!.lastCheck : null, revokedExternally); + return parsed?.revoked || false; }; - - private static toContactFromKey = (email: Email, key: Key | undefined, lastCheck: number | undefined | null, revokedExternally: boolean): Contact | undefined => { +*/ + private static toContactFromKey = (email: Email | undefined, key: Key | undefined, lastCheck: number | undefined | null, revokedExternally: boolean): Contact | undefined => { if (!email) { return; } diff --git a/test/source/mock/google/strategies/send-message-strategy.ts b/test/source/mock/google/strategies/send-message-strategy.ts index 708ac6dbe0e..3b535c99712 100644 --- a/test/source/mock/google/strategies/send-message-strategy.ts +++ b/test/source/mock/google/strategies/send-message-strategy.ts @@ -81,7 +81,7 @@ class MessageWithFooterTestStrategy implements ITestMsgStrategy { public test = async (mimeMsg: ParsedMail) => { const keyInfo = await Config.getKeyInfo(["flowcrypt.compatibility.1pp1", "flowcrypt.compatibility.2pp1"]); - const decrypted = await MsgUtil.decryptMessage({ kisWithPp: keyInfo!, encryptedData: Buf.fromUtfStr(mimeMsg.text || '') }); + const decrypted = await MsgUtil.decryptMessage({ kisWithPp: keyInfo!, encryptedData: Buf.fromUtfStr(mimeMsg.text || ''), verificationPubs: [] }); if (!decrypted.success) { throw new HttpClientErr(`Error: can't decrypt message`); } @@ -98,7 +98,7 @@ class SignedMessageTestStrategy implements ITestMsgStrategy { public test = async (mimeMsg: ParsedMail) => { const keyInfo = await Config.getKeyInfo(["flowcrypt.compatibility.1pp1", "flowcrypt.compatibility.2pp1"]); - const decrypted = await MsgUtil.decryptMessage({ kisWithPp: keyInfo!, encryptedData: Buf.fromUtfStr(mimeMsg.text!) }); + const decrypted = await MsgUtil.decryptMessage({ kisWithPp: keyInfo!, encryptedData: Buf.fromUtfStr(mimeMsg.text!), verificationPubs: [] }); if (!decrypted.success) { throw new HttpClientErr(`Error: Could not successfully verify signed message`); } @@ -141,7 +141,7 @@ class IncludeQuotedPartTestStrategy implements ITestMsgStrategy { public test = async (mimeMsg: ParsedMail) => { const keyInfo = await Config.getKeyInfo(["flowcrypt.compatibility.1pp1", "flowcrypt.compatibility.2pp1"]); - const decrypted = await MsgUtil.decryptMessage({ kisWithPp: keyInfo!, encryptedData: Buf.fromUtfStr(mimeMsg.text!) }); + const decrypted = await MsgUtil.decryptMessage({ kisWithPp: keyInfo!, encryptedData: Buf.fromUtfStr(mimeMsg.text!), verificationPubs: [] }); if (!decrypted.success) { throw new HttpClientErr(`Error: can't decrypt message`); } diff --git a/test/source/platform/store/contact-store.ts b/test/source/platform/store/contact-store.ts index 5d1a8589226..428db5aa59c 100644 --- a/test/source/platform/store/contact-store.ts +++ b/test/source/platform/store/contact-store.ts @@ -21,12 +21,6 @@ export type Pubkey = { expiresOn: number | null; }; -export type PubkeyInfo = { - pubkey: Key, - revoked: boolean, - lastCheck?: number | undefined -}; - export class ContactStore { public static get = async (db: void, emailOrLongid: string[]): Promise<(Contact | undefined)[]> => { diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index 253d36068a9..fe2b331a950 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -994,14 +994,14 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const message = sentMsg.payload!.body!.data!; const encrypted = message.match(/\-\-\-\-\-BEGIN PGP MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP MESSAGE\-\-\-\-\-/s)![0]; const encryptedData = Buf.fromUtfStr(encrypted); - const decrypted0 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData }); + const decrypted0 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [] }); // decryption without a ki should fail expect(decrypted0.success).to.equal(false); // decryption with ki 1 should succeed - const decrypted1 = await MsgUtil.decryptMessage({ kisWithPp: await Config.getKeyInfo(["flowcrypt.compatibility.1pp1"]), encryptedData }); + const decrypted1 = await MsgUtil.decryptMessage({ kisWithPp: await Config.getKeyInfo(["flowcrypt.compatibility.1pp1"]), encryptedData, verificationPubs: [] }); expect(decrypted1.success).to.equal(true); // decryption with ki 2 should succeed - const decrypted2 = await MsgUtil.decryptMessage({ kisWithPp: await Config.getKeyInfo(["flowcrypt.compatibility.2pp1"]), encryptedData }); + const decrypted2 = await MsgUtil.decryptMessage({ kisWithPp: await Config.getKeyInfo(["flowcrypt.compatibility.2pp1"]), encryptedData, verificationPubs: [] }); expect(decrypted2.success).to.equal(true); })); diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 0755bb1753e..6e6ff44402b 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -15,8 +15,7 @@ import { OpenPGPKey } from '../core/crypto/pgp/openpgp-key'; import { DecryptError, DecryptSuccess, MsgUtil, PgpMsgMethod } from '../core/crypto/pgp/msg-util'; import { opgp } from '../core/crypto/pgp/openpgpjs-custom'; import { Attachment } from '../core/attachment.js'; -import { ContactStore } from '../platform/store/contact-store.js'; -import { GoogleData, GmailParser, GmailMsg } from '../mock/google/google-data'; +import { GoogleData, GmailMsg } from '../mock/google/google-data'; import { testConstants } from './tooling/consts'; import { PgpArmor } from '../core/crypto/pgp/pgp-armor'; import { ExpirationCache } from '../core/expiration-cache'; @@ -783,8 +782,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY .match(/\-\-\-\-\-BEGIN PGP SIGNED MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s)![0]; const encryptedData = Buf.fromUtfStr(enc); const pubkey = await KeyUtil.parse(testConstants.pubkey2864E326A5BE488A); - await ContactStore.update(undefined, 'president@forged.com', { name: 'President', pubkey }); - const decrypted = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData }); + const decrypted = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [{ pubkey, revoked: false }] }); expect(decrypted.success).to.equal(true); const verifyRes = (decrypted as DecryptSuccess).signature!; expect(verifyRes.match).to.be.true; @@ -792,6 +790,50 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY t.pass(); }); + ava.default('[unit][MsgUtil.decryptMessage] finds correct key to verify signature', async t => { + const data = await GoogleData.withInitializedData('ci.tests.gmail@flowcrypt.test'); + const msg: GmailMsg = data.getMessage('1766644f13510f58')!; + const enc = Buf.fromBase64Str(msg!.raw!).toUtfStr() + .match(/\-\-\-\-\-BEGIN PGP SIGNED MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s)![0]; + const encryptedData = Buf.fromUtfStr(enc); + // actual key the message was signed with + const pubkey = await KeyUtil.parse(testConstants.pubkey2864E326A5BE488A); + // better key + const betterKey = await KeyUtil.parse("-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: FlowCrypt Email Encryption [BUILD_REPLACEABLE_VERSION]\r\nComment: Seamlessly send and receive encrypted email\r\n\r\nxjMEYZeW2RYJKwYBBAHaRw8BAQdAT5QfLVP3y1yukk3MM/oiuXLNe1f9az5M\r\nBnOlKdF0nKnNJVNvbWVib2R5IDxTYW1zNTBzYW1zNTBzZXB0QEdtYWlsLkNv\r\nbT7CjwQQFgoAIAUCYZeW2QYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJS27QEA7pFlkLfD0KFQ\r\nsH/dwb/NPzn5zCi2L9gjPAC3d8gv1fwA/0FjAy/vKct4D7QH8KwtEGQns5+D\r\nP1WxDr4YI2hp5TkAzjgEYZeW2RIKKwYBBAGXVQEFAQEHQKNLY/bXrhJMWA2+\r\nWTjk3I7KhawyZfLomJ4hovqr7UtOAwEIB8J4BBgWCAAJBQJhl5bZAhsMACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJQnpgD/c1CzfS3YzJUx\r\nnFMrhjiE0WVgqOV/3CkfI4m4RA30QUIA/ju8r4AD2h6lu3Mx/6I6PzIRZQty\r\nLvTkcu4UKodZa4kK\r\n=7C4A\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n"); + { + const decrypted1 = await MsgUtil.decryptMessage({ + kisWithPp: [], encryptedData, verificationPubs: [{ pubkey, revoked: false }, { pubkey: betterKey, revoked: false }] + }); + expect(decrypted1.success).to.equal(true); + const verifyRes1 = (decrypted1 as DecryptSuccess).signature!; + expect(verifyRes1.match).to.be.true; + expect(verifyRes1.signer?.primaryUserId).to.equal('A50 Sam '); + } + { + const decrypted2 = await MsgUtil.decryptMessage({ + kisWithPp: [], encryptedData, verificationPubs: [{ pubkey: betterKey, revoked: false }, { pubkey, revoked: false }] + }); + expect(decrypted2.success).to.equal(true); + const verifyRes2 = (decrypted2 as DecryptSuccess).signature!; + expect(verifyRes2.match).to.be.true; + expect(verifyRes2.signer?.primaryUserId).to.equal('A50 Sam '); + } + { + const decrypted3 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [{ pubkey, revoked: false }] }); + expect(decrypted3.success).to.equal(true); + const verifyRes3 = (decrypted3 as DecryptSuccess).signature!; + expect(verifyRes3.match).to.be.true; + expect(verifyRes3.signer?.primaryUserId).to.equal('A50 Sam '); + } + { + const decrypted4 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [{ pubkey: betterKey, revoked: false }] }); + expect(decrypted4.success).to.equal(true); + const verifyRes4 = (decrypted4 as DecryptSuccess).signature!; + expect(verifyRes4.match).to.not.be.true; + } + t.pass(); + }); + ava.default('[unit][MsgUtil.verifyDetached] verifies Thunderbird html signed message', async t => { const data = await GoogleData.withInitializedData('flowcrypt.compatibility@gmail.com'); const msg: GmailMsg = data.getMessage('1754cfd1b2f1d6e5')!; @@ -805,10 +847,10 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const pubkey = plaintext .match(/\-\-\-\-\-BEGIN PGP PUBLIC KEY BLOCK\-\-\-\-\-.*\-\-\-\-\-END PGP PUBLIC KEY BLOCK\-\-\-\-\-/s)![0] .replace(/=\r\n/g, '').replace(/=3D/g, '='); - const from = GmailParser.findHeader(msg, "from"); - const contact = await ContactStore.obj({ email: from, pubkey }); - await ContactStore.save(undefined, contact); - const result = await MsgUtil.verifyDetached({ plaintext: Buf.fromUtfStr(plaintext), sigText: Buf.fromUtfStr(sigText) }); + const result = await MsgUtil.verifyDetached({ + plaintext: Buf.fromUtfStr(plaintext), sigText: Buf.fromUtfStr(sigText), + verificationPubs: [{ pubkey: await KeyUtil.parse(pubkey), revoked: false }] + }); expect(result.match).to.be.true; t.pass(); }); @@ -826,10 +868,11 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const pubkey = plaintext .match(/\-\-\-\-\-BEGIN PGP PUBLIC KEY BLOCK\-\-\-\-\-.*\-\-\-\-\-END PGP PUBLIC KEY BLOCK\-\-\-\-\-/s)![0] .replace(/=\r\n/g, '').replace(/=3D/g, '='); - const from = GmailParser.findHeader(msg, "from"); - const contact = await ContactStore.obj({ email: from, pubkey }); - await ContactStore.save(undefined, contact); - const result = await MsgUtil.verifyDetached({ plaintext: Buf.fromUtfStr(plaintext), sigText: Buf.fromUtfStr(sigText) }); + const result = await MsgUtil.verifyDetached({ + plaintext: Buf.fromUtfStr(plaintext), + sigText: Buf.fromUtfStr(sigText), + verificationPubs: [{ pubkey: await KeyUtil.parse(pubkey), revoked: false }] + }); expect(result.match).to.be.true; t.pass(); }); @@ -844,14 +887,11 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const plaintext = msgText .match(/Content\-Type: multipart\/mixed;\r?\n? boundary="\-\-\-\-sinikael\-\?=_2\-16054595384320\.6487848448108896".*\-\-\-\-\-\-sinikael\-\?=_2\-16054595384320\.6487848448108896\-\-\r?\n/s)![0] .replace(/\r?\n/g, '\r\n')!; - if ((await ContactStore.get(undefined, ['7FDE685548AEA788'])).length === 0) { - const contact = await ContactStore.obj({ - email: 'flowcrypt.compatibility@gmail.com', - pubkey: testConstants.flowcryptcompatibilityPublicKey7FDE685548AEA788 - }); - await ContactStore.save(undefined, contact); - } - const result = await MsgUtil.verifyDetached({ plaintext: Buf.fromUtfStr(plaintext), sigText }); + const result = await MsgUtil.verifyDetached({ + plaintext: Buf.fromUtfStr(plaintext), + sigText, + verificationPubs: [{ pubkey: await KeyUtil.parse(testConstants.flowcryptcompatibilityPublicKey7FDE685548AEA788), revoked: false }] + }); expect(result.match).to.be.true; t.pass(); }); @@ -969,7 +1009,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const encrypted = await MsgUtil.encryptMessage({ pubkeys, data, armor: true }) as PgpMsgMethod.EncryptPgpArmorResult; const parsed = await KeyUtil.parse(prvEncryptForSubkeyOnlyProtected); const kisWithPp: ExtendedKeyInfo[] = [{ ... await KeyUtil.typedKeyInfoObj(parsed), type: parsed.type, passphrase }]; - const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData: encrypted.data }); + const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData: encrypted.data, verificationPubs: [] }); // todo - later we'll have an org rule for ignoring this, and then it will be expected to pass as follows: // expect(decrypted.success).to.equal(true); // expect(decrypted.content!.toUtfStr()).to.equal(data.toUtfStr()); From b8e5ce58ec97762e7236c3d37185a249cd5e3ff8 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 28 Nov 2021 10:49:45 +0000 Subject: [PATCH 02/31] more refactorings and fixes --- .../pgp-block-decrypt-module.ts | 26 +++--- .../pgp-block-render-module.ts | 5 +- .../pgp-block-signature-module.ts | 23 ++++- extension/js/common/core/crypto/key.ts | 13 +-- .../js/common/core/crypto/pgp/msg-util.ts | 13 +-- .../js/common/platform/store/contact-store.ts | 86 ++++++++++--------- test/source/platform/store/contact-store.ts | 7 +- test/source/tests/unit-node.ts | 28 +++--- 8 files changed, 116 insertions(+), 85 deletions(-) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts index 84056344757..a909867a43e 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts @@ -13,7 +13,7 @@ import { Ui } from '../../../js/common/browser/ui.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; -import { PubkeyInfo } from '../../../js/common/core/crypto/key.js'; +import { KeyUtil } from '../../../js/common/core/crypto/key.js'; import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; export class PgpBlockViewDecryptModule { @@ -27,7 +27,10 @@ export class PgpBlockViewDecryptModule { } public initialize = async (forcePullMsgFromApi = false) => { - const verificationPubs = (await ContactStore.getOneWithAllPubkeys(undefined, this.view.getSigner()))?.sortedPubkeys ?? []; + const parsedPubs = (await ContactStore.getOneWithAllPubkeys(undefined, this.view.getSigner()))?.sortedPubkeys ?? []; + // todo: we don't actually need parsed pubs here because we're going to pass them to the backgorund page + // maybe we can have a method in ContactStore to extract armored keys + const verificationPubs = parsedPubs.map(key => KeyUtil.armor(key.pubkey)); try { if (this.canReadEmails && this.view.signature === true && this.view.msgId) { this.view.renderModule.renderText('Loading signed message...'); @@ -65,22 +68,23 @@ export class PgpBlockViewDecryptModule { } }; - private decryptAndRender = async (encryptedData: Buf, verificationPubs: PubkeyInfo[], optionalPwd?: string, plainSubject?: string) => { + private decryptAndRender = async (encryptedData: Buf, verificationPubs: string[], optionalPwd?: string, plainSubject?: string) => { if (typeof this.view.signature !== 'string') { const kisWithPp = await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail); - const result = await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData, verificationPubs }); + const decrypt = async (verificationPubs: string[]) => await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData, verificationPubs }); + const result = await decrypt(verificationPubs); if (typeof result === 'undefined') { await this.view.errorModule.renderErr(Lang.general.restartBrowserAndTryAgain, undefined); } else if (result.success) { await this.view.renderModule.decideDecryptedContentFormattingAndRender(result.content, Boolean(result.isEncrypted), result.signature, - /* todo: retry? async () => { - const decryptResult = await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData, verificationPubs }); + async (verificationPubs: string[]) => { + const decryptResult = await decrypt(verificationPubs); if (!decryptResult.success) { - return undefined; + return undefined; // note: this internal error results in a wrong "Message Not Signed" badge } else { return decryptResult.signature; } - }*/ plainSubject); + }, plainSubject); } else if (result.error.type === DecryptErrTypes.format) { if (this.canReadEmails && this.msgFetchedFromApi !== 'raw') { console.info(`re-fetching message ${this.view.msgId} from api because looks like bad formatting: ${!this.msgFetchedFromApi ? 'full' : 'raw'}`); @@ -117,9 +121,9 @@ export class PgpBlockViewDecryptModule { } else { // this.view.signature is string // sometimes signatures come wrongly percent-encoded. Here we check for typical "=3Dabcd" at the end const sigText = Buf.fromUtfStr(this.view.signature.replace('\n=3D', '\n=')); - const signatureResult = await BrowserMsg.send.bg.await.pgpMsgVerifyDetached({ plaintext: encryptedData, sigText, verificationPubs }); - await this.view.renderModule.decideDecryptedContentFormattingAndRender(encryptedData, false, signatureResult); - // todo: retry? + const verify = async (verificationPubs: string[]) => await BrowserMsg.send.bg.await.pgpMsgVerifyDetached({ plaintext: encryptedData, sigText, verificationPubs }); + const signatureResult = await verify(verificationPubs); + await this.view.renderModule.decideDecryptedContentFormattingAndRender(encryptedData, false, signatureResult, verify); } }; diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts index e3ea0b911da..b766a036b8d 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts @@ -76,9 +76,10 @@ export class PgpBlockViewRenderModule { } }; - public decideDecryptedContentFormattingAndRender = async (decryptedBytes: Buf, isEncrypted: boolean, sigResult: VerifyRes | undefined, plainSubject?: string) => { + public decideDecryptedContentFormattingAndRender = async (decryptedBytes: Buf, isEncrypted: boolean, sigResult: VerifyRes | undefined, + retryVerification: (verificationPubs: string[]) => Promise, plainSubject?: string) => { this.setFrameColor(isEncrypted ? 'green' : 'gray'); - this.view.signatureModule.renderPgpSignatureCheckResult(sigResult); + await this.view.signatureModule.renderPgpSignatureCheckResult(sigResult, retryVerification); const publicKeys: string[] = []; let renderableAttachments: Attachment[] = []; let decryptedContent = decryptedBytes.toUtfStr(); diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index 6c3e10114f0..eeb16a80fe1 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -2,6 +2,8 @@ 'use strict'; +import { ApiErr } from '../../../js/common/api/shared/api-error.js'; +import { Catch } from '../../../js/common/platform/catch.js'; import { PgpBlockView } from '../pgp_block'; import { Ui } from '../../../js/common/browser/ui.js'; import { VerifyRes } from '../../../js/common/core/crypto/pgp/msg-util.js'; @@ -11,7 +13,7 @@ export class PgpBlockViewSignatureModule { constructor(private view: PgpBlockView) { } - public renderPgpSignatureCheckResult = (signature: VerifyRes | undefined) => { + public renderPgpSignatureCheckResult = async (signature: VerifyRes | undefined, retryVerification?: (verificationPubs: string[]) => Promise) => { this.view.renderModule.doNotSetStateAsReadyYet = true; // so that body state is not marked as ready too soon - automated tests need to know when to check results if (!signature) { $('#pgp_signature').addClass('bad'); @@ -25,6 +27,25 @@ export class PgpBlockViewSignatureModule { $('#pgp_signature').addClass('good'); $('#pgp_signature > .result').text('matching signature'); } else { + if (retryVerification) { + this.view.renderModule.renderText('Verifying message...'); + try { + const { pubkeys: newPubkeys } = await this.view.pubLookup.lookupEmail(this.view.getSigner()); + if (newPubkeys.length) { + await this.renderPgpSignatureCheckResult(await retryVerification(newPubkeys), undefined); + return; + } + } catch (e) { + if (ApiErr.isSignificant(e)) { + Catch.reportErr(e); + $('#pgp_signature').addClass('neutral').find('.result').text(`Could not load sender's pubkey due to an error.`); + } else { + $('#pgp_signature').addClass('neutral').find('.result').text(`Could not look up sender's pubkey due to network error, click to retry.`).click( + this.view.setHandler(() => window.location.reload())); + } + return; + } + } $('#pgp_signature').addClass('bad'); $('#pgp_signature > .result').text('signature does not match'); this.view.renderModule.setFrameColor('red'); diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index fcb4a6d047b..b58af65494c 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -67,8 +67,8 @@ export interface KeyInfo { emails?: string[]; // todo - used to be missing - but migration was supposed to add it? setting back to optional for now } -export type PubkeyInfo = { - pubkey: Key, +export interface PubkeyInfo { + pubkey: Key; // IMPORTANT NOTE: // It might look like we can format PubkeyInfo[] out of Key[], but that's not good, // because in the storage we have the table Revocations that stores fingerprints @@ -77,9 +77,12 @@ export type PubkeyInfo = { // receives a valid key, saves it to the storage, and after re-querying the storage, // this key maybe returned as revoked. This is why PubkeyInfo has revoked property // regardless of the fact that Key itself also has it. - revoked: boolean, - lastCheck?: number | undefined -}; + revoked: boolean; +} + +export interface PubkeyInfoWithLastCheck extends PubkeyInfo { + lastCheck?: number | undefined; +} export interface KeyIdentity { id: string, // a fingerprint of the primary key in OpenPGP, and similarly a fingerprint of the actual cryptographic key (eg RSA fingerprint) in S/MIME diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index c0eac1d5c96..f652e4e703b 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -1,7 +1,7 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ 'use strict'; -import { Key, KeyInfo, ExtendedKeyInfo, KeyUtil, PubkeyInfo } from '../key.js'; +import { Key, KeyInfo, ExtendedKeyInfo, KeyUtil } from '../key.js'; import { MsgBlockType, ReplaceableMsgBlockType } from '../../msg-block.js'; import { Buf } from '../../buf.js'; import { PgpArmor, PreparedForDecrypt } from './pgp-armor.js'; @@ -9,6 +9,7 @@ import { opgp } from './openpgpjs-custom.js'; import { KeyCache } from '../../../platform/key-cache.js'; import { SmimeKey, SmimeMsg } from '../smime/smime-key.js'; import { OpenPGPKey } from './openpgp-key.js'; +import { ContactStore } from '../../../platform/store/contact-store.js'; export class DecryptionError extends Error { public decryptError: DecryptError; @@ -23,9 +24,9 @@ export namespace PgpMsgMethod { export namespace Arg { export type Encrypt = { pubkeys: Key[], signingPrv?: Key, pwd?: string, data: Uint8Array, filename?: string, armor: boolean, date?: Date }; export type Type = { data: Uint8Array | string }; - export type Decrypt = { kisWithPp: ExtendedKeyInfo[], encryptedData: Uint8Array, msgPwd?: string, verificationPubs: PubkeyInfo[] }; + export type Decrypt = { kisWithPp: ExtendedKeyInfo[], encryptedData: Uint8Array, msgPwd?: string, verificationPubs: string[] }; export type DiagnosePubkeys = { armoredPubs: string[], message: Uint8Array }; - export type VerifyDetached = { plaintext: Uint8Array, sigText: Uint8Array, verificationPubs: PubkeyInfo[] }; + export type VerifyDetached = { plaintext: Uint8Array, sigText: Uint8Array, verificationPubs: string[] }; } export type DiagnosePubkeys = (arg: Arg.DiagnosePubkeys) => Promise; export type VerifyDetached = (arg: Arg.VerifyDetached) => Promise; @@ -146,7 +147,7 @@ export class MsgUtil { public static verifyDetached: PgpMsgMethod.VerifyDetached = async ({ plaintext, sigText, verificationPubs }) => { const message = opgp.message.fromText(Buf.fromUint8(plaintext).toUtfStr()); await message.appendSignature(Buf.fromUint8(sigText).toUtfStr()); - return await OpenPGPKey.verify(message, verificationPubs); + return await OpenPGPKey.verify(message, await ContactStore.getPubkeyInfos(undefined, verificationPubs)); }; public static decryptMessage: PgpMsgMethod.Decrypt = async ({ kisWithPp, encryptedData, msgPwd, verificationPubs }) => { @@ -159,7 +160,7 @@ export class MsgUtil { } if (prepared.isCleartext) { // todo: error if no verificationPubs? - const signature = await OpenPGPKey.verify(prepared.message, verificationPubs); + const signature = await OpenPGPKey.verify(prepared.message, await ContactStore.getPubkeyInfos(undefined, verificationPubs)); const content = signature.content || Buf.fromUtfStr('no content'); signature.content = undefined; // no need to duplicate data return { success: true, content, isEncrypted: false, signature }; @@ -190,7 +191,7 @@ export class MsgUtil { const privateKeys = keys.prvForDecryptDecrypted.map(decrypted => decrypted.decrypted); const decrypted = await OpenPGPKey.decryptMessage(msg, privateKeys, passwords); // todo: test when not signed at all - const signature = await OpenPGPKey.verify(decrypted, verificationPubs); + const signature = await OpenPGPKey.verify(decrypted, await ContactStore.getPubkeyInfos(undefined, verificationPubs)); const content = signature?.content || new Buf(await opgp.stream.readToEnd(decrypted.getLiteralData()!)); if (signature?.content) { signature.content = undefined; // already passed as "content" on the response object, don't need it duplicated diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 74552c5f523..5599313ef66 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -4,8 +4,8 @@ import { AbstractStore } from './abstract-store.js'; import { Catch } from '../catch.js'; import { opgp } from '../../core/crypto/pgp/openpgpjs-custom.js'; import { BrowserMsg } from '../../browser/browser-msg.js'; -import { DateUtility, Str } from '../../core/common.js'; -import { Key, Contact, KeyUtil, PubkeyInfo } from '../../core/crypto/key.js'; +import { DateUtility, Str, Value } from '../../core/common.js'; +import { Key, Contact, KeyUtil, PubkeyInfo, PubkeyInfoWithLastCheck } from '../../core/crypto/key.js'; // tslint:disable:no-null-keyword @@ -66,7 +66,7 @@ type DbContactFilter = { hasPgp?: boolean, substring?: string, limit?: number }; type EmailWithSortedPubkeys = { info: Email, // todo: convert to a model class, exclude unnecessary fields like searchable - sortedPubkeys: PubkeyInfo[] + sortedPubkeys: PubkeyInfoWithLastCheck[] }; const x509postfix = "-X509"; @@ -287,10 +287,9 @@ export class ContactStore extends AbstractStore { resolve(email); return; } - const uniqueAndStrippedFingerprints = email.fingerprints. - map(ContactStore.stripFingerprint). - filter((value, index, self) => !self.slice(0, index).find((el) => el === value)); - let countdown = email.fingerprints.length + uniqueAndStrippedFingerprints.length; + // fire requests to query pubkeys and revocations + // when all of them finish, the transaction will complete + ContactStore.setTxHandlers(tx, () => resolve(email), reject); // request all pubkeys by fingerprints for (const fp of email.fingerprints) { const req2 = tx.objectStore('pubkeys').get(fp); @@ -299,24 +298,10 @@ export class ContactStore extends AbstractStore { if (pubkey) { pubkeys.push(pubkey); } - if (!--countdown) { - resolve(email); - } - }, - reject); - } - for (const fp of uniqueAndStrippedFingerprints) { - const range = ContactStore.createFingerprintRange(fp); - const req3 = tx.objectStore('revocations').getAll(range); - ContactStore.setReqPipe(req3, - (revocation: Revocation[]) => { - revocations.push(...revocation); - if (!--countdown) { - resolve(email); - } - }, - reject); + }); } + // fire requests to collect revocations + ContactStore.collectRevocations(tx, revocations, email.fingerprints); }, reject); }); @@ -436,6 +421,30 @@ export class ContactStore extends AbstractStore { }); }; + // construct PubkeyInfo objects out of provided keys and revocation data in the database + // the keys themselves may not be necessarily present in the database + public static getPubkeyInfos = async (db: IDBDatabase | undefined, keys: (Key | string)[]): Promise => { + if (!db) { // relay op through background process + return (await BrowserMsg.send.bg.await.db({ f: 'getPubkeyInfos', args: [keys] })) as PubkeyInfo[]; + } + const parsedKeys = await Promise.all(keys.map(async (key) => await KeyUtil.asPublicKey(typeof key === 'string' ? await KeyUtil.parse(key) : key))); + const unrevokedIds = parsedKeys.filter(key => !key.revoked).map(key => key.id); + const revocations: Revocation[] = []; + if (unrevokedIds.length) { // need to search for external revocations + await new Promise((resolve, reject) => { + const tx = db.transaction(['revocations'], 'readonly'); + ContactStore.setTxHandlers(tx, resolve, reject); + ContactStore.collectRevocations(tx, revocations, unrevokedIds); + }); + } + return parsedKeys.map(key => { + return { + pubkey: key, + revoked: key.revoked || revocations.some(r => ContactStore.equalFingerprints(key.id, r.fingerprint)) + }; + }); + }; + public static sortPubkeyInfos = (pubkeyInfos: PubkeyInfo[]): PubkeyInfo[] => { return pubkeyInfos.sort((a, b) => ContactStore.getSortValue(b) - ContactStore.getSortValue(a)); }; @@ -474,6 +483,18 @@ export class ContactStore extends AbstractStore { return IDBKeyRange.bound(strippedFp, strippedFp + x509postfix, false, false); }; + // fire requests to collect revocations + private static collectRevocations = (tx: IDBTransaction, revocations: Revocation[], fingerprints: string[]) => { + for (const fp of Value.arr.unique(fingerprints.map(ContactStore.stripFingerprint))) { + const range = ContactStore.createFingerprintRange(fp); + const req = tx.objectStore('revocations').getAll(range); + ContactStore.setReqPipe(req, + (revocation: Revocation[]) => { + revocations.push(...revocation); + }); + } + }; + private static updateTxPhase2 = (tx: IDBTransaction, email: string, update: ContactUpdate, existingPubkey: Pubkey | undefined, revocations: Revocation[]) => { let pubkeyEntity: Pubkey | undefined; @@ -680,23 +701,6 @@ export class ContactStore extends AbstractStore { return { fingerprint: key?.id ?? null, expiresOn: DateUtility.asNumber(key?.expiration) }; }; - /* - private static isRevoked = async (db: IDBDatabase, pubkey: Pubkey | undefined): Promise => { - const parsed = pubkey ? await KeyUtil.parse(pubkey.armoredKey) : undefined; - if (parsed && !parsed.revoked) { - const revocations: Revocation[] = await new Promise((resolve, reject) => { - const tx = db.transaction(['revocations'], 'readonly'); - const range = ContactStore.createFingerprintRange(parsed!.id); - const req = tx.objectStore('revocations').getAll(range); - ContactStore.setReqPipe(req, resolve, reject); - }); - if (revocations.length) { - return true; // revoked externally - } - } - return parsed?.revoked || false; - }; -*/ private static toContactFromKey = (email: Email | undefined, key: Key | undefined, lastCheck: number | undefined | null, revokedExternally: boolean): Contact | undefined => { if (!email) { return; diff --git a/test/source/platform/store/contact-store.ts b/test/source/platform/store/contact-store.ts index 428db5aa59c..a5bb1b89ef4 100644 --- a/test/source/platform/store/contact-store.ts +++ b/test/source/platform/store/contact-store.ts @@ -2,7 +2,7 @@ // tslint:disable:no-null-keyword -import { Contact, Key, KeyUtil } from '../../core/crypto/key'; +import { Contact, Key, KeyUtil, PubkeyInfo } from '../../core/crypto/key'; const DATA: Contact[] = []; @@ -30,6 +30,11 @@ export class ContactStore { return result; }; + public static getPubkeyInfos = async (db: IDBDatabase | undefined, keys: (Key | string)[]): Promise => { + const parsedKeys = await Promise.all(keys.map(async (key) => await KeyUtil.asPublicKey(typeof key === 'string' ? await KeyUtil.parse(key) : key))); + return parsedKeys.map(key => { return { pubkey: key, revoked: false }; }); + }; + public static update = async (db: void, email: string | string[], update: ContactUpdate): Promise => { if (Array.isArray(email)) { await Promise.all(email.map(oneEmail => ContactStore.update(db, oneEmail, update))); diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 6e6ff44402b..be9eee938c0 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -781,8 +781,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const enc = Buf.fromBase64Str(msg!.raw!).toUtfStr() .match(/\-\-\-\-\-BEGIN PGP SIGNED MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s)![0]; const encryptedData = Buf.fromUtfStr(enc); - const pubkey = await KeyUtil.parse(testConstants.pubkey2864E326A5BE488A); - const decrypted = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [{ pubkey, revoked: false }] }); + const decrypted = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [testConstants.pubkey2864E326A5BE488A] }); expect(decrypted.success).to.equal(true); const verifyRes = (decrypted as DecryptSuccess).signature!; expect(verifyRes.match).to.be.true; @@ -797,36 +796,32 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY .match(/\-\-\-\-\-BEGIN PGP SIGNED MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s)![0]; const encryptedData = Buf.fromUtfStr(enc); // actual key the message was signed with - const pubkey = await KeyUtil.parse(testConstants.pubkey2864E326A5BE488A); + const pubkey = testConstants.pubkey2864E326A5BE488A; // better key - const betterKey = await KeyUtil.parse("-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: FlowCrypt Email Encryption [BUILD_REPLACEABLE_VERSION]\r\nComment: Seamlessly send and receive encrypted email\r\n\r\nxjMEYZeW2RYJKwYBBAHaRw8BAQdAT5QfLVP3y1yukk3MM/oiuXLNe1f9az5M\r\nBnOlKdF0nKnNJVNvbWVib2R5IDxTYW1zNTBzYW1zNTBzZXB0QEdtYWlsLkNv\r\nbT7CjwQQFgoAIAUCYZeW2QYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJS27QEA7pFlkLfD0KFQ\r\nsH/dwb/NPzn5zCi2L9gjPAC3d8gv1fwA/0FjAy/vKct4D7QH8KwtEGQns5+D\r\nP1WxDr4YI2hp5TkAzjgEYZeW2RIKKwYBBAGXVQEFAQEHQKNLY/bXrhJMWA2+\r\nWTjk3I7KhawyZfLomJ4hovqr7UtOAwEIB8J4BBgWCAAJBQJhl5bZAhsMACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJQnpgD/c1CzfS3YzJUx\r\nnFMrhjiE0WVgqOV/3CkfI4m4RA30QUIA/ju8r4AD2h6lu3Mx/6I6PzIRZQty\r\nLvTkcu4UKodZa4kK\r\n=7C4A\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n"); + const betterKey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: FlowCrypt Email Encryption [BUILD_REPLACEABLE_VERSION]\r\nComment: Seamlessly send and receive encrypted email\r\n\r\nxjMEYZeW2RYJKwYBBAHaRw8BAQdAT5QfLVP3y1yukk3MM/oiuXLNe1f9az5M\r\nBnOlKdF0nKnNJVNvbWVib2R5IDxTYW1zNTBzYW1zNTBzZXB0QEdtYWlsLkNv\r\nbT7CjwQQFgoAIAUCYZeW2QYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJS27QEA7pFlkLfD0KFQ\r\nsH/dwb/NPzn5zCi2L9gjPAC3d8gv1fwA/0FjAy/vKct4D7QH8KwtEGQns5+D\r\nP1WxDr4YI2hp5TkAzjgEYZeW2RIKKwYBBAGXVQEFAQEHQKNLY/bXrhJMWA2+\r\nWTjk3I7KhawyZfLomJ4hovqr7UtOAwEIB8J4BBgWCAAJBQJhl5bZAhsMACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJQnpgD/c1CzfS3YzJUx\r\nnFMrhjiE0WVgqOV/3CkfI4m4RA30QUIA/ju8r4AD2h6lu3Mx/6I6PzIRZQty\r\nLvTkcu4UKodZa4kK\r\n=7C4A\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n"; { - const decrypted1 = await MsgUtil.decryptMessage({ - kisWithPp: [], encryptedData, verificationPubs: [{ pubkey, revoked: false }, { pubkey: betterKey, revoked: false }] - }); + const decrypted1 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [pubkey, betterKey] }); expect(decrypted1.success).to.equal(true); const verifyRes1 = (decrypted1 as DecryptSuccess).signature!; expect(verifyRes1.match).to.be.true; expect(verifyRes1.signer?.primaryUserId).to.equal('A50 Sam '); } { - const decrypted2 = await MsgUtil.decryptMessage({ - kisWithPp: [], encryptedData, verificationPubs: [{ pubkey: betterKey, revoked: false }, { pubkey, revoked: false }] - }); + const decrypted2 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [betterKey, pubkey] }); expect(decrypted2.success).to.equal(true); const verifyRes2 = (decrypted2 as DecryptSuccess).signature!; expect(verifyRes2.match).to.be.true; expect(verifyRes2.signer?.primaryUserId).to.equal('A50 Sam '); } { - const decrypted3 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [{ pubkey, revoked: false }] }); + const decrypted3 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [pubkey] }); expect(decrypted3.success).to.equal(true); const verifyRes3 = (decrypted3 as DecryptSuccess).signature!; expect(verifyRes3.match).to.be.true; expect(verifyRes3.signer?.primaryUserId).to.equal('A50 Sam '); } { - const decrypted4 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [{ pubkey: betterKey, revoked: false }] }); + const decrypted4 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [betterKey] }); expect(decrypted4.success).to.equal(true); const verifyRes4 = (decrypted4 as DecryptSuccess).signature!; expect(verifyRes4.match).to.not.be.true; @@ -847,10 +842,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const pubkey = plaintext .match(/\-\-\-\-\-BEGIN PGP PUBLIC KEY BLOCK\-\-\-\-\-.*\-\-\-\-\-END PGP PUBLIC KEY BLOCK\-\-\-\-\-/s)![0] .replace(/=\r\n/g, '').replace(/=3D/g, '='); - const result = await MsgUtil.verifyDetached({ - plaintext: Buf.fromUtfStr(plaintext), sigText: Buf.fromUtfStr(sigText), - verificationPubs: [{ pubkey: await KeyUtil.parse(pubkey), revoked: false }] - }); + const result = await MsgUtil.verifyDetached({ plaintext: Buf.fromUtfStr(plaintext), sigText: Buf.fromUtfStr(sigText), verificationPubs: [pubkey] }); expect(result.match).to.be.true; t.pass(); }); @@ -871,7 +863,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const result = await MsgUtil.verifyDetached({ plaintext: Buf.fromUtfStr(plaintext), sigText: Buf.fromUtfStr(sigText), - verificationPubs: [{ pubkey: await KeyUtil.parse(pubkey), revoked: false }] + verificationPubs: [pubkey] }); expect(result.match).to.be.true; t.pass(); @@ -890,7 +882,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const result = await MsgUtil.verifyDetached({ plaintext: Buf.fromUtfStr(plaintext), sigText, - verificationPubs: [{ pubkey: await KeyUtil.parse(testConstants.flowcryptcompatibilityPublicKey7FDE685548AEA788), revoked: false }] + verificationPubs: [testConstants.flowcryptcompatibilityPublicKey7FDE685548AEA788] }); expect(result.match).to.be.true; t.pass(); From 6436edfc346286691228d681408ff7de579c6023 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 28 Nov 2021 12:37:21 +0000 Subject: [PATCH 03/31] fix --- .../pgp-block-signature-module.ts | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index eeb16a80fe1..0d6517e34fb 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -28,27 +28,32 @@ export class PgpBlockViewSignatureModule { $('#pgp_signature > .result').text('matching signature'); } else { if (retryVerification) { - this.view.renderModule.renderText('Verifying message...'); - try { - const { pubkeys: newPubkeys } = await this.view.pubLookup.lookupEmail(this.view.getSigner()); - if (newPubkeys.length) { - await this.renderPgpSignatureCheckResult(await retryVerification(newPubkeys), undefined); - return; + const signerEmail = this.view.getSigner(); + if (!signerEmail) { + // in some tests we load the block without sender information + $('#pgp_signature').addClass('neutral').find('.result').text(`Could not verify sender.`); + } else { + this.view.renderModule.renderText('Verifying message...'); + try { + const { pubkeys: newPubkeys } = await this.view.pubLookup.lookupEmail(this.view.getSigner()); + if (newPubkeys.length) { + await this.renderPgpSignatureCheckResult(await retryVerification(newPubkeys), undefined); + return; + } + $('#pgp_signature').addClass('bad'); + $('#pgp_signature > .result').text('signature does not match'); // todo: or "neutral" missing pubkey? + this.view.renderModule.setFrameColor('red'); + } catch (e) { + if (ApiErr.isSignificant(e)) { + Catch.reportErr(e); + $('#pgp_signature').addClass('neutral').find('.result').text(`Could not load sender's pubkey due to an error.`); + } else { + $('#pgp_signature').addClass('neutral').find('.result').text(`Could not look up sender's pubkey due to network error, click to retry.`).click( + this.view.setHandler(() => window.location.reload())); + } } - } catch (e) { - if (ApiErr.isSignificant(e)) { - Catch.reportErr(e); - $('#pgp_signature').addClass('neutral').find('.result').text(`Could not load sender's pubkey due to an error.`); - } else { - $('#pgp_signature').addClass('neutral').find('.result').text(`Could not look up sender's pubkey due to network error, click to retry.`).click( - this.view.setHandler(() => window.location.reload())); - } - return; } } - $('#pgp_signature').addClass('bad'); - $('#pgp_signature > .result').text('signature does not match'); - this.view.renderModule.setFrameColor('red'); } if (signature) { this.setSigner(signature); From 7c9c25c907ad58579c37343170b20e05b77fa641 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 28 Nov 2021 19:56:04 +0000 Subject: [PATCH 04/31] fix unit tests that used to query by longid --- .../browser-unit-tests/unit-ContactStore.js | 184 +++--------------- 1 file changed, 23 insertions(+), 161 deletions(-) diff --git a/test/source/tests/browser-unit-tests/unit-ContactStore.js b/test/source/tests/browser-unit-tests/unit-ContactStore.js index 47e6eb82813..0f36a26484b 100644 --- a/test/source/tests/browser-unit-tests/unit-ContactStore.js +++ b/test/source/tests/browser-unit-tests/unit-ContactStore.js @@ -258,84 +258,7 @@ BROWSER_UNIT_TEST_NAME(`ContactStore saves and returns dates as numbers`); return 'pass'; })(); -BROWSER_UNIT_TEST_NAME(`ContactStore gets a contact by any longid`); -(async () => { - const contactABBDEF = await ContactStore.obj({ - email: 'abbdef@test.com', pubkey: testConstants.abbdefTestComPubkey - }); - const contactABCDEF = await ContactStore.obj({ - email: 'abcdef@test.com', pubkey: testConstants.abcdefTestComPubkey - }); - const contactABCDDF = await ContactStore.obj({ - email: 'abcddf@test.com', pubkey: testConstants.abcddfTestComPubkey - }); - const contactABDDEF = await ContactStore.obj({ - email: 'abddef@test.com', pubkey: testConstants.abddefTestComPubkey - }); - await ContactStore.save(undefined, [contactABBDEF, contactABCDEF, contactABCDDF, contactABDDEF]); - const [abbdefByPrimaryLongid] = await ContactStore.get(undefined, ['DF63659C3B4A81FB']); - if (abbdefByPrimaryLongid.email !== 'abbdef@test.com') { - throw Error(`Expected to get the key for abbdef@test.com by primary longid but got ${abbdefByPrimaryLongid.email}`); - } - if (abbdefByPrimaryLongid.pubkey.id !== 'B790AE8F425DC44633A8C086DF63659C3B4A81FB') { - throw Error(`Expected to get the key fingerprint B790AE8F425DC44633A8C086DF63659C3B4A81FB but got ${abbdefByPrimaryLongid.pubkey.id}`); - } - const [abbdefBySubkeyLongid] = await ContactStore.get(undefined, ['621DE1814AD675E0']); - if (abbdefBySubkeyLongid.email !== 'abbdef@test.com') { - throw Error(`Expected to get the key for abbdef@test.com by subkey longid but got ${abbdefBySubkeyLongid.email}`); - } - if (abbdefBySubkeyLongid.pubkey.id !== 'B790AE8F425DC44633A8C086DF63659C3B4A81FB') { - throw Error(`Expected to get the key fingerprint B790AE8F425DC44633A8C086DF63659C3B4A81FB but got ${abbdefBySubkeyLongid.pubkey.id}`); - } - - const [abcdefByPrimaryLongid] = await ContactStore.get(undefined, ['608BCD797A23FB91']); - if (abcdefByPrimaryLongid.email !== 'abcdef@test.com') { - throw Error(`Expected to get the key for abcdef@test.com by primary longid but got ${abcdefByPrimaryLongid.email}`); - } - if (abcdefByPrimaryLongid.pubkey.id !== '3155F118B6E732B3638A1CE1608BCD797A23FB91') { - throw Error(`Expected to get the key fingerprint 3155F118B6E732B3638A1CE1608BCD797A23FB91 but got ${abcdefByPrimaryLongid.pubkey.id}`); - } - const [abcdefBySubkeyLongid] = await ContactStore.get(undefined, ['2D47A41943DFAFCE']); - if (abcdefBySubkeyLongid.email !== 'abcdef@test.com') { - throw Error(`Expected to get the key for abcdef@test.com by subkey longid but got ${abcdefBySubkeyLongid.email}`); - } - if (abcdefBySubkeyLongid.pubkey.id !== '3155F118B6E732B3638A1CE1608BCD797A23FB91') { - throw Error(`Expected to get the key fingerprint 3155F118B6E732B3638A1CE1608BCD797A23FB91 but got ${abcdefBySubkeyLongid.pubkey.id}`); - } - - const [abcddfByPrimaryLongid] = await ContactStore.get(undefined, ['75AA44AB8930F7E9']); - if (abcddfByPrimaryLongid.email !== 'abcddf@test.com') { - throw Error(`Expected to get the key for abcddf@test.com by primary longid but got ${abcddfByPrimaryLongid.email}`); - } - if (abcddfByPrimaryLongid.pubkey.id !== '6CF53D2329C2A80828F499D375AA44AB8930F7E9') { - throw Error(`Expected to get the key fingerprint 6CF53D2329C2A80828F499D375AA44AB8930F7E9 but got ${abcddfByPrimaryLongid.pubkey.id}`); - } - const [abcddfBySubkeyLongid] = await ContactStore.get(undefined, ['92CFDAC7AA3A4253']); - if (abcddfBySubkeyLongid.email !== 'abcddf@test.com') { - throw Error(`Expected to get the key for abcddf@test.com by subkey longid but got ${abcddfBySubkeyLongid.email}`); - } - if (abcddfBySubkeyLongid.pubkey.id !== '6CF53D2329C2A80828F499D375AA44AB8930F7E9') { - throw Error(`Expected to get the key fingerprint 6CF53D2329C2A80828F499D375AA44AB8930F7E9 but got ${abcddfBySubkeyLongid.pubkey.id}`); - } - - const [abddefByPrimaryLongid] = await ContactStore.get(undefined, ['5FCC1541CF282951']); - if (abddefByPrimaryLongid.email !== 'abddef@test.com') { - throw Error(`Expected to get the key for abddef@test.com by primary longid but got ${abddefByPrimaryLongid.email}`); - } - if (abddefByPrimaryLongid.pubkey.id !== '9E020D9B752FD3FFF17ED9B65FCC1541CF282951') { - throw Error(`Expected to get the key fingerprint 9E020D9B752FD3FFF17ED9B65FCC1541CF282951 but got ${abddefByPrimaryLongid.pubkey.id}`); - } - const [abddefBySubkeyLongid] = await ContactStore.get(undefined, ['EAA7A05FE34F3A1A']); - if (abddefBySubkeyLongid.email !== 'abddef@test.com') { - throw Error(`Expected to get the key for abddef@test.com by subkey longid but got ${abddefBySubkeyLongid.email}`); - } - if (abddefBySubkeyLongid.pubkey.id !== '9E020D9B752FD3FFF17ED9B65FCC1541CF282951') { - throw Error(`Expected to get the key fingerprint 9E020D9B752FD3FFF17ED9B65FCC1541CF282951 but got ${abddefBySubkeyLongid.pubkey.id}`); - } - return 'pass'; -})(); - -BROWSER_UNIT_TEST_NAME(`ContactStore gets a valid pubkey by e-mail, or exact pubkey by longid`); +BROWSER_UNIT_TEST_NAME(`ContactStore gets a valid pubkey by e-mail and all pubkeys with getOneWithAllPubkeys()`); (async () => { // Note 1: email differs from pubkey id // Note 2: not necessary to call ContactStore.save, it's possible to always use ContactStore.update @@ -347,13 +270,18 @@ BROWSER_UNIT_TEST_NAME(`ContactStore gets a valid pubkey by e-mail, or exact pub if (expectedValid.pubkey.id !== 'D6662C5FB9BDE9DA01F3994AAA1EF832D8CCA4F2') { throw Error(`Expected to get the key fingerprint D6662C5FB9BDE9DA01F3994AAA1EF832D8CCA4F2 but got ${expectedValid.pubkey.id}`); } - const [expectedRevoked1] = await ContactStore.get(undefined, ['097EEBF354259A5E']); - if (expectedRevoked1.pubkey.id !== 'A5CFC8E8EA4AE69989FE2631097EEBF354259A5E') { - throw Error(`Expected to get the key fingerprint A5CFC8E8EA4AE69989FE2631097EEBF354259A5E but got ${expectedRevoked1.pubkey.id}`); + const {sortedPubkeys: pubs} = await ContactStore.getOneWithAllPubkeys(undefined, `some.revoked@otherhost.com`); + if (pubs.length !== 3) { + throw new Error(`3 pubkeys were expected to be retrieved from the storage but got ${pubs.length}`); + } + if (!pubs.some(x => x.pubkey.id === 'A5CFC8E8EA4AE69989FE2631097EEBF354259A5E')) { + throw Error(`Expected to get the key with fingerprint A5CFC8E8EA4AE69989FE2631097EEBF354259A5E but missing it`); } - const [expectedRevoked2] = await ContactStore.get(undefined, ['DE8538DDA1648C76']); - if (expectedRevoked2.pubkey.id !== '3930752556D57C46A1C56B63DE8538DDA1648C76') { - throw Error(`Expected to get the key fingerprint 3930752556D57C46A1C56B63DE8538DDA1648C76 but got ${expectedRevoked2.pubkey.id}`); + if (!pubs.some(x => x.pubkey.id === '3930752556D57C46A1C56B63DE8538DDA1648C76')) { + throw Error(`Expected to get the key with fingerprint 3930752556D57C46A1C56B63DE8538DDA1648C76 but missing it`); + } + if (!pubs.some(x => x.pubkey.id === 'D6662C5FB9BDE9DA01F3994AAA1EF832D8CCA4F2')) { + throw Error(`Expected to get the key with fingerprint D6662C5FB9BDE9DA01F3994AAA1EF832D8CCA4F2 but missing it`); } return 'pass'; })(); @@ -371,9 +299,6 @@ BROWSER_UNIT_TEST_NAME(`ContactStore stores postfixed fingerprint internally for // extract the entity directly from the database const entityFp = '16BB407403A3ADC55E1E0E4AF93EEC8FB187C923-X509'; const fingerprint = '16BB407403A3ADC55E1E0E4AF93EEC8FB187C923'; - const longid = 'X509-MIGiMIGNMQswCQYDVQQGEwJJVDEQMA4GA1UECAwHQmVyZ2FtbzEZMBcGA1UEBwwQUG9udGUgU2Fu' + - 'IFBpZXRybzEjMCEGA1UECgwaQWN0YWxpcyBTLnAuQS4vMDMzNTg1MjA5NjcxLDAqBgNVBAMMI0FjdGFsaXMgQ2xpZW50IE' + - 'F1dGhlbnRpY2F0aW9uIENBIEcyAhBj9wJecA85RTAfsvulZ0+E'; const entity = await new Promise((resolve, reject) => { const req = db.transaction(['pubkeys'], 'readonly').objectStore('pubkeys').get(entityFp); ContactStore.setReqPipe(req, resolve, reject); @@ -381,10 +306,6 @@ BROWSER_UNIT_TEST_NAME(`ContactStore stores postfixed fingerprint internally for if (entity.fingerprint !== entityFp) { throw Error(`Failed to extract pubkey ${fingerprint}`); } - const [contactByLongid] = await ContactStore.get(db, [longid]); - if (contactByLongid.pubkey.id !== fingerprint) { - throw Error(`Failed to extract pubkey ${fingerprint}`); - } const [contactByEmail] = await ContactStore.get(db, [email]); if (contactByEmail.pubkey.id !== fingerprint) { throw Error(`Failed to extract pubkey ${fingerprint}`); @@ -392,36 +313,6 @@ BROWSER_UNIT_TEST_NAME(`ContactStore stores postfixed fingerprint internally for return 'pass'; })(); -BROWSER_UNIT_TEST_NAME(`ContactStore searches S/MIME Certificate by PKCS#7 message recipient`); -(async () => { - const db = await ContactStore.dbOpen(); - const email = 'actalis@meta.33mail.com'; - const pubkey = testConstants.expiredSmimeCert; - const contacts = [await ContactStore.obj({ email, pubkey })]; - await ContactStore.save(db, contacts); - const p7 = forge.pkcs7.createEnvelopedData(); - const certificate = forge.pki.certificateFromPem(pubkey); - p7.addRecipient(certificate); - const recipient = p7.recipients[0]; - const issuerAndSerialNumberAsn1 = - forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [ - // Name - forge.pki.distinguishedNameToAsn1({ attributes: recipient.issuer }), - // Serial - forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, - forge.util.hexToBytes(recipient.serialNumber)) - ]); - const der = forge.asn1.toDer(issuerAndSerialNumberAsn1).getBytes(); - const buf = Buf.fromRawBytesStr(der); - const [contact] = await ContactStore.get(db, ['X509-' + buf.toBase64Str()]); - const foundCert = KeyUtil.armor(contact.pubkey); - const foundCertStripped = foundCert.match(/(.*\-\-\-\-\-END CERTIFICATE\-\-\-\-\-)\r?\n?/s)[1]; - if (foundCertStripped !== pubkey) { - throw new Error(`The certificate wasn't found by S/MIME IssuerAndSerialNumber`); - } - return 'pass'; -})(); - BROWSER_UNIT_TEST_NAME(`ContactStore: X-509 revocation affects OpenPGP key`); (async () => { const db = await ContactStore.dbOpen(); @@ -431,13 +322,9 @@ BROWSER_UNIT_TEST_NAME(`ContactStore: X-509 revocation affects OpenPGP key`); throw new Error(`Valid OpenPGP Key is expected to have fingerprint ${fingerprint} but actually is ${opgpKeyOldAndValid.id}`); } await ContactStore.update(db, 'some.revoked@localhost.com', { pubkey: opgpKeyOldAndValid }); - const [loadedOpgpKey1] = await ContactStore.get(db, [`some.revoked@localhost.com`]); - if (loadedOpgpKey1.pubkey.revoked) { - throw new Error(`The loaded OpenPGP Key (1) was expected to be valid but it is revoked.`); - } - const [loadedOpgpKey2] = await ContactStore.get(db, [`AA1EF832D8CCA4F2`]); - if (loadedOpgpKey2.pubkey.revoked) { - throw new Error(`The loaded OpenPGP Key (2) was expected to be valid but it is revoked.`); + const {sortedPubkeys: pubkeys1} = await ContactStore.getOneWithAllPubkeys(db, `some.revoked@localhost.com`); + if (pubkeys1.some(x => x.revoked)) { + throw new Error('The pubkey was expected to be valid but it is revoked.'); } // emulate X-509 revocation await new Promise((resolve, reject) => { @@ -446,13 +333,9 @@ BROWSER_UNIT_TEST_NAME(`ContactStore: X-509 revocation affects OpenPGP key`); tx.objectStore('revocations').put({ fingerprint: fingerprint + "-X509" }); }); // original key should be either revoked or missing - const [loadedOpgpKey3] = await ContactStore.get(db, [`some.revoked@localhost.com`]); - if (loadedOpgpKey3.pubkey && !loadedOpgpKey3.pubkey.revoked) { - throw new Error(`The loaded OpenPGP Key (3) was expected to be revoked but it is not.`); - } - const [loadedOpgpKey4] = await ContactStore.get(db, [`AA1EF832D8CCA4F2`]); - if (loadedOpgpKey4.pubkey && !loadedOpgpKey4.pubkey.revoked) { - throw new Error(`The loaded OpenPGP Key (4) was expected to be revoked but it is not.`); + const {sortedPubkeys: pubkeys2} = await ContactStore.getOneWithAllPubkeys(db, `some.revoked@localhost.com`); + if (pubkeys2.some(x => !x.revoked)) { + throw new Error('The pubkey was expected to be revoked but it is not.'); } return 'pass'; })(); @@ -463,14 +346,9 @@ BROWSER_UNIT_TEST_NAME(`ContactStore: OpenPGP revocation affects X.509 certifica const smimeKey = await KeyUtil.parse(testConstants.expiredSmimeCert); await ContactStore.update(db, 'actalis@meta.33mail.com', { pubkey: smimeKey }); const [loadedCert1] = await ContactStore.get(db, [`actalis@meta.33mail.com`]); - const longid = KeyUtil.getPrimaryLongid(smimeKey); if (loadedCert1.pubkey.revoked) { throw new Error(`The loaded X.509 certificate (1) was expected to be valid but it is revoked.`); } - const [loadedCert2] = await ContactStore.get(db, [longid]); - if (loadedCert2.pubkey.revoked) { - throw new Error(`The loaded X.509 certificate (2) was expected to be valid but it is revoked.`); - } // emulate openPGP revocation await new Promise((resolve, reject) => { const tx = db.transaction(['revocations'], 'readwrite'); @@ -482,10 +360,6 @@ BROWSER_UNIT_TEST_NAME(`ContactStore: OpenPGP revocation affects X.509 certifica if (loadedCert3.pubkey && !loadedCert3.pubkey.revoked) { throw new Error(`The loaded X.509 certificate (3) was expected to be revoked but it is not.`); } - const [loadedCert4] = await ContactStore.get(db, [longid]); - if (loadedCert4.pubkey && !loadedCert4.pubkey.revoked) { - throw new Error(`The loaded X.509 certificate (4) was expected to be revoked but it is not.`); - } return 'pass'; })(); @@ -506,28 +380,16 @@ BROWSER_UNIT_TEST_NAME(`ContactStore doesn't replace revoked key with older vers if (loadedOpgpKey1.pubkey.revoked) { throw new Error(`The loaded OpenPGP Key (1) was expected to be valid but it is revoked.`); } - const [loadedOpgpKey2] = await ContactStore.get(db, [`AA1EF832D8CCA4F2`]); - if (loadedOpgpKey2.pubkey.revoked) { - throw new Error(`The loaded OpenPGP Key (2) was expected to be valid but it is revoked.`); - } await ContactStore.update(db, 'some.revoked@localhost.com', { pubkey: opgpKeyRevoked }); + const [loadedOpgpKey2] = await ContactStore.get(db, [`some.revoked@localhost.com`]); + if (loadedOpgpKey2.pubkey && !loadedOpgpKey2.pubkey.revoked) { + throw new Error(`The loaded OpenPGP Key (2) was expected to be revoked but it is not.`); + } + await ContactStore.update(db, 'some.revoked@localhost.com', { pubkey: opgpKeyOldAndValid }); const [loadedOpgpKey3] = await ContactStore.get(db, [`some.revoked@localhost.com`]); if (loadedOpgpKey3.pubkey && !loadedOpgpKey3.pubkey.revoked) { throw new Error(`The loaded OpenPGP Key (3) was expected to be revoked but it is not.`); } - const [loadedOpgpKey4] = await ContactStore.get(db, [`AA1EF832D8CCA4F2`]); - if (loadedOpgpKey4.pubkey && !loadedOpgpKey4.pubkey.revoked) { - throw new Error(`The loaded OpenPGP Key (4) was expected to be revoked but it is not.`); - } - await ContactStore.update(db, 'some.revoked@localhost.com', { pubkey: opgpKeyOldAndValid }); - const [loadedOpgpKey5] = await ContactStore.get(db, [`some.revoked@localhost.com`]); - if (loadedOpgpKey5.pubkey && !loadedOpgpKey5.pubkey.revoked) { - throw new Error(`The loaded OpenPGP Key (5) was expected to be revoked but it is not.`); - } - const [loadedOpgpKey6] = await ContactStore.get(db, [`AA1EF832D8CCA4F2`]); - if (loadedOpgpKey6.pubkey && !loadedOpgpKey6.pubkey.revoked) { - throw new Error(`The loaded OpenPGP Key (6) was expected to be revoked but it is not.`); - } return 'pass'; })(); From ba745e8553ccad3d9dd408b42fe3a02ff21c01b8 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 30 Nov 2021 15:06:08 +0000 Subject: [PATCH 05/31] fixed some tests --- .../message-export-16a9c109bc51687d.json | 4 +-- test/source/tests/decrypt.ts | 29 +++++++++++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/test/source/mock/google/exported-messages/message-export-16a9c109bc51687d.json b/test/source/mock/google/exported-messages/message-export-16a9c109bc51687d.json index b6e764d9898..551c5c78ff4 100644 --- a/test/source/mock/google/exported-messages/message-export-16a9c109bc51687d.json +++ b/test/source/mock/google/exported-messages/message-export-16a9c109bc51687d.json @@ -73,11 +73,11 @@ }, { "name": "From", - "value": "FlowCrypt Compatibility " + "value": "FlowCrypt Compatibility " }, { "name": "Reply-To", - "value": "FlowCrypt Compatibility " + "value": "FlowCrypt Compatibility " }, { "name": "Subject", diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts index 5b08cbe0b8e..3750ccde94b 100644 --- a/test/source/tests/decrypt.ts +++ b/test/source/tests/decrypt.ts @@ -394,26 +394,43 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: url, content: ['How is my message signed?'], - signature: ['Sams50sams50sept@Gmail.Com', 'matching signature'] + signature: ['President@Forged.Com', 'matching signature'] + }); + })); + + ava.default('signature - verification succeeds when signed with a second-best key', testWithBrowser('ci.tests.gmail', async (t, browser) => { + const threadId = '1766644f13510f58'; + const acctEmail = 'ci.tests.gmail@flowcrypt.test'; + await PageRecipe.addPubkey(t, browser, acctEmail, '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: FlowCrypt Email Encryption [BUILD_REPLACEABLE_VERSION]\r\nComment: Seamlessly send and receive encrypted email\r\n\r\nxjMEYZeW2RYJKwYBBAHaRw8BAQdAT5QfLVP3y1yukk3MM/oiuXLNe1f9az5M\r\nBnOlKdF0nKnNJVNvbWVib2R5IDxTYW1zNTBzYW1zNTBzZXB0QEdtYWlsLkNv\r\nbT7CjwQQFgoAIAUCYZeW2QYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJS27QEA7pFlkLfD0KFQ\r\nsH/dwb/NPzn5zCi2L9gjPAC3d8gv1fwA/0FjAy/vKct4D7QH8KwtEGQns5+D\r\nP1WxDr4YI2hp5TkAzjgEYZeW2RIKKwYBBAGXVQEFAQEHQKNLY/bXrhJMWA2+\r\nWTjk3I7KhawyZfLomJ4hovqr7UtOAwEIB8J4BBgWCAAJBQJhl5bZAhsMACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJQnpgD/c1CzfS3YzJUx\r\nnFMrhjiE0WVgqOV/3CkfI4m4RA30QUIA/ju8r4AD2h6lu3Mx/6I6PzIRZQty\r\nLvTkcu4UKodZa4kK\r\n=7C4A\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n', + 'president@forged.com'); + const inboxPage = await browser.newPage(t, TestUrls.extension(`chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}&threadId=${threadId}`)); + await inboxPage.waitAll('iframe', { timeout: 2 }); + const urls = await inboxPage.getFramesUrls(['/chrome/elements/pgp_block.htm'], { sleep: 10, appearIn: 20 }); + expect(urls.length).to.equal(1); + const url = urls[0].split('/chrome/elements/pgp_block.htm')[1]; + await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { + params: url, + content: ['How is my message signed?'], + signature: ['President@Forged.Com', 'matching signature'] }); })); ava.default('decrypt - protonmail - load pubkey into contact + verify detached msg', testWithBrowser('compatibility', async (t, browser) => { const textParams = `?frameId=none&message=&msgId=16a9c109bc51687d&` + - `senderEmail=mismatch%40mail.com&isOutgoing=___cu_false___&signature=___cu_true___&acctEmail=flowcrypt.compatibility%40gmail.com`; + `senderEmail=some.alias%40protonmail.com&isOutgoing=___cu_false___&signature=___cu_true___&acctEmail=flowcrypt.compatibility%40gmail.com`; await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: textParams, content: ["1234"], signature: ["Missing pubkey"] }); - await PageRecipe.addPubkey(t, browser, 'flowcrypt.compatibility%40gmail.com', testConstants.protonCompatPub); + await PageRecipe.addPubkey(t, browser, 'flowcrypt.compatibility%40gmail.com', testConstants.protonCompatPub, 'some.alias@protonmail.com'); await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: textParams, content: ["1234"], - signature: ["matching signature", "Flowcrypt.Compatibility@Protonmail.Com"] + signature: ["matching signature", "Some.Alias@Protonmail.Com"] }); const htmlParams = `?frameId=none&message=&msgId=16a9c0fe4e034bc2&` + - `senderEmail=&isOutgoing=___cu_false___&signature=___cu_true___&acctEmail=flowcrypt.compatibility%40gmail.com`; + `senderEmail=some.alias%40protonmail.com&isOutgoing=___cu_false___&signature=___cu_true___&acctEmail=flowcrypt.compatibility%40gmail.com`; await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: htmlParams, content: ["1234"], - signature: ["matching signature", "Flowcrypt.Compatibility@Protonmail.Com"] + signature: ["matching signature", "Some.Alias@Protonmail.Com"] }); })); From 8ae8d04e25d0b13bb506015a9080018fcac085df Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 30 Nov 2021 15:07:44 +0000 Subject: [PATCH 06/31] removed unused browser msg --- extension/js/common/browser/browser-msg.ts | 23 +++++----------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index 482b4aa2791..f72365318cd 100644 --- a/extension/js/common/browser/browser-msg.ts +++ b/extension/js/common/browser/browser-msg.ts @@ -17,8 +17,7 @@ import { MsgUtil } from '../core/crypto/pgp/msg-util.js'; import { Ui } from './ui.js'; import { GlobalStoreDict, GlobalIndex } from '../platform/store/global-store.js'; import { AcctStoreDict, AccountIndex } from '../platform/store/acct-store.js'; -import { Contact, Key, KeyUtil } from '../core/crypto/key.js'; -import { OpenPGPKey } from '../core/crypto/pgp/openpgp-key.js'; +import { Contact } from '../core/crypto/key.js'; export type GoogleAuthWindowResult$result = 'Success' | 'Denied' | 'Error' | 'Closed'; @@ -62,8 +61,6 @@ export namespace Bm { export type PgpMsgVerifyDetached = PgpMsgMethod.Arg.VerifyDetached; export type PgpHashChallengeAnswer = { answer: string }; export type PgpMsgType = PgpMsgMethod.Arg.Type; - export type KeyParse = { armored: string }; - export type KeyMatch = { pubkeys: string[], longid: string }; export type Ajax = { req: JQueryAjaxSettings, stack: string }; export type AjaxGmailAttachmentGetChunk = { acctEmail: string, msgId: string, attachmentId: string }; export type ShowAttachmentPreview = { iframeUrl: string }; @@ -83,26 +80,24 @@ export namespace Bm { export type PgpMsgVerify = VerifyRes; export type PgpMsgType = PgpMsgTypeResult; export type PgpHashChallengeAnswer = { hashed: string }; - export type KeyParse = { key: Key }; - export type KeyMatch = { key: Key | undefined }; export type AjaxGmailAttachmentGetChunk = { chunk: Buf }; export type _tab_ = { tabId: string | null | undefined }; export type Db = any; // not included in Any below export type Ajax = any; // not included in Any below export type Any = GetActiveTabInfo | _tab_ | ReconnectAcctAuthPopup - | PgpMsgDecrypt | PgpMsgDiagnoseMsgPubkeys | PgpMsgVerify | PgpHashChallengeAnswer | PgpMsgType | KeyParse + | PgpMsgDecrypt | PgpMsgDiagnoseMsgPubkeys | PgpMsgVerify | PgpHashChallengeAnswer | PgpMsgType | InMemoryStoreGet | InMemoryStoreSet | StoreAcctGet | StoreAcctSet | StoreGlobalGet | StoreGlobalSet - | AjaxGmailAttachmentGetChunk | KeyMatch; + | AjaxGmailAttachmentGetChunk; } export type AnyRequest = PassphraseEntry | StripeResult | OpenPage | OpenGoogleAuthDialog | Redirect | Reload | AddPubkeyDialog | ReinsertReplyBox | ComposeWindow | ScrollToReplyBox | ScrollToCursorInReplyBox | SubscribeDialog | RenderPublicKeys | NotificationShowAuthPopupNeeded | ComposeWindowOpenDraft | NotificationShow | PassphraseDialog | PassphraseDialog | Settings | SetCss | AddOrRemoveClass | ReconnectAcctAuthPopup | - Db | InMemoryStoreSet | InMemoryStoreGet | StoreGlobalGet | StoreGlobalSet | StoreAcctGet | StoreAcctSet | KeyParse | + Db | InMemoryStoreSet | InMemoryStoreGet | StoreGlobalGet | StoreGlobalSet | StoreAcctGet | StoreAcctSet | PgpMsgDecrypt | PgpMsgDiagnoseMsgPubkeys | PgpMsgVerifyDetached | PgpHashChallengeAnswer | PgpMsgType | Ajax | - ShowAttachmentPreview | ReRenderRecipient | KeyMatch; + ShowAttachmentPreview | ReRenderRecipient; // export type RawResponselessHandler = (req: AnyRequest) => Promise; // export type RawRespoHandler = (req: AnyRequest) => Promise; @@ -148,8 +143,6 @@ export class BrowserMsg { pgpHashChallengeAnswer: (bm: Bm.PgpHashChallengeAnswer) => BrowserMsg.sendAwait(undefined, 'pgpHashChallengeAnswer', bm, true) as Promise, pgpMsgDecrypt: (bm: Bm.PgpMsgDecrypt) => BrowserMsg.sendAwait(undefined, 'pgpMsgDecrypt', bm, true) as Promise, pgpMsgVerifyDetached: (bm: Bm.PgpMsgVerifyDetached) => BrowserMsg.sendAwait(undefined, 'pgpMsgVerifyDetached', bm, true) as Promise, - keyParse: (bm: Bm.KeyParse) => BrowserMsg.sendAwait(undefined, 'keyParse', bm, true) as Promise, - keyMatch: (bm: Bm.KeyMatch) => BrowserMsg.sendAwait(undefined, 'keyMatch', bm, true) as Promise, pgpMsgType: (bm: Bm.PgpMsgType) => BrowserMsg.sendAwait(undefined, 'pgpMsgType', bm, true) as Promise, }, }, @@ -247,12 +240,6 @@ export class BrowserMsg { BrowserMsg.bgAddListener('pgpMsgDecrypt', MsgUtil.decryptMessage); BrowserMsg.bgAddListener('pgpMsgVerifyDetached', MsgUtil.verifyDetached); BrowserMsg.bgAddListener('pgpMsgType', MsgUtil.type); - BrowserMsg.bgAddListener('keyParse', async (r: Bm.KeyParse) => ({ key: await KeyUtil.parse(r.armored) })); - BrowserMsg.bgAddListener('keyMatch', async (r: Bm.KeyMatch) => ({ - key: - (await Promise.all(r.pubkeys.map(async (pub) => await KeyUtil.parse(pub)))). - find(k => k.allIds.map(id => OpenPGPKey.fingerprintToLongid(id).includes(r.longid))) - })); }; public static addListener = (name: string, handler: Handler) => { From b0c43d45d5c4b355fc1ae507775f2a272e88b306 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 30 Nov 2021 15:08:00 +0000 Subject: [PATCH 07/31] renames --- .../js/common/platform/store/contact-store.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 5599313ef66..6203bfa4ee7 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -223,17 +223,17 @@ export class ContactStore extends AbstractStore { }); }; - public static get = async (db: undefined | IDBDatabase, emailOrLongid: string[]): Promise<(Contact | undefined)[]> => { + public static get = async (db: undefined | IDBDatabase, emails: string[]): Promise<(Contact | undefined)[]> => { if (!db) { // relay op through background process - return await BrowserMsg.send.bg.await.db({ f: 'get', args: [emailOrLongid] }) as (Contact | undefined)[]; + return await BrowserMsg.send.bg.await.db({ f: 'get', args: [emails] }) as (Contact | undefined)[]; } - if (emailOrLongid.length === 1) { - const contact = await ContactStore.dbContactInternalGetOne(db, emailOrLongid[0]); + if (emails.length === 1) { + const contact = await ContactStore.dbContactInternalGetOne(db, emails[0]); return [contact]; } else { const results: (Contact | undefined)[] = []; - for (const singleEmailOrLongid of emailOrLongid) { - const [contact] = await ContactStore.get(db, [singleEmailOrLongid]); + for (const email of emails) { + const [contact] = await ContactStore.get(db, [email]); results.push(contact); } return results; @@ -677,9 +677,9 @@ export class ContactStore extends AbstractStore { .map(normalized => ContactStore.dbIndex(emailEntity.fingerprints.length > 0, normalized)); }; - private static dbContactInternalGetOne = async (db: IDBDatabase, emailOrLongid: string): Promise => { - if (emailOrLongid.includes('@')) { // email - const contactWithAllPubkeys = await ContactStore.getOneWithAllPubkeys(db, emailOrLongid); + private static dbContactInternalGetOne = async (db: IDBDatabase, email: string): Promise => { + if (email.includes('@')) { // email + const contactWithAllPubkeys = await ContactStore.getOneWithAllPubkeys(db, email); if (!contactWithAllPubkeys) { return contactWithAllPubkeys; } From 482fff074843a67b544f1aa51387cc743c4f1aa0 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 1 Dec 2021 04:51:38 +0000 Subject: [PATCH 08/31] fixed Message Not Signed and Missing Pubkey --- .../pgp-block-signature-module.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index 0d6517e34fb..9651e7577cb 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -15,7 +15,8 @@ export class PgpBlockViewSignatureModule { public renderPgpSignatureCheckResult = async (signature: VerifyRes | undefined, retryVerification?: (verificationPubs: string[]) => Promise) => { this.view.renderModule.doNotSetStateAsReadyYet = true; // so that body state is not marked as ready too soon - automated tests need to know when to check results - if (!signature) { + const signerLongid = signature?.signer?.longid; + if (!signerLongid) { $('#pgp_signature').addClass('bad'); $('#pgp_signature > .cursive').remove(); $('#pgp_signature > .result').text('Message Not Signed'); @@ -27,6 +28,12 @@ export class PgpBlockViewSignatureModule { $('#pgp_signature').addClass('good'); $('#pgp_signature > .result').text('matching signature'); } else { + // todo: bad signature when pubkey is hit + /* + $('#pgp_signature').addClass('bad'); + $('#pgp_signature > .result').text('signature does not match'); + this.view.renderModule.setFrameColor('red'); + */ if (retryVerification) { const signerEmail = this.view.getSigner(); if (!signerEmail) { @@ -40,9 +47,7 @@ export class PgpBlockViewSignatureModule { await this.renderPgpSignatureCheckResult(await retryVerification(newPubkeys), undefined); return; } - $('#pgp_signature').addClass('bad'); - $('#pgp_signature > .result').text('signature does not match'); // todo: or "neutral" missing pubkey? - this.view.renderModule.setFrameColor('red'); + this.renderMissingPubkey(signerLongid); } catch (e) { if (ApiErr.isSignificant(e)) { Catch.reportErr(e); @@ -53,6 +58,8 @@ export class PgpBlockViewSignatureModule { } } } + } else { // !retryVerification + this.renderMissingPubkey(signerLongid); } } if (signature) { @@ -67,6 +74,10 @@ export class PgpBlockViewSignatureModule { $('#pgp_signature > .cursive > span').text(signerEmail || 'Unknown Signer'); }; + private renderMissingPubkey = (signerLongid: string) => { + $('#pgp_signature').addClass('neutral').find('.result').text(`Missing pubkey ${signerLongid}`); + }; + /** * don't have appropriate pubkey by longid in contacts * From d4160b793510d80fb4dc699ce5522114f6bbe43b Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 1 Dec 2021 09:55:55 +0000 Subject: [PATCH 09/31] fix --- .../pgp_block_modules/pgp-block-signature-module.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index 9651e7577cb..976864588a5 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -16,14 +16,14 @@ export class PgpBlockViewSignatureModule { public renderPgpSignatureCheckResult = async (signature: VerifyRes | undefined, retryVerification?: (verificationPubs: string[]) => Promise) => { this.view.renderModule.doNotSetStateAsReadyYet = true; // so that body state is not marked as ready too soon - automated tests need to know when to check results const signerLongid = signature?.signer?.longid; - if (!signerLongid) { - $('#pgp_signature').addClass('bad'); - $('#pgp_signature > .cursive').remove(); - $('#pgp_signature > .result').text('Message Not Signed'); - } else if (signature.error) { + if (signature?.error) { $('#pgp_signature').addClass('bad'); $('#pgp_signature > .result').text(signature.error); this.view.renderModule.setFrameColor('red'); + } else if (!signerLongid) { + $('#pgp_signature').addClass('bad'); + $('#pgp_signature > .cursive').remove(); + $('#pgp_signature > .result').text('Message Not Signed'); } else if (signature.match) { $('#pgp_signature').addClass('good'); $('#pgp_signature > .result').text('matching signature'); From 94a6846cb7d3ca8db52df45dbdb716617b5c44ce Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 1 Dec 2021 10:09:41 +0000 Subject: [PATCH 10/31] added remarks to link to #4158 --- extension/chrome/elements/attachment.ts | 2 +- extension/chrome/elements/attachment_preview.ts | 2 +- .../chrome/elements/compose-modules/compose-quote-module.ts | 2 +- .../elements/pgp_block_modules/pgp-block-attachmens-module.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extension/chrome/elements/attachment.ts b/extension/chrome/elements/attachment.ts index b1a1bec737e..ae7b52b92ca 100644 --- a/extension/chrome/elements/attachment.ts +++ b/extension/chrome/elements/attachment.ts @@ -261,7 +261,7 @@ export class AttachmentDownloadView extends View { const result = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), encryptedData: this.attachment.getData(), - verificationPubs: [] // todo: signature? + verificationPubs: [] // todo: #4158 signature verification of attachments }); Xss.sanitizeRender(this.downloadButton, this.originalButtonHTML || ''); if (result.success) { diff --git a/extension/chrome/elements/attachment_preview.ts b/extension/chrome/elements/attachment_preview.ts index f358b5e337a..fb8da6222c7 100644 --- a/extension/chrome/elements/attachment_preview.ts +++ b/extension/chrome/elements/attachment_preview.ts @@ -90,7 +90,7 @@ View.run(class AttachmentPreviewView extends AttachmentDownloadView { const result = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), encryptedData: this.attachment.getData(), - verificationPubs: [] // todo: signature? + verificationPubs: [] // todo: #4158 signature verification of attachments }); if ((result as DecryptSuccess).content) { return result.content; diff --git a/extension/chrome/elements/compose-modules/compose-quote-module.ts b/extension/chrome/elements/compose-modules/compose-quote-module.ts index 2052951628d..3caf8f33d6e 100644 --- a/extension/chrome/elements/compose-modules/compose-quote-module.ts +++ b/extension/chrome/elements/compose-modules/compose-quote-module.ts @@ -126,7 +126,7 @@ export class ComposeQuoteModule extends ViewModule { const result = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail), encryptedData: block.attachmentMeta.data, - verificationPubs: [] // todo: signature? + verificationPubs: [] // todo: #4158 signature verification of attachments }); if (result.success) { attachmentMeta = { content: result.content, filename: result.filename }; diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts index b988e9eae8b..6ebee1b38ef 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-attachmens-module.ts @@ -69,7 +69,7 @@ export class PgpBlockViewAttachmentsModule { private decryptAndSaveAttachmentToDownloads = async (encrypted: Attachment) => { const kisWithPp = await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail); - // todo: signature? + // todo: #4158 signature verification of attachments const decrypted = await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData: encrypted.getData(), verificationPubs: [] }); if (decrypted.success) { const attachment = new Attachment({ name: encrypted.name.replace(/\.(pgp|gpg)$/, ''), type: encrypted.type, data: decrypted.content }); From 7130cf8b7f23e7a64e3627b7a3d5042835880312 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 1 Dec 2021 10:09:57 +0000 Subject: [PATCH 11/31] remark --- extension/chrome/settings/modules/decrypt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/chrome/settings/modules/decrypt.ts b/extension/chrome/settings/modules/decrypt.ts index 4283a97bfab..4a0e47c61d3 100644 --- a/extension/chrome/settings/modules/decrypt.ts +++ b/extension/chrome/settings/modules/decrypt.ts @@ -58,7 +58,7 @@ View.run(class ManualDecryptView extends View { const result = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), encryptedData: encrypted.getData(), - verificationPubs: [] // todo: signature? + verificationPubs: [] }); if (result.success) { const attachment = new Attachment({ name: encrypted.name.replace(/\.(pgp|gpg|asc)$/i, ''), type: encrypted.type, data: result.content }); From e048d6125c06ea8bf767cf2b3acfd1ca9b66ac2b Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 2 Dec 2021 13:04:34 +0000 Subject: [PATCH 12/31] refactorings --- .../pgp-block-signature-module.ts | 21 +++---- .../js/common/core/crypto/pgp/msg-util.ts | 5 +- .../js/common/core/crypto/pgp/openpgp-key.ts | 18 +++--- .../strategies/send-message-strategy.ts | 4 +- test/source/tests/unit-node.ts | 56 +++++++++++++------ 5 files changed, 60 insertions(+), 44 deletions(-) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index 976864588a5..8f899ac81b0 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -13,18 +13,19 @@ export class PgpBlockViewSignatureModule { constructor(private view: PgpBlockView) { } - public renderPgpSignatureCheckResult = async (signature: VerifyRes | undefined, retryVerification?: (verificationPubs: string[]) => Promise) => { + public renderPgpSignatureCheckResult = async (verifyRes: VerifyRes | undefined, retryVerification?: (verificationPubs: string[]) => Promise) => { this.view.renderModule.doNotSetStateAsReadyYet = true; // so that body state is not marked as ready too soon - automated tests need to know when to check results - const signerLongid = signature?.signer?.longid; - if (signature?.error) { + const signerLongids = verifyRes?.signerLongids; + if (verifyRes?.error) { + // todo: retry raw here! $('#pgp_signature').addClass('bad'); - $('#pgp_signature > .result').text(signature.error); + $('#pgp_signature > .result').text(verifyRes.error); this.view.renderModule.setFrameColor('red'); - } else if (!signerLongid) { + } else if (!signerLongids?.length) { $('#pgp_signature').addClass('bad'); $('#pgp_signature > .cursive').remove(); $('#pgp_signature > .result').text('Message Not Signed'); - } else if (signature.match) { + } else if (verifyRes?.match) { $('#pgp_signature').addClass('good'); $('#pgp_signature > .result').text('matching signature'); } else { @@ -47,7 +48,7 @@ export class PgpBlockViewSignatureModule { await this.renderPgpSignatureCheckResult(await retryVerification(newPubkeys), undefined); return; } - this.renderMissingPubkey(signerLongid); + this.renderMissingPubkey(signerLongids[0]); } catch (e) { if (ApiErr.isSignificant(e)) { Catch.reportErr(e); @@ -59,11 +60,11 @@ export class PgpBlockViewSignatureModule { } } } else { // !retryVerification - this.renderMissingPubkey(signerLongid); + this.renderMissingPubkey(signerLongids[0]); } } - if (signature) { - this.setSigner(signature); + if (verifyRes) { + this.setSigner(verifyRes); } this.view.renderModule.doNotSetStateAsReadyYet = false; Ui.setTestState('ready'); diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index f652e4e703b..61c302a6c26 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -61,10 +61,7 @@ type DecryptError$longids = { message: string[]; matching: string[]; chosen: str export type DecryptError = { success: false; error: DecryptError$error; longids: DecryptError$longids; content?: Buf; isEncrypted?: boolean; }; export type VerifyRes = { - // longid is set up even if the signature isn't verified - // primaryUserId is set up from the found key - // todo: make `match` a structure and move `primaryUserId` inside it or remove at all (#2147 is no longer appropriate) - signer?: { primaryUserId: string | undefined, longid: string }; + signerLongids: string[]; match: boolean | null; // we can return some pubkey information here error?: string; isErrFatal?: boolean, diff --git a/extension/js/common/core/crypto/pgp/openpgp-key.ts b/extension/js/common/core/crypto/pgp/openpgp-key.ts index fdf8532ef0d..731eb4d594f 100644 --- a/extension/js/common/core/crypto/pgp/openpgp-key.ts +++ b/extension/js/common/core/crypto/pgp/openpgp-key.ts @@ -2,7 +2,7 @@ import { Key, PrvPacket, KeyAlgo, KeyUtil, UnexpectedKeyTypeError, PubkeyInfo } from '../key.js'; import { opgp } from './openpgpjs-custom.js'; import { Catch } from '../../../platform/catch.js'; -import { Str } from '../../common.js'; +import { Str, Value } from '../../common.js'; import { Buf } from '../../buf.js'; import { PgpMsgMethod, VerifyRes } from './msg-util.js'; @@ -436,7 +436,7 @@ export class OpenPGPKey { }; public static verify = async (msg: OpenpgpMsgOrCleartext, pubs: PubkeyInfo[]): Promise => { - const verifyRes: VerifyRes = { match: null }; // tslint:disable-line:no-null-keyword + const verifyRes: VerifyRes = { match: null, signerLongids: [] }; // tslint:disable-line:no-null-keyword // msg.getSigningKeyIds ? msg.getSigningKeyIds() // todo: double-check if S/MIME ever gets here const opgpKeys = pubs.filter(x => !x.revoked && x.pubkey.type === 'openpgp').map(x => OpenPGPKey.extractExternalLibraryObjFromKey(x.pubkey)); @@ -451,20 +451,16 @@ export class OpenPGPKey { verifyRes.content = data instanceof Uint8Array ? new Buf(data) : Buf.fromUtfStr(data); } // third step below + const signerLongids: string[] = []; for (const verification of verifications) { // todo - a valid signature is a valid signature, and should be surfaced. Currently, if any of the signatures are not valid, it's showing all as invalid // .. as it is now this could allow an attacker to append bogus signatures to validly signed messages, making otherwise correct messages seem incorrect // .. which is not really an issue - an attacker that can append signatures could have also just slightly changed the message, causing the same experience // .. so for now #wontfix unless a reasonable usecase surfaces verifyRes.match = (verifyRes.match === true || verifyRes.match === null) && await verification.verified; - if (!verifyRes.signer) { - // todo - currently only the first signer will be reported. Should we be showing all signers? How common is that? - verifyRes.signer = { - longid: OpenPGPKey.bytesToLongid(verification.keyid.bytes), - primaryUserId: await OpenPGPKey.getPrimaryUserId(opgpKeys, verification.keyid) - }; - } + signerLongids.push(OpenPGPKey.bytesToLongid(verification.keyid.bytes)); } + verifyRes.signerLongids.push(...Value.arr.unique(signerLongids)); } catch (verifyErr) { verifyRes.match = null; // tslint:disable-line:no-null-keyword if (verifyErr instanceof Error && verifyErr.message === 'Can only verify message with one literal data packet.') { @@ -754,10 +750,10 @@ export class OpenPGPKey { if (verifyResult.error !== null && typeof verifyResult.error !== 'undefined') { output.push(`verify failed: ${verifyResult.error}`); } else { - if (verifyResult.match && verifyResult.signer?.longid === OpenPGPKey.bytesToLongid(key.getKeyId().bytes)) { + if (verifyResult.match && verifyResult.signerLongids.includes(OpenPGPKey.bytesToLongid(key.getKeyId().bytes))) { output.push('verify ok'); } else { - output.push(`verify mismatch: match[${verifyResult.match}] signer.uid[${verifyResult.signer?.primaryUserId}] signer.longid[${verifyResult.signer?.longid}]`); + output.push(`verify mismatch: match[${verifyResult.match}]`); } } } catch (e) { diff --git a/test/source/mock/google/strategies/send-message-strategy.ts b/test/source/mock/google/strategies/send-message-strategy.ts index 3b535c99712..44455befda3 100644 --- a/test/source/mock/google/strategies/send-message-strategy.ts +++ b/test/source/mock/google/strategies/send-message-strategy.ts @@ -105,8 +105,8 @@ class SignedMessageTestStrategy implements ITestMsgStrategy { if (!decrypted.signature) { throw new HttpClientErr(`Error: The message isn't signed.`); } - if (decrypted.signature.signer?.longid !== this.signedBy) { - throw new HttpClientErr(`Error: expected message signed by ${this.signedBy} but was actually signed by ${decrypted.signature.signer?.longid}`); + if (!decrypted.signature.signerLongids.includes(this.signedBy)) { + throw new HttpClientErr(`Error: expected message signed by ${this.signedBy} but was actually signed by ${decrypted.signature.signerLongids.length} other signers`); } const content = decrypted.content.toUtfStr(); if (!content.includes(this.expectedText)) { diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index be9eee938c0..89aadd1e703 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -775,20 +775,6 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY t.pass(); }); - ava.default('[unit][MsgUtil.decryptMessage] extracts Primary User ID from key', async t => { - const data = await GoogleData.withInitializedData('ci.tests.gmail@flowcrypt.test'); - const msg: GmailMsg = data.getMessage('1766644f13510f58')!; - const enc = Buf.fromBase64Str(msg!.raw!).toUtfStr() - .match(/\-\-\-\-\-BEGIN PGP SIGNED MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s)![0]; - const encryptedData = Buf.fromUtfStr(enc); - const decrypted = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [testConstants.pubkey2864E326A5BE488A] }); - expect(decrypted.success).to.equal(true); - const verifyRes = (decrypted as DecryptSuccess).signature!; - expect(verifyRes.match).to.be.true; - expect(verifyRes.signer?.primaryUserId).to.equal('A50 Sam '); - t.pass(); - }); - ava.default('[unit][MsgUtil.decryptMessage] finds correct key to verify signature', async t => { const data = await GoogleData.withInitializedData('ci.tests.gmail@flowcrypt.test'); const msg: GmailMsg = data.getMessage('1766644f13510f58')!; @@ -804,21 +790,18 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY expect(decrypted1.success).to.equal(true); const verifyRes1 = (decrypted1 as DecryptSuccess).signature!; expect(verifyRes1.match).to.be.true; - expect(verifyRes1.signer?.primaryUserId).to.equal('A50 Sam '); } { const decrypted2 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [betterKey, pubkey] }); expect(decrypted2.success).to.equal(true); const verifyRes2 = (decrypted2 as DecryptSuccess).signature!; expect(verifyRes2.match).to.be.true; - expect(verifyRes2.signer?.primaryUserId).to.equal('A50 Sam '); } { const decrypted3 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [pubkey] }); expect(decrypted3.success).to.equal(true); const verifyRes3 = (decrypted3 as DecryptSuccess).signature!; expect(verifyRes3.match).to.be.true; - expect(verifyRes3.signer?.primaryUserId).to.equal('A50 Sam '); } { const decrypted4 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [betterKey] }); @@ -888,6 +871,45 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY t.pass(); }); + ava.default(`[unit][MsgUtil.verifyDetached] returns errorwhen signature doesn't match`, async t => { + const sigText = Buf.fromUtfStr(`-----BEGIN PGP SIGNATURE----- + +wsD5BAABCAAjFiEE3CZFSvtx0Y6rutc9HH5tPFVjqUEFAl+QotYFAwAAAAAACgkQHH5tPFVjqUF4 +xgv+MrdQ07MfCVU93ptZg+S+OOkQ1AcZxGFdiivs10KkNGtLm9s+w/iEUAySSWbtKjbLV6O3AYvC +QFKsFRFr17Ekz6mSPj99zifFMBvTOIAev/d08dmX0kGd6YlP+GyZL3Wqcgy1T1H3obgOmToDtk7R +V52Ki1aTJYH/Z7v6PsQRWn8emfH/yGYplBhzZy2XjO6UIar9T8wtAJOd6+Ii2sfyGyEPjzGckLaR +JZOxQ4jpJJUszz2WsvLNwtKoqwV15Eg3oxZzHWYE8P63xXoE4G762604SIqv/ggyQZTt/Es6Scun +A1BJflFm+cHzQTW2yQfwCCvlzEZNiNwXfwGfV99K5iG1eW3lv7sMLJnitwTidNIlD5LTNdeUnTXj +XJvkEQsyTUI4qbzzJbUNYz7lraizC2nPiwFzLv692mS0urtD3mUhOBA9hZwk3l/20GsGia0FeUIS +E1d8Vh/Ey7IJ8TXbfFrdv5ZP3HqMK0089SooZwx/GN2QIaOYQXsS0u7IFNhU\n=q5Sf +-----END PGP SIGNATURE-----`); + const data = await GoogleData.withInitializedData('flowcrypt.compatibility@gmail.com'); + const msg = data.getMessage('1754cfc37886899e')!; + const msgText = Buf.fromBase64Str(msg!.raw!).toUtfStr(); + { + const dhartleyPubkey = msgText + .match(/\-\-\-\-\-BEGIN PGP PUBLIC KEY BLOCK\-\-\-\-\-.*\-\-\-\-\-END PGP PUBLIC KEY BLOCK\-\-\-\-\-/s)![0] + .replace(/=\r\n/g, '').replace(/=3D/g, '='); + const resultRightKey = await MsgUtil.verifyDetached({ + plaintext: Buf.fromUtfStr('some irrelevant text'), + sigText, + verificationPubs: [dhartleyPubkey] + }); + expect(resultRightKey.match).to.be.null; + expect(resultRightKey.error).to.not.be.undefined; + } + { + const resultWrongKey = await MsgUtil.verifyDetached({ + plaintext: Buf.fromUtfStr('some irrelevant text'), + sigText, + verificationPubs: [testConstants.flowcryptcompatibilityPublicKey7FDE685548AEA788] + }); + expect(resultWrongKey.match).to.be.null; + expect(resultWrongKey.error).to.be.undefined; + } + t.pass(); + }); + ava.default('[unit][MsgUtil.getSortedKeys,matchingKeyids] must be able to find matching keys', async t => { const passphrase = 'some pass for testing'; const key1 = await OpenPGPKey.create([{ name: 'Key1', email: 'key1@test.com' }], 'curve25519', passphrase, 0); From 2609b7d9936e09f6900674bbda975e6bf512223c Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 5 Dec 2021 04:44:37 +0000 Subject: [PATCH 13/31] re-fetching signed-only message on non-fatal verification error --- extension/chrome/elements/pgp_block.ts | 19 +++++++++-- .../pgp-block-decrypt-module.ts | 28 +++++++--------- .../pgp-block-render-module.ts | 4 +-- .../pgp-block-signature-module.ts | 11 +++++-- test/source/tests/decrypt.ts | 32 +++++++++++++++++++ test/source/tests/unit-node.ts | 3 +- 6 files changed, 72 insertions(+), 25 deletions(-) diff --git a/extension/chrome/elements/pgp_block.ts b/extension/chrome/elements/pgp_block.ts index 1b17014187d..00a3c4aa0f9 100644 --- a/extension/chrome/elements/pgp_block.ts +++ b/extension/chrome/elements/pgp_block.ts @@ -18,6 +18,8 @@ import { View } from '../../js/common/view.js'; import { PubLookup } from '../../js/common/api/pub-lookup.js'; import { OrgRules } from '../../js/common/org-rules.js'; import { AcctStore } from '../../js/common/platform/store/acct-store.js'; +import { ContactStore } from '../../js/common/platform/store/contact-store.js'; +import { KeyUtil } from '../../js/common/core/crypto/key.js'; export class PgpBlockView extends View { @@ -28,7 +30,10 @@ export class PgpBlockView extends View { public readonly senderEmail: string; public readonly msgId: string | undefined; public readonly encryptedMsgUrlParam: Buf | undefined; - public signature: string | boolean | undefined; // when supplied with "true", decryptModule will replace this with actual signature data + public readonly signature?: { + // when parsedSignature is undefined, decryptModule will try to fetch the message + parsedSignature?: string + }; public gmail: Gmail; public orgRules!: OrgRules; @@ -56,7 +61,11 @@ export class PgpBlockView extends View { throw new Error('API path traversal forbidden'); } this.encryptedMsgUrlParam = uncheckedUrlParams.message ? Buf.fromUtfStr(Assert.urlParamRequire.string(uncheckedUrlParams, 'message')) : undefined; - this.signature = uncheckedUrlParams.signature === true ? true : (uncheckedUrlParams.signature ? String(uncheckedUrlParams.signature) : undefined); + if (uncheckedUrlParams.signature === true) { + this.signature = {}; + } else if (uncheckedUrlParams.signature) { + this.signature = { parsedSignature: String(uncheckedUrlParams.signature) }; + } this.gmail = new Gmail(this.acctEmail); // modules this.attachmentsModule = new PgpBlockViewAttachmentsModule(this); @@ -78,7 +87,11 @@ export class PgpBlockView extends View { const scopes = await AcctStore.getScopes(this.acctEmail); this.decryptModule.canReadEmails = scopes.read || scopes.modify; if (storage.setup_done) { - await this.decryptModule.initialize(); + const parsedPubs = (await ContactStore.getOneWithAllPubkeys(undefined, this.getSigner()))?.sortedPubkeys ?? []; + // todo: we don't actually need parsed pubs here because we're going to pass them to the backgorund page + // maybe we can have a method in ContactStore to extract armored keys + const verificationPubs = parsedPubs.map(key => KeyUtil.armor(key.pubkey)); + await this.decryptModule.initialize(verificationPubs, false); } else { await this.errorModule.renderErr(Lang.pgpBlock.refreshWindow, this.encryptedMsgUrlParam ? this.encryptedMsgUrlParam.toUtfStr() : undefined); } diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts index a909867a43e..56d15746254 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts @@ -13,8 +13,6 @@ import { Ui } from '../../../js/common/browser/ui.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; -import { KeyUtil } from '../../../js/common/core/crypto/key.js'; -import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; export class PgpBlockViewDecryptModule { @@ -26,20 +24,16 @@ export class PgpBlockViewDecryptModule { constructor(private view: PgpBlockView) { } - public initialize = async (forcePullMsgFromApi = false) => { - const parsedPubs = (await ContactStore.getOneWithAllPubkeys(undefined, this.view.getSigner()))?.sortedPubkeys ?? []; - // todo: we don't actually need parsed pubs here because we're going to pass them to the backgorund page - // maybe we can have a method in ContactStore to extract armored keys - const verificationPubs = parsedPubs.map(key => KeyUtil.armor(key.pubkey)); + public initialize = async (verificationPubs: string[], forcePullMsgFromApi: boolean) => { try { - if (this.canReadEmails && this.view.signature === true && this.view.msgId) { + if (this.canReadEmails && this.view.signature && !this.view.signature.parsedSignature && this.view.msgId) { this.view.renderModule.renderText('Loading signed message...'); const { raw } = await this.view.gmail.msgGet(this.view.msgId, 'raw'); this.msgFetchedFromApi = 'raw'; const mimeMsg = Buf.fromBase64UrlStr(raw!); // used 'raw' above const parsed = await Mime.decode(mimeMsg); if (parsed && typeof parsed.rawSignedContent === 'string' && parsed.signature) { - this.view.signature = parsed.signature; + this.view.signature.parsedSignature = parsed.signature; await this.decryptAndRender(Buf.fromUtfStr(parsed.rawSignedContent), verificationPubs); } else { await this.view.errorModule.renderErr('Error: could not properly parse signed message', parsed.rawSignedContent || parsed.text || parsed.html || mimeMsg.toUtfStr()); @@ -68,8 +62,10 @@ export class PgpBlockViewDecryptModule { } }; + public canFetchFromApi = () => this.canReadEmails && this.msgFetchedFromApi !== 'raw'; + private decryptAndRender = async (encryptedData: Buf, verificationPubs: string[], optionalPwd?: string, plainSubject?: string) => { - if (typeof this.view.signature !== 'string') { + if (!this.view.signature?.parsedSignature) { const kisWithPp = await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail); const decrypt = async (verificationPubs: string[]) => await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData, verificationPubs }); const result = await decrypt(verificationPubs); @@ -77,7 +73,7 @@ export class PgpBlockViewDecryptModule { await this.view.errorModule.renderErr(Lang.general.restartBrowserAndTryAgain, undefined); } else if (result.success) { await this.view.renderModule.decideDecryptedContentFormattingAndRender(result.content, Boolean(result.isEncrypted), result.signature, - async (verificationPubs: string[]) => { + verificationPubs, async (verificationPubs: string[]) => { const decryptResult = await decrypt(verificationPubs); if (!decryptResult.success) { return undefined; // note: this internal error results in a wrong "Message Not Signed" badge @@ -86,9 +82,9 @@ export class PgpBlockViewDecryptModule { } }, plainSubject); } else if (result.error.type === DecryptErrTypes.format) { - if (this.canReadEmails && this.msgFetchedFromApi !== 'raw') { + if (this.canFetchFromApi()) { console.info(`re-fetching message ${this.view.msgId} from api because looks like bad formatting: ${!this.msgFetchedFromApi ? 'full' : 'raw'}`); - await this.initialize(true); + await this.initialize(verificationPubs, true); } else { await this.view.errorModule.renderErr(Lang.pgpBlock.badFormat + '\n\n' + result.error.message, encryptedData.toUtfStr()); } @@ -118,12 +114,12 @@ export class PgpBlockViewDecryptModule { await this.view.errorModule.renderErr(Lang.pgpBlock.cantOpen + Lang.pgpBlock.writeMe + '\n\nDiagnostic info: "' + JSON.stringify(result) + '"', encryptedData.toUtfStr()); } } - } else { // this.view.signature is string + } else { // this.view.signature.parsedSignature is defined // sometimes signatures come wrongly percent-encoded. Here we check for typical "=3Dabcd" at the end - const sigText = Buf.fromUtfStr(this.view.signature.replace('\n=3D', '\n=')); + const sigText = Buf.fromUtfStr(this.view.signature.parsedSignature.replace('\n=3D', '\n=')); const verify = async (verificationPubs: string[]) => await BrowserMsg.send.bg.await.pgpMsgVerifyDetached({ plaintext: encryptedData, sigText, verificationPubs }); const signatureResult = await verify(verificationPubs); - await this.view.renderModule.decideDecryptedContentFormattingAndRender(encryptedData, false, signatureResult, verify); + await this.view.renderModule.decideDecryptedContentFormattingAndRender(encryptedData, false, signatureResult, verificationPubs, verify); } }; diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts index ee7a0019591..2ecc50b40dc 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts @@ -77,9 +77,9 @@ export class PgpBlockViewRenderModule { }; public decideDecryptedContentFormattingAndRender = async (decryptedBytes: Buf, isEncrypted: boolean, sigResult: VerifyRes | undefined, - retryVerification: (verificationPubs: string[]) => Promise, plainSubject?: string) => { + verificationPubs: string[], retryVerification: (verificationPubs: string[]) => Promise, plainSubject?: string) => { this.setFrameColor(isEncrypted ? 'green' : 'gray'); - await this.view.signatureModule.renderPgpSignatureCheckResult(sigResult, retryVerification); + await this.view.signatureModule.renderPgpSignatureCheckResult(sigResult, verificationPubs, retryVerification); const publicKeys: string[] = []; let renderableAttachments: Attachment[] = []; let decryptedContent = decryptedBytes.toUtfStr(); diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index 8f899ac81b0..6fcf2676cc3 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -13,11 +13,16 @@ export class PgpBlockViewSignatureModule { constructor(private view: PgpBlockView) { } - public renderPgpSignatureCheckResult = async (verifyRes: VerifyRes | undefined, retryVerification?: (verificationPubs: string[]) => Promise) => { + public renderPgpSignatureCheckResult = async (verifyRes: VerifyRes | undefined, verificationPubs: string[], + retryVerification?: (verificationPubs: string[]) => Promise) => { this.view.renderModule.doNotSetStateAsReadyYet = true; // so that body state is not marked as ready too soon - automated tests need to know when to check results const signerLongids = verifyRes?.signerLongids; if (verifyRes?.error) { - // todo: retry raw here! + if (!verifyRes.isErrFatal && this.view.decryptModule.canFetchFromApi()) { + this.view.signature!.parsedSignature = undefined; // force to re-parse + await this.view.decryptModule.initialize(verificationPubs, true); + return; + } $('#pgp_signature').addClass('bad'); $('#pgp_signature > .result').text(verifyRes.error); this.view.renderModule.setFrameColor('red'); @@ -45,7 +50,7 @@ export class PgpBlockViewSignatureModule { try { const { pubkeys: newPubkeys } = await this.view.pubLookup.lookupEmail(this.view.getSigner()); if (newPubkeys.length) { - await this.renderPgpSignatureCheckResult(await retryVerification(newPubkeys), undefined); + await this.renderPgpSignatureCheckResult(await retryVerification(newPubkeys), newPubkeys, undefined); return; } this.renderMissingPubkey(signerLongids[0]); diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts index 3750ccde94b..aba85f2061b 100644 --- a/test/source/tests/decrypt.ts +++ b/test/source/tests/decrypt.ts @@ -5,6 +5,7 @@ import * as ava from 'ava'; import { Config, TestVariant, Util } from './../util'; import { testConstants } from './tooling/consts'; import { BrowserRecipe } from './tooling/browser-recipe'; +import { GoogleData } from './../mock/google/google-data'; import { InboxPageRecipe } from './page-recipe/inbox-page-recipe'; import { SettingsPageRecipe } from './page-recipe/settings-page-recipe'; import { TestUrls } from './../browser/test-urls'; @@ -12,6 +13,7 @@ import { TestWithBrowser } from './../test'; import { expect } from "chai"; import { ComposePageRecipe } from './page-recipe/compose-page-recipe'; import { PageRecipe } from './page-recipe/abstract-page-recipe'; +import { Buf } from '../core/buf'; // tslint:disable:no-blank-lines-func // tslint:disable:max-line-length @@ -415,6 +417,36 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te }); })); + ava.default(`decrypt - missing pubkey in "incorrect message digest" scenario`, testWithBrowser('ci.tests.gmail', async (t, browser) => { + const msgId = '1766644f13510f58'; + const acctEmail = 'ci.tests.gmail@flowcrypt.test'; + const signerEmail = 'sender.for.refetch@domain.com'; + const data = await GoogleData.withInitializedData(acctEmail); + const msg = data.getMessage(msgId)!; + const signature = Buf.fromBase64Str(msg!.raw!).toUtfStr() + .match(/\-\-\-\-\-BEGIN PGP SIGNATURE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s)![0]; + const params = `?frameId=none&account_email=${acctEmail}&senderEmail=${signerEmail}&msgId=${msgId}&message=Some%20corrupted%20message&signature=${encodeURIComponent(signature)}`; + // as the verification pubkey is not known, this scenario doesn't trigger message re-fetch + await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params, content: ['Some corrupted message'], signature: ["Missing pubkey 2864E326A5BE488A"] }); + })); + + ava.default('decrypt - re-fetch signed-only message from API on non-fatal verification error', testWithBrowser('compatibility', async (t, browser) => { + const msgId = '1754cfd1b2f1d6e5'; + const acctEmail = 'flowcrypt.compatibility@gmail.com'; + const signerEmail = 'dhartley@verdoncollege.school.nz'; + const data = await GoogleData.withInitializedData(acctEmail); + const msg = data.getMessage(msgId)!; + const signature = Buf.fromBase64Str(msg!.raw!).toUtfStr() + .match(/\-\-\-\-\-BEGIN PGP SIGNATURE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s)![0]; + const params = `?frameId=none&account_email=${acctEmail}&senderEmail=${signerEmail}&msgId=${msgId}&message=Some%20corrupted%20message&signature=${encodeURIComponent(signature)}`; + // as the verification pubkey is retrieved from the attester, the incorrect message digest will trigger re-fetching from API + await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { + params, + content: [], // todo: #4164 I would expect '1234' here + signature: ['Dhartley@Verdoncollege.School.Nz', 'matching signature'] + }); + })); + ava.default('decrypt - protonmail - load pubkey into contact + verify detached msg', testWithBrowser('compatibility', async (t, browser) => { const textParams = `?frameId=none&message=&msgId=16a9c109bc51687d&` + `senderEmail=some.alias%40protonmail.com&isOutgoing=___cu_false___&signature=___cu_true___&acctEmail=flowcrypt.compatibility%40gmail.com`; diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 89aadd1e703..42ed4cca9c8 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -871,7 +871,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY t.pass(); }); - ava.default(`[unit][MsgUtil.verifyDetached] returns errorwhen signature doesn't match`, async t => { + ava.default(`[unit][MsgUtil.verifyDetached] returns non-fatal error when signature doesn't match`, async t => { const sigText = Buf.fromUtfStr(`-----BEGIN PGP SIGNATURE----- wsD5BAABCAAjFiEE3CZFSvtx0Y6rutc9HH5tPFVjqUEFAl+QotYFAwAAAAAACgkQHH5tPFVjqUF4 @@ -897,6 +897,7 @@ E1d8Vh/Ey7IJ8TXbfFrdv5ZP3HqMK0089SooZwx/GN2QIaOYQXsS0u7IFNhU\n=q5Sf }); expect(resultRightKey.match).to.be.null; expect(resultRightKey.error).to.not.be.undefined; + expect(resultRightKey.isErrFatal).to.be.undefined; } { const resultWrongKey = await MsgUtil.verifyDetached({ From 7dfdb80eeaa8800cac45786ee7e5690194db7495 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 5 Dec 2021 11:04:40 +0000 Subject: [PATCH 14/31] remark --- .../elements/pgp_block_modules/pgp-block-signature-module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index 6fcf2676cc3..3a7c2d8a396 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -19,6 +19,8 @@ export class PgpBlockViewSignatureModule { const signerLongids = verifyRes?.signerLongids; if (verifyRes?.error) { if (!verifyRes.isErrFatal && this.view.decryptModule.canFetchFromApi()) { + // Sometimes the signed content is slightly modified when parsed from DOM, + // so the message should be re-fetched straight from API to make sure we get the original signed data and verify again this.view.signature!.parsedSignature = undefined; // force to re-parse await this.view.decryptModule.initialize(verificationPubs, true); return; From 99b40fc20bbb768014012b739ea2e0dc78483243 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 6 Dec 2021 15:42:13 +0000 Subject: [PATCH 15/31] differentiate bad signature and missing pubkey, red color any outcome except successful verification --- .../pgp-block-signature-module.ts | 91 +++++-------------- extension/js/common/core/common.ts | 1 + .../js/common/core/crypto/pgp/msg-util.ts | 3 +- .../js/common/core/crypto/pgp/openpgp-key.ts | 11 ++- 4 files changed, 35 insertions(+), 71 deletions(-) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index 3a7c2d8a396..b4a9353d678 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -7,6 +7,7 @@ import { Catch } from '../../../js/common/platform/catch.js'; import { PgpBlockView } from '../pgp_block'; import { Ui } from '../../../js/common/browser/ui.js'; import { VerifyRes } from '../../../js/common/core/crypto/pgp/msg-util.js'; +import { Value } from '../../../js/common/core/common.js'; export class PgpBlockViewSignatureModule { @@ -16,7 +17,6 @@ export class PgpBlockViewSignatureModule { public renderPgpSignatureCheckResult = async (verifyRes: VerifyRes | undefined, verificationPubs: string[], retryVerification?: (verificationPubs: string[]) => Promise) => { this.view.renderModule.doNotSetStateAsReadyYet = true; // so that body state is not marked as ready too soon - automated tests need to know when to check results - const signerLongids = verifyRes?.signerLongids; if (verifyRes?.error) { if (!verifyRes.isErrFatal && this.view.decryptModule.canFetchFromApi()) { // Sometimes the signed content is slightly modified when parsed from DOM, @@ -28,46 +28,50 @@ export class PgpBlockViewSignatureModule { $('#pgp_signature').addClass('bad'); $('#pgp_signature > .result').text(verifyRes.error); this.view.renderModule.setFrameColor('red'); - } else if (!signerLongids?.length) { + } else if (!verifyRes || !verifyRes.signerLongids.length) { $('#pgp_signature').addClass('bad'); $('#pgp_signature > .cursive').remove(); $('#pgp_signature > .result').text('Message Not Signed'); - } else if (verifyRes?.match) { + } else if (verifyRes.match) { $('#pgp_signature').addClass('good'); $('#pgp_signature > .result').text('matching signature'); } else { - // todo: bad signature when pubkey is hit - /* - $('#pgp_signature').addClass('bad'); - $('#pgp_signature > .result').text('signature does not match'); - this.view.renderModule.setFrameColor('red'); - */ + // is there intersection between supplied longids and signers from the message? + const intersection = Value.arr.intersection(verifyRes.signerLongids, verifyRes.suppliedLongids); if (retryVerification) { const signerEmail = this.view.getSigner(); if (!signerEmail) { // in some tests we load the block without sender information - $('#pgp_signature').addClass('neutral').find('.result').text(`Could not verify sender.`); + $('#pgp_signature').addClass('bad').find('.result').text(`Cannot verify: missing pubkey, missing sender info`); } else { this.view.renderModule.renderText('Verifying message...'); try { - const { pubkeys: newPubkeys } = await this.view.pubLookup.lookupEmail(this.view.getSigner()); + const { pubkeys: newPubkeys } = await this.view.pubLookup.lookupEmail(signerEmail); if (newPubkeys.length) { await this.renderPgpSignatureCheckResult(await retryVerification(newPubkeys), newPubkeys, undefined); return; } - this.renderMissingPubkey(signerLongids[0]); + if (intersection.length) { + this.renderBadSignature(); + } else { + this.renderMissingPubkey(verifyRes.signerLongids[0]); + } } catch (e) { if (ApiErr.isSignificant(e)) { Catch.reportErr(e); - $('#pgp_signature').addClass('neutral').find('.result').text(`Could not load sender's pubkey due to an error.`); + $('#pgp_signature').addClass('bad').find('.result').text(`Could not load sender's pubkey due to an error.`); } else { - $('#pgp_signature').addClass('neutral').find('.result').text(`Could not look up sender's pubkey due to network error, click to retry.`).click( + $('#pgp_signature').addClass('bad').find('.result').text(`Could not look up sender's pubkey due to network error, click to retry.`).click( this.view.setHandler(() => window.location.reload())); } } } } else { // !retryVerification - this.renderMissingPubkey(signerLongids[0]); + if (intersection.length) { + this.renderBadSignature(); + } else { + this.renderMissingPubkey(verifyRes.signerLongids[0]); + } } } if (verifyRes) { @@ -83,59 +87,12 @@ export class PgpBlockViewSignatureModule { }; private renderMissingPubkey = (signerLongid: string) => { - $('#pgp_signature').addClass('neutral').find('.result').text(`Missing pubkey ${signerLongid}`); + $('#pgp_signature').addClass('bad').find('.result').text(`Missing pubkey ${signerLongid}`); }; - /** - * don't have appropriate pubkey by longid in contacts - * - */ - /* - private renderPgpSignatureCheckMissingPubkeyOptions = async (signerLongid: string, senderEmail: string, - retryVerification?: () => Promise): Promise => { - const render = (note: string, action: () => void) => $('#pgp_signature').addClass('neutral').find('.result').text(note).click(this.view.setHandler(action)); - try { - if (senderEmail) { // we know who sent it - const [senderContactByEmail] = await ContactStore.get(undefined, [senderEmail]); - if (senderContactByEmail && senderContactByEmail.pubkey) { - const foundId = senderContactByEmail.pubkey.id; - render(`Fetched the right pubkey ${signerLongid} from keyserver, but will not use it because you have conflicting pubkey ${foundId} loaded.`, () => undefined); - return undefined; - } - // ---> and user doesn't have pubkey for that email addr - const { pubkeys } = await this.view.pubLookup.lookupEmail(senderEmail); - if (!pubkeys.length) { - render(`Missing pubkey ${signerLongid}`, () => undefined); - return undefined; - } - // ---> and pubkey found on keyserver by sender email - const { key: pubkey } = await BrowserMsg.send.bg.await.keyMatch({ pubkeys, longid: signerLongid }); - if (!pubkey) { - render(`Fetched ${pubkeys.length} sender's pubkeys but message was signed with a different key: ${signerLongid}, will not verify.`, () => undefined); - return undefined; - } - // ---> and longid it matches signature - await ContactStore.update(undefined, senderEmail, { pubkey }); // <= TOFU auto-import - if (retryVerification) { - const newResult = await retryVerification(); - if (newResult) { - return newResult; - } - } - render('Fetched pubkey, click to verify', () => window.location.reload()); - } else { // don't know who sent it - render('Cannot verify: missing pubkey, missing sender info', () => undefined); - // todo - try to fetch pubkey by longid, offer to import it, show warning explaining what it means - } - } catch (e) { - if (ApiErr.isSignificant(e)) { - Catch.reportErr(e); - render(`Could not load sender pubkey ${signerLongid} due to an error.`, () => undefined); - } else { - render(`Could not look up sender's pubkey due to network error, click to retry.`, () => window.location.reload()); - } - } - return undefined; + private renderBadSignature = () => { + $('#pgp_signature').addClass('bad'); + $('#pgp_signature > .result').text('signature does not match'); + this.view.renderModule.setFrameColor('red'); // todo: in what other cases should we set the frame red? }; -*/ } diff --git a/extension/js/common/core/common.ts b/extension/js/common/core/common.ts index b96ee7c55c5..1233397dd97 100644 --- a/extension/js/common/core/common.ts +++ b/extension/js/common/core/common.ts @@ -203,6 +203,7 @@ export class Value { return result; }, contains: (arr: T[] | string, value: T): boolean => Boolean(arr && typeof arr.indexOf === 'function' && (arr as any[]).indexOf(value) !== -1), + intersection: (array1: T[], array2: T[]): T[] => array1.filter(value => array2.includes(value)), sum: (arr: number[]) => arr.reduce((a, b) => a + b, 0), average: (arr: number[]) => Value.arr.sum(arr) / arr.length, zeroes: (length: number): number[] => new Array(length).map(() => 0) diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index 61c302a6c26..65eb4c0dcd8 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -61,7 +61,8 @@ type DecryptError$longids = { message: string[]; matching: string[]; chosen: str export type DecryptError = { success: false; error: DecryptError$error; longids: DecryptError$longids; content?: Buf; isEncrypted?: boolean; }; export type VerifyRes = { - signerLongids: string[]; + signerLongids: string[]; // signers longids from the message + suppliedLongids: string[]; // longids from keys supplied to verify the message match: boolean | null; // we can return some pubkey information here error?: string; isErrFatal?: boolean, diff --git a/extension/js/common/core/crypto/pgp/openpgp-key.ts b/extension/js/common/core/crypto/pgp/openpgp-key.ts index 731eb4d594f..ecb01be1bab 100644 --- a/extension/js/common/core/crypto/pgp/openpgp-key.ts +++ b/extension/js/common/core/crypto/pgp/openpgp-key.ts @@ -436,10 +436,15 @@ export class OpenPGPKey { }; public static verify = async (msg: OpenpgpMsgOrCleartext, pubs: PubkeyInfo[]): Promise => { - const verifyRes: VerifyRes = { match: null, signerLongids: [] }; // tslint:disable-line:no-null-keyword - // msg.getSigningKeyIds ? msg.getSigningKeyIds() // todo: double-check if S/MIME ever gets here - const opgpKeys = pubs.filter(x => !x.revoked && x.pubkey.type === 'openpgp').map(x => OpenPGPKey.extractExternalLibraryObjFromKey(x.pubkey)); + const validKeys = pubs.filter(x => !x.revoked && x.pubkey.type === 'openpgp').map(x => x.pubkey); + // todo: #4172 revoked longid may result in incorrect "Missing pubkey..." output + const verifyRes: VerifyRes = { + match: null, // tslint:disable-line:no-null-keyword + signerLongids: [], + suppliedLongids: validKeys.map(x => x.allIds.map(fp => OpenPGPKey.fingerprintToLongid(fp))).reduce((a, b) => a.concat(b), []) + }; + const opgpKeys = validKeys.map(x => OpenPGPKey.extractExternalLibraryObjFromKey(x)); // todo: expired? try { // this is here to ensure execution order when 1) verify, 2) read data, 3) processing signatures From ca2750433268a78ae3c469c0bbdec6d9008b1d7c Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 7 Dec 2021 13:42:42 +0000 Subject: [PATCH 16/31] save fetched pubkeys to ContactStore --- .../pgp-block-signature-module.ts | 11 +++++-- extension/js/background_page/migrations.ts | 4 +-- .../js/common/platform/store/contact-store.ts | 20 +++++++++---- test/source/tests/decrypt.ts | 30 +++++++++++++++++++ 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index b4a9353d678..fe5ae90857a 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -8,6 +8,7 @@ import { PgpBlockView } from '../pgp_block'; import { Ui } from '../../../js/common/browser/ui.js'; import { VerifyRes } from '../../../js/common/core/crypto/pgp/msg-util.js'; import { Value } from '../../../js/common/core/common.js'; +import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; export class PgpBlockViewSignatureModule { @@ -46,9 +47,13 @@ export class PgpBlockViewSignatureModule { } else { this.view.renderModule.renderText('Verifying message...'); try { - const { pubkeys: newPubkeys } = await this.view.pubLookup.lookupEmail(signerEmail); - if (newPubkeys.length) { - await this.renderPgpSignatureCheckResult(await retryVerification(newPubkeys), newPubkeys, undefined); + const { pubkeys: fetchedPubkeys } = await this.view.pubLookup.lookupEmail(signerEmail); + if (fetchedPubkeys.length) { + for (const fetched of fetchedPubkeys) { // todo: should we ignore the ones we already have in verificationPubs? + // or better yet, create a common method to use both here and in ComposeStorageModule + ContactStore.update(undefined, signerEmail, { pubkey: fetched, pubkeyLastCheck: Date.now() }).catch(Catch.reportErr); + } + await this.renderPgpSignatureCheckResult(await retryVerification(fetchedPubkeys), fetchedPubkeys, undefined); return; } if (intersection.length) { diff --git a/extension/js/background_page/migrations.ts b/extension/js/background_page/migrations.ts index d5a0f644c38..1203668a869 100644 --- a/extension/js/background_page/migrations.ts +++ b/extension/js/background_page/migrations.ts @@ -6,7 +6,7 @@ import { storageLocalGetAll, storageLocalRemove } from '../common/browser/chrome import { KeyInfo, KeyUtil } from '../common/core/crypto/key.js'; import { SmimeKey } from '../common/core/crypto/smime/smime-key.js'; import { Str } from '../common/core/common.js'; -import { ContactStore, ContactUpdate, Email, Pubkey } from '../common/platform/store/contact-store.js'; +import { ContactStore, Email, Pubkey } from '../common/platform/store/contact-store.js'; import { GlobalStore, LocalDraft } from '../common/platform/store/global-store.js'; import { KeyStore } from '../common/platform/store/key-store.js'; @@ -212,7 +212,7 @@ const moveContactsBatchToEmailsAndPubkeys = async (db: IDBDatabase, count?: numb pubkey, lastUse: entry.last_use, pubkeyLastCheck: pubkey ? entry.pubkey_last_check : undefined - } as ContactUpdate + } }; })); { diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 6203bfa4ee7..db9098fad65 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -56,6 +56,13 @@ export type ContactPreview = { }; export type ContactUpdate = { + name?: string | null; + lastUse?: number | null; + pubkey?: Key | string; + pubkeyLastCheck?: number | null; // when non-null, `pubkey` must be supplied +}; + +type ContactUpdateParsed = { name?: string | null; lastUse?: number | null; pubkey?: Key; @@ -212,14 +219,15 @@ export class ContactStore extends AbstractStore { if (!validEmail) { throw Error(`Cannot update contact because email is not valid: ${email}`); } - if (update.pubkey?.isPrivate) { - Catch.report(`Wrongly updating prv ${update.pubkey.id} as contact - converting to pubkey`); - update.pubkey = await KeyUtil.asPublicKey(update.pubkey); + let pubkey = typeof update.pubkey === 'string' ? await KeyUtil.parse(update.pubkey) : update.pubkey; + if (pubkey?.isPrivate) { + Catch.report(`Wrongly updating prv ${pubkey.id} as contact - converting to pubkey`); + pubkey = await KeyUtil.asPublicKey(pubkey); } const tx = db.transaction(['emails', 'pubkeys', 'revocations'], 'readwrite'); await new Promise((resolve, reject) => { ContactStore.setTxHandlers(tx, resolve, reject); - ContactStore.updateTx(tx, validEmail, update); + ContactStore.updateTx(tx, validEmail, { ...update, pubkey }); }); }; @@ -348,7 +356,7 @@ export class ContactStore extends AbstractStore { }); }; - public static updateTx = (tx: IDBTransaction, email: string, update: ContactUpdate) => { + public static updateTx = (tx: IDBTransaction, email: string, update: ContactUpdateParsed) => { if (update.pubkey && !update.pubkeyLastCheck) { const req = tx.objectStore('pubkeys').get(ContactStore.getPubkeyId(update.pubkey)); ContactStore.setReqPipe(req, (pubkey: Pubkey) => { @@ -495,7 +503,7 @@ export class ContactStore extends AbstractStore { } }; - private static updateTxPhase2 = (tx: IDBTransaction, email: string, update: ContactUpdate, + private static updateTxPhase2 = (tx: IDBTransaction, email: string, update: ContactUpdateParsed, existingPubkey: Pubkey | undefined, revocations: Revocation[]) => { let pubkeyEntity: Pubkey | undefined; if (update.pubkey) { diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts index aba85f2061b..47702edc3b1 100644 --- a/test/source/tests/decrypt.ts +++ b/test/source/tests/decrypt.ts @@ -373,6 +373,36 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: url, content: ['1234'], signature }); })); + ava.default('decrypt - fetched pubkey is automatically saved to contacts', testWithBrowser('compatibility', async (t, browser) => { + const msgId = '1754cfc37886899e'; + const acctEmail = 'flowcrypt.compatibility@gmail.com'; + { + const settingsPage = await browser.newPage(t, TestUrls.extensionSettings(acctEmail)); + await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); + const contactsFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, '@action-open-contacts-page', ['contacts.htm', 'placement=settings']); + await contactsFrame.waitAll('@page-contacts'); + await Util.sleep(1); + expect(await contactsFrame.isElementPresent('@action-show-email-flowcryptcompatibilitygmailcom')).to.be.true; + expect(await contactsFrame.isElementPresent('@action-show-email-dhartleyverdoncollegeschoolnz')).to.be.false; + } + const params = `?frameId=none&acctEmail=${acctEmail}&msgId=${msgId}&signature=___cu_true___&senderEmail=dhartley@verdoncollege.school.nz`; + await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params, content: ['1234'] }); + // the fetched pubkey is saved to ContactStore asynchronously, so let's wait a little + await Util.sleep(1); + { + const settingsPage = await browser.newPage(t, TestUrls.extensionSettings(acctEmail)); + await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); + const contactsFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, '@action-open-contacts-page', ['contacts.htm', 'placement=settings']); + await contactsFrame.waitAll('@page-contacts'); + await Util.sleep(1); + expect(await contactsFrame.isElementPresent('@action-show-email-flowcryptcompatibilitygmailcom')).to.be.true; + expect(await contactsFrame.isElementPresent('@action-show-email-dhartleyverdoncollegeschoolnz')).to.be.true; + await contactsFrame.waitAndClick('@action-show-email-dhartleyverdoncollegeschoolnz'); + // contains newly fetched key + await contactsFrame.waitForContent('@page-contacts', 'openpgp - active - DC26 454A FB71 D18E ABBA D73D 1C7E 6D3C 5563 A941'); + } + })); + ava.default('decrypt - unsigned encrypted message', testWithBrowser('compatibility', async (t, browser) => { const threadId = '17918a9d7ca2fbac'; const acctEmail = 'flowcrypt.compatibility@gmail.com'; From 48f6bf7ac27fa670b161c3ada64eb776351d10dc Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 7 Dec 2021 14:33:16 +0000 Subject: [PATCH 17/31] lint fix --- test/source/tests/decrypt.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts index 47702edc3b1..693f86ef8d9 100644 --- a/test/source/tests/decrypt.ts +++ b/test/source/tests/decrypt.ts @@ -18,6 +18,8 @@ import { Buf } from '../core/buf'; // tslint:disable:no-blank-lines-func // tslint:disable:max-line-length /* eslint-disable max-len */ +// tslint:disable:no-unused-expression +/* eslint-disable no-unused-expressions */ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: TestWithBrowser) => { From 2de48fa3dca5d683a033d101aac5b27c9da86e10 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 7 Dec 2021 14:53:27 +0000 Subject: [PATCH 18/31] remarks --- extension/chrome/elements/attachment.ts | 2 +- extension/js/common/core/crypto/pgp/msg-util.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/extension/chrome/elements/attachment.ts b/extension/chrome/elements/attachment.ts index ae7b52b92ca..99dbb1d77b6 100644 --- a/extension/chrome/elements/attachment.ts +++ b/extension/chrome/elements/attachment.ts @@ -208,7 +208,7 @@ export class AttachmentDownloadView extends View { const decrRes = await MsgUtil.decryptMessage({ kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), encryptedData: data, - verificationPubs: [] // todo: signature? + verificationPubs: [] // no need to worry about the public key signature, as public key exchange is inherently unsafe }); if (decrRes.success && decrRes.content) { const openpgpType = await MsgUtil.type({ data: decrRes.content }); diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index 65eb4c0dcd8..b84c12eaa5d 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -157,7 +157,6 @@ export class MsgUtil { return { success: false, error: { type: DecryptErrTypes.format, message: String(formatErr) }, longids }; } if (prepared.isCleartext) { - // todo: error if no verificationPubs? const signature = await OpenPGPKey.verify(prepared.message, await ContactStore.getPubkeyInfos(undefined, verificationPubs)); const content = signature.content || Buf.fromUtfStr('no content'); signature.content = undefined; // no need to duplicate data @@ -188,7 +187,6 @@ export class MsgUtil { const passwords = msgPwd ? [msgPwd] : undefined; const privateKeys = keys.prvForDecryptDecrypted.map(decrypted => decrypted.decrypted); const decrypted = await OpenPGPKey.decryptMessage(msg, privateKeys, passwords); - // todo: test when not signed at all const signature = await OpenPGPKey.verify(decrypted, await ContactStore.getPubkeyInfos(undefined, verificationPubs)); const content = signature?.content || new Buf(await opgp.stream.readToEnd(decrypted.getLiteralData()!)); if (signature?.content) { From 4f009eda3c4eef6e2648ccb361f38140a8700e15 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 8 Dec 2021 06:19:04 +0000 Subject: [PATCH 19/31] fixes --- extension/chrome/elements/pgp_block.ts | 11 ++++++-- .../pgp-block-decrypt-module.ts | 4 +-- .../pgp-block-signature-module.ts | 28 +++++++++---------- extension/js/common/core/common.ts | 1 + 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/extension/chrome/elements/pgp_block.ts b/extension/chrome/elements/pgp_block.ts index 00a3c4aa0f9..0de136eadda 100644 --- a/extension/chrome/elements/pgp_block.ts +++ b/extension/chrome/elements/pgp_block.ts @@ -62,7 +62,7 @@ export class PgpBlockView extends View { } this.encryptedMsgUrlParam = uncheckedUrlParams.message ? Buf.fromUtfStr(Assert.urlParamRequire.string(uncheckedUrlParams, 'message')) : undefined; if (uncheckedUrlParams.signature === true) { - this.signature = {}; + this.signature = { parsedSignature: undefined }; // decryptModule will try to fetch the message } else if (uncheckedUrlParams.signature) { this.signature = { parsedSignature: String(uncheckedUrlParams.signature) }; } @@ -76,7 +76,12 @@ export class PgpBlockView extends View { this.decryptModule = new PgpBlockViewDecryptModule(this); } - public getSigner = () => { + public getExpectedSignerEmail = () => { + // We always attempt to verify all signatures as "signed by sender", with public keys of the sender. + // That way, signature spoofing attacks are prevented: if Joe manages to spoof a sending address + // of Jane (send an email from Jane address), then we expect Jane to be this signer: we look up + // keys recorded for Jane and the signature either succeeds or fails to verify. If it fails (that pubkey + // which Joe used is not recorded for Jane), it will show an error. return this.senderEmail; }; @@ -87,7 +92,7 @@ export class PgpBlockView extends View { const scopes = await AcctStore.getScopes(this.acctEmail); this.decryptModule.canReadEmails = scopes.read || scopes.modify; if (storage.setup_done) { - const parsedPubs = (await ContactStore.getOneWithAllPubkeys(undefined, this.getSigner()))?.sortedPubkeys ?? []; + const parsedPubs = (await ContactStore.getOneWithAllPubkeys(undefined, this.getExpectedSignerEmail()))?.sortedPubkeys ?? []; // todo: we don't actually need parsed pubs here because we're going to pass them to the backgorund page // maybe we can have a method in ContactStore to extract armored keys const verificationPubs = parsedPubs.map(key => KeyUtil.armor(key.pubkey)); diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts index 56d15746254..cafa84618dd 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts @@ -62,7 +62,7 @@ export class PgpBlockViewDecryptModule { } }; - public canFetchFromApi = () => this.canReadEmails && this.msgFetchedFromApi !== 'raw'; + public canAndShouldFetchFromApi = () => this.canReadEmails && this.msgFetchedFromApi !== 'raw'; private decryptAndRender = async (encryptedData: Buf, verificationPubs: string[], optionalPwd?: string, plainSubject?: string) => { if (!this.view.signature?.parsedSignature) { @@ -82,7 +82,7 @@ export class PgpBlockViewDecryptModule { } }, plainSubject); } else if (result.error.type === DecryptErrTypes.format) { - if (this.canFetchFromApi()) { + if (this.canAndShouldFetchFromApi()) { console.info(`re-fetching message ${this.view.msgId} from api because looks like bad formatting: ${!this.msgFetchedFromApi ? 'full' : 'raw'}`); await this.initialize(verificationPubs, true); } else { diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index fe5ae90857a..2d59a92a426 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -19,7 +19,7 @@ export class PgpBlockViewSignatureModule { retryVerification?: (verificationPubs: string[]) => Promise) => { this.view.renderModule.doNotSetStateAsReadyYet = true; // so that body state is not marked as ready too soon - automated tests need to know when to check results if (verifyRes?.error) { - if (!verifyRes.isErrFatal && this.view.decryptModule.canFetchFromApi()) { + if (!verifyRes.isErrFatal && this.view.decryptModule.canAndShouldFetchFromApi()) { // Sometimes the signed content is slightly modified when parsed from DOM, // so the message should be re-fetched straight from API to make sure we get the original signed data and verify again this.view.signature!.parsedSignature = undefined; // force to re-parse @@ -37,10 +37,8 @@ export class PgpBlockViewSignatureModule { $('#pgp_signature').addClass('good'); $('#pgp_signature > .result').text('matching signature'); } else { - // is there intersection between supplied longids and signers from the message? - const intersection = Value.arr.intersection(verifyRes.signerLongids, verifyRes.suppliedLongids); if (retryVerification) { - const signerEmail = this.view.getSigner(); + const signerEmail = this.view.getExpectedSignerEmail(); if (!signerEmail) { // in some tests we load the block without sender information $('#pgp_signature').addClass('bad').find('.result').text(`Cannot verify: missing pubkey, missing sender info`); @@ -56,11 +54,7 @@ export class PgpBlockViewSignatureModule { await this.renderPgpSignatureCheckResult(await retryVerification(fetchedPubkeys), fetchedPubkeys, undefined); return; } - if (intersection.length) { - this.renderBadSignature(); - } else { - this.renderMissingPubkey(verifyRes.signerLongids[0]); - } + this.renderMissingPubkeyOrBadSignature(verifyRes); } catch (e) { if (ApiErr.isSignificant(e)) { Catch.reportErr(e); @@ -72,11 +66,7 @@ export class PgpBlockViewSignatureModule { } } } else { // !retryVerification - if (intersection.length) { - this.renderBadSignature(); - } else { - this.renderMissingPubkey(verifyRes.signerLongids[0]); - } + this.renderMissingPubkeyOrBadSignature(verifyRes); } } if (verifyRes) { @@ -87,7 +77,7 @@ export class PgpBlockViewSignatureModule { }; private setSigner = (signature: VerifyRes): void => { - const signerEmail = signature.match ? this.view.getSigner() : undefined; + const signerEmail = signature.match ? this.view.getExpectedSignerEmail() : undefined; $('#pgp_signature > .cursive > span').text(signerEmail || 'Unknown Signer'); }; @@ -100,4 +90,12 @@ export class PgpBlockViewSignatureModule { $('#pgp_signature > .result').text('signature does not match'); this.view.renderModule.setFrameColor('red'); // todo: in what other cases should we set the frame red? }; + + private renderMissingPubkeyOrBadSignature = (verifyRes: VerifyRes): void => { + if (verifyRes.match === null || !Value.arr.hasIntersection(verifyRes.signerLongids, verifyRes.suppliedLongids)) { + this.renderMissingPubkey(verifyRes.signerLongids[0]); + } else { + this.renderBadSignature(); + } + } } diff --git a/extension/js/common/core/common.ts b/extension/js/common/core/common.ts index 1233397dd97..622d1d11f71 100644 --- a/extension/js/common/core/common.ts +++ b/extension/js/common/core/common.ts @@ -204,6 +204,7 @@ export class Value { }, contains: (arr: T[] | string, value: T): boolean => Boolean(arr && typeof arr.indexOf === 'function' && (arr as any[]).indexOf(value) !== -1), intersection: (array1: T[], array2: T[]): T[] => array1.filter(value => array2.includes(value)), + hasIntersection: (array1: T[], array2: T[]): boolean => array1.some(value => array2.includes(value)), sum: (arr: number[]) => arr.reduce((a, b) => a + b, 0), average: (arr: number[]) => Value.arr.sum(arr) / arr.length, zeroes: (length: number): number[] => new Array(length).map(() => 0) From 12b3f67b208a99e65c698ba13169ec915d1eab2b Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 8 Dec 2021 06:21:43 +0000 Subject: [PATCH 20/31] lint fix --- extension/chrome/elements/pgp_block.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extension/chrome/elements/pgp_block.ts b/extension/chrome/elements/pgp_block.ts index 0de136eadda..4ca0b98c3e7 100644 --- a/extension/chrome/elements/pgp_block.ts +++ b/extension/chrome/elements/pgp_block.ts @@ -78,9 +78,9 @@ export class PgpBlockView extends View { public getExpectedSignerEmail = () => { // We always attempt to verify all signatures as "signed by sender", with public keys of the sender. - // That way, signature spoofing attacks are prevented: if Joe manages to spoof a sending address - // of Jane (send an email from Jane address), then we expect Jane to be this signer: we look up - // keys recorded for Jane and the signature either succeeds or fails to verify. If it fails (that pubkey + // That way, signature spoofing attacks are prevented: if Joe manages to spoof a sending address + // of Jane (send an email from Jane address), then we expect Jane to be this signer: we look up + // keys recorded for Jane and the signature either succeeds or fails to verify. If it fails (that pubkey // which Joe used is not recorded for Jane), it will show an error. return this.senderEmail; }; From a07a89564c9508f62f824a57cd024629014ac673 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Fri, 10 Dec 2021 09:14:55 +0000 Subject: [PATCH 21/31] lint fix --- .../elements/pgp_block_modules/pgp-block-signature-module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index 2d59a92a426..df1282aa650 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -97,5 +97,5 @@ export class PgpBlockViewSignatureModule { } else { this.renderBadSignature(); } - } + }; } From 84d92980d10539647b937d67414b628da8f6f640 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Fri, 10 Dec 2021 11:52:33 +0000 Subject: [PATCH 22/31] removed capitalization and changed sender's email for one message --- extension/css/cryptup.css | 1 - .../mock/attester/attester-endpoints.ts | 2 +- .../message-export-1766644f13510f58.json | 2 +- test/source/tests/decrypt.ts | 20 +++++++++---------- test/source/tests/gmail.ts | 4 ++-- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/extension/css/cryptup.css b/extension/css/cryptup.css index 24735fd26b2..8afb17b0b53 100644 --- a/extension/css/cryptup.css +++ b/extension/css/cryptup.css @@ -1323,7 +1323,6 @@ td { #pgp_signature > .cursive { font-family: signature; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ font-size: 24px; - text-transform: capitalize; font-weight: bold; } diff --git a/test/source/mock/attester/attester-endpoints.ts b/test/source/mock/attester/attester-endpoints.ts index 68da93de4f0..65ae870419a 100644 --- a/test/source/mock/attester/attester-endpoints.ts +++ b/test/source/mock/attester/attester-endpoints.ts @@ -62,7 +62,7 @@ export const mockAttesterEndpoints: HandlersDefinition = { if (['dhartley@verdoncollege.school.nz', '1C7E6D3C5563A941'.toLowerCase()].includes(emailOrLongid)) { return await getDC26454AFB71D18EABBAD73D1C7E6D3C5563A941(); } - if (['sams50sams50sept@gmail.com', 'president@forged.com', '2864E326A5BE488A'.toLowerCase()].includes(emailOrLongid)) { + if (['sams50sams50sept@gmail.com', 'sender@example.com'].includes(emailOrLongid)) { return testConstants.pubkey2864E326A5BE488A; } if (emailOrLongid.startsWith('martin@p')) { diff --git a/test/source/mock/google/exported-messages/message-export-1766644f13510f58.json b/test/source/mock/google/exported-messages/message-export-1766644f13510f58.json index e55605ca61d..ef3a03d12d5 100644 --- a/test/source/mock/google/exported-messages/message-export-1766644f13510f58.json +++ b/test/source/mock/google/exported-messages/message-export-1766644f13510f58.json @@ -24,7 +24,7 @@ }, { "name": "From", - "value": "Mr President " + "value": "Mr President " }, { "name": "MIME-Version", diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts index 693f86ef8d9..39d551417e5 100644 --- a/test/source/tests/decrypt.ts +++ b/test/source/tests/decrypt.ts @@ -371,7 +371,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te const urls = await inboxPage.getFramesUrls(['/chrome/elements/pgp_block.htm'], { sleep: 10, appearIn: 20 }); expect(urls.length).to.equal(1); const url = urls[0].split('/chrome/elements/pgp_block.htm')[1]; - const signature = ['Dhartley@Verdoncollege.School.Nz', 'matching signature']; + const signature = ['dhartley@verdoncollege.school.nz', 'matching signature']; await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: url, content: ['1234'], signature }); })); @@ -417,7 +417,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: url, content: ['This is unsigned, encrypted message'], signature }); })); - ava.default('signature - sender is different from pubkey email', testWithBrowser('ci.tests.gmail', async (t, browser) => { + ava.default('signature - sender is different from pubkey uid', testWithBrowser('ci.tests.gmail', async (t, browser) => { const threadId = '1766644f13510f58'; const acctEmail = 'ci.tests.gmail@flowcrypt.test'; const inboxPage = await browser.newPage(t, TestUrls.extension(`chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}&threadId=${threadId}`)); @@ -428,7 +428,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: url, content: ['How is my message signed?'], - signature: ['President@Forged.Com', 'matching signature'] + signature: ['sender@example.com', 'matching signature'] }); })); @@ -436,7 +436,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te const threadId = '1766644f13510f58'; const acctEmail = 'ci.tests.gmail@flowcrypt.test'; await PageRecipe.addPubkey(t, browser, acctEmail, '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: FlowCrypt Email Encryption [BUILD_REPLACEABLE_VERSION]\r\nComment: Seamlessly send and receive encrypted email\r\n\r\nxjMEYZeW2RYJKwYBBAHaRw8BAQdAT5QfLVP3y1yukk3MM/oiuXLNe1f9az5M\r\nBnOlKdF0nKnNJVNvbWVib2R5IDxTYW1zNTBzYW1zNTBzZXB0QEdtYWlsLkNv\r\nbT7CjwQQFgoAIAUCYZeW2QYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJS27QEA7pFlkLfD0KFQ\r\nsH/dwb/NPzn5zCi2L9gjPAC3d8gv1fwA/0FjAy/vKct4D7QH8KwtEGQns5+D\r\nP1WxDr4YI2hp5TkAzjgEYZeW2RIKKwYBBAGXVQEFAQEHQKNLY/bXrhJMWA2+\r\nWTjk3I7KhawyZfLomJ4hovqr7UtOAwEIB8J4BBgWCAAJBQJhl5bZAhsMACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJQnpgD/c1CzfS3YzJUx\r\nnFMrhjiE0WVgqOV/3CkfI4m4RA30QUIA/ju8r4AD2h6lu3Mx/6I6PzIRZQty\r\nLvTkcu4UKodZa4kK\r\n=7C4A\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n', - 'president@forged.com'); + 'sender@example.com'); const inboxPage = await browser.newPage(t, TestUrls.extension(`chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}&threadId=${threadId}`)); await inboxPage.waitAll('iframe', { timeout: 2 }); const urls = await inboxPage.getFramesUrls(['/chrome/elements/pgp_block.htm'], { sleep: 10, appearIn: 20 }); @@ -445,7 +445,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: url, content: ['How is my message signed?'], - signature: ['President@Forged.Com', 'matching signature'] + signature: ['sender@example.com', 'matching signature'] }); })); @@ -475,7 +475,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params, content: [], // todo: #4164 I would expect '1234' here - signature: ['Dhartley@Verdoncollege.School.Nz', 'matching signature'] + signature: ['dhartley@verdoncollege.school.nz', 'matching signature'] }); })); @@ -487,14 +487,14 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: textParams, content: ["1234"], - signature: ["matching signature", "Some.Alias@Protonmail.Com"] + signature: ["matching signature", "some.alias@protonmail.com"] }); const htmlParams = `?frameId=none&message=&msgId=16a9c0fe4e034bc2&` + `senderEmail=some.alias%40protonmail.com&isOutgoing=___cu_false___&signature=___cu_true___&acctEmail=flowcrypt.compatibility%40gmail.com`; await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: htmlParams, content: ["1234"], - signature: ["matching signature", "Some.Alias@Protonmail.Com"] + signature: ["matching signature", "some.alias@protonmail.com"] }); })); @@ -504,7 +504,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params, content: ["1234"], - signature: ["matching signature", "Flowcrypt.Compatibility@Protonmail.Com"] + signature: ["matching signature", "flowcrypt.compatibility@protonmail.com"] }); })); @@ -513,7 +513,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params, content: ['4) signed + encrypted email if supported'], - signature: ["matching signature", "Martin@Politick.Ca"] + signature: ["matching signature", "martin@politick.ca"] }); })); diff --git a/test/source/tests/gmail.ts b/test/source/tests/gmail.ts index f894ddc11b6..31733b43d2a 100644 --- a/test/source/tests/gmail.ts +++ b/test/source/tests/gmail.ts @@ -213,7 +213,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test const pgpBlockUrls = await gmailPage.getFramesUrls(['/chrome/elements/pgp_block.htm'], { sleep: 10, appearIn: 20 }); expect(pgpBlockUrls.length).to.equal(1); const url = pgpBlockUrls[0].split('/chrome/elements/pgp_block.htm')[1]; - const signature = ['Limon.Monte@Gmail.Com', 'matching signature']; + const signature = ['limon.monte@gmail.com', 'matching signature']; await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: url, content: ['1234'], signature }); await pageHasSecureReplyContainer(t, browser, gmailPage); await testMinimumElementHeight(gmailPage, '.pgp_block.signedMsg', 80); @@ -231,7 +231,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test await testMinimumElementHeight(gmailPage, '.pgp_block.signedMsg', 80); await testMinimumElementHeight(gmailPage, '.pgp_block.publicKey', 120); const url = pgpBlockUrls[0].split('/chrome/elements/pgp_block.htm')[1]; - const signature = ['Limon.Monte@Gmail.Com', 'matching signature']; + const signature = ['limon.monte@gmail.com', 'matching signature']; await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: url, content: ['1234'], signature }); await pageHasSecureReplyContainer(t, browser, gmailPage); const pubkeyPage = await gmailPage.getFrame(['/chrome/elements/pgp_pubkey.htm']); From 3bbd2ad85a1b69332961a363de6ea10255f6acbd Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 11 Dec 2021 14:14:45 +0000 Subject: [PATCH 23/31] common method to save fetched pubkeys to storage --- .../compose-modules/compose-storage-module.ts | 12 +++---- .../pgp-block-signature-module.ts | 13 +++----- extension/js/common/browser/browser-msg.ts | 9 ++++-- extension/js/common/shared.ts | 32 +++++++++++++++++++ 4 files changed, 48 insertions(+), 18 deletions(-) create mode 100644 extension/js/common/shared.ts diff --git a/extension/chrome/elements/compose-modules/compose-storage-module.ts b/extension/chrome/elements/compose-modules/compose-storage-module.ts index 703b8d35736..b9377e75905 100644 --- a/extension/chrome/elements/compose-modules/compose-storage-module.ts +++ b/extension/chrome/elements/compose-modules/compose-storage-module.ts @@ -16,6 +16,7 @@ import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; import { Settings } from '../../../js/common/settings.js'; +import { compareAndSavePubkeysToStorage } from '../../../js/common/shared.js'; export class ComposeStorageModule extends ViewModule { // if `type` is supplied, returns undefined if no keys of this type are found @@ -176,15 +177,10 @@ export class ComposeStorageModule extends ViewModule { } try { const lookupResult = await this.view.pubLookup.lookupEmail(email); - const fetchedPubkeys = await Promise.all(lookupResult.pubkeys.map(KeyUtil.parse)); - for (const fetched of fetchedPubkeys) { - const stored = storedPubkeys.find(p => KeyUtil.identityEquals(p.pubkey, fetched))?.pubkey; - if (!stored || KeyUtil.isFetchedNewer({ fetched, stored })) { - await ContactStore.update(undefined, email, { name, pubkey: fetched, pubkeyLastCheck: Date.now() }); - await this.view.recipientsModule.reRenderRecipientFor(email); - } + if (await compareAndSavePubkeysToStorage(email, lookupResult.pubkeys, storedPubkeys)) { + await this.view.recipientsModule.reRenderRecipientFor(email); } - if (!fetchedPubkeys.length && name) { // update just name + if (name) { // update name await ContactStore.update(undefined, email, { name }); } } catch (e) { diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index df1282aa650..157000d2174 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -8,7 +8,7 @@ import { PgpBlockView } from '../pgp_block'; import { Ui } from '../../../js/common/browser/ui.js'; import { VerifyRes } from '../../../js/common/core/crypto/pgp/msg-util.js'; import { Value } from '../../../js/common/core/common.js'; -import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; +import { BrowserMsg } from '../../../js/common/browser/browser-msg.js'; export class PgpBlockViewSignatureModule { @@ -45,13 +45,10 @@ export class PgpBlockViewSignatureModule { } else { this.view.renderModule.renderText('Verifying message...'); try { - const { pubkeys: fetchedPubkeys } = await this.view.pubLookup.lookupEmail(signerEmail); - if (fetchedPubkeys.length) { - for (const fetched of fetchedPubkeys) { // todo: should we ignore the ones we already have in verificationPubs? - // or better yet, create a common method to use both here and in ComposeStorageModule - ContactStore.update(undefined, signerEmail, { pubkey: fetched, pubkeyLastCheck: Date.now() }).catch(Catch.reportErr); - } - await this.renderPgpSignatureCheckResult(await retryVerification(fetchedPubkeys), fetchedPubkeys, undefined); + const { pubkeys } = await this.view.pubLookup.lookupEmail(signerEmail); + if (pubkeys.length) { + await BrowserMsg.send.bg.await.saveFetchedPubkeys({ email: signerEmail, pubkeys }); + await this.renderPgpSignatureCheckResult(await retryVerification(pubkeys), pubkeys, undefined); return; } this.renderMissingPubkeyOrBadSignature(verifyRes); diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index f72365318cd..bfaa920defc 100644 --- a/extension/js/common/browser/browser-msg.ts +++ b/extension/js/common/browser/browser-msg.ts @@ -18,6 +18,7 @@ import { Ui } from './ui.js'; import { GlobalStoreDict, GlobalIndex } from '../platform/store/global-store.js'; import { AcctStoreDict, AccountIndex } from '../platform/store/acct-store.js'; import { Contact } from '../core/crypto/key.js'; +import { saveFetchedPubkeysIfNewerThanInStorage } from '../shared.js'; export type GoogleAuthWindowResult$result = 'Success' | 'Denied' | 'Error' | 'Closed'; @@ -65,6 +66,7 @@ export namespace Bm { export type AjaxGmailAttachmentGetChunk = { acctEmail: string, msgId: string, attachmentId: string }; export type ShowAttachmentPreview = { iframeUrl: string }; export type ReRenderRecipient = { contact: Contact }; + export type SaveFetchedPubkeys = { email: string, pubkeys: string[] }; export namespace Res { export type GetActiveTabInfo = { provider: 'gmail' | undefined, acctEmail: string | undefined, sameWorld: boolean | undefined }; @@ -82,13 +84,14 @@ export namespace Bm { export type PgpHashChallengeAnswer = { hashed: string }; export type AjaxGmailAttachmentGetChunk = { chunk: Buf }; export type _tab_ = { tabId: string | null | undefined }; + export type SaveFetchedPubkeys = boolean; export type Db = any; // not included in Any below export type Ajax = any; // not included in Any below export type Any = GetActiveTabInfo | _tab_ | ReconnectAcctAuthPopup | PgpMsgDecrypt | PgpMsgDiagnoseMsgPubkeys | PgpMsgVerify | PgpHashChallengeAnswer | PgpMsgType | InMemoryStoreGet | InMemoryStoreSet | StoreAcctGet | StoreAcctSet | StoreGlobalGet | StoreGlobalSet - | AjaxGmailAttachmentGetChunk; + | AjaxGmailAttachmentGetChunk | SaveFetchedPubkeys; } export type AnyRequest = PassphraseEntry | StripeResult | OpenPage | OpenGoogleAuthDialog | Redirect | Reload | @@ -97,7 +100,7 @@ export namespace Bm { NotificationShow | PassphraseDialog | PassphraseDialog | Settings | SetCss | AddOrRemoveClass | ReconnectAcctAuthPopup | Db | InMemoryStoreSet | InMemoryStoreGet | StoreGlobalGet | StoreGlobalSet | StoreAcctGet | StoreAcctSet | PgpMsgDecrypt | PgpMsgDiagnoseMsgPubkeys | PgpMsgVerifyDetached | PgpHashChallengeAnswer | PgpMsgType | Ajax | - ShowAttachmentPreview | ReRenderRecipient; + ShowAttachmentPreview | ReRenderRecipient | SaveFetchedPubkeys; // export type RawResponselessHandler = (req: AnyRequest) => Promise; // export type RawRespoHandler = (req: AnyRequest) => Promise; @@ -144,6 +147,7 @@ export class BrowserMsg { pgpMsgDecrypt: (bm: Bm.PgpMsgDecrypt) => BrowserMsg.sendAwait(undefined, 'pgpMsgDecrypt', bm, true) as Promise, pgpMsgVerifyDetached: (bm: Bm.PgpMsgVerifyDetached) => BrowserMsg.sendAwait(undefined, 'pgpMsgVerifyDetached', bm, true) as Promise, pgpMsgType: (bm: Bm.PgpMsgType) => BrowserMsg.sendAwait(undefined, 'pgpMsgType', bm, true) as Promise, + saveFetchedPubkeys: (bm: Bm.SaveFetchedPubkeys) => BrowserMsg.sendAwait(undefined, 'saveFetchedPubkeys', bm, true) as Promise, }, }, passphraseEntry: (dest: Bm.Dest, bm: Bm.PassphraseEntry) => BrowserMsg.sendCatch(dest, 'passphrase_entry', bm), @@ -240,6 +244,7 @@ export class BrowserMsg { BrowserMsg.bgAddListener('pgpMsgDecrypt', MsgUtil.decryptMessage); BrowserMsg.bgAddListener('pgpMsgVerifyDetached', MsgUtil.verifyDetached); BrowserMsg.bgAddListener('pgpMsgType', MsgUtil.type); + BrowserMsg.bgAddListener('saveFetchedPubkeys', saveFetchedPubkeysIfNewerThanInStorage); }; public static addListener = (name: string, handler: Handler) => { diff --git a/extension/js/common/shared.ts b/extension/js/common/shared.ts new file mode 100644 index 00000000000..de916078e5f --- /dev/null +++ b/extension/js/common/shared.ts @@ -0,0 +1,32 @@ +/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ + +'use strict'; + +import { KeyUtil, PubkeyInfo } from './core/crypto/key.js'; +import { ContactStore } from './platform/store/contact-store.js'; + +/** + * Save fetched keys if they are newer versions of public keys we already have (compared by fingerprint) + */ +export const compareAndSavePubkeysToStorage = async (email: string, fetchedPubkeys: string[], storedPubkeys: PubkeyInfo[]): Promise => { + let updated = false; + for (const fetched of await Promise.all(fetchedPubkeys.map(KeyUtil.parse))) { + const stored = storedPubkeys.find(p => KeyUtil.identityEquals(p.pubkey, fetched))?.pubkey; + if (!stored || KeyUtil.isFetchedNewer({ fetched, stored })) { + await ContactStore.update(undefined, email, { pubkey: fetched, pubkeyLastCheck: Date.now() }); + updated = true; + } + } + return updated; +}; + +/** + * Save fetched keys if they are newer versions of public keys we already have (compared by fingerprint) + */ +export const saveFetchedPubkeysIfNewerThanInStorage = async ({ email, pubkeys }: { email: string, pubkeys: string[] }): Promise => { + if (!pubkeys.length) { + return false; + } + const storedContact = await ContactStore.getOneWithAllPubkeys(undefined, email); + return await compareAndSavePubkeysToStorage(email, pubkeys, storedContact?.sortedPubkeys ?? []); +}; From 1456c40e2c3bb84cd5d7ad04111ded697e8773d6 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 11 Dec 2021 18:09:37 +0000 Subject: [PATCH 24/31] remark regarding cleartext --- extension/js/common/core/crypto/pgp/msg-util.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index b84c12eaa5d..bf4c1ce09b4 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -156,6 +156,11 @@ export class MsgUtil { } catch (formatErr) { return { success: false, error: { type: DecryptErrTypes.format, message: String(formatErr) }, longids }; } + // there are 3 types of messages possible at this point + // 1. PKCS#7 if isPkcs7 is true + // 2. OpenPGP cleartext message if isCleartext is true + // 3. Other types of OpenPGP message + // Hence isCleartext and isPkcs7 are mutually exclusive if (prepared.isCleartext) { const signature = await OpenPGPKey.verify(prepared.message, await ContactStore.getPubkeyInfos(undefined, verificationPubs)); const content = signature.content || Buf.fromUtfStr('no content'); From a73fc487bbb392121a20a19d92fce7163bfb62b0 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 12 Dec 2021 04:29:22 +0000 Subject: [PATCH 25/31] VerifyRes.match=false on message digest mismatch --- extension/js/common/core/crypto/pgp/openpgp-key.ts | 1 + test/source/tests/unit-node.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/extension/js/common/core/crypto/pgp/openpgp-key.ts b/extension/js/common/core/crypto/pgp/openpgp-key.ts index ecb01be1bab..66b5859b08a 100644 --- a/extension/js/common/core/crypto/pgp/openpgp-key.ts +++ b/extension/js/common/core/crypto/pgp/openpgp-key.ts @@ -479,6 +479,7 @@ export class OpenPGPKey { verifyRes.isErrFatal = true; // don't try to re-fetch the message from API } else if (verifyErr instanceof Error && verifyErr.message === 'Message digest did not match') { verifyRes.error = verifyErr.message; + verifyRes.match = false; } else { verifyRes.error = `Error verifying this message: ${String(verifyErr)}`; Catch.reportErr(verifyErr); diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 42ed4cca9c8..074a4e8b537 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -895,9 +895,9 @@ E1d8Vh/Ey7IJ8TXbfFrdv5ZP3HqMK0089SooZwx/GN2QIaOYQXsS0u7IFNhU\n=q5Sf sigText, verificationPubs: [dhartleyPubkey] }); - expect(resultRightKey.match).to.be.null; + expect(resultRightKey.match).to.be.false; expect(resultRightKey.error).to.not.be.undefined; - expect(resultRightKey.isErrFatal).to.be.undefined; + expect(resultRightKey.isErrFatal).to.not.be.true; } { const resultWrongKey = await MsgUtil.verifyDetached({ From 362b7b583c8a8e3d4b55160d133ef330e18384d3 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 12 Dec 2021 14:45:26 +0000 Subject: [PATCH 26/31] removed PII --- .../mock/attester/attester-endpoints.ts | 13 +- .../message-export-1754cfc37886899e.json | 213 ------------------ .../message-export-1754cfd1b2f1d6e5.json | 211 ----------------- .../message-export-17dad75e63e47f97.json | 211 +++++++++++++++++ .../message-export-17daefa0eb077da6.json | 213 ++++++++++++++++++ test/source/tests/decrypt.ts | 37 +-- test/source/tests/unit-node.ts | 28 ++- 7 files changed, 461 insertions(+), 465 deletions(-) delete mode 100644 test/source/mock/google/exported-messages/message-export-1754cfc37886899e.json delete mode 100644 test/source/mock/google/exported-messages/message-export-1754cfd1b2f1d6e5.json create mode 100644 test/source/mock/google/exported-messages/message-export-17dad75e63e47f97.json create mode 100644 test/source/mock/google/exported-messages/message-export-17daefa0eb077da6.json diff --git a/test/source/mock/attester/attester-endpoints.ts b/test/source/mock/attester/attester-endpoints.ts index 65ae870419a..eeb040797c8 100644 --- a/test/source/mock/attester/attester-endpoints.ts +++ b/test/source/mock/attester/attester-endpoints.ts @@ -24,18 +24,15 @@ const knownMockEmails = [ let data: GoogleData; export const MOCK_ATTESTER_LAST_INSERTED_PUB: { [email: string]: string } = {}; -const getDC26454AFB71D18EABBAD73D1C7E6D3C5563A941 = async () => { +const get203FAE7076005381 = async () => { if (!data) { data = await GoogleData.withInitializedData('flowcrypt.compatibility@gmail.com'); } - - const msg = data.getMessage('1754cfc37886899e')!; + const msg = data.getMessage('17dad75e63e47f97')!; const msgText = Buf.fromBase64Str(msg!.raw!).toUtfStr(); - const dhartleyPubkey = msgText + return msgText .match(/\-\-\-\-\-BEGIN PGP PUBLIC KEY BLOCK\-\-\-\-\-.*\-\-\-\-\-END PGP PUBLIC KEY BLOCK\-\-\-\-\-/s)![0] .replace(/=\r\n/g, '').replace(/=3D/g, '='); - - return dhartleyPubkey; }; export const mockAttesterEndpoints: HandlersDefinition = { @@ -59,8 +56,8 @@ export const mockAttesterEndpoints: HandlersDefinition = { if (emailOrLongid === 'flowcrypt.compatibility@protonmail.com') { return protonMailCompatKey; } - if (['dhartley@verdoncollege.school.nz', '1C7E6D3C5563A941'.toLowerCase()].includes(emailOrLongid)) { - return await getDC26454AFB71D18EABBAD73D1C7E6D3C5563A941(); + if (emailOrLongid === 'some.sender@test.com') { + return await get203FAE7076005381(); } if (['sams50sams50sept@gmail.com', 'sender@example.com'].includes(emailOrLongid)) { return testConstants.pubkey2864E326A5BE488A; diff --git a/test/source/mock/google/exported-messages/message-export-1754cfc37886899e.json b/test/source/mock/google/exported-messages/message-export-1754cfc37886899e.json deleted file mode 100644 index 601c1e31239..00000000000 --- a/test/source/mock/google/exported-messages/message-export-1754cfc37886899e.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "acctEmail": "flowcrypt.compatibility@gmail.com", - "full": { - "id": "1754cfc37886899e", - "threadId": "1754cfc37886899e", - "labelIds": [ - "Label_6255111949068864933", - "IMPORTANT", - "Label_7", - "Label_9", - "CATEGORY_PERSONAL", - "Label_4", - "INBOX" - ], - "snippet": "1234", - "payload": { - "partId": "", - "mimeType": "multipart/signed", - "filename": "", - "headers": [ - { - "name": "X-Gm-Message-State", - "value": "AOAM531dg7t33gFjAD74BUjtjzTNRfrG7ueIIKQX6Tm0jpT1FopiHote p+prqCFS2pHyF9PrSrrWcLyOrt/fhbdFEC3MECTg3fy5n95NtSSH4prb1ahSRE9QtL1d5WnmE5H so5pyceMjckNOuLs/DlgX7dq63A9R+DYekQVJfAh3EhTJuA/M3PxQeZfBH0fr+rZHUlhzKa38SN XmR2KgXNj34sL3b2lHI8JZ7tsp" - }, - { - "name": "To", - "value": "flowcrypt.compatibility@gmail.com" - }, - { - "name": "From", - "value": "dhartley@verdoncollege.school.nz" - }, - { - "name": "Autocrypt", - "value": "addr=dhartley@verdoncollege.school.nz; keydata= xsDNBF1wPuQBDAChI2DAh6K/bmwH5hfCJwkxewFUfWaAxzAIWyXe/w5wjFJBpu74MJvqO+8T WkyVMVXQqOm5dH36YkMCVcNqoGgC0mp/JZUmVIdPqKH1QYK988rRKP2HIl3CNgbyiHLjyKVf +RW4OKxeQLM1MJpQ/39V5ymWftPszz1j+LO2FKpfAsUapAImZNwhXSKZcrxvUEnoayQg17HA 9ThXadQ/UU8FDlibKQoBUvhfyig7Gf4s/Ol3p76a57M/ZClgS5hPoM77oPaC8n8QVYA/t5Bu YSd1n3CE67EU6IzJr3A83c34EZO/wHu6gwCCllouWNtU7fDvKJEbcgbTfb0o7qo224lAUwDq VB2zCVQTEKXV20h/qvujNnLxo2DjcBg+Fbd7GrZEEqK5+psc3U3ljD9mToJoM4lG7Ixy1Ev2 Sx8xfUFtL9PIktl8OwA6U/1XZvauoH5upBain8bc+0ai8axXL2/O0rAKX2DlPfHD9LOim74P NqliLUtTr4TlByMZ5IuYCjMAEQEAAc0vRGF2ZSBIYXJ0bGV5IDxkaGFydGxleUB2ZXJkb25j b2xsZWdlLnNjaG9vbC5uej7CwRQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AW IQTcJkVK+3HRjqu61z0cfm08VWOpQQUCX4eCvAUJBdmq2AAKCRAcfm08VWOpQcRiC/sFhdZh FB5gQbHcs8F9+aPi8gfwu3atUFwadN6AShSa6K3pnd7V1xYlDFwRPKrAEWBBYeX9dsAWl9gx PNuCo5utRkgPR8ZSO4ceXR4ZgKTvF3k6UzzGx8HFC+MJCvdiTTPnMayUOOX/f4+79uWt+fRK SBtckwud7+Dt/88ux8j8TpvBZMhciJ8Zwr9ouVWXfgg2/RMzZNTophQHKdUOhPQ/DBzVaLxV f3QEqKPs3lhrS08ZOMpubV938KdCLgqeqeaClEtURUZEs/5Ixg7lb+SbbOo12K/lRnYu4U1p WcHVueccqkTxDZxgCDBtqN4sBMnmKtSuYAcbyy4SyWGA6Z0KIP87FxMi5Yf+04R+hEG9KWD3 fuk1JIIUeNVoiAn4aERjrIUN5q4NJAyY0d6KXmpyJxOK2Dj3ZyQglNrvXitchXJJqMfdGexB 03lzDYxyHSlu+WBp2exaSOyQIKSniQ1BhAHyp4VQBkLKKVAEV+fVeTdFxarxVH5Hs8tP7Yag JSTOwM0EXXA+5QEMAOoo+4IBfjaiZjEe4azMFDrVwvVkeEw3soMrBjAOjlySQCpE/q0qDId5 k0eSAAhttans3Us166xKW2QMfd2ojFBTLi06Zpha1jDo5Aj2kWDEGT5y+0HBRVY0NmPtvBA1 igRfF1ZSUsqIhO/dSqirdnSh08z5bZnEEyIWObBhC8leE2BAdXWgG1Wk0/02DkrA9N16lZ1/ wVGtYU09rnZBJpeDOFM0dCh7uhfA5LGzmxKzn25/rDSFUrUcmYfawAn13OS02COIlstl6lUg VJ6xtBxOaT4NLka4hkqyzpamCF6okeZQI6tHEmlm5FXuhCb/Lz/n9k9Pk793Ior0PP+xMPO8 gbWlFvPfl/MRKc3Hy2SIZw9RQ7A/33V/ozXVzg1A4drrXKkKec8JznNTkoRI6fSx/TGVpNh1 +5SOHFGgKtZ+RA1BD1vmsMz/5xmdoWGisoR0/hEtdjudxpK1Zm0r1nvNpFZmKRZB26ImfMC4 kbRVXXX5LxE6yIyJ3urFn2+6PwARAQABwsD8BBgBCAAmAhsMFiEE3CZFSvtx0Y6rutc9HH5t PFVjqUEFAl+Hgr0FCQXZqtgACgkQHH5tPFVjqUEnEwv9Ea+syu+U72SeNc3BQ3Gm6llARyQm 7gz2GZBak/epzD+3TFNKFLLh9AyBOM+wWRpGfKh1X+s9GLdSQISEA06bsW4d1KfHqEg0HTRw 4p9SRwOWHvy1L6maA1VHRQCW0/FN/7kYDrUH3bigISTkJJpmq8j4YOH66HVEbFqBO31ycYUG Gt/Bazhe7rOhAxZuGYzcVJQdSE020vjbqM5l34HfiDubuUNRhHMIaWa5IYYIyq33STbZu5Fl n/5zoGN8+Ae4wNNzzfOyWCKUzqybgbDxPWCnX2tgDEDlCt0X5X3PcItVeUpQCecDRut4eGEz OkkdNo2EM9UpBjGfei+GD8vrz9apxvQ+nKYIOi4FM8GkXrsag7gvIdEcGzWcokhmdaFqWqwe QALMUhhA86TckOwaouKkwk3CwnHtqdWryXmpvkbJxWRVrZOGgSfG+/QIku0oNY5XKRbdXURp 1dPshT490JLUzFbM+OcF++HyMZ70qj/k64f6idz5w6TvK0TCKe/M" - }, - { - "name": "Subject", - "value": "sig from new Thunderbird v78 not recognized, plain" - }, - { - "name": "Date", - "value": "Thu, 22 Oct 2020 10:06:29 +1300" - }, - { - "name": "User-Agent", - "value": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Thunderbird/78.3.3" - }, - { - "name": "MIME-Version", - "value": "1.0" - }, - { - "name": "Content-Type", - "value": "multipart/signed; micalg=pgp-sha256; protocol=\"application/pgp-signature\"; boundary=\"zUh80el73HdgHOoDVnJsTtQZDI9bELswe\"" - } - ], - "body": { - "size": 0 - }, - "parts": [ - { - "partId": "0", - "mimeType": "multipart/mixed", - "filename": "", - "headers": [ - { - "name": "Content-Type", - "value": "multipart/mixed; boundary=\"XWwnusC4nxhk2LRvLCC6Skcb8YiKQ4Lu0\"; protected-headers=\"v1\"" - }, - { - "name": "From", - "value": "Dave Hartley " - }, - { - "name": "To", - "value": "flowcrypt.compatibility@gmail.com" - }, - { - "name": "Message-ID", - "value": "<27f923b8-0cec-3a14-db54-d6a5c3b922d8@verdoncollege.school.nz>" - }, - { - "name": "Subject", - "value": "sig from new Thunderbird v78 not recognized, plain" - } - ], - "body": { - "size": 0 - }, - "parts": [ - { - "partId": "0.0", - "mimeType": "multipart/mixed", - "filename": "", - "headers": [ - { - "name": "Content-Type", - "value": "multipart/mixed; boundary=\"------------8D9D547E804CC8A49BF70E54\"" - }, - { - "name": "Content-Language", - "value": "en-NZ" - } - ], - "body": { - "size": 0 - }, - "parts": [ - { - "partId": "0.0.0", - "mimeType": "text/plain", - "filename": "", - "headers": [ - { - "name": "Content-Type", - "value": "text/plain; charset=utf-8" - }, - { - "name": "Content-Transfer-Encoding", - "value": "quoted-printable" - } - ], - "body": { - "size": 6, - "data": "MTIzNA0K" - } - }, - { - "partId": "0.0.1", - "mimeType": "application/pgp-keys", - "filename": "OpenPGP_0x1C7E6D3C5563A941.asc", - "headers": [ - { - "name": "Content-Type", - "value": "application/pgp-keys; name=\"OpenPGP_0x1C7E6D3C5563A941.asc\"" - }, - { - "name": "Content-Transfer-Encoding", - "value": "quoted-printable" - }, - { - "name": "Content-Disposition", - "value": "attachment; filename=\"OpenPGP_0x1C7E6D3C5563A941.asc\"" - } - ], - "body": { - "attachmentId": "ANGjdJ-yDvfUi4GXx4ZMIHVuKfjT32ffJF5mfNLlV6tjeq_OaFeiVfeVKVoZd4iSUTqunOwrlyroaITF3oL75s_jE4drJMONBjsNrA8Zq7IN2_vMmPASTtq_WAeV9TATa0xKofTE7CeN0gi8Uv_v4RxauXgC5eWdZ-tGRPpLMQYVVT9sGmKiD5nNiMfc_lGQJaYsiGUTii-i_Jl-oxdtTS76OKtJq1MXVaAkg7GJ_Oi2B4PvXiPnX8N0LAYHR7q5L20kuEdvBSDLrpalgScpdBCCxiVGZ4NSz4tfhXQTnqyhOUshqkUUfiCg2GVvRmt0IR7JvGX01q21JpJRq9cbJjI8e1higUzGjkgVcda5gUGa2QuFlG19vgHNxIJK_tM", - "size": 3147 - } - } - ] - } - ] - }, - { - "partId": "1", - "mimeType": "application/pgp-signature", - "filename": "OpenPGP_signature", - "headers": [ - { - "name": "Content-Type", - "value": "application/pgp-signature; name=\"OpenPGP_signature.asc\"" - }, - { - "name": "Content-Description", - "value": "OpenPGP digital signature" - }, - { - "name": "Content-Disposition", - "value": "attachment; filename=\"OpenPGP_signature\"" - } - ], - "body": { - "attachmentId": "ANGjdJ8iNjciNtU09awKCwED3qNYlqvsQFBKDYwSTRsaDQ_5-xbD0AjnVgB9qA2n323WQInPe_Bb4vTENJrJlfgftVeRN7kIUxtLb2TgWk3-cxLQ5OuAqiS932nGlZ31rkFhhuLs7YNwmS84--6Wxo2UVM3cTO5E0GOScpfoNCuO9W3oY9219sAd-RmQ_v6QRYfAeFzaA2HCetLT3K6rzf9GQ5BlIKBD92brbABKjV6gJiDCCm0m67lrUPyNfFsiLhlGrYOr2FK_FecuHf9N5J_xLPE0Z-Vaaea8BRr_2xaeyOHYw0rRfccVWEY4bSkj37qCu_gazJk0NLercRCJfdZxysV3KftFCtem0K-J_6dJwVEQEx9HaR_lFH5PQIg", - "size": 677 - } - } - ] - }, - "sizeEstimate": 13211, - "historyId": "1206346", - "internalDate": "1603314389000" - }, - "attachments": { - "ANGjdJ-yDvfUi4GXx4ZMIHVuKfjT32ffJF5mfNLlV6tjeq_OaFeiVfeVKVoZd4iSUTqunOwrlyroaITF3oL75s_jE4drJMONBjsNrA8Zq7IN2_vMmPASTtq_WAeV9TATa0xKofTE7CeN0gi8Uv_v4RxauXgC5eWdZ-tGRPpLMQYVVT9sGmKiD5nNiMfc_lGQJaYsiGUTii-i_Jl-oxdtTS76OKtJq1MXVaAkg7GJ_Oi2B4PvXiPnX8N0LAYHR7q5L20kuEdvBSDLrpalgScpdBCCxiVGZ4NSz4tfhXQTnqyhOUshqkUUfiCg2GVvRmt0IR7JvGX01q21JpJRq9cbJjI8e1higUzGjkgVcda5gUGa2QuFlG19vgHNxIJK_tM": { - "data": "LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tDQoNCnhzRE5CRjF3UHVRQkRBQ2hJMkRBaDZLL2Jtd0g1aGZDSndreGV3RlVmV2FBeHpBSVd5WGUvdzV3akZKQnB1NzRNSnZxTys4VFdreVYNCk1WWFFxT201ZEgzNllrTUNWY05xb0dnQzBtcC9KWlVtVklkUHFLSDFRWUs5ODhyUktQMkhJbDNDTmdieWlITGp5S1ZmK1JXNE9LeGUNClFMTTFNSnBRLzM5VjV5bVdmdFBzenoxaitMTzJGS3BmQXNVYXBBSW1aTndoWFNLWmNyeHZVRW5vYXlRZzE3SEE5VGhYYWRRL1VVOEYNCkRsaWJLUW9CVXZoZnlpZzdHZjRzL09sM3A3NmE1N00vWkNsZ1M1aFBvTTc3b1BhQzhuOFFWWUEvdDVCdVlTZDFuM0NFNjdFVTZJekoNCnIzQTgzYzM0RVpPL3dIdTZnd0NDbGxvdVdOdFU3ZkR2S0pFYmNnYlRmYjBvN3FvMjI0bEFVd0RxVkIyekNWUVRFS1hWMjBoL3F2dWoNCk5uTHhvMkRqY0JnK0ZiZDdHclpFRXFLNStwc2MzVTNsakQ5bVRvSm9NNGxHN0l4eTFFdjJTeDh4ZlVGdEw5UElrdGw4T3dBNlUvMVgNClp2YXVvSDV1cEJhaW44YmMrMGFpOGF4WEwyL08wckFLWDJEbFBmSEQ5TE9pbTc0UE5xbGlMVXRUcjRUbEJ5TVo1SXVZQ2pNQUVRRUENCkFjMHZSR0YyWlNCSVlYSjBiR1Y1SUR4a2FHRnlkR3hsZVVCMlpYSmtiMjVqYjJ4c1pXZGxMbk5qYUc5dmJDNXVlajdDd1JRRUV3RUkNCkFENENHd01GQ3drSUJ3SUdGUW9KQ0FzQ0JCWUNBd0VDSGdFQ0Y0QVdJUVRjSmtWSyszSFJqcXU2MXowY2ZtMDhWV09wUVFVQ1g0ZUMNCnZBVUpCZG1xMkFBS0NSQWNmbTA4VldPcFFjUmlDL3NGaGRaaEZCNWdRYkhjczhGOSthUGk4Z2Z3dTNhdFVGd2FkTjZBU2hTYTZLM3ANCm5kN1YxeFlsREZ3UlBLckFFV0JCWWVYOWRzQVdsOWd4UE51Q281dXRSa2dQUjhaU080Y2VYUjRaZ0tUdkYzazZVenpHeDhIRkMrTUoNCkN2ZGlUVFBuTWF5VU9PWC9mNCs3OXVXdCtmUktTQnRja3d1ZDcrRHQvODh1eDhqOFRwdkJaTWhjaUo4WndyOW91VldYZmdnMi9STXoNClpOVG9waFFIS2RVT2hQUS9EQnpWYUx4VmYzUUVxS1BzM2xoclMwOFpPTXB1YlY5MzhLZENMZ3FlcWVhQ2xFdFVSVVpFcy81SXhnN2wNCmIrU2JiT28xMksvbFJuWXU0VTFwV2NIVnVlY2Nxa1R4RFp4Z0NEQnRxTjRzQk1ubUt0U3VZQWNieXk0U3lXR0E2WjBLSVA4N0Z4TWkNCjVZZiswNFIraEVHOUtXRDNmdWsxSklJVWVOVm9pQW40YUVSanJJVU41cTROSkF5WTBkNktYbXB5SnhPSzJEajNaeVFnbE5ydlhpdGMNCmhYSkpxTWZkR2V4QjAzbHpEWXh5SFNsdStXQnAyZXhhU095UUlLU25pUTFCaEFIeXA0VlFCa0xLS1ZBRVYrZlZlVGRGeGFyeFZINUgNCnM4dFA3WWFnSlNUQ3dSUUVFd0VJQUQ0V0lRVGNKa1ZLKzNIUmpxdTYxejBjZm0wOFZXT3BRUVVDWFhBKzVRSWJBd1VKQWVFemdBVUwNCkNRZ0hBZ1lWQ2drSUN3SUVGZ0lEQVFJZUFRSVhnQUFLQ1JBY2ZtMDhWV09wUVEzMkMvNHAzaHNqZlRlQXpRUjBhSG9XR0tUUmNGb1kNCm5jOUlkWWpPNzhmbmFmZWhKSXQ1TExvTGZkK01TR1B6Njc4NWhtTmdyeWo2WHRFMWVJTU42cHZrSy9CeTZON1Y2T04xOWp6S3h2QXENCi8zZi9yUWNpZVRKbDV0dHRRUkdXc3VGWCtEZHYraXdmbEcrOVpTTTNFOWxaTDJwbzNFdzZLaG1pd0graSsrQ1JGN3V0UlJyUGorRHINCm1NQmdYajB5WXloUm1oZVVta1BqN282S2ZNK2NuL3JRUFZTSVdXSHJCSW1BbEVna05sOWZJOS9TTWhvZ052NlpzbUtYN2FlTko5eHgNClhlNnFBVzI3NnJqZEsvM2VoMHJieHdja0lZMkJaWVJlTzczZkxmRnpYckYxa0gwZ0kvVnErQWFhaDkvYzU2WkJNK1ZTRVBhK3pveFANCmR5eWMzenVsN1ExNlZMV0FnblUzUElCeWNFcS9naWxLQnpSM0dHTDJWakxValU0d0F4UTdKd1djMDdvbkttL1dTUnJmY2lnNEdDbHgNClBNb3BPS1FwNWd4MEdPSUVDTWFxRHFvV3pUQWpLK1BTR0pLZUx1eTNydGhyN09rS3JIc2ZhV1BQZ0RwZzVOSlM0MG52VVF4Z3pwQnINCmFRZ2pTS0FjU1NkVlo4bm5WVnozQkdhZm1aT0FEL3pPd00wRVhYQSs1UUVNQU9vbys0SUJmamFpWmpFZTRhek1GRHJWd3ZWa2VFdzMNCnNvTXJCakFPamx5U1FDcEUvcTBxRElkNWswZVNBQWh0dGFuczNVczE2NnhLVzJRTWZkMm9qRkJUTGkwNlpwaGExakRvNUFqMmtXREUNCkdUNXkrMEhCUlZZME5tUHR2QkExaWdSZkYxWlNVc3FJaE8vZFNxaXJkblNoMDh6NWJabkVFeUlXT2JCaEM4bGVFMkJBZFhXZ0cxV2sNCjAvMDJEa3JBOU4xNmxaMS93Vkd0WVUwOXJuWkJKcGVET0ZNMGRDaDd1aGZBNUxHem14S3puMjUvckRTRlVyVWNtWWZhd0FuMTNPUzANCjJDT0lsc3RsNmxVZ1ZKNnh0QnhPYVQ0TkxrYTRoa3F5enBhbUNGNm9rZVpRSTZ0SEVtbG01Rlh1aENiL0x6L245azlQazc5M0lvcjANClBQK3hNUE84Z2JXbEZ2UGZsL01SS2MzSHkyU0ladzlSUTdBLzMzVi9velhWemcxQTRkcnJYS2tLZWM4SnpuTlRrb1JJNmZTeC9UR1YNCnBOaDErNVNPSEZHZ0t0WitSQTFCRDF2bXNNei81eG1kb1dHaXNvUjAvaEV0ZGp1ZHhwSzFabTByMW52TnBGWm1LUlpCMjZJbWZNQzQNCmtiUlZYWFg1THhFNnlJeUozdXJGbjIrNlB3QVJBUUFCd3NEOEJCZ0JDQUFtQWhzTUZpRUUzQ1pGU3Z0eDBZNnJ1dGM5SEg1dFBGVmoNCnFVRUZBbCtIZ3IwRkNRWFpxdGdBQ2drUUhINXRQRlZqcVVFbkV3djlFYStzeXUrVTcyU2VOYzNCUTNHbTZsbEFSeVFtN2d6MkdaQmENCmsvZXB6RCszVEZOS0ZMTGg5QXlCT00rd1dScEdmS2gxWCtzOUdMZFNRSVNFQTA2YnNXNGQxS2ZIcUVnMEhUUnc0cDlTUndPV0h2eTENCkw2bWFBMVZIUlFDVzAvRk4vN2tZRHJVSDNiaWdJU1RrSkpwbXE4ajRZT0g2NkhWRWJGcUJPMzF5Y1lVR0d0L0JhemhlN3JPaEF4WnUNCkdZemNWSlFkU0UwMjB2amJxTTVsMzRIZmlEdWJ1VU5SaEhNSWFXYTVJWVlJeXEzM1NUYlp1NUZsbi81em9HTjgrQWU0d05OenpmT3kNCldDS1V6cXliZ2JEeFBXQ25YMnRnREVEbEN0MFg1WDNQY0l0VmVVcFFDZWNEUnV0NGVHRXpPa2tkTm8yRU05VXBCakdmZWkrR0Q4dnINCno5YXB4dlErbktZSU9pNEZNOEdrWHJzYWc3Z3ZJZEVjR3pXY29raG1kYUZxV3F3ZVFBTE1VaGhBODZUY2tPd2FvdUtrd2szQ3duSHQNCnFkV3J5WG1wdmtiSnhXUlZyWk9HZ1NmRysvUUlrdTBvTlk1WEtSYmRYVVJwMWRQc2hUNDkwSkxVekZiTStPY0YrK0h5TVo3MHFqL2sNCjY0ZjZpZHo1dzZUdkswVENLZS9NDQo9ajl6MA0KLS0tLS1FTkQgUEdQIFBVQkxJQyBLRVkgQkxPQ0stLS0tLQ0K", - "size": 3147 - }, - "ANGjdJ8iNjciNtU09awKCwED3qNYlqvsQFBKDYwSTRsaDQ_5-xbD0AjnVgB9qA2n323WQInPe_Bb4vTENJrJlfgftVeRN7kIUxtLb2TgWk3-cxLQ5OuAqiS932nGlZ31rkFhhuLs7YNwmS84--6Wxo2UVM3cTO5E0GOScpfoNCuO9W3oY9219sAd-RmQ_v6QRYfAeFzaA2HCetLT3K6rzf9GQ5BlIKBD92brbABKjV6gJiDCCm0m67lrUPyNfFsiLhlGrYOr2FK_FecuHf9N5J_xLPE0Z-Vaaea8BRr_2xaeyOHYw0rRfccVWEY4bSkj37qCu_gazJk0NLercRCJfdZxysV3KftFCtem0K-J_6dJwVEQEx9HaR_lFH5PQIg": { - "data": "LS0tLS1CRUdJTiBQR1AgU0lHTkFUVVJFLS0tLS0NCg0Kd3NENUJBQUJDQUFqRmlFRTNDWkZTdnR4MFk2cnV0YzlISDV0UEZWanFVRUZBbCtRb3RZRkF3QUFBQUFBQ2drUUhINXRQRlZqcVVGNA0KeGd2K01yZFEwN01mQ1ZVOTNwdFpnK1MrT09rUTFBY1p4R0ZkaWl2czEwS2tOR3RMbTlzK3cvaUVVQXlTU1didEtqYkxWNk8zQVl2Qw0KUUZLc0ZSRnIxN0VrejZtU1BqOTl6aWZGTUJ2VE9JQWV2L2QwOGRtWDBrR2Q2WWxQK0d5WkwzV3FjZ3kxVDFIM29iZ09tVG9EdGs3Ug0KVjUyS2kxYVRKWUgvWjd2NlBzUVJXbjhlbWZIL3lHWXBsQmh6WnkyWGpPNlVJYXI5VDh3dEFKT2Q2K0lpMnNmeUd5RVBqekdja0xhUg0KSlpPeFE0anBKSlVzenoyV3N2TE53dEtvcXdWMTVFZzNveFp6SFdZRThQNjN4WG9FNEc3NjI2MDRTSXF2L2dneVFaVHQvRXM2U2N1bg0KQTFCSmZsRm0rY0h6UVRXMnlRZndDQ3ZsekVaTmlOd1hmd0dmVjk5SzVpRzFlVzNsdjdzTUxKbml0d1RpZE5JbEQ1TFROZGVVblRYag0KWEp2a0VRc3lUVUk0cWJ6ekpiVU5ZejdscmFpekMyblBpd0Z6THY2OTJtUzB1cnREM21VaE9CQTloWndrM2wvMjBHc0dpYTBGZVVJUw0KRTFkOFZoL0V5N0lKOFRYYmZGcmR2NVpQM0hxTUswMDg5U29vWnd4L0dOMlFJYU9ZUVhzUzB1N0lGTmhVDQo9cTVTZg0KLS0tLS1FTkQgUEdQIFNJR05BVFVSRS0tLS0tDQo", - "size": 677 - } - }, - "raw": { - "id": "1754cfc37886899e", - "threadId": "1754cfc37886899e", - "labelIds": [ - "Label_6255111949068864933", - "IMPORTANT", - "Label_7", - "Label_9", - "CATEGORY_PERSONAL", - "Label_4", - "INBOX" - ], - "snippet": "1234", - "sizeEstimate": 13211, - "raw": "RGVsaXZlcmVkLVRvOiBmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20NClJlY2VpdmVkOiBieSAyMDAyOmFlZDoyNDZhOjA6MDowOjA6MCB3aXRoIFNNVFAgaWQgczM5Y3NwNTc0ODE3OXF0YzsNCiAgICAgICAgV2VkLCAyMSBPY3QgMjAyMCAxNDowNjozOSAtMDcwMCAoUERUKQ0KWC1SZWNlaXZlZDogYnkgMjAwMjphYTc6OTAwOTowOmIwMjk6MTU0OmY4MjI6MjdjOSB3aXRoIFNNVFAgaWQgbTktMjAwMjBhYTc5MDA5MDAwMGIwMjkwMTU0ZjgyMjI3YzltcjU0NDk0OTFwZm8uNDguMTYwMzMxNDM5ODkyMDsNCiAgICAgICAgV2VkLCAyMSBPY3QgMjAyMCAxNDowNjozOCAtMDcwMCAoUERUKQ0KQVJDLVNlYWw6IGk9MTsgYT1yc2Etc2hhMjU2OyB0PTE2MDMzMTQzOTg7IGN2PW5vbmU7DQogICAgICAgIGQ9Z29vZ2xlLmNvbTsgcz1hcmMtMjAxNjA4MTY7DQogICAgICAgIGI9aW9JRFpUa0RwN0RVVktWaTBrcDdpSjQ1bTZaTWxvMTI1SE5VdWJRSkhKWXNudXZXVVVteVBSYWIxd3M1eEhNSUpLDQogICAgICAgICBVc2xBMjducWY0aHhJY3hmNkNuem0xQVQxVVRlajd4d3BzcG14dXZlRXVxTEFKZ3hUYVZsRlBQaVozTTZQNnJSRForTQ0KICAgICAgICAgcVQrNG9hblhWYXZuSS82c3ZkMkJlbmt6WTI3UWhlU0RnTTdZYWlidmppaTk1eVpQWlhqbVBYNjN3TTNKZzV6ck1LZHANCiAgICAgICAgIHFyV2Z0bWlIMFhuSHpMWk0xMTNVNTJUUHpmS05iQ0t5VklhYk9qdy91VUk2OUFFSTd3clFVUTlXeW5vS1VkcE1SRXJODQogICAgICAgICBXOEF2ak1VYmFOT2RmTUVvWjBwSTJaY2VLejdid01TN3JHWXEzeUU4NGY4MnhqcG5wZTJ0TmZWL3VPNUxsejM4bnIvaA0KICAgICAgICAgaFhBdz09DQpBUkMtTWVzc2FnZS1TaWduYXR1cmU6IGk9MTsgYT1yc2Etc2hhMjU2OyBjPXJlbGF4ZWQvcmVsYXhlZDsgZD1nb29nbGUuY29tOyBzPWFyYy0yMDE2MDgxNjsNCiAgICAgICAgaD1taW1lLXZlcnNpb246dXNlci1hZ2VudDpkYXRlOm1lc3NhZ2UtaWQ6c3ViamVjdDphdXRvY3J5cHQ6ZnJvbTp0bw0KICAgICAgICAgOmRraW0tc2lnbmF0dXJlOw0KICAgICAgICBiaD1DT3A2TkVud2lNVytSM2dQd3EwbVVXSTFQdm5PUzY4ZVNFckxheStRU2ZVPTsNCiAgICAgICAgYj1DQVNWUmZ6bm1VOWZFL1JydUhNdGhUeWx1T2s0NGgxTmFMU1gwRGh5REFWSVB1ZkEycjBuK1N6NEJBb2t5K0FzSXkNCiAgICAgICAgIGhudTQzQisyWFVXY1N4L1V1YkNrR09FdkRVTnF5NDBhTTFQVnUzc3N2NDdTa014WFRzeUlLWlFWNkZ3WnYybCtsYno2DQogICAgICAgICAwcVdWOGJDeVJmYVpuQVdheHVSS3cvMlQ5WCtPbUFCTS8veDBKNDJKOWVkTlZvZnN5QitLSU9GVlo1WGhvM3MyckJrUg0KICAgICAgICAgVU1WZ3UyeFNNZzVkNWkzV1FPOFUzR2d3TlRmRC90eVpYczRlelRkbnFoMG5KNjJyNnF6TzJPWWdXTzdWNFplVGc1UnoNCiAgICAgICAgIGNSV2cxN0pnY0h5V3pRWHhoUHdST1Q2dHpDYS9naVRya1ZIeXRmOFNPa1JJNDNUbGNoandQeU5EVlEwcTFhUENncTVuDQogICAgICAgICAwcGxnPT0NCkFSQy1BdXRoZW50aWNhdGlvbi1SZXN1bHRzOiBpPTE7IG14Lmdvb2dsZS5jb207DQogICAgICAgZGtpbT1wYXNzIGhlYWRlci5pPUB2ZXJkb25jb2xsZWdlLXNjaG9vbC1uei4yMDE1MDYyMy5nYXBwc3NtdHAuY29tIGhlYWRlci5zPTIwMTUwNjIzIGhlYWRlci5iPWxUZ0J3K29ROw0KICAgICAgIHNwZj1wYXNzIChnb29nbGUuY29tOiBkb21haW4gb2YgZGhhcnRsZXlAdmVyZG9uY29sbGVnZS5zY2hvb2wubnogZGVzaWduYXRlcyAyMDkuODUuMjIwLjQxIGFzIHBlcm1pdHRlZCBzZW5kZXIpIHNtdHAubWFpbGZyb209ZGhhcnRsZXlAdmVyZG9uY29sbGVnZS5zY2hvb2wubnoNClJldHVybi1QYXRoOiA8ZGhhcnRsZXlAdmVyZG9uY29sbGVnZS5zY2hvb2wubno-DQpSZWNlaXZlZDogZnJvbSBtYWlsLXNvci1mNDEuZ29vZ2xlLmNvbSAobWFpbC1zb3ItZjQxLmdvb2dsZS5jb20uIFsyMDkuODUuMjIwLjQxXSkNCiAgICAgICAgYnkgbXguZ29vZ2xlLmNvbSB3aXRoIFNNVFBTIGlkIDIwc29yMTkzMzc5OXBsby43MC4yMDIwLjEwLjIxLjE0LjA2LjM4DQogICAgICAgIGZvciA8Zmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tPg0KICAgICAgICAoR29vZ2xlIFRyYW5zcG9ydCBTZWN1cml0eSk7DQogICAgICAgIFdlZCwgMjEgT2N0IDIwMjAgMTQ6MDY6MzggLTA3MDAgKFBEVCkNClJlY2VpdmVkLVNQRjogcGFzcyAoZ29vZ2xlLmNvbTogZG9tYWluIG9mIGRoYXJ0bGV5QHZlcmRvbmNvbGxlZ2Uuc2Nob29sLm56IGRlc2lnbmF0ZXMgMjA5Ljg1LjIyMC40MSBhcyBwZXJtaXR0ZWQgc2VuZGVyKSBjbGllbnQtaXA9MjA5Ljg1LjIyMC40MTsNCkF1dGhlbnRpY2F0aW9uLVJlc3VsdHM6IG14Lmdvb2dsZS5jb207DQogICAgICAgZGtpbT1wYXNzIGhlYWRlci5pPUB2ZXJkb25jb2xsZWdlLXNjaG9vbC1uei4yMDE1MDYyMy5nYXBwc3NtdHAuY29tIGhlYWRlci5zPTIwMTUwNjIzIGhlYWRlci5iPWxUZ0J3K29ROw0KICAgICAgIHNwZj1wYXNzIChnb29nbGUuY29tOiBkb21haW4gb2YgZGhhcnRsZXlAdmVyZG9uY29sbGVnZS5zY2hvb2wubnogZGVzaWduYXRlcyAyMDkuODUuMjIwLjQxIGFzIHBlcm1pdHRlZCBzZW5kZXIpIHNtdHAubWFpbGZyb209ZGhhcnRsZXlAdmVyZG9uY29sbGVnZS5zY2hvb2wubnoNCkRLSU0tU2lnbmF0dXJlOiB2PTE7IGE9cnNhLXNoYTI1NjsgYz1yZWxheGVkL3JlbGF4ZWQ7DQogICAgICAgIGQ9dmVyZG9uY29sbGVnZS1zY2hvb2wtbnouMjAxNTA2MjMuZ2FwcHNzbXRwLmNvbTsgcz0yMDE1MDYyMzsNCiAgICAgICAgaD10bzpmcm9tOmF1dG9jcnlwdDpzdWJqZWN0Om1lc3NhZ2UtaWQ6ZGF0ZTp1c2VyLWFnZW50Om1pbWUtdmVyc2lvbjsNCiAgICAgICAgYmg9Q09wNk5FbndpTVcrUjNnUHdxMG1VV0kxUHZuT1M2OGVTRXJMYXkrUVNmVT07DQogICAgICAgIGI9bFRnQncrb1FMOFRibGdBTFI4aEg3eTlmVERZYUU3QmhJcHZPOGFUQlErTmFMWURvUktUeEYzWW5sYWNJaXpXSmVuDQogICAgICAgICA5bGhaRlZKR255ZHhLMlZMVzZjV25RWlFjMUtrWlZibXFmKy8yanlTRFdYdEVZYTZIdUdJeCtSN0lqT0JVNnBOaUVGbA0KICAgICAgICAgYlBQeDZzRFdKZG5tVW9uTkpreklsZFIvWFc2VUtCbVJEM0d3ajcrdWN6NFZoZzJWVWMwdHhkSEU2VFZlL2w4MFAwa0cNCiAgICAgICAgIGkwSzEzYVQ3OWFxT2FRbS9oR21KYkJCMm5PRC93cmxZMzlYUUQrTG40VlczV3lOa1Y2Q2dubFYvY01tbUwvakZ4WVpxDQogICAgICAgICAvZGNlcUxsU2l6R01aQ0NuSzJIZnNBcFY0TTRWUHFGT2g4aURJQ0EyVDFZOVFWNm5rWTR2MTVjYU1mNnBDUCtjTjhYNA0KICAgICAgICAgeS90Zz09DQpYLUdvb2dsZS1ES0lNLVNpZ25hdHVyZTogdj0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOw0KICAgICAgICBkPTFlMTAwLm5ldDsgcz0yMDE2MTAyNTsNCiAgICAgICAgaD14LWdtLW1lc3NhZ2Utc3RhdGU6dG86ZnJvbTphdXRvY3J5cHQ6c3ViamVjdDptZXNzYWdlLWlkOmRhdGUNCiAgICAgICAgIDp1c2VyLWFnZW50Om1pbWUtdmVyc2lvbjsNCiAgICAgICAgYmg9Q09wNk5FbndpTVcrUjNnUHdxMG1VV0kxUHZuT1M2OGVTRXJMYXkrUVNmVT07DQogICAgICAgIGI9bnl5Z3RzMktOcTBlQndMM0dvUDUwMzVDc1ZxN2xRSVd6UWJsQit4NElxbWxRZVdWd3RHWmpoby9BMUhWQ3NOeDNjDQogICAgICAgICBuRWlxQXJtNzhPSUN6d2RpOEdNZkI1SXlkWVpOK3ZqZHVMelV0WlNmR0JJanV3eFFPdEFYQnZxNnBIV0N0WmdrbkRTSQ0KICAgICAgICAgZXFReDN1QW4za25ZQnFMcVpaUWx1b0g1dm5BL0tlYjRQczFBY3hkN2NBRGw5b1N1ZlJvTEtPcHRndDVVdC8zakhGQTMNCiAgICAgICAgIHprbmk2Tm9xdUlsblN6emVXc2NKTEJHeGI4VENhZU81b3JzT21CbUw4blVlNkF3ZGpNbWhxSmdnQnhEdkM1RzFFRGc5DQogICAgICAgICBFK3c3UFJJaE5sVVd1K1JMUG0rMlhaNTVpOHptN21xSGFjNXhzYjdKN3pWVUZEaVoxSWp3YVVvcFZ6ekxQRS9wWGRYaA0KICAgICAgICAgMGZHdz09DQpYLUdtLU1lc3NhZ2UtU3RhdGU6IEFPQU01MzFkZzd0MzNnRmpBRDc0QlVqdGp6VE5SZnJHN3VlSUlLUVg2VG0wanBUMUZvcGlIb3RlDQoJcCtwcnFDRlMycEh5RjlQclNycldjTHlPcnQvZmhiZEZFQzNNRUNUZzNmeTVuOTVOdFNTSDRwcmIxYWhTUkU5UXRMMWQ1V25tRTVIDQoJc281cHljZU1qY2tOT3VMcy9EbGdYN2RxNjNBOVIrRFlla1FWSmZBaDNFaFRKdUEvTTNQeFFlWmZCSDBmcityWkhVbGh6S2EzOFNODQoJWG1SMktnWE5qMzRzTDNiMmxISThKWjd0c3ANClgtR29vZ2xlLVNtdHAtU291cmNlOiBBQmRoUEp6UTRHSHJkcGQxVjZuTVdMeVc4NlN4Nk5WcXFsQ00zSkdOd3k4ZGpRWjB6eEJrT0xLOWt4N2pRbEdISnNzZEZaYWdzY2ZmcXc9PQ0KWC1SZWNlaXZlZDogYnkgMjAwMjphMTc6OTAyOjQyMzpiMDI5OmQ1OmY5Njc6ODZmZSB3aXRoIFNNVFAgaWQgMzItMjAwMjBhMTcwOTAyMDQyM2IwMjkwMGQ1Zjk2Nzg2ZmVtcjUzOTM0NzZwbGUuNTUuMTYwMzMxNDM5NzgwNDsNCiAgICAgICAgV2VkLCAyMSBPY3QgMjAyMCAxNDowNjozNyAtMDcwMCAoUERUKQ0KUmV0dXJuLVBhdGg6IDxkaGFydGxleUB2ZXJkb25jb2xsZWdlLnNjaG9vbC5uej4NClJlY2VpdmVkOiBmcm9tIFsxMC4wLjYuOV0gKFsyMTAuNTUuNzkuMTNdKQ0KICAgICAgICBieSBzbXRwLmdtYWlsLmNvbSB3aXRoIEVTTVRQU0EgaWQgaTEyNnNtMzA2OTY1OXBmYy40OC4yMDIwLjEwLjIxLjE0LjA2LjM0DQogICAgICAgIGZvciA8Zmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tPg0KICAgICAgICAodmVyc2lvbj1UTFMxXzMgY2lwaGVyPVRMU19BRVNfMTI4X0dDTV9TSEEyNTYgYml0cz0xMjgvMTI4KTsNCiAgICAgICAgV2VkLCAyMSBPY3QgMjAyMCAxNDowNjozNyAtMDcwMCAoUERUKQ0KVG86IGZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbQ0KRnJvbTogRGF2ZSBIYXJ0bGV5IDxkaGFydGxleUB2ZXJkb25jb2xsZWdlLnNjaG9vbC5uej4NCkF1dG9jcnlwdDogYWRkcj1kaGFydGxleUB2ZXJkb25jb2xsZWdlLnNjaG9vbC5uejsga2V5ZGF0YT0NCiB4c0ROQkYxd1B1UUJEQUNoSTJEQWg2Sy9ibXdINWhmQ0p3a3hld0ZVZldhQXh6QUlXeVhlL3c1d2pGSkJwdTc0TUp2cU8rOFQNCiBXa3lWTVZYUXFPbTVkSDM2WWtNQ1ZjTnFvR2dDMG1wL0paVW1WSWRQcUtIMVFZSzk4OHJSS1AySElsM0NOZ2J5aUhManlLVmYNCiArUlc0T0t4ZVFMTTFNSnBRLzM5VjV5bVdmdFBzenoxaitMTzJGS3BmQXNVYXBBSW1aTndoWFNLWmNyeHZVRW5vYXlRZzE3SEENCiA5VGhYYWRRL1VVOEZEbGliS1FvQlV2aGZ5aWc3R2Y0cy9PbDNwNzZhNTdNL1pDbGdTNWhQb003N29QYUM4bjhRVllBL3Q1QnUNCiBZU2QxbjNDRTY3RVU2SXpKcjNBODNjMzRFWk8vd0h1Nmd3Q0NsbG91V050VTdmRHZLSkViY2diVGZiMG83cW8yMjRsQVV3RHENCiBWQjJ6Q1ZRVEVLWFYyMGgvcXZ1ak5uTHhvMkRqY0JnK0ZiZDdHclpFRXFLNStwc2MzVTNsakQ5bVRvSm9NNGxHN0l4eTFFdjINCiBTeDh4ZlVGdEw5UElrdGw4T3dBNlUvMVhadmF1b0g1dXBCYWluOGJjKzBhaThheFhMMi9PMHJBS1gyRGxQZkhEOUxPaW03NFANCiBOcWxpTFV0VHI0VGxCeU1aNUl1WUNqTUFFUUVBQWMwdlJHRjJaU0JJWVhKMGJHVjVJRHhrYUdGeWRHeGxlVUIyWlhKa2IyNWoNCiBiMnhzWldkbExuTmphRzl2YkM1dWVqN0N3UlFFRXdFSUFENENHd01GQ3drSUJ3SUdGUW9KQ0FzQ0JCWUNBd0VDSGdFQ0Y0QVcNCiBJUVRjSmtWSyszSFJqcXU2MXowY2ZtMDhWV09wUVFVQ1g0ZUN2QVVKQmRtcTJBQUtDUkFjZm0wOFZXT3BRY1JpQy9zRmhkWmgNCiBGQjVnUWJIY3M4RjkrYVBpOGdmd3UzYXRVRndhZE42QVNoU2E2SzNwbmQ3VjF4WWxERndSUEtyQUVXQkJZZVg5ZHNBV2w5Z3gNCiBQTnVDbzV1dFJrZ1BSOFpTTzRjZVhSNFpnS1R2RjNrNlV6ekd4OEhGQytNSkN2ZGlUVFBuTWF5VU9PWC9mNCs3OXVXdCtmUksNCiBTQnRja3d1ZDcrRHQvODh1eDhqOFRwdkJaTWhjaUo4WndyOW91VldYZmdnMi9STXpaTlRvcGhRSEtkVU9oUFEvREJ6VmFMeFYNCiBmM1FFcUtQczNsaHJTMDhaT01wdWJWOTM4S2RDTGdxZXFlYUNsRXRVUlVaRXMvNUl4ZzdsYitTYmJPbzEySy9sUm5ZdTRVMXANCiBXY0hWdWVjY3FrVHhEWnhnQ0RCdHFONHNCTW5tS3RTdVlBY2J5eTRTeVdHQTZaMEtJUDg3RnhNaTVZZiswNFIraEVHOUtXRDMNCiBmdWsxSklJVWVOVm9pQW40YUVSanJJVU41cTROSkF5WTBkNktYbXB5SnhPSzJEajNaeVFnbE5ydlhpdGNoWEpKcU1mZEdleEINCiAwM2x6RFl4eUhTbHUrV0JwMmV4YVNPeVFJS1NuaVExQmhBSHlwNFZRQmtMS0tWQUVWK2ZWZVRkRnhhcnhWSDVIczh0UDdZYWcNCiBKU1RPd00wRVhYQSs1UUVNQU9vbys0SUJmamFpWmpFZTRhek1GRHJWd3ZWa2VFdzNzb01yQmpBT2pseVNRQ3BFL3EwcURJZDUNCiBrMGVTQUFodHRhbnMzVXMxNjZ4S1cyUU1mZDJvakZCVExpMDZacGhhMWpEbzVBajJrV0RFR1Q1eSswSEJSVlkwTm1QdHZCQTENCiBpZ1JmRjFaU1VzcUloTy9kU3FpcmRuU2gwOHo1YlpuRUV5SVdPYkJoQzhsZUUyQkFkWFdnRzFXazAvMDJEa3JBOU4xNmxaMS8NCiB3Vkd0WVUwOXJuWkJKcGVET0ZNMGRDaDd1aGZBNUxHem14S3puMjUvckRTRlVyVWNtWWZhd0FuMTNPUzAyQ09JbHN0bDZsVWcNCiBWSjZ4dEJ4T2FUNE5Ma2E0aGtxeXpwYW1DRjZva2VaUUk2dEhFbWxtNUZYdWhDYi9Mei9uOWs5UGs3OTNJb3IwUFAreE1QTzgNCiBnYldsRnZQZmwvTVJLYzNIeTJTSVp3OVJRN0EvMzNWL296WFZ6ZzFBNGRyclhLa0tlYzhKem5OVGtvUkk2ZlN4L1RHVnBOaDENCiArNVNPSEZHZ0t0WitSQTFCRDF2bXNNei81eG1kb1dHaXNvUjAvaEV0ZGp1ZHhwSzFabTByMW52TnBGWm1LUlpCMjZJbWZNQzQNCiBrYlJWWFhYNUx4RTZ5SXlKM3VyRm4yKzZQd0FSQVFBQndzRDhCQmdCQ0FBbUFoc01GaUVFM0NaRlN2dHgwWTZydXRjOUhINXQNCiBQRlZqcVVFRkFsK0hncjBGQ1FYWnF0Z0FDZ2tRSEg1dFBGVmpxVUVuRXd2OUVhK3N5dStVNzJTZU5jM0JRM0dtNmxsQVJ5UW0NCiA3Z3oyR1pCYWsvZXB6RCszVEZOS0ZMTGg5QXlCT00rd1dScEdmS2gxWCtzOUdMZFNRSVNFQTA2YnNXNGQxS2ZIcUVnMEhUUncNCiA0cDlTUndPV0h2eTFMNm1hQTFWSFJRQ1cwL0ZOLzdrWURyVUgzYmlnSVNUa0pKcG1xOGo0WU9INjZIVkViRnFCTzMxeWNZVUcNCiBHdC9CYXpoZTdyT2hBeFp1R1l6Y1ZKUWRTRTAyMHZqYnFNNWwzNEhmaUR1YnVVTlJoSE1JYVdhNUlZWUl5cTMzU1RiWnU1RmwNCiBuLzV6b0dOOCtBZTR3Tk56emZPeVdDS1V6cXliZ2JEeFBXQ25YMnRnREVEbEN0MFg1WDNQY0l0VmVVcFFDZWNEUnV0NGVHRXoNCiBPa2tkTm8yRU05VXBCakdmZWkrR0Q4dnJ6OWFweHZRK25LWUlPaTRGTThHa1hyc2FnN2d2SWRFY0d6V2Nva2htZGFGcVdxd2UNCiBRQUxNVWhoQTg2VGNrT3dhb3VLa3drM0N3bkh0cWRXcnlYbXB2a2JKeFdSVnJaT0dnU2ZHKy9RSWt1MG9OWTVYS1JiZFhVUnANCiAxZFBzaFQ0OTBKTFV6RmJNK09jRisrSHlNWjcwcWovazY0ZjZpZHo1dzZUdkswVENLZS9NDQpTdWJqZWN0OiBzaWcgZnJvbSBuZXcgVGh1bmRlcmJpcmQgdjc4IG5vdCByZWNvZ25pemVkLCBwbGFpbg0KTWVzc2FnZS1JRDogPDI3ZjkyM2I4LTBjZWMtM2ExNC1kYjU0LWQ2YTVjM2I5MjJkOEB2ZXJkb25jb2xsZWdlLnNjaG9vbC5uej4NCkRhdGU6IFRodSwgMjIgT2N0IDIwMjAgMTA6MDY6MjkgKzEzMDANClVzZXItQWdlbnQ6IE1vemlsbGEvNS4wIChYMTE7IExpbnV4IHg4Nl82NDsgcnY6NzguMCkgR2Vja28vMjAxMDAxMDENCiBUaHVuZGVyYmlyZC83OC4zLjMNCk1JTUUtVmVyc2lvbjogMS4wDQpDb250ZW50LVR5cGU6IG11bHRpcGFydC9zaWduZWQ7IG1pY2FsZz1wZ3Atc2hhMjU2Ow0KIHByb3RvY29sPSJhcHBsaWNhdGlvbi9wZ3Atc2lnbmF0dXJlIjsNCiBib3VuZGFyeT0ielVoODBlbDczSGRnSE9vRFZuSnNUdFFaREk5YkVMc3dlIg0KDQpUaGlzIGlzIGFuIE9wZW5QR1AvTUlNRSBzaWduZWQgbWVzc2FnZSAoUkZDIDQ4ODAgYW5kIDMxNTYpDQotLXpVaDgwZWw3M0hkZ0hPb0RWbkpzVHRRWkRJOWJFTHN3ZQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7IGJvdW5kYXJ5PSJYV3dudXNDNG54aGsyTFJ2TENDNlNrY2I4WWlLUTRMdTAiOw0KIHByb3RlY3RlZC1oZWFkZXJzPSJ2MSINCkZyb206IERhdmUgSGFydGxleSA8ZGhhcnRsZXlAdmVyZG9uY29sbGVnZS5zY2hvb2wubno-DQpUbzogZmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tDQpNZXNzYWdlLUlEOiA8MjdmOTIzYjgtMGNlYy0zYTE0LWRiNTQtZDZhNWMzYjkyMmQ4QHZlcmRvbmNvbGxlZ2Uuc2Nob29sLm56Pg0KU3ViamVjdDogc2lnIGZyb20gbmV3IFRodW5kZXJiaXJkIHY3OCBub3QgcmVjb2duaXplZCwgcGxhaW4NCg0KLS1YV3dudXNDNG54aGsyTFJ2TENDNlNrY2I4WWlLUTRMdTANCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOw0KIGJvdW5kYXJ5PSItLS0tLS0tLS0tLS04RDlENTQ3RTgwNENDOEE0OUJGNzBFNTQiDQpDb250ZW50LUxhbmd1YWdlOiBlbi1OWg0KDQpUaGlzIGlzIGEgbXVsdGktcGFydCBtZXNzYWdlIGluIE1JTUUgZm9ybWF0Lg0KLS0tLS0tLS0tLS0tLS04RDlENTQ3RTgwNENDOEE0OUJGNzBFNTQNCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD11dGYtOA0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogcXVvdGVkLXByaW50YWJsZQ0KDQoxMjM0DQoNCi0tLS0tLS0tLS0tLS0tOEQ5RDU0N0U4MDRDQzhBNDlCRjcwRTU0DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL3BncC1rZXlzOw0KIG5hbWU9Ik9wZW5QR1BfMHgxQzdFNkQzQzU1NjNBOTQxLmFzYyINCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUNCkNvbnRlbnQtRGlzcG9zaXRpb246IGF0dGFjaG1lbnQ7DQogZmlsZW5hbWU9Ik9wZW5QR1BfMHgxQzdFNkQzQzU1NjNBOTQxLmFzYyINCg0KLS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tDQoNCnhzRE5CRjF3UHVRQkRBQ2hJMkRBaDZLL2Jtd0g1aGZDSndreGV3RlVmV2FBeHpBSVd5WGUvdzV3akZKQnB1NzRNSnZxTys4VFc9DQpreVYNCk1WWFFxT201ZEgzNllrTUNWY05xb0dnQzBtcC9KWlVtVklkUHFLSDFRWUs5ODhyUktQMkhJbDNDTmdieWlITGp5S1ZmK1JXNE89DQpLeGUNClFMTTFNSnBRLzM5VjV5bVdmdFBzenoxaitMTzJGS3BmQXNVYXBBSW1aTndoWFNLWmNyeHZVRW5vYXlRZzE3SEE5VGhYYWRRL1U9DQpVOEYNCkRsaWJLUW9CVXZoZnlpZzdHZjRzL09sM3A3NmE1N00vWkNsZ1M1aFBvTTc3b1BhQzhuOFFWWUEvdDVCdVlTZDFuM0NFNjdFVTY9DQpJekoNCnIzQTgzYzM0RVpPL3dIdTZnd0NDbGxvdVdOdFU3ZkR2S0pFYmNnYlRmYjBvN3FvMjI0bEFVd0RxVkIyekNWUVRFS1hWMjBoL3E9DQp2dWoNCk5uTHhvMkRqY0JnK0ZiZDdHclpFRXFLNStwc2MzVTNsakQ5bVRvSm9NNGxHN0l4eTFFdjJTeDh4ZlVGdEw5UElrdGw4T3dBNlU9DQovMVgNClp2YXVvSDV1cEJhaW44YmMrMGFpOGF4WEwyL08wckFLWDJEbFBmSEQ5TE9pbTc0UE5xbGlMVXRUcjRUbEJ5TVo1SXVZQ2pNQUU9DQpRRUENCkFjMHZSR0YyWlNCSVlYSjBiR1Y1SUR4a2FHRnlkR3hsZVVCMlpYSmtiMjVqYjJ4c1pXZGxMbk5qYUc5dmJDNXVlajdDd1JRRUU9DQp3RUkNCkFENENHd01GQ3drSUJ3SUdGUW9KQ0FzQ0JCWUNBd0VDSGdFQ0Y0QVdJUVRjSmtWSyszSFJqcXU2MXowY2ZtMDhWV09wUVFVQ1g9DQo0ZUMNCnZBVUpCZG1xMkFBS0NSQWNmbTA4VldPcFFjUmlDL3NGaGRaaEZCNWdRYkhjczhGOSthUGk4Z2Z3dTNhdFVGd2FkTjZBU2hTYTY9DQpLM3ANCm5kN1YxeFlsREZ3UlBLckFFV0JCWWVYOWRzQVdsOWd4UE51Q281dXRSa2dQUjhaU080Y2VYUjRaZ0tUdkYzazZVenpHeDhIRkM9DQorTUoNCkN2ZGlUVFBuTWF5VU9PWC9mNCs3OXVXdCtmUktTQnRja3d1ZDcrRHQvODh1eDhqOFRwdkJaTWhjaUo4WndyOW91VldYZmdnMi89DQpSTXoNClpOVG9waFFIS2RVT2hQUS9EQnpWYUx4VmYzUUVxS1BzM2xoclMwOFpPTXB1YlY5MzhLZENMZ3FlcWVhQ2xFdFVSVVpFcy81SXg9DQpnN2wNCmIrU2JiT28xMksvbFJuWXU0VTFwV2NIVnVlY2Nxa1R4RFp4Z0NEQnRxTjRzQk1ubUt0U3VZQWNieXk0U3lXR0E2WjBLSVA4N0Y9DQp4TWkNCjVZZiswNFIraEVHOUtXRDNmdWsxSklJVWVOVm9pQW40YUVSanJJVU41cTROSkF5WTBkNktYbXB5SnhPSzJEajNaeVFnbE5ydlg9DQppdGMNCmhYSkpxTWZkR2V4QjAzbHpEWXh5SFNsdStXQnAyZXhhU095UUlLU25pUTFCaEFIeXA0VlFCa0xLS1ZBRVYrZlZlVGRGeGFyeFY9DQpINUgNCnM4dFA3WWFnSlNUQ3dSUUVFd0VJQUQ0V0lRVGNKa1ZLKzNIUmpxdTYxejBjZm0wOFZXT3BRUVVDWFhBKzVRSWJBd1VKQWVFemc9DQpBVUwNCkNRZ0hBZ1lWQ2drSUN3SUVGZ0lEQVFJZUFRSVhnQUFLQ1JBY2ZtMDhWV09wUVEzMkMvNHAzaHNqZlRlQXpRUjBhSG9XR0tUUmM9DQpGb1kNCm5jOUlkWWpPNzhmbmFmZWhKSXQ1TExvTGZkK01TR1B6Njc4NWhtTmdyeWo2WHRFMWVJTU42cHZrSy9CeTZON1Y2T04xOWp6S3g9DQp2QXENCi8zZi9yUWNpZVRKbDV0dHRRUkdXc3VGWCtEZHYraXdmbEcrOVpTTTNFOWxaTDJwbzNFdzZLaG1pd0graSsrQ1JGN3V0UlJyUGo9DQorRHINCm1NQmdYajB5WXloUm1oZVVta1BqN282S2ZNK2NuL3JRUFZTSVdXSHJCSW1BbEVna05sOWZJOS9TTWhvZ052NlpzbUtYN2FlTko9DQo5eHgNClhlNnFBVzI3NnJqZEsvM2VoMHJieHdja0lZMkJaWVJlTzczZkxmRnpYckYxa0gwZ0kvVnErQWFhaDkvYzU2WkJNK1ZTRVBhK3o9DQpveFANCmR5eWMzenVsN1ExNlZMV0FnblUzUElCeWNFcS9naWxLQnpSM0dHTDJWakxValU0d0F4UTdKd1djMDdvbkttL1dTUnJmY2lnNEc9DQpDbHgNClBNb3BPS1FwNWd4MEdPSUVDTWFxRHFvV3pUQWpLK1BTR0pLZUx1eTNydGhyN09rS3JIc2ZhV1BQZ0RwZzVOSlM0MG52VVF4Z3o9DQpwQnINCmFRZ2pTS0FjU1NkVlo4bm5WVnozQkdhZm1aT0FEL3pPd00wRVhYQSs1UUVNQU9vbys0SUJmamFpWmpFZTRhek1GRHJWd3ZWa2U9DQpFdzMNCnNvTXJCakFPamx5U1FDcEUvcTBxRElkNWswZVNBQWh0dGFuczNVczE2NnhLVzJRTWZkMm9qRkJUTGkwNlpwaGExakRvNUFqMms9DQpXREUNCkdUNXkrMEhCUlZZME5tUHR2QkExaWdSZkYxWlNVc3FJaE8vZFNxaXJkblNoMDh6NWJabkVFeUlXT2JCaEM4bGVFMkJBZFhXZ0c9DQoxV2sNCjAvMDJEa3JBOU4xNmxaMS93Vkd0WVUwOXJuWkJKcGVET0ZNMGRDaDd1aGZBNUxHem14S3puMjUvckRTRlVyVWNtWWZhd0FuMTM9DQpPUzANCjJDT0lsc3RsNmxVZ1ZKNnh0QnhPYVQ0TkxrYTRoa3F5enBhbUNGNm9rZVpRSTZ0SEVtbG01Rlh1aENiL0x6L245azlQazc5M0k9DQpvcjANClBQK3hNUE84Z2JXbEZ2UGZsL01SS2MzSHkyU0ladzlSUTdBLzMzVi9velhWemcxQTRkcnJYS2tLZWM4SnpuTlRrb1JJNmZTeC89DQpUR1YNCnBOaDErNVNPSEZHZ0t0WitSQTFCRDF2bXNNei81eG1kb1dHaXNvUjAvaEV0ZGp1ZHhwSzFabTByMW52TnBGWm1LUlpCMjZJbWY9DQpNQzQNCmtiUlZYWFg1THhFNnlJeUozdXJGbjIrNlB3QVJBUUFCd3NEOEJCZ0JDQUFtQWhzTUZpRUUzQ1pGU3Z0eDBZNnJ1dGM5SEg1dFA9DQpGVmoNCnFVRUZBbCtIZ3IwRkNRWFpxdGdBQ2drUUhINXRQRlZqcVVFbkV3djlFYStzeXUrVTcyU2VOYzNCUTNHbTZsbEFSeVFtN2d6Mkc9DQpaQmENCmsvZXB6RCszVEZOS0ZMTGg5QXlCT00rd1dScEdmS2gxWCtzOUdMZFNRSVNFQTA2YnNXNGQxS2ZIcUVnMEhUUnc0cDlTUndPV0g9DQp2eTENCkw2bWFBMVZIUlFDVzAvRk4vN2tZRHJVSDNiaWdJU1RrSkpwbXE4ajRZT0g2NkhWRWJGcUJPMzF5Y1lVR0d0L0JhemhlN3JPaEE9DQp4WnUNCkdZemNWSlFkU0UwMjB2amJxTTVsMzRIZmlEdWJ1VU5SaEhNSWFXYTVJWVlJeXEzM1NUYlp1NUZsbi81em9HTjgrQWU0d05Oeno9DQpmT3kNCldDS1V6cXliZ2JEeFBXQ25YMnRnREVEbEN0MFg1WDNQY0l0VmVVcFFDZWNEUnV0NGVHRXpPa2tkTm8yRU05VXBCakdmZWkrR0Q9DQo4dnINCno5YXB4dlErbktZSU9pNEZNOEdrWHJzYWc3Z3ZJZEVjR3pXY29raG1kYUZxV3F3ZVFBTE1VaGhBODZUY2tPd2FvdUtrd2szQ3c9DQpuSHQNCnFkV3J5WG1wdmtiSnhXUlZyWk9HZ1NmRysvUUlrdTBvTlk1WEtSYmRYVVJwMWRQc2hUNDkwSkxVekZiTStPY0YrK0h5TVo3MHE9DQpqL2sNCjY0ZjZpZHo1dzZUdkswVENLZS9NDQo9M0RqOXowDQotLS0tLUVORCBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tDQoNCi0tLS0tLS0tLS0tLS0tOEQ5RDU0N0U4MDRDQzhBNDlCRjcwRTU0LS0NCg0KLS1YV3dudXNDNG54aGsyTFJ2TENDNlNrY2I4WWlLUTRMdTAtLQ0KDQotLXpVaDgwZWw3M0hkZ0hPb0RWbkpzVHRRWkRJOWJFTHN3ZQ0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wZ3Atc2lnbmF0dXJlOyBuYW1lPSJPcGVuUEdQX3NpZ25hdHVyZS5hc2MiDQpDb250ZW50LURlc2NyaXB0aW9uOiBPcGVuUEdQIGRpZ2l0YWwgc2lnbmF0dXJlDQpDb250ZW50LURpc3Bvc2l0aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT0iT3BlblBHUF9zaWduYXR1cmUiDQoNCi0tLS0tQkVHSU4gUEdQIFNJR05BVFVSRS0tLS0tDQoNCndzRDVCQUFCQ0FBakZpRUUzQ1pGU3Z0eDBZNnJ1dGM5SEg1dFBGVmpxVUVGQWwrUW90WUZBd0FBQUFBQUNna1FISDV0UEZWanFVRjQNCnhnditNcmRRMDdNZkNWVTkzcHRaZytTK09Pa1ExQWNaeEdGZGlpdnMxMEtrTkd0TG05cyt3L2lFVUF5U1NXYnRLamJMVjZPM0FZdkMNClFGS3NGUkZyMTdFa3o2bVNQajk5emlmRk1CdlRPSUFldi9kMDhkbVgwa0dkNllsUCtHeVpMM1dxY2d5MVQxSDNvYmdPbVRvRHRrN1INClY1MktpMWFUSllIL1o3djZQc1FSV244ZW1mSC95R1lwbEJoelp5MlhqTzZVSWFyOVQ4d3RBSk9kNitJaTJzZnlHeUVQanpHY2tMYVINCkpaT3hRNGpwSkpVc3p6MldzdkxOd3RLb3F3VjE1RWczb3haekhXWUU4UDYzeFhvRTRHNzYyNjA0U0lxdi9nZ3lRWlR0L0VzNlNjdW4NCkExQkpmbEZtK2NIelFUVzJ5UWZ3Q0N2bHpFWk5pTndYZndHZlY5OUs1aUcxZVczbHY3c01MSm5pdHdUaWROSWxENUxUTmRlVW5UWGoNClhKdmtFUXN5VFVJNHFienpKYlVOWXo3bHJhaXpDMm5QaXdGekx2NjkybVMwdXJ0RDNtVWhPQkE5aFp3azNsLzIwR3NHaWEwRmVVSVMNCkUxZDhWaC9FeTdJSjhUWGJmRnJkdjVaUDNIcU1LMDA4OVNvb1p3eC9HTjJRSWFPWVFYc1MwdTdJRk5oVQ0KPXE1U2YNCi0tLS0tRU5EIFBHUCBTSUdOQVRVUkUtLS0tLQ0KDQotLXpVaDgwZWw3M0hkZ0hPb0RWbkpzVHRRWkRJOWJFTHN3ZS0tDQo=", - "historyId": "1206346", - "internalDate": "1603314389000" - } -} \ No newline at end of file diff --git a/test/source/mock/google/exported-messages/message-export-1754cfd1b2f1d6e5.json b/test/source/mock/google/exported-messages/message-export-1754cfd1b2f1d6e5.json deleted file mode 100644 index 37d81e235a4..00000000000 --- a/test/source/mock/google/exported-messages/message-export-1754cfd1b2f1d6e5.json +++ /dev/null @@ -1,211 +0,0 @@ -{ - "acctEmail": "flowcrypt.compatibility@gmail.com", - "full": { - "id": "1754cfd1b2f1d6e5", - "threadId": "1754cfd1b2f1d6e5", - "labelIds": [ - "Label_6255111949068864933", - "Label_7", - "Label_9", - "CATEGORY_PERSONAL", - "Label_4", - "INBOX" - ], - "snippet": "1234", - "payload": { - "partId": "", - "mimeType": "multipart/signed", - "filename": "", - "headers": [ - { - "name": "X-Gm-Message-State", - "value": "AOAM531rM4nAzGrXic86hgGRR70gbKiyBvs3SRmMh9kgVH8yJ1s0GvC+ TRE++jlINOPVOuuf+S0qf8T0f7HFESDK5k6CFQOVwoWzicXfXCo+aVjCup7J5DaeH2TLBSp5rsJ hSbi5GkVHnqAHw7bnoSeCZ+whg45LZ2A71oJ0Vg8Faxesbx6gSyiUl/prVCA02n2wAl7o8WQaFJ OvPH9H8c4Y6jDGuz7DmsvYOYKz" - }, - { - "name": "To", - "value": "flowcrypt.compatibility@gmail.com" - }, - { - "name": "From", - "value": "dhartley@verdoncollege.school.nz" - }, - { - "name": "Subject", - "value": "sig from new Thunderbird v78 not recognized, html" - }, - { - "name": "Autocrypt", - "value": "addr=dhartley@verdoncollege.school.nz; keydata= xsDNBF1wPuQBDAChI2DAh6K/bmwH5hfCJwkxewFUfWaAxzAIWyXe/w5wjFJBpu74MJvqO+8T WkyVMVXQqOm5dH36YkMCVcNqoGgC0mp/JZUmVIdPqKH1QYK988rRKP2HIl3CNgbyiHLjyKVf +RW4OKxeQLM1MJpQ/39V5ymWftPszz1j+LO2FKpfAsUapAImZNwhXSKZcrxvUEnoayQg17HA 9ThXadQ/UU8FDlibKQoBUvhfyig7Gf4s/Ol3p76a57M/ZClgS5hPoM77oPaC8n8QVYA/t5Bu YSd1n3CE67EU6IzJr3A83c34EZO/wHu6gwCCllouWNtU7fDvKJEbcgbTfb0o7qo224lAUwDq VB2zCVQTEKXV20h/qvujNnLxo2DjcBg+Fbd7GrZEEqK5+psc3U3ljD9mToJoM4lG7Ixy1Ev2 Sx8xfUFtL9PIktl8OwA6U/1XZvauoH5upBain8bc+0ai8axXL2/O0rAKX2DlPfHD9LOim74P NqliLUtTr4TlByMZ5IuYCjMAEQEAAc0vRGF2ZSBIYXJ0bGV5IDxkaGFydGxleUB2ZXJkb25j b2xsZWdlLnNjaG9vbC5uej7CwRQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AW IQTcJkVK+3HRjqu61z0cfm08VWOpQQUCX4eCvAUJBdmq2AAKCRAcfm08VWOpQcRiC/sFhdZh FB5gQbHcs8F9+aPi8gfwu3atUFwadN6AShSa6K3pnd7V1xYlDFwRPKrAEWBBYeX9dsAWl9gx PNuCo5utRkgPR8ZSO4ceXR4ZgKTvF3k6UzzGx8HFC+MJCvdiTTPnMayUOOX/f4+79uWt+fRK SBtckwud7+Dt/88ux8j8TpvBZMhciJ8Zwr9ouVWXfgg2/RMzZNTophQHKdUOhPQ/DBzVaLxV f3QEqKPs3lhrS08ZOMpubV938KdCLgqeqeaClEtURUZEs/5Ixg7lb+SbbOo12K/lRnYu4U1p WcHVueccqkTxDZxgCDBtqN4sBMnmKtSuYAcbyy4SyWGA6Z0KIP87FxMi5Yf+04R+hEG9KWD3 fuk1JIIUeNVoiAn4aERjrIUN5q4NJAyY0d6KXmpyJxOK2Dj3ZyQglNrvXitchXJJqMfdGexB 03lzDYxyHSlu+WBp2exaSOyQIKSniQ1BhAHyp4VQBkLKKVAEV+fVeTdFxarxVH5Hs8tP7Yag JSTOwM0EXXA+5QEMAOoo+4IBfjaiZjEe4azMFDrVwvVkeEw3soMrBjAOjlySQCpE/q0qDId5 k0eSAAhttans3Us166xKW2QMfd2ojFBTLi06Zpha1jDo5Aj2kWDEGT5y+0HBRVY0NmPtvBA1 igRfF1ZSUsqIhO/dSqirdnSh08z5bZnEEyIWObBhC8leE2BAdXWgG1Wk0/02DkrA9N16lZ1/ wVGtYU09rnZBJpeDOFM0dCh7uhfA5LGzmxKzn25/rDSFUrUcmYfawAn13OS02COIlstl6lUg VJ6xtBxOaT4NLka4hkqyzpamCF6okeZQI6tHEmlm5FXuhCb/Lz/n9k9Pk793Ior0PP+xMPO8 gbWlFvPfl/MRKc3Hy2SIZw9RQ7A/33V/ozXVzg1A4drrXKkKec8JznNTkoRI6fSx/TGVpNh1 +5SOHFGgKtZ+RA1BD1vmsMz/5xmdoWGisoR0/hEtdjudxpK1Zm0r1nvNpFZmKRZB26ImfMC4 kbRVXXX5LxE6yIyJ3urFn2+6PwARAQABwsD8BBgBCAAmAhsMFiEE3CZFSvtx0Y6rutc9HH5t PFVjqUEFAl+Hgr0FCQXZqtgACgkQHH5tPFVjqUEnEwv9Ea+syu+U72SeNc3BQ3Gm6llARyQm 7gz2GZBak/epzD+3TFNKFLLh9AyBOM+wWRpGfKh1X+s9GLdSQISEA06bsW4d1KfHqEg0HTRw 4p9SRwOWHvy1L6maA1VHRQCW0/FN/7kYDrUH3bigISTkJJpmq8j4YOH66HVEbFqBO31ycYUG Gt/Bazhe7rOhAxZuGYzcVJQdSE020vjbqM5l34HfiDubuUNRhHMIaWa5IYYIyq33STbZu5Fl n/5zoGN8+Ae4wNNzzfOyWCKUzqybgbDxPWCnX2tgDEDlCt0X5X3PcItVeUpQCecDRut4eGEz OkkdNo2EM9UpBjGfei+GD8vrz9apxvQ+nKYIOi4FM8GkXrsag7gvIdEcGzWcokhmdaFqWqwe QALMUhhA86TckOwaouKkwk3CwnHtqdWryXmpvkbJxWRVrZOGgSfG+/QIku0oNY5XKRbdXURp 1dPshT490JLUzFbM+OcF++HyMZ70qj/k64f6idz5w6TvK0TCKe/M" - }, - { - "name": "Date", - "value": "Thu, 22 Oct 2020 10:07:30 +1300" - }, - { - "name": "User-Agent", - "value": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Thunderbird/78.3.3" - }, - { - "name": "MIME-Version", - "value": "1.0" - }, - { - "name": "Content-Type", - "value": "multipart/signed; micalg=pgp-sha256; protocol=\"application/pgp-signature\"; boundary=\"LLgksp3P0YEtQwjYvuvakoXawSV5875WB\"" - } - ], - "body": { - "size": 0 - }, - "parts": [ - { - "partId": "0", - "mimeType": "multipart/mixed", - "filename": "", - "headers": [ - { - "name": "Content-Type", - "value": "multipart/mixed; boundary=\"vv8xtFOOk2SxbnIpwvxkobfET7PglPfc3\"; protected-headers=\"v1\"" - }, - { - "name": "From", - "value": "Dave Hartley " - }, - { - "name": "To", - "value": "flowcrypt.compatibility@gmail.com" - }, - { - "name": "Message-ID", - "value": "<4a694f09-0ea9-461d-f352-db93f7381f29@verdoncollege.school.nz>" - }, - { - "name": "Subject", - "value": "sig from new Thunderbird v78 not recognized, html" - } - ], - "body": { - "size": 0 - }, - "parts": [ - { - "partId": "0.0", - "mimeType": "multipart/mixed", - "filename": "", - "headers": [ - { - "name": "Content-Type", - "value": "multipart/mixed; boundary=\"------------F36435F7FA51E9E42E13F8F3\"" - }, - { - "name": "Content-Language", - "value": "en-NZ" - } - ], - "body": { - "size": 0 - }, - "parts": [ - { - "partId": "0.0.0", - "mimeType": "text/html", - "filename": "", - "headers": [ - { - "name": "Content-Type", - "value": "text/html; charset=utf-8" - }, - { - "name": "Content-Transfer-Encoding", - "value": "quoted-printable" - } - ], - "body": { - "size": 144, - "data": "PGh0bWw-DQogIDxoZWFkPg0KDQogICAgPG1ldGEgaHR0cC1lcXVpdj0iY29udGVudC10eXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgiPg0KICA8L2hlYWQ-DQogIDxib2R5Pg0KICAgIDEyMzQNCiAgPC9ib2R5Pg0KPC9odG1sPg0K" - } - }, - { - "partId": "0.0.1", - "mimeType": "application/pgp-keys", - "filename": "OpenPGP_0x1C7E6D3C5563A941.asc", - "headers": [ - { - "name": "Content-Type", - "value": "application/pgp-keys; name=\"OpenPGP_0x1C7E6D3C5563A941.asc\"" - }, - { - "name": "Content-Transfer-Encoding", - "value": "quoted-printable" - }, - { - "name": "Content-Disposition", - "value": "attachment; filename=\"OpenPGP_0x1C7E6D3C5563A941.asc\"" - } - ], - "body": { - "attachmentId": "ANGjdJ98dLnf8-MM_CprvW1HRrALLB4B1vrufS-w_yxsc1U_An0r0qJgdXjFEZkKKh5_j_TGMM6hQ2dX8Wq7ztAJ0zxWx1h6LwVQ8D-Ve2wPsz3JJS3cVg4BxbOHDynlATGrvTHWSSczfcSV31JB3o7_Hm2bgJdrhOVyAyKQTvlKk2a7SExx3gQYcEoGQ2iBS7qXhCZesUfd_Cse-XDmHSgJiqgSlaKPVPayfKlI0W7B5YH9-DOXE5-TQFw5-Vu4VAO9pzGzhH-QODyNt6CoStib3QnaOh7KHOPqO3n1hj6heIPBa4lGiN_E8xT6-meBXrEfhL3moestUdU34TE1-2YghkKA-UuC8E4WdysKmfHfYmC_uwfR7DNwz4xLBCA", - "size": 3147 - } - } - ] - } - ] - }, - { - "partId": "1", - "mimeType": "application/pgp-signature", - "filename": "OpenPGP_signature", - "headers": [ - { - "name": "Content-Type", - "value": "application/pgp-signature; name=\"OpenPGP_signature.asc\"" - }, - { - "name": "Content-Description", - "value": "OpenPGP digital signature" - }, - { - "name": "Content-Disposition", - "value": "attachment; filename=\"OpenPGP_signature\"" - } - ], - "body": { - "attachmentId": "ANGjdJ8FiIGNSkBlPAvyT91HLJdwguQUROftsSrJDR6AOAzF3nobr7knygwgNT_dNId--icULb1T57Bc4EJ0fg1_uaYEjZ-LcsnVG2byxiy8JmM5_8NozU32t8oaTcvEOIX6JbhekbknemZl9ETfmdCHtJi5-hgQClTRiIgniSB-0M4BBCacf4a01Bu834aNbAfCGFuB4GVvwgvdBzRd3SE56C8guEF74TEvUXY4Yh54Vbi_bbNLdvJl6tg0NM2JpgO_23HYf6IWVf6Sg6gkaBp18to-FuRU6gcVCgNF6PX26MQ27uySjvoAziaZFHpnXIA-ucd1Ge26HJN_6-46MumoXYPTdjetUlcb12CDdDqfnMYdxB5wbmNO665XlNg", - "size": 677 - } - } - ] - }, - "sizeEstimate": 13258, - "historyId": "1206137", - "internalDate": "1603314450000" - }, - "attachments": { - "ANGjdJ98dLnf8-MM_CprvW1HRrALLB4B1vrufS-w_yxsc1U_An0r0qJgdXjFEZkKKh5_j_TGMM6hQ2dX8Wq7ztAJ0zxWx1h6LwVQ8D-Ve2wPsz3JJS3cVg4BxbOHDynlATGrvTHWSSczfcSV31JB3o7_Hm2bgJdrhOVyAyKQTvlKk2a7SExx3gQYcEoGQ2iBS7qXhCZesUfd_Cse-XDmHSgJiqgSlaKPVPayfKlI0W7B5YH9-DOXE5-TQFw5-Vu4VAO9pzGzhH-QODyNt6CoStib3QnaOh7KHOPqO3n1hj6heIPBa4lGiN_E8xT6-meBXrEfhL3moestUdU34TE1-2YghkKA-UuC8E4WdysKmfHfYmC_uwfR7DNwz4xLBCA": { - "data": "LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tDQoNCnhzRE5CRjF3UHVRQkRBQ2hJMkRBaDZLL2Jtd0g1aGZDSndreGV3RlVmV2FBeHpBSVd5WGUvdzV3akZKQnB1NzRNSnZxTys4VFdreVYNCk1WWFFxT201ZEgzNllrTUNWY05xb0dnQzBtcC9KWlVtVklkUHFLSDFRWUs5ODhyUktQMkhJbDNDTmdieWlITGp5S1ZmK1JXNE9LeGUNClFMTTFNSnBRLzM5VjV5bVdmdFBzenoxaitMTzJGS3BmQXNVYXBBSW1aTndoWFNLWmNyeHZVRW5vYXlRZzE3SEE5VGhYYWRRL1VVOEYNCkRsaWJLUW9CVXZoZnlpZzdHZjRzL09sM3A3NmE1N00vWkNsZ1M1aFBvTTc3b1BhQzhuOFFWWUEvdDVCdVlTZDFuM0NFNjdFVTZJekoNCnIzQTgzYzM0RVpPL3dIdTZnd0NDbGxvdVdOdFU3ZkR2S0pFYmNnYlRmYjBvN3FvMjI0bEFVd0RxVkIyekNWUVRFS1hWMjBoL3F2dWoNCk5uTHhvMkRqY0JnK0ZiZDdHclpFRXFLNStwc2MzVTNsakQ5bVRvSm9NNGxHN0l4eTFFdjJTeDh4ZlVGdEw5UElrdGw4T3dBNlUvMVgNClp2YXVvSDV1cEJhaW44YmMrMGFpOGF4WEwyL08wckFLWDJEbFBmSEQ5TE9pbTc0UE5xbGlMVXRUcjRUbEJ5TVo1SXVZQ2pNQUVRRUENCkFjMHZSR0YyWlNCSVlYSjBiR1Y1SUR4a2FHRnlkR3hsZVVCMlpYSmtiMjVqYjJ4c1pXZGxMbk5qYUc5dmJDNXVlajdDd1JRRUV3RUkNCkFENENHd01GQ3drSUJ3SUdGUW9KQ0FzQ0JCWUNBd0VDSGdFQ0Y0QVdJUVRjSmtWSyszSFJqcXU2MXowY2ZtMDhWV09wUVFVQ1g0ZUMNCnZBVUpCZG1xMkFBS0NSQWNmbTA4VldPcFFjUmlDL3NGaGRaaEZCNWdRYkhjczhGOSthUGk4Z2Z3dTNhdFVGd2FkTjZBU2hTYTZLM3ANCm5kN1YxeFlsREZ3UlBLckFFV0JCWWVYOWRzQVdsOWd4UE51Q281dXRSa2dQUjhaU080Y2VYUjRaZ0tUdkYzazZVenpHeDhIRkMrTUoNCkN2ZGlUVFBuTWF5VU9PWC9mNCs3OXVXdCtmUktTQnRja3d1ZDcrRHQvODh1eDhqOFRwdkJaTWhjaUo4WndyOW91VldYZmdnMi9STXoNClpOVG9waFFIS2RVT2hQUS9EQnpWYUx4VmYzUUVxS1BzM2xoclMwOFpPTXB1YlY5MzhLZENMZ3FlcWVhQ2xFdFVSVVpFcy81SXhnN2wNCmIrU2JiT28xMksvbFJuWXU0VTFwV2NIVnVlY2Nxa1R4RFp4Z0NEQnRxTjRzQk1ubUt0U3VZQWNieXk0U3lXR0E2WjBLSVA4N0Z4TWkNCjVZZiswNFIraEVHOUtXRDNmdWsxSklJVWVOVm9pQW40YUVSanJJVU41cTROSkF5WTBkNktYbXB5SnhPSzJEajNaeVFnbE5ydlhpdGMNCmhYSkpxTWZkR2V4QjAzbHpEWXh5SFNsdStXQnAyZXhhU095UUlLU25pUTFCaEFIeXA0VlFCa0xLS1ZBRVYrZlZlVGRGeGFyeFZINUgNCnM4dFA3WWFnSlNUQ3dSUUVFd0VJQUQ0V0lRVGNKa1ZLKzNIUmpxdTYxejBjZm0wOFZXT3BRUVVDWFhBKzVRSWJBd1VKQWVFemdBVUwNCkNRZ0hBZ1lWQ2drSUN3SUVGZ0lEQVFJZUFRSVhnQUFLQ1JBY2ZtMDhWV09wUVEzMkMvNHAzaHNqZlRlQXpRUjBhSG9XR0tUUmNGb1kNCm5jOUlkWWpPNzhmbmFmZWhKSXQ1TExvTGZkK01TR1B6Njc4NWhtTmdyeWo2WHRFMWVJTU42cHZrSy9CeTZON1Y2T04xOWp6S3h2QXENCi8zZi9yUWNpZVRKbDV0dHRRUkdXc3VGWCtEZHYraXdmbEcrOVpTTTNFOWxaTDJwbzNFdzZLaG1pd0graSsrQ1JGN3V0UlJyUGorRHINCm1NQmdYajB5WXloUm1oZVVta1BqN282S2ZNK2NuL3JRUFZTSVdXSHJCSW1BbEVna05sOWZJOS9TTWhvZ052NlpzbUtYN2FlTko5eHgNClhlNnFBVzI3NnJqZEsvM2VoMHJieHdja0lZMkJaWVJlTzczZkxmRnpYckYxa0gwZ0kvVnErQWFhaDkvYzU2WkJNK1ZTRVBhK3pveFANCmR5eWMzenVsN1ExNlZMV0FnblUzUElCeWNFcS9naWxLQnpSM0dHTDJWakxValU0d0F4UTdKd1djMDdvbkttL1dTUnJmY2lnNEdDbHgNClBNb3BPS1FwNWd4MEdPSUVDTWFxRHFvV3pUQWpLK1BTR0pLZUx1eTNydGhyN09rS3JIc2ZhV1BQZ0RwZzVOSlM0MG52VVF4Z3pwQnINCmFRZ2pTS0FjU1NkVlo4bm5WVnozQkdhZm1aT0FEL3pPd00wRVhYQSs1UUVNQU9vbys0SUJmamFpWmpFZTRhek1GRHJWd3ZWa2VFdzMNCnNvTXJCakFPamx5U1FDcEUvcTBxRElkNWswZVNBQWh0dGFuczNVczE2NnhLVzJRTWZkMm9qRkJUTGkwNlpwaGExakRvNUFqMmtXREUNCkdUNXkrMEhCUlZZME5tUHR2QkExaWdSZkYxWlNVc3FJaE8vZFNxaXJkblNoMDh6NWJabkVFeUlXT2JCaEM4bGVFMkJBZFhXZ0cxV2sNCjAvMDJEa3JBOU4xNmxaMS93Vkd0WVUwOXJuWkJKcGVET0ZNMGRDaDd1aGZBNUxHem14S3puMjUvckRTRlVyVWNtWWZhd0FuMTNPUzANCjJDT0lsc3RsNmxVZ1ZKNnh0QnhPYVQ0TkxrYTRoa3F5enBhbUNGNm9rZVpRSTZ0SEVtbG01Rlh1aENiL0x6L245azlQazc5M0lvcjANClBQK3hNUE84Z2JXbEZ2UGZsL01SS2MzSHkyU0ladzlSUTdBLzMzVi9velhWemcxQTRkcnJYS2tLZWM4SnpuTlRrb1JJNmZTeC9UR1YNCnBOaDErNVNPSEZHZ0t0WitSQTFCRDF2bXNNei81eG1kb1dHaXNvUjAvaEV0ZGp1ZHhwSzFabTByMW52TnBGWm1LUlpCMjZJbWZNQzQNCmtiUlZYWFg1THhFNnlJeUozdXJGbjIrNlB3QVJBUUFCd3NEOEJCZ0JDQUFtQWhzTUZpRUUzQ1pGU3Z0eDBZNnJ1dGM5SEg1dFBGVmoNCnFVRUZBbCtIZ3IwRkNRWFpxdGdBQ2drUUhINXRQRlZqcVVFbkV3djlFYStzeXUrVTcyU2VOYzNCUTNHbTZsbEFSeVFtN2d6MkdaQmENCmsvZXB6RCszVEZOS0ZMTGg5QXlCT00rd1dScEdmS2gxWCtzOUdMZFNRSVNFQTA2YnNXNGQxS2ZIcUVnMEhUUnc0cDlTUndPV0h2eTENCkw2bWFBMVZIUlFDVzAvRk4vN2tZRHJVSDNiaWdJU1RrSkpwbXE4ajRZT0g2NkhWRWJGcUJPMzF5Y1lVR0d0L0JhemhlN3JPaEF4WnUNCkdZemNWSlFkU0UwMjB2amJxTTVsMzRIZmlEdWJ1VU5SaEhNSWFXYTVJWVlJeXEzM1NUYlp1NUZsbi81em9HTjgrQWU0d05OenpmT3kNCldDS1V6cXliZ2JEeFBXQ25YMnRnREVEbEN0MFg1WDNQY0l0VmVVcFFDZWNEUnV0NGVHRXpPa2tkTm8yRU05VXBCakdmZWkrR0Q4dnINCno5YXB4dlErbktZSU9pNEZNOEdrWHJzYWc3Z3ZJZEVjR3pXY29raG1kYUZxV3F3ZVFBTE1VaGhBODZUY2tPd2FvdUtrd2szQ3duSHQNCnFkV3J5WG1wdmtiSnhXUlZyWk9HZ1NmRysvUUlrdTBvTlk1WEtSYmRYVVJwMWRQc2hUNDkwSkxVekZiTStPY0YrK0h5TVo3MHFqL2sNCjY0ZjZpZHo1dzZUdkswVENLZS9NDQo9ajl6MA0KLS0tLS1FTkQgUEdQIFBVQkxJQyBLRVkgQkxPQ0stLS0tLQ0K", - "size": 3147 - }, - "ANGjdJ8FiIGNSkBlPAvyT91HLJdwguQUROftsSrJDR6AOAzF3nobr7knygwgNT_dNId--icULb1T57Bc4EJ0fg1_uaYEjZ-LcsnVG2byxiy8JmM5_8NozU32t8oaTcvEOIX6JbhekbknemZl9ETfmdCHtJi5-hgQClTRiIgniSB-0M4BBCacf4a01Bu834aNbAfCGFuB4GVvwgvdBzRd3SE56C8guEF74TEvUXY4Yh54Vbi_bbNLdvJl6tg0NM2JpgO_23HYf6IWVf6Sg6gkaBp18to-FuRU6gcVCgNF6PX26MQ27uySjvoAziaZFHpnXIA-ucd1Ge26HJN_6-46MumoXYPTdjetUlcb12CDdDqfnMYdxB5wbmNO665XlNg": { - "data": "LS0tLS1CRUdJTiBQR1AgU0lHTkFUVVJFLS0tLS0NCg0Kd3NENUJBQUJDQUFqRmlFRTNDWkZTdnR4MFk2cnV0YzlISDV0UEZWanFVRUZBbCtRb3hJRkF3QUFBQUFBQ2drUUhINXRQRlZqcVVIZQ0KOFF2L1VGdzJkalNqMEpNdVRjOGt2dEkvMkJkL09FU1ZTZW1Sb09JNkVJczZ3U1pVVTdUVCtBdzIzZnJuaXdxck1YQkd4OHN3em5mYQ0KbHBXb0ZtNFZFbFZDeTlTaE9SSWNKKzNBTkVBZUo1aytnNzZZUjRQMVlCZUVJT3hzc3ZudGU0TDZtbG1hQk5hVXUzZUZHSjVnbHRkQg0KQWk1dmtPTW1lMy9taTNLaU9JbHJxdGcvZG8rZTAzOW9QNUpSR1RHZGVlTzQ0bEh1SU5XL1Y5cmlhTEppL2ZjV0hsaUh2NElLSGxQWg0KSitUUGpRQWs4NHhnd3ZKc2ZwRGhNTVV6b210MTR5cjJHUExOUkorTTdVdEdHRDNMdWRUcVNwNnI2MGp0Vi9DWXo5STh2ME1pTnZabw0KYnFrUDd4dVFCSUdLaVpVdWluTCthTUQvd2hwWWtxK3l0N1ZORGVlVElrSm81VGVZcUJqYTcyaVU2bkNrMVJTVHUrSWd3M0tmaEppcQ0KZitMbmM4Z0N1dG91Q0ZEa0Vpc3cybUd3NHRvY1E3dkRGOFR4alpyZFQ2NHlPdmhGR0s0ek5VcVNQZ0FTUlpwd0pxSy9uUzRTbnhXMw0KdG5pcm16d1RFUFdpL2pnTytNa0o0ejFNN3BlTW5zbEJoeXRXdk00alh6RTZtMmRaOW4yL0t4OE9PMW1IDQo9cFJ5dg0KLS0tLS1FTkQgUEdQIFNJR05BVFVSRS0tLS0tDQo", - "size": 677 - } - }, - "raw": { - "id": "1754cfd1b2f1d6e5", - "threadId": "1754cfd1b2f1d6e5", - "labelIds": [ - "Label_6255111949068864933", - "Label_7", - "Label_9", - "CATEGORY_PERSONAL", - "Label_4", - "INBOX" - ], - "snippet": "1234", - "sizeEstimate": 13258, - "raw": "RGVsaXZlcmVkLVRvOiBmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20NClJlY2VpdmVkOiBieSAyMDAyOmFlZDoyNDZhOjA6MDowOjA6MCB3aXRoIFNNVFAgaWQgczM5Y3NwNTc0ODcxMXF0YzsNCiAgICAgICAgV2VkLCAyMSBPY3QgMjAyMCAxNDowNzozNyAtMDcwMCAoUERUKQ0KWC1SZWNlaXZlZDogYnkgMjAwMjphMTc6OTBiOjQ2MTc6OiB3aXRoIFNNVFAgaWQgaWEyM21yNTIxMTM4OXBqYi4xMjAuMTYwMzMxNDQ1NzM0NjsNCiAgICAgICAgV2VkLCAyMSBPY3QgMjAyMCAxNDowNzozNyAtMDcwMCAoUERUKQ0KQVJDLVNlYWw6IGk9MTsgYT1yc2Etc2hhMjU2OyB0PTE2MDMzMTQ0NTc7IGN2PW5vbmU7DQogICAgICAgIGQ9Z29vZ2xlLmNvbTsgcz1hcmMtMjAxNjA4MTY7DQogICAgICAgIGI9Z3BtMUswNG0xK0s5WG1kZmU5RnlxRHExU2pBaWRrR05tTVVXcFl6c2dYMmlrbmJPQWM4SThLL2tLelJrbVl1MGZXDQogICAgICAgICBOSUZTb2ZkS0FKTXNBelVzeU5HWEh6MnV1RUozZmtwSXlwRks4cWpSNml2OThuU09yK2VGcTRDOXIyR3RTUUVHRUMrUw0KICAgICAgICAgMUtFVTAxY1Y5aGdyRlhFNU5la0ZvbmNhTzVHMUltSjFnM3hLbkE5OEltT2UvM0xVMU91UjQzNmFWZ1hEM1BuQmxWQTUNCiAgICAgICAgIDE1R2FIS1BGbVdNT3pRNXAwd2lQVFd1WDUvZ2syMmI3NzVhVXkzcnkwUVZJWkNIK1djQ0tuUkEwUmhOTk15RUhSL0JLDQogICAgICAgICBxUWNkSVNhRnVJWk80bmtTMlFpTkxzRFViVjVMdDZQSEpMSHhNenQyd3hYM0Yzc1hZRGNzd3FMZXp5b2F0bXBVRnFlaA0KICAgICAgICAgaEJudz09DQpBUkMtTWVzc2FnZS1TaWduYXR1cmU6IGk9MTsgYT1yc2Etc2hhMjU2OyBjPXJlbGF4ZWQvcmVsYXhlZDsgZD1nb29nbGUuY29tOyBzPWFyYy0yMDE2MDgxNjsNCiAgICAgICAgaD1taW1lLXZlcnNpb246dXNlci1hZ2VudDpkYXRlOm1lc3NhZ2UtaWQ6YXV0b2NyeXB0OnN1YmplY3Q6ZnJvbTp0bw0KICAgICAgICAgOmRraW0tc2lnbmF0dXJlOw0KICAgICAgICBiaD1JVVJUaklYa3VtYUVSdmNrNXhxQ1dqWEtkcWYrdGI5emNrTEpnbUs3dzRVPTsNCiAgICAgICAgYj1iazQyc1l3M3RzZXB5ZFNzcmVXUFgxTnRrdjNuZmlMVXZtbk5RSG42VXFGeTdjTE41R0Ura0JPWmNpK1g2MFZXZlYNCiAgICAgICAgIFgwcG1hOGlxTGR6LzVGMWd0RFZUREhTcDRuemVoTjB3Y3ZQTjc3ZE1kRm92VWZ0RXl0OTlCWmtRSXRkSmlycTk4RUZzDQogICAgICAgICBkYjRoVkFrdjc4cGJRNzFYK0hEOXRDNlRPZmZobUhwM2F3Yjk3ZVdLM24yaUFBN2R2UXlnSWpSMzVwTmZmT21jUU8wRw0KICAgICAgICAgM0t0bUZSeFJvUmR4ZHQyTjRJelJPZy81SHh1TGJHV2xnUXhnemdpc3gwNlBWb0FkcC8yY2pyN2x1K1BKelFNS0k4SUsNCiAgICAgICAgIEpIQXNpTUFSR2NRNE4wOGFTa1EzODdUcCtRSUJ2NHpHL3Y5TTFNTTYzaEl0OHVVazFQMitSUTJMQ1dLbWgyM1Q1ZzlUDQogICAgICAgICBDL29RPT0NCkFSQy1BdXRoZW50aWNhdGlvbi1SZXN1bHRzOiBpPTE7IG14Lmdvb2dsZS5jb207DQogICAgICAgZGtpbT1wYXNzIGhlYWRlci5pPUB2ZXJkb25jb2xsZWdlLXNjaG9vbC1uei4yMDE1MDYyMy5nYXBwc3NtdHAuY29tIGhlYWRlci5zPTIwMTUwNjIzIGhlYWRlci5iPWt2SzlydEFPOw0KICAgICAgIHNwZj1wYXNzIChnb29nbGUuY29tOiBkb21haW4gb2YgZGhhcnRsZXlAdmVyZG9uY29sbGVnZS5zY2hvb2wubnogZGVzaWduYXRlcyAyMDkuODUuMjIwLjQxIGFzIHBlcm1pdHRlZCBzZW5kZXIpIHNtdHAubWFpbGZyb209ZGhhcnRsZXlAdmVyZG9uY29sbGVnZS5zY2hvb2wubnoNClJldHVybi1QYXRoOiA8ZGhhcnRsZXlAdmVyZG9uY29sbGVnZS5zY2hvb2wubno-DQpSZWNlaXZlZDogZnJvbSBtYWlsLXNvci1mNDEuZ29vZ2xlLmNvbSAobWFpbC1zb3ItZjQxLmdvb2dsZS5jb20uIFsyMDkuODUuMjIwLjQxXSkNCiAgICAgICAgYnkgbXguZ29vZ2xlLmNvbSB3aXRoIFNNVFBTIGlkIGg2c29yMTAwOTI2NXBnaS42MC4yMDIwLjEwLjIxLjE0LjA3LjM3DQogICAgICAgIGZvciA8Zmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tPg0KICAgICAgICAoR29vZ2xlIFRyYW5zcG9ydCBTZWN1cml0eSk7DQogICAgICAgIFdlZCwgMjEgT2N0IDIwMjAgMTQ6MDc6MzcgLTA3MDAgKFBEVCkNClJlY2VpdmVkLVNQRjogcGFzcyAoZ29vZ2xlLmNvbTogZG9tYWluIG9mIGRoYXJ0bGV5QHZlcmRvbmNvbGxlZ2Uuc2Nob29sLm56IGRlc2lnbmF0ZXMgMjA5Ljg1LjIyMC40MSBhcyBwZXJtaXR0ZWQgc2VuZGVyKSBjbGllbnQtaXA9MjA5Ljg1LjIyMC40MTsNCkF1dGhlbnRpY2F0aW9uLVJlc3VsdHM6IG14Lmdvb2dsZS5jb207DQogICAgICAgZGtpbT1wYXNzIGhlYWRlci5pPUB2ZXJkb25jb2xsZWdlLXNjaG9vbC1uei4yMDE1MDYyMy5nYXBwc3NtdHAuY29tIGhlYWRlci5zPTIwMTUwNjIzIGhlYWRlci5iPWt2SzlydEFPOw0KICAgICAgIHNwZj1wYXNzIChnb29nbGUuY29tOiBkb21haW4gb2YgZGhhcnRsZXlAdmVyZG9uY29sbGVnZS5zY2hvb2wubnogZGVzaWduYXRlcyAyMDkuODUuMjIwLjQxIGFzIHBlcm1pdHRlZCBzZW5kZXIpIHNtdHAubWFpbGZyb209ZGhhcnRsZXlAdmVyZG9uY29sbGVnZS5zY2hvb2wubnoNCkRLSU0tU2lnbmF0dXJlOiB2PTE7IGE9cnNhLXNoYTI1NjsgYz1yZWxheGVkL3JlbGF4ZWQ7DQogICAgICAgIGQ9dmVyZG9uY29sbGVnZS1zY2hvb2wtbnouMjAxNTA2MjMuZ2FwcHNzbXRwLmNvbTsgcz0yMDE1MDYyMzsNCiAgICAgICAgaD10bzpmcm9tOnN1YmplY3Q6YXV0b2NyeXB0Om1lc3NhZ2UtaWQ6ZGF0ZTp1c2VyLWFnZW50Om1pbWUtdmVyc2lvbjsNCiAgICAgICAgYmg9SVVSVGpJWGt1bWFFUnZjazV4cUNXalhLZHFmK3RiOXpja0xKZ21LN3c0VT07DQogICAgICAgIGI9a3ZLOXJ0QU9CMDczdmNMQ05wM2ZKVVBZV0VqWUl0OCtOT2Y3MTJtWWN2QUF3bHZPamYrM3dKVjdudVYxWXpqY2hRDQogICAgICAgICBHdTBmUWR1VFcwb09SbUJPaUx0UXcyRHZLWTdYTFpRd2pDS3dnT2g3Z1lQM3hrci9XN1U4cjdIbmo0d1ExbEY2amFScg0KICAgICAgICAgaDBDdjFBa01pYTJaMEpmQy8rdHpXU3Qra0RxR1lFdnllQUtlL0dqSVROUGNZbFlKWUExL1ByR1gvTXgzZW5raTZjS0kNCiAgICAgICAgIEtWaE5CNWdzbi9icDB6bUVXTzhZWExGNjJVT1ZId3ZRMkpzZ0ZHZVdLR1lSU0lxTUs1dDdFRjI2SExqS05YbTRyU1loDQogICAgICAgICBsbzcyWFhFUmVESjU2N0V1eWNrMTRGN1Rwd2Y2bTlqdzY3cER2UU43bVg5MHplMDFOaHdQcnFlWTY1RHZEaUxkTzlUTQ0KICAgICAgICAgZVU0dz09DQpYLUdvb2dsZS1ES0lNLVNpZ25hdHVyZTogdj0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOw0KICAgICAgICBkPTFlMTAwLm5ldDsgcz0yMDE2MTAyNTsNCiAgICAgICAgaD14LWdtLW1lc3NhZ2Utc3RhdGU6dG86ZnJvbTpzdWJqZWN0OmF1dG9jcnlwdDptZXNzYWdlLWlkOmRhdGUNCiAgICAgICAgIDp1c2VyLWFnZW50Om1pbWUtdmVyc2lvbjsNCiAgICAgICAgYmg9SVVSVGpJWGt1bWFFUnZjazV4cUNXalhLZHFmK3RiOXpja0xKZ21LN3c0VT07DQogICAgICAgIGI9SjlKcUFqbzlxbHd4RU9NU20zNlQ1TjRJd0NEQmNMQWYwL3NJZEFldytuS1VHSVZyOFVyMkh4dm1zakdVQlcwZzhCDQogICAgICAgICA5eDA3UzFPOXFobmZTWmZIZ0Q5Z3BTV25uQXFlQWlKVExYaXJhai9ERFZNbS9GZXUvclpNTXgxbDVyb0hvRUdGMjY1cw0KICAgICAgICAgb1lBbjVjN3ZUY3RMS2hkR1ZtdldsbDljUzEwZVIrRU91Vi83aVBDdVVPQ0xhVStadlovZnZYQXRaQnE4QmxUR2hoQkcNCiAgICAgICAgIGdQT3lJN1h6Q3Z3RlhNcFVOWjZOaWtxNllFaFd4L0VESVpnRmFnaUZwRzNSVVZKUFVNVEx1a3pSSXlwbUlwT0tjK0l1DQogICAgICAgICBqTmdWUUdYYko4MS9DM3gwNEZyeDlGWjhod1JGYlkxbEcxZVhEejZqMFJ2aXA3TGlUalNjNGRVaExkUXhyM05TTUhTaQ0KICAgICAgICAgRXUvUT09DQpYLUdtLU1lc3NhZ2UtU3RhdGU6IEFPQU01MzFyTTRuQXpHclhpYzg2aGdHUlI3MGdiS2l5QnZzM1NSbU1oOWtnVkg4eUoxczBHdkMrDQoJVFJFKytqbElOT1BWT3V1ZitTMHFmOFQwZjdIRkVTREs1azZDRlFPVndvV3ppY1hmWENvK2FWakN1cDdKNURhZUgyVExCU3A1cnNKDQoJaFNiaTVHa1ZIbnFBSHc3Ym5vU2VDWit3aGc0NUxaMkE3MW9KMFZnOEZheGVzYng2Z1N5aVVsL3ByVkNBMDJuMndBbDdvOFdRYUZKDQoJT3ZQSDlIOGM0WTZqREd1ejdEbXN2WU9ZS3oNClgtR29vZ2xlLVNtdHAtU291cmNlOiBBQmRoUEp6TjB2ZXNvRlVybGZ1YXRaNENDbUdQZ3g4ZUNGdVhqSmY0Z0FDcmt5eEdHQzBITmVLdHF5bE1Gbkt1Y1NQR0ZPKzNkdzA1NEE9PQ0KWC1SZWNlaXZlZDogYnkgMjAwMjphNjM6NWQ0Mzo6IHdpdGggU01UUCBpZCBvM21yNDgyNDA2M3BnbS44OS4xNjAzMzE0NDU2NjY3Ow0KICAgICAgICBXZWQsIDIxIE9jdCAyMDIwIDE0OjA3OjM2IC0wNzAwIChQRFQpDQpSZXR1cm4tUGF0aDogPGRoYXJ0bGV5QHZlcmRvbmNvbGxlZ2Uuc2Nob29sLm56Pg0KUmVjZWl2ZWQ6IGZyb20gWzEwLjAuNi45XSAoWzIxMC41NS43OS4xM10pDQogICAgICAgIGJ5IHNtdHAuZ21haWwuY29tIHdpdGggRVNNVFBTQSBpZCB1MTVzbTMxMzE2MDNwZmwuMjE1LjIwMjAuMTAuMjEuMTQuMDcuMzMNCiAgICAgICAgZm9yIDxmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20-DQogICAgICAgICh2ZXJzaW9uPVRMUzFfMyBjaXBoZXI9VExTX0FFU18xMjhfR0NNX1NIQTI1NiBiaXRzPTEyOC8xMjgpOw0KICAgICAgICBXZWQsIDIxIE9jdCAyMDIwIDE0OjA3OjM2IC0wNzAwIChQRFQpDQpUbzogZmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tDQpGcm9tOiBEYXZlIEhhcnRsZXkgPGRoYXJ0bGV5QHZlcmRvbmNvbGxlZ2Uuc2Nob29sLm56Pg0KU3ViamVjdDogc2lnIGZyb20gbmV3IFRodW5kZXJiaXJkIHY3OCBub3QgcmVjb2duaXplZCwgaHRtbA0KQXV0b2NyeXB0OiBhZGRyPWRoYXJ0bGV5QHZlcmRvbmNvbGxlZ2Uuc2Nob29sLm56OyBrZXlkYXRhPQ0KIHhzRE5CRjF3UHVRQkRBQ2hJMkRBaDZLL2Jtd0g1aGZDSndreGV3RlVmV2FBeHpBSVd5WGUvdzV3akZKQnB1NzRNSnZxTys4VA0KIFdreVZNVlhRcU9tNWRIMzZZa01DVmNOcW9HZ0MwbXAvSlpVbVZJZFBxS0gxUVlLOTg4clJLUDJISWwzQ05nYnlpSExqeUtWZg0KICtSVzRPS3hlUUxNMU1KcFEvMzlWNXltV2Z0UHN6ejFqK0xPMkZLcGZBc1VhcEFJbVpOd2hYU0taY3J4dlVFbm9heVFnMTdIQQ0KIDlUaFhhZFEvVVU4RkRsaWJLUW9CVXZoZnlpZzdHZjRzL09sM3A3NmE1N00vWkNsZ1M1aFBvTTc3b1BhQzhuOFFWWUEvdDVCdQ0KIFlTZDFuM0NFNjdFVTZJekpyM0E4M2MzNEVaTy93SHU2Z3dDQ2xsb3VXTnRVN2ZEdktKRWJjZ2JUZmIwbzdxbzIyNGxBVXdEcQ0KIFZCMnpDVlFURUtYVjIwaC9xdnVqTm5MeG8yRGpjQmcrRmJkN0dyWkVFcUs1K3BzYzNVM2xqRDltVG9Kb000bEc3SXh5MUV2Mg0KIFN4OHhmVUZ0TDlQSWt0bDhPd0E2VS8xWFp2YXVvSDV1cEJhaW44YmMrMGFpOGF4WEwyL08wckFLWDJEbFBmSEQ5TE9pbTc0UA0KIE5xbGlMVXRUcjRUbEJ5TVo1SXVZQ2pNQUVRRUFBYzB2UkdGMlpTQklZWEowYkdWNUlEeGthR0Z5ZEd4bGVVQjJaWEprYjI1ag0KIGIyeHNaV2RsTG5OamFHOXZiQzV1ZWo3Q3dSUUVFd0VJQUQ0Q0d3TUZDd2tJQndJR0ZRb0pDQXNDQkJZQ0F3RUNIZ0VDRjRBVw0KIElRVGNKa1ZLKzNIUmpxdTYxejBjZm0wOFZXT3BRUVVDWDRlQ3ZBVUpCZG1xMkFBS0NSQWNmbTA4VldPcFFjUmlDL3NGaGRaaA0KIEZCNWdRYkhjczhGOSthUGk4Z2Z3dTNhdFVGd2FkTjZBU2hTYTZLM3BuZDdWMXhZbERGd1JQS3JBRVdCQlllWDlkc0FXbDlneA0KIFBOdUNvNXV0UmtnUFI4WlNPNGNlWFI0WmdLVHZGM2s2VXp6R3g4SEZDK01KQ3ZkaVRUUG5NYXlVT09YL2Y0Kzc5dVd0K2ZSSw0KIFNCdGNrd3VkNytEdC84OHV4OGo4VHB2QlpNaGNpSjhad3I5b3VWV1hmZ2cyL1JNelpOVG9waFFIS2RVT2hQUS9EQnpWYUx4Vg0KIGYzUUVxS1BzM2xoclMwOFpPTXB1YlY5MzhLZENMZ3FlcWVhQ2xFdFVSVVpFcy81SXhnN2xiK1NiYk9vMTJLL2xSbll1NFUxcA0KIFdjSFZ1ZWNjcWtUeERaeGdDREJ0cU40c0JNbm1LdFN1WUFjYnl5NFN5V0dBNlowS0lQODdGeE1pNVlmKzA0UitoRUc5S1dEMw0KIGZ1azFKSUlVZU5Wb2lBbjRhRVJqcklVTjVxNE5KQXlZMGQ2S1htcHlKeE9LMkRqM1p5UWdsTnJ2WGl0Y2hYSkpxTWZkR2V4Qg0KIDAzbHpEWXh5SFNsdStXQnAyZXhhU095UUlLU25pUTFCaEFIeXA0VlFCa0xLS1ZBRVYrZlZlVGRGeGFyeFZINUhzOHRQN1lhZw0KIEpTVE93TTBFWFhBKzVRRU1BT29vKzRJQmZqYWlaakVlNGF6TUZEclZ3dlZrZUV3M3NvTXJCakFPamx5U1FDcEUvcTBxRElkNQ0KIGswZVNBQWh0dGFuczNVczE2NnhLVzJRTWZkMm9qRkJUTGkwNlpwaGExakRvNUFqMmtXREVHVDV5KzBIQlJWWTBObVB0dkJBMQ0KIGlnUmZGMVpTVXNxSWhPL2RTcWlyZG5TaDA4ejViWm5FRXlJV09iQmhDOGxlRTJCQWRYV2dHMVdrMC8wMkRrckE5TjE2bFoxLw0KIHdWR3RZVTA5cm5aQkpwZURPRk0wZENoN3VoZkE1TEd6bXhLem4yNS9yRFNGVXJVY21ZZmF3QW4xM09TMDJDT0lsc3RsNmxVZw0KIFZKNnh0QnhPYVQ0TkxrYTRoa3F5enBhbUNGNm9rZVpRSTZ0SEVtbG01Rlh1aENiL0x6L245azlQazc5M0lvcjBQUCt4TVBPOA0KIGdiV2xGdlBmbC9NUktjM0h5MlNJWnc5UlE3QS8zM1Yvb3pYVnpnMUE0ZHJyWEtrS2VjOEp6bk5Ua29SSTZmU3gvVEdWcE5oMQ0KICs1U09IRkdnS3RaK1JBMUJEMXZtc016LzV4bWRvV0dpc29SMC9oRXRkanVkeHBLMVptMHIxbnZOcEZabUtSWkIyNkltZk1DNA0KIGtiUlZYWFg1THhFNnlJeUozdXJGbjIrNlB3QVJBUUFCd3NEOEJCZ0JDQUFtQWhzTUZpRUUzQ1pGU3Z0eDBZNnJ1dGM5SEg1dA0KIFBGVmpxVUVGQWwrSGdyMEZDUVhacXRnQUNna1FISDV0UEZWanFVRW5Fd3Y5RWErc3l1K1U3MlNlTmMzQlEzR202bGxBUnlRbQ0KIDdnejJHWkJhay9lcHpEKzNURk5LRkxMaDlBeUJPTSt3V1JwR2ZLaDFYK3M5R0xkU1FJU0VBMDZic1c0ZDFLZkhxRWcwSFRSdw0KIDRwOVNSd09XSHZ5MUw2bWFBMVZIUlFDVzAvRk4vN2tZRHJVSDNiaWdJU1RrSkpwbXE4ajRZT0g2NkhWRWJGcUJPMzF5Y1lVRw0KIEd0L0JhemhlN3JPaEF4WnVHWXpjVkpRZFNFMDIwdmpicU01bDM0SGZpRHVidVVOUmhITUlhV2E1SVlZSXlxMzNTVGJadTVGbA0KIG4vNXpvR044K0FlNHdOTnp6Zk95V0NLVXpxeWJnYkR4UFdDblgydGdERURsQ3QwWDVYM1BjSXRWZVVwUUNlY0RSdXQ0ZUdFeg0KIE9ra2RObzJFTTlVcEJqR2ZlaStHRDh2cno5YXB4dlErbktZSU9pNEZNOEdrWHJzYWc3Z3ZJZEVjR3pXY29raG1kYUZxV3F3ZQ0KIFFBTE1VaGhBODZUY2tPd2FvdUtrd2szQ3duSHRxZFdyeVhtcHZrYkp4V1JWclpPR2dTZkcrL1FJa3Uwb05ZNVhLUmJkWFVScA0KIDFkUHNoVDQ5MEpMVXpGYk0rT2NGKytIeU1aNzBxai9rNjRmNmlkejV3NlR2SzBUQ0tlL00NCk1lc3NhZ2UtSUQ6IDw0YTY5NGYwOS0wZWE5LTQ2MWQtZjM1Mi1kYjkzZjczODFmMjlAdmVyZG9uY29sbGVnZS5zY2hvb2wubno-DQpEYXRlOiBUaHUsIDIyIE9jdCAyMDIwIDEwOjA3OjMwICsxMzAwDQpVc2VyLUFnZW50OiBNb3ppbGxhLzUuMCAoWDExOyBMaW51eCB4ODZfNjQ7IHJ2Ojc4LjApIEdlY2tvLzIwMTAwMTAxDQogVGh1bmRlcmJpcmQvNzguMy4zDQpNSU1FLVZlcnNpb246IDEuMA0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvc2lnbmVkOyBtaWNhbGc9cGdwLXNoYTI1NjsNCiBwcm90b2NvbD0iYXBwbGljYXRpb24vcGdwLXNpZ25hdHVyZSI7DQogYm91bmRhcnk9IkxMZ2tzcDNQMFlFdFF3all2dXZha29YYXdTVjU4NzVXQiINCg0KVGhpcyBpcyBhbiBPcGVuUEdQL01JTUUgc2lnbmVkIG1lc3NhZ2UgKFJGQyA0ODgwIGFuZCAzMTU2KQ0KLS1MTGdrc3AzUDBZRXRRd2pZdnV2YWtvWGF3U1Y1ODc1V0INCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOyBib3VuZGFyeT0idnY4eHRGT09rMlN4Ym5JcHd2eGtvYmZFVDdQZ2xQZmMzIjsNCiBwcm90ZWN0ZWQtaGVhZGVycz0idjEiDQpGcm9tOiBEYXZlIEhhcnRsZXkgPGRoYXJ0bGV5QHZlcmRvbmNvbGxlZ2Uuc2Nob29sLm56Pg0KVG86IGZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbQ0KTWVzc2FnZS1JRDogPDRhNjk0ZjA5LTBlYTktNDYxZC1mMzUyLWRiOTNmNzM4MWYyOUB2ZXJkb25jb2xsZWdlLnNjaG9vbC5uej4NClN1YmplY3Q6IHNpZyBmcm9tIG5ldyBUaHVuZGVyYmlyZCB2Nzggbm90IHJlY29nbml6ZWQsIGh0bWwNCg0KLS12djh4dEZPT2syU3hibklwd3Z4a29iZkVUN1BnbFBmYzMNCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOw0KIGJvdW5kYXJ5PSItLS0tLS0tLS0tLS1GMzY0MzVGN0ZBNTFFOUU0MkUxM0Y4RjMiDQpDb250ZW50LUxhbmd1YWdlOiBlbi1OWg0KDQpUaGlzIGlzIGEgbXVsdGktcGFydCBtZXNzYWdlIGluIE1JTUUgZm9ybWF0Lg0KLS0tLS0tLS0tLS0tLS1GMzY0MzVGN0ZBNTFFOUU0MkUxM0Y4RjMNCkNvbnRlbnQtVHlwZTogdGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04DQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBxdW90ZWQtcHJpbnRhYmxlDQoNCjxodG1sPg0KICA8aGVhZD4NCg0KICAgIDxtZXRhIGh0dHAtZXF1aXY9M0QiY29udGVudC10eXBlIiBjb250ZW50PTNEInRleHQvaHRtbDsgY2hhcnNldD0zRFVURj0NCi04Ij4NCiAgPC9oZWFkPg0KICA8Ym9keT4NCiAgICAxMjM0DQogIDwvYm9keT4NCjwvaHRtbD4NCg0KLS0tLS0tLS0tLS0tLS1GMzY0MzVGN0ZBNTFFOUU0MkUxM0Y4RjMNCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGdwLWtleXM7DQogbmFtZT0iT3BlblBHUF8weDFDN0U2RDNDNTU2M0E5NDEuYXNjIg0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogcXVvdGVkLXByaW50YWJsZQ0KQ29udGVudC1EaXNwb3NpdGlvbjogYXR0YWNobWVudDsNCiBmaWxlbmFtZT0iT3BlblBHUF8weDFDN0U2RDNDNTU2M0E5NDEuYXNjIg0KDQotLS0tLUJFR0lOIFBHUCBQVUJMSUMgS0VZIEJMT0NLLS0tLS0NCg0KeHNETkJGMXdQdVFCREFDaEkyREFoNksvYm13SDVoZkNKd2t4ZXdGVWZXYUF4ekFJV3lYZS93NXdqRkpCcHU3NE1KdnFPKzhUVz0NCmt5Vg0KTVZYUXFPbTVkSDM2WWtNQ1ZjTnFvR2dDMG1wL0paVW1WSWRQcUtIMVFZSzk4OHJSS1AySElsM0NOZ2J5aUhManlLVmYrUlc0Tz0NCkt4ZQ0KUUxNMU1KcFEvMzlWNXltV2Z0UHN6ejFqK0xPMkZLcGZBc1VhcEFJbVpOd2hYU0taY3J4dlVFbm9heVFnMTdIQTlUaFhhZFEvVT0NClU4Rg0KRGxpYktRb0JVdmhmeWlnN0dmNHMvT2wzcDc2YTU3TS9aQ2xnUzVoUG9NNzdvUGFDOG44UVZZQS90NUJ1WVNkMW4zQ0U2N0VVNj0NCkl6Sg0KcjNBODNjMzRFWk8vd0h1Nmd3Q0NsbG91V050VTdmRHZLSkViY2diVGZiMG83cW8yMjRsQVV3RHFWQjJ6Q1ZRVEVLWFYyMGgvcT0NCnZ1ag0KTm5MeG8yRGpjQmcrRmJkN0dyWkVFcUs1K3BzYzNVM2xqRDltVG9Kb000bEc3SXh5MUV2MlN4OHhmVUZ0TDlQSWt0bDhPd0E2VT0NCi8xWA0KWnZhdW9INXVwQmFpbjhiYyswYWk4YXhYTDIvTzByQUtYMkRsUGZIRDlMT2ltNzRQTnFsaUxVdFRyNFRsQnlNWjVJdVlDak1BRT0NClFFQQ0KQWMwdlJHRjJaU0JJWVhKMGJHVjVJRHhrYUdGeWRHeGxlVUIyWlhKa2IyNWpiMnhzWldkbExuTmphRzl2YkM1dWVqN0N3UlFFRT0NCndFSQ0KQUQ0Q0d3TUZDd2tJQndJR0ZRb0pDQXNDQkJZQ0F3RUNIZ0VDRjRBV0lRVGNKa1ZLKzNIUmpxdTYxejBjZm0wOFZXT3BRUVVDWD0NCjRlQw0KdkFVSkJkbXEyQUFLQ1JBY2ZtMDhWV09wUWNSaUMvc0ZoZFpoRkI1Z1FiSGNzOEY5K2FQaThnZnd1M2F0VUZ3YWRONkFTaFNhNj0NCkszcA0KbmQ3VjF4WWxERndSUEtyQUVXQkJZZVg5ZHNBV2w5Z3hQTnVDbzV1dFJrZ1BSOFpTTzRjZVhSNFpnS1R2RjNrNlV6ekd4OEhGQz0NCitNSg0KQ3ZkaVRUUG5NYXlVT09YL2Y0Kzc5dVd0K2ZSS1NCdGNrd3VkNytEdC84OHV4OGo4VHB2QlpNaGNpSjhad3I5b3VWV1hmZ2cyLz0NClJNeg0KWk5Ub3BoUUhLZFVPaFBRL0RCelZhTHhWZjNRRXFLUHMzbGhyUzA4Wk9NcHViVjkzOEtkQ0xncWVxZWFDbEV0VVJVWkVzLzVJeD0NCmc3bA0KYitTYmJPbzEySy9sUm5ZdTRVMXBXY0hWdWVjY3FrVHhEWnhnQ0RCdHFONHNCTW5tS3RTdVlBY2J5eTRTeVdHQTZaMEtJUDg3Rj0NCnhNaQ0KNVlmKzA0UitoRUc5S1dEM2Z1azFKSUlVZU5Wb2lBbjRhRVJqcklVTjVxNE5KQXlZMGQ2S1htcHlKeE9LMkRqM1p5UWdsTnJ2WD0NCml0Yw0KaFhKSnFNZmRHZXhCMDNsekRZeHlIU2x1K1dCcDJleGFTT3lRSUtTbmlRMUJoQUh5cDRWUUJrTEtLVkFFVitmVmVUZEZ4YXJ4Vj0NCkg1SA0Kczh0UDdZYWdKU1RDd1JRRUV3RUlBRDRXSVFUY0prVksrM0hSanF1NjF6MGNmbTA4VldPcFFRVUNYWEErNVFJYkF3VUpBZUV6Zz0NCkFVTA0KQ1FnSEFnWVZDZ2tJQ3dJRUZnSURBUUllQVFJWGdBQUtDUkFjZm0wOFZXT3BRUTMyQy80cDNoc2pmVGVBelFSMGFIb1dHS1RSYz0NCkZvWQ0KbmM5SWRZak83OGZuYWZlaEpJdDVMTG9MZmQrTVNHUHo2Nzg1aG1OZ3J5ajZYdEUxZUlNTjZwdmtLL0J5Nk43VjZPTjE5anpLeD0NCnZBcQ0KLzNmL3JRY2llVEpsNXR0dFFSR1dzdUZYK0Rkditpd2ZsRys5WlNNM0U5bFpMMnBvM0V3NktobWl3SCtpKytDUkY3dXRSUnJQaj0NCitEcg0KbU1CZ1hqMHlZeWhSbWhlVW1rUGo3bzZLZk0rY24vclFQVlNJV1dIckJJbUFsRWdrTmw5Zkk5L1NNaG9nTnY2WnNtS1g3YWVOSj0NCjl4eA0KWGU2cUFXMjc2cmpkSy8zZWgwcmJ4d2NrSVkyQlpZUmVPNzNmTGZGelhyRjFrSDBnSS9WcStBYWFoOS9jNTZaQk0rVlNFUGErej0NCm94UA0KZHl5YzN6dWw3UTE2VkxXQWduVTNQSUJ5Y0VxL2dpbEtCelIzR0dMMlZqTFVqVTR3QXhRN0p3V2MwN29uS20vV1NScmZjaWc0Rz0NCkNseA0KUE1vcE9LUXA1Z3gwR09JRUNNYXFEcW9XelRBaksrUFNHSktlTHV5M3J0aHI3T2tLckhzZmFXUFBnRHBnNU5KUzQwbnZVUXhnej0NCnBCcg0KYVFnalNLQWNTU2RWWjhublZWejNCR2FmbVpPQUQvek93TTBFWFhBKzVRRU1BT29vKzRJQmZqYWlaakVlNGF6TUZEclZ3dlZrZT0NCkV3Mw0Kc29NckJqQU9qbHlTUUNwRS9xMHFESWQ1azBlU0FBaHR0YW5zM1VzMTY2eEtXMlFNZmQyb2pGQlRMaTA2WnBoYTFqRG81QWoyaz0NCldERQ0KR1Q1eSswSEJSVlkwTm1QdHZCQTFpZ1JmRjFaU1VzcUloTy9kU3FpcmRuU2gwOHo1YlpuRUV5SVdPYkJoQzhsZUUyQkFkWFdnRz0NCjFXaw0KMC8wMkRrckE5TjE2bFoxL3dWR3RZVTA5cm5aQkpwZURPRk0wZENoN3VoZkE1TEd6bXhLem4yNS9yRFNGVXJVY21ZZmF3QW4xMz0NCk9TMA0KMkNPSWxzdGw2bFVnVko2eHRCeE9hVDROTGthNGhrcXl6cGFtQ0Y2b2tlWlFJNnRIRW1sbTVGWHVoQ2IvTHovbjlrOVBrNzkzST0NCm9yMA0KUFAreE1QTzhnYldsRnZQZmwvTVJLYzNIeTJTSVp3OVJRN0EvMzNWL296WFZ6ZzFBNGRyclhLa0tlYzhKem5OVGtvUkk2ZlN4Lz0NClRHVg0KcE5oMSs1U09IRkdnS3RaK1JBMUJEMXZtc016LzV4bWRvV0dpc29SMC9oRXRkanVkeHBLMVptMHIxbnZOcEZabUtSWkIyNkltZj0NCk1DNA0Ka2JSVlhYWDVMeEU2eUl5SjN1ckZuMis2UHdBUkFRQUJ3c0Q4QkJnQkNBQW1BaHNNRmlFRTNDWkZTdnR4MFk2cnV0YzlISDV0UD0NCkZWag0KcVVFRkFsK0hncjBGQ1FYWnF0Z0FDZ2tRSEg1dFBGVmpxVUVuRXd2OUVhK3N5dStVNzJTZU5jM0JRM0dtNmxsQVJ5UW03Z3oyRz0NClpCYQ0Kay9lcHpEKzNURk5LRkxMaDlBeUJPTSt3V1JwR2ZLaDFYK3M5R0xkU1FJU0VBMDZic1c0ZDFLZkhxRWcwSFRSdzRwOVNSd09XSD0NCnZ5MQ0KTDZtYUExVkhSUUNXMC9GTi83a1lEclVIM2JpZ0lTVGtKSnBtcThqNFlPSDY2SFZFYkZxQk8zMXljWVVHR3QvQmF6aGU3ck9oQT0NCnhadQ0KR1l6Y1ZKUWRTRTAyMHZqYnFNNWwzNEhmaUR1YnVVTlJoSE1JYVdhNUlZWUl5cTMzU1RiWnU1RmxuLzV6b0dOOCtBZTR3Tk56ej0NCmZPeQ0KV0NLVXpxeWJnYkR4UFdDblgydGdERURsQ3QwWDVYM1BjSXRWZVVwUUNlY0RSdXQ0ZUdFek9ra2RObzJFTTlVcEJqR2ZlaStHRD0NCjh2cg0KejlhcHh2UStuS1lJT2k0Rk04R2tYcnNhZzdndklkRWNHeldjb2tobWRhRnFXcXdlUUFMTVVoaEE4NlRja093YW91S2t3azNDdz0NCm5IdA0KcWRXcnlYbXB2a2JKeFdSVnJaT0dnU2ZHKy9RSWt1MG9OWTVYS1JiZFhVUnAxZFBzaFQ0OTBKTFV6RmJNK09jRisrSHlNWjcwcT0NCmovaw0KNjRmNmlkejV3NlR2SzBUQ0tlL00NCj0zRGo5ejANCi0tLS0tRU5EIFBHUCBQVUJMSUMgS0VZIEJMT0NLLS0tLS0NCg0KLS0tLS0tLS0tLS0tLS1GMzY0MzVGN0ZBNTFFOUU0MkUxM0Y4RjMtLQ0KDQotLXZ2OHh0Rk9PazJTeGJuSXB3dnhrb2JmRVQ3UGdsUGZjMy0tDQoNCi0tTExna3NwM1AwWUV0UXdqWXZ1dmFrb1hhd1NWNTg3NVdCDQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL3BncC1zaWduYXR1cmU7IG5hbWU9Ik9wZW5QR1Bfc2lnbmF0dXJlLmFzYyINCkNvbnRlbnQtRGVzY3JpcHRpb246IE9wZW5QR1AgZGlnaXRhbCBzaWduYXR1cmUNCkNvbnRlbnQtRGlzcG9zaXRpb246IGF0dGFjaG1lbnQ7IGZpbGVuYW1lPSJPcGVuUEdQX3NpZ25hdHVyZSINCg0KLS0tLS1CRUdJTiBQR1AgU0lHTkFUVVJFLS0tLS0NCg0Kd3NENUJBQUJDQUFqRmlFRTNDWkZTdnR4MFk2cnV0YzlISDV0UEZWanFVRUZBbCtRb3hJRkF3QUFBQUFBQ2drUUhINXRQRlZqcVVIZQ0KOFF2L1VGdzJkalNqMEpNdVRjOGt2dEkvMkJkL09FU1ZTZW1Sb09JNkVJczZ3U1pVVTdUVCtBdzIzZnJuaXdxck1YQkd4OHN3em5mYQ0KbHBXb0ZtNFZFbFZDeTlTaE9SSWNKKzNBTkVBZUo1aytnNzZZUjRQMVlCZUVJT3hzc3ZudGU0TDZtbG1hQk5hVXUzZUZHSjVnbHRkQg0KQWk1dmtPTW1lMy9taTNLaU9JbHJxdGcvZG8rZTAzOW9QNUpSR1RHZGVlTzQ0bEh1SU5XL1Y5cmlhTEppL2ZjV0hsaUh2NElLSGxQWg0KSitUUGpRQWs4NHhnd3ZKc2ZwRGhNTVV6b210MTR5cjJHUExOUkorTTdVdEdHRDNMdWRUcVNwNnI2MGp0Vi9DWXo5STh2ME1pTnZabw0KYnFrUDd4dVFCSUdLaVpVdWluTCthTUQvd2hwWWtxK3l0N1ZORGVlVElrSm81VGVZcUJqYTcyaVU2bkNrMVJTVHUrSWd3M0tmaEppcQ0KZitMbmM4Z0N1dG91Q0ZEa0Vpc3cybUd3NHRvY1E3dkRGOFR4alpyZFQ2NHlPdmhGR0s0ek5VcVNQZ0FTUlpwd0pxSy9uUzRTbnhXMw0KdG5pcm16d1RFUFdpL2pnTytNa0o0ejFNN3BlTW5zbEJoeXRXdk00alh6RTZtMmRaOW4yL0t4OE9PMW1IDQo9cFJ5dg0KLS0tLS1FTkQgUEdQIFNJR05BVFVSRS0tLS0tDQoNCi0tTExna3NwM1AwWUV0UXdqWXZ1dmFrb1hhd1NWNTg3NVdCLS0NCg==", - "historyId": "1206137", - "internalDate": "1603314450000" - } -} \ No newline at end of file diff --git a/test/source/mock/google/exported-messages/message-export-17dad75e63e47f97.json b/test/source/mock/google/exported-messages/message-export-17dad75e63e47f97.json new file mode 100644 index 00000000000..bd16df8f72d --- /dev/null +++ b/test/source/mock/google/exported-messages/message-export-17dad75e63e47f97.json @@ -0,0 +1,211 @@ +{ + "acctEmail": "flowcrypt.compatibility@gmail.com", + "full": { + "id": "17dad75e63e47f97", + "threadId": "17dad75e63e47f97", + "labelIds": [ + "IMPORTANT", + "CATEGORY_PERSONAL", + "Label_15", + "INBOX" + ], + "snippet": "1234", + "payload": { + "partId": "", + "mimeType": "multipart/signed", + "filename": "", + "headers": [ + { + "name": "X-Gm-Message-State", + "value": "AOAM532JteVyKuMwmUH7EV/J7RYigGV0NdeRyUWpXXGOtttedTa+Km6R /QAKvHofCBjBLWfz0DEbLFZhGngx3lQfzw==" + }, + { + "name": "From", + "value": "some.sender@test.com" + }, + { + "name": "Date", + "value": "Sun, 12 Dec 2021 10:05:22 +0300" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:91.0) Gecko/20100101 Thunderbird/91.3.2" + }, + { + "name": "Content-Language", + "value": "en-US" + }, + { + "name": "To", + "value": "flowcrypt.compatibility@gmail.com" + }, + { + "name": "Subject", + "value": "Thunderbird 91.3.2 signed only with key attached" + }, + { + "name": "Autocrypt", + "value": "addr=some.sender@test.com; keydata= xsBNBGG1nOIBCAC1CCjvL0wJ6bYAxP1xWCDJGe4ykbRl9RNxJ8F31baifLPvFDIJdpB7sxOF 7WtFWS6TqfZPRdRhKKAvqlGALNEMJFgi2oK7lElZxrPB8OFBii+oa6IoQn/qWK+FXAEn3k7T eiuEKjmYjMyE2DvDB5P8pMUluwZhvB8H/rmVLLfiK8qm/fnQdU6RCfQ5++IR78kMdOrfRk6e LbuM6D3cyqS+cBq4RWd+VbJ7BT6SJrRJVDQCz3DVlCROJwxW68RRpyK/LY481grXUxqzNoDE IC9gVWD5XtBD6DmZ9LeH2dEj0nmgMsLBqFIUWGccyGOpeHZgl32tDgyhJbKOjDSMwQBVABEB AAHNG0tleTEgPHNvbWUuc2VuZGVyQHRlc3QuY29tPsLAjQQQAQgAIAUCYbWc4gYLCQcIAwIE FQgKAgQWAgEAAhkBAhsDAh4BACEJECA/rnB2AFOBFiEEK7IZd28jzkjruGCcID+ucHYAU4GW ygf/SWos4aPwxX+GdDqFtGXyRLGRh7lL19SY8vZefjUW94rnl+3ATuEx5vtDIrjWlDkryYwo c64B6c86dv6nLP+WPnQDM2n6mRejvNQ+b2KGmn8HLa7mZ7I+pf4gff9T9jjfwa1TZA870L/G QHiAB+6Lf2uPuwH4wjz66JSdRSgOTnsGkgIsIrZDdVEGhqcHdNeL9+oTR5NqtBUCaS8pLPSH OqTFISJhclKtUlBpwz8XLpG3fDdZZCTGsp1dI0PK2xSZY9U/iEREeqKzyqOOoaYBdPhdpGup 3QGEFIRAm0dreMoZpvSAihAC4wN6NGccFGa9R22ytXlSfLD0oNxNeQdA/87ATQRhtZziAQgA 0mmw+727SdQwbwti7AaGQrDo7TpytFKNyrlZVMmEQ2d77KDYGklSQmY2Sg6XL7HAotfSVBnQ si/FzvwyCX2qR9UXC/Upvm4aVQWwgW3V8NeYZiR0pWzJWj61hihAWCRjKBnDdeDwOREiBaku yM+krnHkf/vd9+kh4ZZyQje74Mw69ZuZv5wkGMhaZ5RoOS+E5m1lujPNo/1atNF5pFlVuyxv 8dedFTULzPPE6RgkeZD1W+zmMIqFB7pRMy16U79xcrzrVehnh2dd4AQ4spkgnR4sQZ6XhnvV WEYPU9T59XkxLoWMldq1XIFhuf/Lt3PhsGku7a55wnpDMA50CJMPSQARAQABwsB2BBgBCAAJ BQJhtZziAhsMACEJECA/rnB2AFOBFiEEK7IZd28jzkjruGCcID+ucHYAU4H/EAf9GXlraJYm o1UqKlUR6WuFnTonNvVY6+83uekuHXHj1xBPAFYzuVFd9dnyhQ8ob5VAfSSIBz4EV243steQ L8G+zU5t0uOoSKFHiB/36a6tW2gAmrktGkyERtXENmgAUjsy2C8nuAZq9oyDCNUZT6U//Suk EzvUrkCmVZPpE/sfiEvvo+pgFtaWRGKb6iBNaa7CXosIcJDWi5+ODaM2xeWkbfZrJOBqmKAu TGqaGrN33kRES3fupYst7Q7HWJ0iuSv6qohdt/YubvjXk3yO/XnuqDKLHVrdYgSJkRnvvx3f VZI/m1sX1+3wVCPyQ9hK0ix1ZTZdGYd76dun/fEmuNQHBg==" + }, + { + "name": "Content-Type", + "value": "multipart/signed; micalg=pgp-sha256; protocol=\"application/pgp-signature\"; boundary=\"------------bVKASqf5GXvcY0BXX7G4C4Zl\"" + } + ], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0", + "mimeType": "multipart/mixed", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "multipart/mixed; boundary=\"------------FQ7CfxuiGriwTfTfyc4i1ppF\"; protected-headers=\"v1\"" + }, + { + "name": "From", + "value": "Some sender " + }, + { + "name": "To", + "value": "flowcrypt.compatibility@gmail.com" + }, + { + "name": "Message-ID", + "value": "" + }, + { + "name": "Subject", + "value": "Thunderbird 91.3.2 signed only with key attached" + } + ], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0.0", + "mimeType": "multipart/mixed", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "multipart/mixed; boundary=\"------------05200f1IHPj8uoi0mQ7uNFC3\"" + } + ], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0.0.0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/plain; charset=UTF-8; format=flowed" + }, + { + "name": "Content-Transfer-Encoding", + "value": "base64" + } + ], + "body": { + "size": 8, + "data": "MTIzNA0KDQo=" + } + }, + { + "partId": "0.0.1", + "mimeType": "application/pgp-keys", + "filename": "OpenPGP_0x203FAE7076005381.asc", + "headers": [ + { + "name": "Content-Type", + "value": "application/pgp-keys; name=\"OpenPGP_0x203FAE7076005381.asc\"" + }, + { + "name": "Content-Disposition", + "value": "attachment; filename=\"OpenPGP_0x203FAE7076005381.asc\"" + }, + { + "name": "Content-Description", + "value": "OpenPGP public key" + }, + { + "name": "Content-Transfer-Encoding", + "value": "quoted-printable" + } + ], + "body": { + "attachmentId": "ANGjdJ8RZUqVqturW8st-eLvfwIiBnZkD5mSAWq6qtJNj5B2jFnA2SENzTTMotBFSpGFfpS69jhWqoQy3rs0kbOwzVI66DL_zibOaUODFww7aawDpNtFCAgQ3UCBj3WGBAn1u7uooQbcRh7OOMt-LRWkXF5L-5_Hakkcy7KgN-tbsVdbzcWQAoj7k-u1YZ2uXSsAE0BDuJjMYwolzibvOGAkZSTM03_solwyiMeaBpsyLOukG5CM5kUqiqY5ZNQmVciJ5A4iXAlwodT2nwCfhX02e0eaNntF1YiI-meLNnPkry8FdXJ5axgsXqkeJuMa40uVishRvSarLPrYgm-fvVymkayZ7i3msFVC4GdSnGGjr3mwMZ-blhqGWR418bU", + "size": 1765 + } + } + ] + } + ] + }, + { + "partId": "1", + "mimeType": "application/pgp-signature", + "filename": "OpenPGP_signature", + "headers": [ + { + "name": "Content-Type", + "value": "application/pgp-signature; name=\"OpenPGP_signature.asc\"" + }, + { + "name": "Content-Description", + "value": "OpenPGP digital signature" + }, + { + "name": "Content-Disposition", + "value": "attachment; filename=\"OpenPGP_signature\"" + } + ], + "body": { + "attachmentId": "ANGjdJ9sCwY4lW9nwiNusDWRePPRmV5rJo4qxiJbEukdrzEAA23KBuFIFo8V3Szfz4vuxzGCWMW59PCXmybBwlHRJo6QsQVVHKn_DkDApioRnwy1HFYWs_i4K3hr1K6db9CgYkAN-z7FZPfQCcRgxxWY7y10pxPMCHd2Hr8vMb1fsPN2t2ceCLPK_K4RytKCJsCXoFVXYhnxWwnOaExKmCB3jWwYEs1EJbRuiS0R5nG7T7652-rmKtxSucTQT_cS1TnP821wPIqqD0pwgPR6KhbF-8N2oXdniflQOB9DLvvVk3QmTMK3qTQXY4HIHEAZ0JeECquFedzHHJzaP19iT2Fx6P4zn5nJ8eJlrd44cmDdTuSimC0LPR5UL_l5cIM", + "size": 505 + } + } + ] + }, + "sizeEstimate": 10699, + "historyId": "1332034", + "internalDate": "1639292722000" + }, + "attachments": { + "ANGjdJ8RZUqVqturW8st-eLvfwIiBnZkD5mSAWq6qtJNj5B2jFnA2SENzTTMotBFSpGFfpS69jhWqoQy3rs0kbOwzVI66DL_zibOaUODFww7aawDpNtFCAgQ3UCBj3WGBAn1u7uooQbcRh7OOMt-LRWkXF5L-5_Hakkcy7KgN-tbsVdbzcWQAoj7k-u1YZ2uXSsAE0BDuJjMYwolzibvOGAkZSTM03_solwyiMeaBpsyLOukG5CM5kUqiqY5ZNQmVciJ5A4iXAlwodT2nwCfhX02e0eaNntF1YiI-meLNnPkry8FdXJ5axgsXqkeJuMa40uVishRvSarLPrYgm-fvVymkayZ7i3msFVC4GdSnGGjr3mwMZ-blhqGWR418bU": { + "data": "LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tDQoNCnhzQk5CR0cxbk9JQkNBQzFDQ2p2TDB3SjZiWUF4UDF4V0NESkdlNHlrYlJsOVJOeEo4RjMxYmFpZkxQdkZESUoNCmRwQjdzeE9GN1d0RldTNlRxZlpQUmRSaEtLQXZxbEdBTE5FTUpGZ2kyb0s3bEVsWnhyUEI4T0ZCaWkrb2E2SW8NClFuL3FXSytGWEFFbjNrN1RlaXVFS2ptWWpNeUUyRHZEQjVQOHBNVWx1d1podkI4SC9ybVZMTGZpSzhxbS9mblENCmRVNlJDZlE1KytJUjc4a01kT3JmUms2ZUxidU02RDNjeXFTK2NCcTRSV2QrVmJKN0JUNlNKclJKVkRRQ3ozRFYNCmxDUk9Kd3hXNjhSUnB5Sy9MWTQ4MWdyWFV4cXpOb0RFSUM5Z1ZXRDVYdEJENkRtWjlMZUgyZEVqMG5tZ01zTEINCnFGSVVXR2NjeUdPcGVIWmdsMzJ0RGd5aEpiS09qRFNNd1FCVkFCRUJBQUhORzB0bGVURWdQSE52YldVdWMyVnUNClpHVnlRSFJsYzNRdVkyOXRQc0xBalFRUUFRZ0FJQVVDWWJXYzRnWUxDUWNJQXdJRUZRZ0tBZ1FXQWdFQUFoa0INCkFoc0RBaDRCQUNFSkVDQS9ybkIyQUZPQkZpRUVLN0laZDI4anpranJ1R0NjSUQrdWNIWUFVNEdXeWdmL1NXb3MNCjRhUHd4WCtHZERxRnRHWHlSTEdSaDdsTDE5U1k4dlplZmpVVzk0cm5sKzNBVHVFeDV2dERJcmpXbERrcnlZd28NCmM2NEI2Yzg2ZHY2bkxQK1dQblFETTJuNm1SZWp2TlErYjJLR21uOEhMYTdtWjdJK3BmNGdmZjlUOWpqZndhMVQNClpBODcwTC9HUUhpQUIrNkxmMnVQdXdINHdqejY2SlNkUlNnT1Ruc0drZ0lzSXJaRGRWRUdocWNIZE5lTDkrb1QNClI1TnF0QlVDYVM4cExQU0hPcVRGSVNKaGNsS3RVbEJwd3o4WExwRzNmRGRaWkNUR3NwMWRJMFBLMnhTWlk5VS8NCmlFUkVlcUt6eXFPT29hWUJkUGhkcEd1cDNRR0VGSVJBbTBkcmVNb1pwdlNBaWhBQzR3TjZOR2NjRkdhOVIyMnkNCnRYbFNmTEQwb054TmVRZEEvODdBVFFSaHRaemlBUWdBMG1tdys3MjdTZFF3Ynd0aTdBYUdRckRvN1RweXRGS04NCnlybFpWTW1FUTJkNzdLRFlHa2xTUW1ZMlNnNlhMN0hBb3RmU1ZCblFzaS9GenZ3eUNYMnFSOVVYQy9VcHZtNGENClZRV3dnVzNWOE5lWVppUjBwV3pKV2o2MWhpaEFXQ1JqS0JuRGRlRHdPUkVpQmFrdXlNK2tybkhrZi92ZDkra2gNCjRaWnlRamU3NE13NjladVp2NXdrR01oYVo1Um9PUytFNW0xbHVqUE5vLzFhdE5GNXBGbFZ1eXh2OGRlZEZUVUwNCnpQUEU2UmdrZVpEMVcrem1NSXFGQjdwUk15MTZVNzl4Y3J6clZlaG5oMmRkNEFRNHNwa2duUjRzUVo2WGhudlYNCldFWVBVOVQ1OVhreExvV01sZHExWElGaHVmL0x0M1Boc0drdTdhNTV3bnBETUE1MENKTVBTUUFSQVFBQndzQjINCkJCZ0JDQUFKQlFKaHRaemlBaHNNQUNFSkVDQS9ybkIyQUZPQkZpRUVLN0laZDI4anpranJ1R0NjSUQrdWNIWUENClU0SC9FQWY5R1hscmFKWW1vMVVxS2xVUjZXdUZuVG9uTnZWWTYrODN1ZWt1SFhIajF4QlBBRll6dVZGZDlkbnkNCmhROG9iNVZBZlNTSUJ6NEVWMjQzc3RlUUw4Ryt6VTV0MHVPb1NLRkhpQi8zNmE2dFcyZ0Ftcmt0R2t5RVJ0WEUNCk5tZ0FVanN5MkM4bnVBWnE5b3lEQ05VWlQ2VS8vU3VrRXp2VXJrQ21WWlBwRS9zZmlFdnZvK3BnRnRhV1JHS2INCjZpQk5hYTdDWG9zSWNKRFdpNStPRGFNMnhlV2tiZlpySk9CcW1LQXVUR3FhR3JOMzNrUkVTM2Z1cFlzdDdRN0gNCldKMGl1U3Y2cW9oZHQvWXVidmpYazN5Ty9YbnVxREtMSFZyZFlnU0prUm52dngzZlZaSS9tMXNYMSszd1ZDUHkNClE5aEswaXgxWlRaZEdZZDc2ZHVuL2ZFbXVOUUhCZz09DQo9eURCcA0KLS0tLS1FTkQgUEdQIFBVQkxJQyBLRVkgQkxPQ0stLS0tLQ", + "size": 1765 + }, + "ANGjdJ9sCwY4lW9nwiNusDWRePPRmV5rJo4qxiJbEukdrzEAA23KBuFIFo8V3Szfz4vuxzGCWMW59PCXmybBwlHRJo6QsQVVHKn_DkDApioRnwy1HFYWs_i4K3hr1K6db9CgYkAN-z7FZPfQCcRgxxWY7y10pxPMCHd2Hr8vMb1fsPN2t2ceCLPK_K4RytKCJsCXoFVXYhnxWwnOaExKmCB3jWwYEs1EJbRuiS0R5nG7T7652-rmKtxSucTQT_cS1TnP821wPIqqD0pwgPR6KhbF-8N2oXdniflQOB9DLvvVk3QmTMK3qTQXY4HIHEAZ0JeECquFedzHHJzaP19iT2Fx6P4zn5nJ8eJlrd44cmDdTuSimC0LPR5UL_l5cIM": { + "data": "LS0tLS1CRUdJTiBQR1AgU0lHTkFUVVJFLS0tLS0NCg0Kd3NCNUJBQUJDQUFqRmlFRUs3SVpkMjhqemtqcnVHQ2NJRCt1Y0hZQVU0RUZBbUcxbnpJRkF3QUFBQUFBQ2drUUlEK3VjSFlBVTRIMQ0KOUFnQW1pNVFVbXJ6bE1hL1Y4U2VFdjdWeWRBM3Y3SGNhL0VNMThvNG90L3lnUWdTMUJvQ205dEFhak9HV2d6bzdlRUp3REs4TFJqMg0KYy9YY0tXRXh4Y3FrTGppZW03Q2RlUGJpL3hyNWpNc1BZek9sTXRjRmFEM3pZOWg4emFiaWlHTTBrSXBUOFBWQ29mZ0ZKTXFRZEJ5cg0KZ0YwTnVpb016QWlDWStXOWFpYVN6cXVIOUZWVkUrQzRid3NVNGxlVGtBTkRHaTA1WEJVSVlhb2NOaWxIblVnaEc2RHlGV1M2cVlGVw0KY1U0U3ZSY041eUREVVVqcnRGSnFwMmEyQ3M3NktnYkJyM0tRY0Q0MkV5cFVMNC9aUys3LzRNTjRTQTA1Ui9tTXRtZks0SHdBS2NDMg0KalNCNkE5M0ptblFHSWtBZW0va3pHa0tjbG1mQWRHZmM0RlMrM0NuKzZRPT0NCj1YbXJ6DQotLS0tLUVORCBQR1AgU0lHTkFUVVJFLS0tLS0NCg", + "size": 505 + } + }, + "raw": { + "id": "17dad75e63e47f97", + "threadId": "17dad75e63e47f97", + "labelIds": [ + "IMPORTANT", + "CATEGORY_PERSONAL", + "Label_15", + "INBOX" + ], + "snippet": "1234", + "sizeEstimate": 10699, + "raw": "RGVsaXZlcmVkLVRvOiBmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20NClJlY2VpdmVkOiBieSAyMDAyOmE2NzpmZDUxOjA6MDowOjA6MCB3aXRoIFNNVFAgaWQgZzE3Y3NwNDIxNzkyNnZzcjsNCiAgICAgICAgU2F0LCAxMSBEZWMgMjAyMSAyMzowNToyNSAtMDgwMCAoUFNUKQ0KWC1SZWNlaXZlZDogYnkgMjAwMjphMDU6NjUxMjoxM2FhOjogd2l0aCBTTVRQIGlkIHA0Mm1yMjEyNDIyODdsZmEuNDc0LjE2MzkyOTI3MjUyMjY7DQogICAgICAgIFNhdCwgMTEgRGVjIDIwMjEgMjM6MDU6MjUgLTA4MDAgKFBTVCkNCkFSQy1TZWFsOiBpPTE7IGE9cnNhLXNoYTI1NjsgdD0xNjM5MjkyNzI1OyBjdj1ub25lOw0KICAgICAgICBkPWdvb2dsZS5jb207IHM9YXJjLTIwMTYwODE2Ow0KICAgICAgICBiPW9KS3dsWklVaFJ2WWt3akdBNWIwNUViNkpUdGdwamFhWWhjTVZ1YVE4b1BybGZiZ0RDaHBadTllYVQ5R0NhT2E1ag0KICAgICAgICAgQ3ZjWWFGQ3VPOHpHdGg1anF6ZXBBbUpkYnNaM2haSVh5ZHIxNXVkcXFhSmVMbk9UMjMyNUU0M2VjT002NG5taFh5S2UNCiAgICAgICAgIEUybmRrWjlUbng1K01QMmxFOUF6eVlqTElxVFlWVDVIbEZtSjhQMlRFbFFkWHVSaFZtVDFNM3F2Q3FPWDlTenlaOXZGDQogICAgICAgICAxSnJmMkhnY1VZbVRmVkFDN1VKY3VNbUJFQ2FRRFViOTlBWlEreEF5TDhrdGx4dTViK0wyM0ZnSituM01oR2c0QVV6bA0KICAgICAgICAgRzJLR1U1NVVtVmZHK0liR2lxOUdVakdBN21CbXdDcnFlQndBQTVlQkhFQ21ISjlnWW5GcnJ3V3B2cG93aHlJVkRvbkUNCiAgICAgICAgIFJQakE9PQ0KQVJDLU1lc3NhZ2UtU2lnbmF0dXJlOiBpPTE7IGE9cnNhLXNoYTI1NjsgYz1yZWxheGVkL3JlbGF4ZWQ7IGQ9Z29vZ2xlLmNvbTsgcz1hcmMtMjAxNjA4MTY7DQogICAgICAgIGg9YXV0b2NyeXB0OnN1YmplY3Q6dG86Y29udGVudC1sYW5ndWFnZTp1c2VyLWFnZW50Om1pbWUtdmVyc2lvbjpkYXRlDQogICAgICAgICA6bWVzc2FnZS1pZDpmcm9tOmRraW0tc2lnbmF0dXJlOw0KICAgICAgICBiaD1weHV0cVNsemZud293K21pUHExZm5rTUxrRmdaNUo1MGVyZEkwN1dkT1ZVPTsNCiAgICAgICAgYj1LZGJWZWdIUG1KaDRzenlEL2RibWpSZVo2U3N6V3JNRjUyNkI2VW1vbUxiQ2NkWElDWjJNMER3K0J4cEpkeTZnV2cNCiAgICAgICAgIFoveVNqbVg5YjlobTM5QUFjNjBtamNtQjFRRWhSNnFCYUxtTzQ5VWdPLzQrTzJPNDY4T0E5aEpkY1lKYzdOdWVHeXhwDQogICAgICAgICBkOTFlK1JSb0dobGxQSjVSWDg5dTYrOVczUmJUbjc5ZDNxVzdKU1hiNlJUZ3YvYVdqemN1S1kyT2cyckNBbWxZNWd1dg0KICAgICAgICAgT3BENEZFRmlmNjcvc0YvT2pSTCtEek9jVmNIWWhJWGZmWmdNMyt5d2dpVWczZEMrWGpJUnhEOHkvakZWVEowK2VUSUwNCiAgICAgICAgIFd4b0VGNVd1VURRQjBsWUpQdjdoZmtWa09xTmw5UitqWEtWb1JYMFBrWmhxR0NmTmVKV3ZBV3NTYnhWNmN3bW1ocnpMDQogICAgICAgICBDWDdRPT0NCkFSQy1BdXRoZW50aWNhdGlvbi1SZXN1bHRzOiBpPTE7IG14Lmdvb2dsZS5jb207DQogICAgICAgZGtpbT1wYXNzIGhlYWRlci5pPUBmbG93Y3J5cHQtZGV2LjIwMjEwMTEyLmdhcHBzc210cC5jb20gaGVhZGVyLnM9MjAyMTAxMTIgaGVhZGVyLmI9enI2eWZYUHI7DQogICAgICAgc3BmPW5ldXRyYWwgKGdvb2dsZS5jb206IDIwOS44NS4yMjAuNDEgaXMgbmVpdGhlciBwZXJtaXR0ZWQgbm9yIGRlbmllZCBieSBiZXN0IGd1ZXNzIHJlY29yZCBmb3IgZG9tYWluIG9mIGNpLnRlc3RzLmdtYWlsQGZsb3djcnlwdC5kZXYpIHNtdHAubWFpbGZyb209Y2kudGVzdHMuZ21haWxAZmxvd2NyeXB0LmRldg0KUmV0dXJuLVBhdGg6IDxjaS50ZXN0cy5nbWFpbEBmbG93Y3J5cHQuZGV2Pg0KUmVjZWl2ZWQ6IGZyb20gbWFpbC1zb3ItZjQxLmdvb2dsZS5jb20gKG1haWwtc29yLWY0MS5nb29nbGUuY29tLiBbMjA5Ljg1LjIyMC40MV0pDQogICAgICAgIGJ5IG14Lmdvb2dsZS5jb20gd2l0aCBTTVRQUyBpZCBzMTZzb3IxOTU3ODUwbGZnLjg3LjIwMjEuMTIuMTEuMjMuMDUuMjUNCiAgICAgICAgZm9yIDxmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20-DQogICAgICAgIChHb29nbGUgVHJhbnNwb3J0IFNlY3VyaXR5KTsNCiAgICAgICAgU2F0LCAxMSBEZWMgMjAyMSAyMzowNToyNSAtMDgwMCAoUFNUKQ0KUmVjZWl2ZWQtU1BGOiBuZXV0cmFsIChnb29nbGUuY29tOiAyMDkuODUuMjIwLjQxIGlzIG5laXRoZXIgcGVybWl0dGVkIG5vciBkZW5pZWQgYnkgYmVzdCBndWVzcyByZWNvcmQgZm9yIGRvbWFpbiBvZiBjaS50ZXN0cy5nbWFpbEBmbG93Y3J5cHQuZGV2KSBjbGllbnQtaXA9MjA5Ljg1LjIyMC40MTsNCkF1dGhlbnRpY2F0aW9uLVJlc3VsdHM6IG14Lmdvb2dsZS5jb207DQogICAgICAgZGtpbT1wYXNzIGhlYWRlci5pPUBmbG93Y3J5cHQtZGV2LjIwMjEwMTEyLmdhcHBzc210cC5jb20gaGVhZGVyLnM9MjAyMTAxMTIgaGVhZGVyLmI9enI2eWZYUHI7DQogICAgICAgc3BmPW5ldXRyYWwgKGdvb2dsZS5jb206IDIwOS44NS4yMjAuNDEgaXMgbmVpdGhlciBwZXJtaXR0ZWQgbm9yIGRlbmllZCBieSBiZXN0IGd1ZXNzIHJlY29yZCBmb3IgZG9tYWluIG9mIGNpLnRlc3RzLmdtYWlsQGZsb3djcnlwdC5kZXYpIHNtdHAubWFpbGZyb209Y2kudGVzdHMuZ21haWxAZmxvd2NyeXB0LmRldg0KREtJTS1TaWduYXR1cmU6IHY9MTsgYT1yc2Etc2hhMjU2OyBjPXJlbGF4ZWQvcmVsYXhlZDsNCiAgICAgICAgZD1mbG93Y3J5cHQtZGV2LjIwMjEwMTEyLmdhcHBzc210cC5jb207IHM9MjAyMTAxMTI7DQogICAgICAgIGg9ZnJvbTptZXNzYWdlLWlkOmRhdGU6bWltZS12ZXJzaW9uOnVzZXItYWdlbnQ6Y29udGVudC1sYW5ndWFnZTp0bw0KICAgICAgICAgOnN1YmplY3Q6YXV0b2NyeXB0Ow0KICAgICAgICBiaD1weHV0cVNsemZud293K21pUHExZm5rTUxrRmdaNUo1MGVyZEkwN1dkT1ZVPTsNCiAgICAgICAgYj16cjZ5ZlhQcmR3RUhXeVdXTWtZYlE2T0tqZURWUGhRbllQSVhkU294OE9zV0ZKWEhHOWlRWU9qQzU1OTJVejhHRjcNCiAgICAgICAgIGpRazVtc0kvdVVrYnFKVFZ5VE1UN281UzJIZkxOdFpidHRGSkpadmJJc1pNN1FlNVRvcDN0T2pPQUM2TXQ0aGZzNDYrDQogICAgICAgICBETzhPeWZzdmRVNnFOaVhjU1puSFA4QmdwWkdxZE5mTVRMaktGWkRwTlVJbUdUSk1qeXNESnd2RkZ2WXhML3FtV0p2cg0KICAgICAgICAgR2haaTh6ZjU2eFZNVEl3OWR6N1lrUjZ5ZVEyWFJPdmlITWk5SUF1WjVGSEZGS2RSRVVibWFsN3c0M2dHblFhL0U3aXYNCiAgICAgICAgICtFVnE5eVVCbU4zNUNlUkFmL1ByeGdGVXh3dXhBWTlKMjRnc1RuT2NWLzNOcldVYmdHUHkxS3RFdFZSbk5IZkljSi8vDQogICAgICAgICB5bE13PT0NClgtR29vZ2xlLURLSU0tU2lnbmF0dXJlOiB2PTE7IGE9cnNhLXNoYTI1NjsgYz1yZWxheGVkL3JlbGF4ZWQ7DQogICAgICAgIGQ9MWUxMDAubmV0OyBzPTIwMjEwMTEyOw0KICAgICAgICBoPXgtZ20tbWVzc2FnZS1zdGF0ZTpmcm9tOm1lc3NhZ2UtaWQ6ZGF0ZTptaW1lLXZlcnNpb246dXNlci1hZ2VudA0KICAgICAgICAgOmNvbnRlbnQtbGFuZ3VhZ2U6dG86c3ViamVjdDphdXRvY3J5cHQ7DQogICAgICAgIGJoPXB4dXRxU2x6Zm53b3crbWlQcTFmbmtNTGtGZ1o1SjUwZXJkSTA3V2RPVlU9Ow0KICAgICAgICBiPVFxb2JEbmZPZmc2QjV6T3VoZkZ4V3lwamN5UzBjM3RpOVMxaTVSbVMvWm96cnRiQk1nZGc3NHI0ZXcwZ3ZxQlBkbw0KICAgICAgICAgb1RDdmY3dFQweUVCQXpPRVgzbVcrTGV0RVhzempyYmpjV0hNRzBWdTA4YkF2UlZXNGV6YmdjVk9uMnp4TnN6b2lqUmUNCiAgICAgICAgIFpldjhlVG1yczlJUzY0VWUzNlcvalR5UU9QVlRPQ3JiblQrZ2FvREt4TnBKQTIvS08vVy9Pa2pmajJvcG14M0dIa2FzDQogICAgICAgICBnSFczcnJGbmp3a2lOZGZTY0MxY29FbXZwRXBLdUtvY1d1ZjZTUlBuUk1IYWovNlVwWnRlalJVRDdXVzE4Yzlnd2RyeA0KICAgICAgICAgYVZ6Qm85R0FFd1F1V3NwL2dlVXdqdWwrUlIvVERKckVtRm52ZmUxM2NiUnJ4aksxR0cyeWo1dWgwU2pTYlpIRzNHbk0NCiAgICAgICAgIEhkTEE9PQ0KWC1HbS1NZXNzYWdlLVN0YXRlOiBBT0FNNTMySnRlVnlLdU13bVVIN0VWL0o3UllpZ0dWME5kZVJ5VVdwWFhHT3R0dGVkVGErS202Ug0KCS9RQUt2SG9mQ0JqQkxXZnowREViTEZaaEduZ3gzbFFmenc9PQ0KWC1Hb29nbGUtU210cC1Tb3VyY2U6IEFCZGhQSndNcnd1enB3L0VhclFkUkNCeXcwYS9IcmlPaTF6NEZJRHcyRy9CMGV3YVplSkVsV0lJVEhFVnhqWUl2TFNwRUcwSkNsT3Mydz09DQpYLVJlY2VpdmVkOiBieSAyMDAyOmFjMjo1NjA3Ojogd2l0aCBTTVRQIGlkIHY3bXIyMjE2MjUzNWxmZC43MS4xNjM5MjkyNzI0NDY5Ow0KICAgICAgICBTYXQsIDExIERlYyAyMDIxIDIzOjA1OjI0IC0wODAwIChQU1QpDQpSZXR1cm4tUGF0aDogPGNpLnRlc3RzLmdtYWlsQGZsb3djcnlwdC5kZXY-DQpSZWNlaXZlZDogZnJvbSBbMTkyLjE2OC4xLjhdICgwODkyNjMxNzA2LnN0YXRpYy5jb3JiaW5hLnJ1LiBbNzguMTA3LjE5NS4xMTNdKQ0KICAgICAgICBieSBzbXRwLmdtYWlsLmNvbSB3aXRoIEVTTVRQU0EgaWQgZzRzbTkyNjIwOWxmdi4yODguMjAyMS4xMi4xMS4yMy4wNS4yMw0KICAgICAgICBmb3IgPGZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbT4NCiAgICAgICAgKHZlcnNpb249VExTMV8zIGNpcGhlcj1UTFNfQUVTXzEyOF9HQ01fU0hBMjU2IGJpdHM9MTI4LzEyOCk7DQogICAgICAgIFNhdCwgMTEgRGVjIDIwMjEgMjM6MDU6MjMgLTA4MDAgKFBTVCkNCkZyb206IENJIFRlc3QgNjggPGNpLnRlc3RzLmdtYWlsQGZsb3djcnlwdC5kZXY-DQpYLUdvb2dsZS1PcmlnaW5hbC1Gcm9tOiBDSSBUZXN0IDY4IDxzb21lLnNlbmRlckB0ZXN0LmNvbT4NCk1lc3NhZ2UtSUQ6IDxjZjk2NjNiNC05NTVhLTRmODQtY2M0Yi1mNGQwMWM4M2FmYzRAdGVzdC5jb20-DQpEYXRlOiBTdW4sIDEyIERlYyAyMDIxIDEwOjA1OjIyICswMzAwDQpNSU1FLVZlcnNpb246IDEuMA0KVXNlci1BZ2VudDogTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgNi4zOyBXaW42NDsgeDY0OyBydjo5MS4wKSBHZWNrby8yMDEwMDEwMQ0KIFRodW5kZXJiaXJkLzkxLjMuMg0KQ29udGVudC1MYW5ndWFnZTogZW4tVVMNClRvOiBmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20NClN1YmplY3Q6IFRodW5kZXJiaXJkIDkxLjMuMiBzaWduZWQgb25seSB3aXRoIGtleSBhdHRhY2hlZA0KQXV0b2NyeXB0OiBhZGRyPXNvbWUuc2VuZGVyQHRlc3QuY29tOyBrZXlkYXRhPQ0KIHhzQk5CR0cxbk9JQkNBQzFDQ2p2TDB3SjZiWUF4UDF4V0NESkdlNHlrYlJsOVJOeEo4RjMxYmFpZkxQdkZESUpkcEI3c3hPRg0KIDdXdEZXUzZUcWZaUFJkUmhLS0F2cWxHQUxORU1KRmdpMm9LN2xFbFp4clBCOE9GQmlpK29hNklvUW4vcVdLK0ZYQUVuM2s3VA0KIGVpdUVLam1Zak15RTJEdkRCNVA4cE1VbHV3Wmh2QjhIL3JtVkxMZmlLOHFtL2ZuUWRVNlJDZlE1KytJUjc4a01kT3JmUms2ZQ0KIExidU02RDNjeXFTK2NCcTRSV2QrVmJKN0JUNlNKclJKVkRRQ3ozRFZsQ1JPSnd4VzY4UlJweUsvTFk0ODFnclhVeHF6Tm9ERQ0KIElDOWdWV0Q1WHRCRDZEbVo5TGVIMmRFajBubWdNc0xCcUZJVVdHY2N5R09wZUhaZ2wzMnREZ3loSmJLT2pEU013UUJWQUJFQg0KIEFBSE5HMHRsZVRFZ1BITnZiV1V1YzJWdVpHVnlRSFJsYzNRdVkyOXRQc0xBalFRUUFRZ0FJQVVDWWJXYzRnWUxDUWNJQXdJRQ0KIEZRZ0tBZ1FXQWdFQUFoa0JBaHNEQWg0QkFDRUpFQ0Evcm5CMkFGT0JGaUVFSzdJWmQyOGp6a2pydUdDY0lEK3VjSFlBVTRHVw0KIHlnZi9TV29zNGFQd3hYK0dkRHFGdEdYeVJMR1JoN2xMMTlTWTh2WmVmalVXOTRybmwrM0FUdUV4NXZ0RElyaldsRGtyeVl3bw0KIGM2NEI2Yzg2ZHY2bkxQK1dQblFETTJuNm1SZWp2TlErYjJLR21uOEhMYTdtWjdJK3BmNGdmZjlUOWpqZndhMVRaQTg3MEwvRw0KIFFIaUFCKzZMZjJ1UHV3SDR3ano2NkpTZFJTZ09UbnNHa2dJc0lyWkRkVkVHaHFjSGROZUw5K29UUjVOcXRCVUNhUzhwTFBTSA0KIE9xVEZJU0poY2xLdFVsQnB3ejhYTHBHM2ZEZFpaQ1RHc3AxZEkwUEsyeFNaWTlVL2lFUkVlcUt6eXFPT29hWUJkUGhkcEd1cA0KIDNRR0VGSVJBbTBkcmVNb1pwdlNBaWhBQzR3TjZOR2NjRkdhOVIyMnl0WGxTZkxEMG9OeE5lUWRBLzg3QVRRUmh0WnppQVFnQQ0KIDBtbXcrNzI3U2RRd2J3dGk3QWFHUXJEbzdUcHl0RktOeXJsWlZNbUVRMmQ3N0tEWUdrbFNRbVkyU2c2WEw3SEFvdGZTVkJuUQ0KIHNpL0Z6dnd5Q1gycVI5VVhDL1Vwdm00YVZRV3dnVzNWOE5lWVppUjBwV3pKV2o2MWhpaEFXQ1JqS0JuRGRlRHdPUkVpQmFrdQ0KIHlNK2tybkhrZi92ZDkra2g0Wlp5UWplNzRNdzY5WnVadjV3a0dNaGFaNVJvT1MrRTVtMWx1alBOby8xYXRORjVwRmxWdXl4dg0KIDhkZWRGVFVMelBQRTZSZ2tlWkQxVyt6bU1JcUZCN3BSTXkxNlU3OXhjcnpyVmVobmgyZGQ0QVE0c3BrZ25SNHNRWjZYaG52Vg0KIFdFWVBVOVQ1OVhreExvV01sZHExWElGaHVmL0x0M1Boc0drdTdhNTV3bnBETUE1MENKTVBTUUFSQVFBQndzQjJCQmdCQ0FBSg0KIEJRSmh0WnppQWhzTUFDRUpFQ0Evcm5CMkFGT0JGaUVFSzdJWmQyOGp6a2pydUdDY0lEK3VjSFlBVTRIL0VBZjlHWGxyYUpZbQ0KIG8xVXFLbFVSNld1Rm5Ub25OdlZZNis4M3Vla3VIWEhqMXhCUEFGWXp1VkZkOWRueWhROG9iNVZBZlNTSUJ6NEVWMjQzc3RlUQ0KIEw4Ryt6VTV0MHVPb1NLRkhpQi8zNmE2dFcyZ0Ftcmt0R2t5RVJ0WEVObWdBVWpzeTJDOG51QVpxOW95RENOVVpUNlUvL1N1aw0KIEV6dlVya0NtVlpQcEUvc2ZpRXZ2bytwZ0Z0YVdSR0tiNmlCTmFhN0NYb3NJY0pEV2k1K09EYU0yeGVXa2JmWnJKT0JxbUtBdQ0KIFRHcWFHck4zM2tSRVMzZnVwWXN0N1E3SFdKMGl1U3Y2cW9oZHQvWXVidmpYazN5Ty9YbnVxREtMSFZyZFlnU0prUm52dngzZg0KIFZaSS9tMXNYMSszd1ZDUHlROWhLMGl4MVpUWmRHWWQ3NmR1bi9mRW11TlFIQmc9PQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvc2lnbmVkOyBtaWNhbGc9cGdwLXNoYTI1NjsNCiBwcm90b2NvbD0iYXBwbGljYXRpb24vcGdwLXNpZ25hdHVyZSI7DQogYm91bmRhcnk9Ii0tLS0tLS0tLS0tLWJWS0FTcWY1R1h2Y1kwQlhYN0c0QzRabCINCg0KVGhpcyBpcyBhbiBPcGVuUEdQL01JTUUgc2lnbmVkIG1lc3NhZ2UgKFJGQyA0ODgwIGFuZCAzMTU2KQ0KLS0tLS0tLS0tLS0tLS1iVktBU3FmNUdYdmNZMEJYWDdHNEM0WmwNCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOyBib3VuZGFyeT0iLS0tLS0tLS0tLS0tRlE3Q2Z4dWlHcml3VGZUZnljNGkxcHBGIjsNCiBwcm90ZWN0ZWQtaGVhZGVycz0idjEiDQpGcm9tOiBDSSBUZXN0IDY4IDxzb21lLnNlbmRlckB0ZXN0LmNvbT4NClRvOiBmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20NCk1lc3NhZ2UtSUQ6IDxjZjk2NjNiNC05NTVhLTRmODQtY2M0Yi1mNGQwMWM4M2FmYzRAdGVzdC5jb20-DQpTdWJqZWN0OiBUaHVuZGVyYmlyZCA5MS4zLjIgc2lnbmVkIG9ubHkgd2l0aCBrZXkgYXR0YWNoZWQNCg0KLS0tLS0tLS0tLS0tLS1GUTdDZnh1aUdyaXdUZlRmeWM0aTFwcEYNCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOyBib3VuZGFyeT0iLS0tLS0tLS0tLS0tMDUyMDBmMUlIUGo4dW9pMG1RN3VORkMzIg0KDQotLS0tLS0tLS0tLS0tLTA1MjAwZjFJSFBqOHVvaTBtUTd1TkZDMw0KQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PVVURi04OyBmb3JtYXQ9Zmxvd2VkDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBiYXNlNjQNCg0KTVRJek5BMEtEUW89DQotLS0tLS0tLS0tLS0tLTA1MjAwZjFJSFBqOHVvaTBtUTd1TkZDMw0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wZ3Ata2V5czsgbmFtZT0iT3BlblBHUF8weDIwM0ZBRTcwNzYwMDUzODEuYXNjIg0KQ29udGVudC1EaXNwb3NpdGlvbjogYXR0YWNobWVudDsgZmlsZW5hbWU9Ik9wZW5QR1BfMHgyMDNGQUU3MDc2MDA1MzgxLmFzYyINCkNvbnRlbnQtRGVzY3JpcHRpb246IE9wZW5QR1AgcHVibGljIGtleQ0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogcXVvdGVkLXByaW50YWJsZQ0KDQotLS0tLUJFR0lOIFBHUCBQVUJMSUMgS0VZIEJMT0NLLS0tLS0NCg0KeHNCTkJHRzFuT0lCQ0FDMUNDanZMMHdKNmJZQXhQMXhXQ0RKR2U0eWtiUmw5Uk54SjhGMzFiYWlmTFB2RkRJSg0KZHBCN3N4T0Y3V3RGV1M2VHFmWlBSZFJoS0tBdnFsR0FMTkVNSkZnaTJvSzdsRWxaeHJQQjhPRkJpaStvYTZJbw0KUW4vcVdLK0ZYQUVuM2s3VGVpdUVLam1Zak15RTJEdkRCNVA4cE1VbHV3Wmh2QjhIL3JtVkxMZmlLOHFtL2ZuUQ0KZFU2UkNmUTUrK0lSNzhrTWRPcmZSazZlTGJ1TTZEM2N5cVMrY0JxNFJXZCtWYko3QlQ2U0pyUkpWRFFDejNEVg0KbENST0p3eFc2OFJScHlLL0xZNDgxZ3JYVXhxek5vREVJQzlnVldENVh0QkQ2RG1aOUxlSDJkRWowbm1nTXNMQg0KcUZJVVdHY2N5R09wZUhaZ2wzMnREZ3loSmJLT2pEU013UUJWQUJFQkFBSE5HMHRsZVRFZ1BITnZiV1V1YzJWdQ0KWkdWeVFIUmxjM1F1WTI5dFBzTEFqUVFRQVFnQUlBVUNZYldjNGdZTENRY0lBd0lFRlFnS0FnUVdBZ0VBQWhrQg0KQWhzREFoNEJBQ0VKRUNBL3JuQjJBRk9CRmlFRUs3SVpkMjhqemtqcnVHQ2NJRCt1Y0hZQVU0R1d5Z2YvU1dvcw0KNGFQd3hYK0dkRHFGdEdYeVJMR1JoN2xMMTlTWTh2WmVmalVXOTRybmwrM0FUdUV4NXZ0RElyaldsRGtyeVl3bw0KYzY0QjZjODZkdjZuTFArV1BuUURNMm42bVJlanZOUStiMktHbW44SExhN21aN0krcGY0Z2ZmOVQ5ampmd2ExVA0KWkE4NzBML0dRSGlBQis2TGYydVB1d0g0d2p6NjZKU2RSU2dPVG5zR2tnSXNJclpEZFZFR2hxY0hkTmVMOStvVA0KUjVOcXRCVUNhUzhwTFBTSE9xVEZJU0poY2xLdFVsQnB3ejhYTHBHM2ZEZFpaQ1RHc3AxZEkwUEsyeFNaWTlVLw0KaUVSRWVxS3p5cU9Pb2FZQmRQaGRwR3VwM1FHRUZJUkFtMGRyZU1vWnB2U0FpaEFDNHdONk5HY2NGR2E5UjIyeQ0KdFhsU2ZMRDBvTnhOZVFkQS84N0FUUVJodFp6aUFRZ0EwbW13KzcyN1NkUXdid3RpN0FhR1FyRG83VHB5dEZLTg0KeXJsWlZNbUVRMmQ3N0tEWUdrbFNRbVkyU2c2WEw3SEFvdGZTVkJuUXNpL0Z6dnd5Q1gycVI5VVhDL1Vwdm00YQ0KVlFXd2dXM1Y4TmVZWmlSMHBXekpXajYxaGloQVdDUmpLQm5EZGVEd09SRWlCYWt1eU0ra3JuSGtmL3ZkOStraA0KNFpaeVFqZTc0TXc2OVp1WnY1d2tHTWhhWjVSb09TK0U1bTFsdWpQTm8vMWF0TkY1cEZsVnV5eHY4ZGVkRlRVTA0KelBQRTZSZ2tlWkQxVyt6bU1JcUZCN3BSTXkxNlU3OXhjcnpyVmVobmgyZGQ0QVE0c3BrZ25SNHNRWjZYaG52Vg0KV0VZUFU5VDU5WGt4TG9XTWxkcTFYSUZodWYvTHQzUGhzR2t1N2E1NXducERNQTUwQ0pNUFNRQVJBUUFCd3NCMg0KQkJnQkNBQUpCUUpodFp6aUFoc01BQ0VKRUNBL3JuQjJBRk9CRmlFRUs3SVpkMjhqemtqcnVHQ2NJRCt1Y0hZQQ0KVTRIL0VBZjlHWGxyYUpZbW8xVXFLbFVSNld1Rm5Ub25OdlZZNis4M3Vla3VIWEhqMXhCUEFGWXp1VkZkOWRueQ0KaFE4b2I1VkFmU1NJQno0RVYyNDNzdGVRTDhHK3pVNXQwdU9vU0tGSGlCLzM2YTZ0VzJnQW1ya3RHa3lFUnRYRQ0KTm1nQVVqc3kyQzhudUFacTlveURDTlVaVDZVLy9TdWtFenZVcmtDbVZaUHBFL3NmaUV2dm8rcGdGdGFXUkdLYg0KNmlCTmFhN0NYb3NJY0pEV2k1K09EYU0yeGVXa2JmWnJKT0JxbUtBdVRHcWFHck4zM2tSRVMzZnVwWXN0N1E3SA0KV0owaXVTdjZxb2hkdC9ZdWJ2alhrM3lPL1hudXFES0xIVnJkWWdTSmtSbnZ2eDNmVlpJL20xc1gxKzN3VkNQeQ0KUTloSzBpeDFaVFpkR1lkNzZkdW4vZkVtdU5RSEJnPTNEPTNEDQo9M0R5REJwDQotLS0tLUVORCBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tDQotLS0tLS0tLS0tLS0tLTA1MjAwZjFJSFBqOHVvaTBtUTd1TkZDMy0tDQoNCg0KLS0tLS0tLS0tLS0tLS1GUTdDZnh1aUdyaXdUZlRmeWM0aTFwcEYtLQ0KDQotLS0tLS0tLS0tLS0tLWJWS0FTcWY1R1h2Y1kwQlhYN0c0QzRabA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wZ3Atc2lnbmF0dXJlOyBuYW1lPSJPcGVuUEdQX3NpZ25hdHVyZS5hc2MiDQpDb250ZW50LURlc2NyaXB0aW9uOiBPcGVuUEdQIGRpZ2l0YWwgc2lnbmF0dXJlDQpDb250ZW50LURpc3Bvc2l0aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT0iT3BlblBHUF9zaWduYXR1cmUiDQoNCi0tLS0tQkVHSU4gUEdQIFNJR05BVFVSRS0tLS0tDQoNCndzQjVCQUFCQ0FBakZpRUVLN0laZDI4anpranJ1R0NjSUQrdWNIWUFVNEVGQW1HMW56SUZBd0FBQUFBQUNna1FJRCt1Y0hZQVU0SDENCjlBZ0FtaTVRVW1yemxNYS9WOFNlRXY3VnlkQTN2N0hjYS9FTTE4bzRvdC95Z1FnUzFCb0NtOXRBYWpPR1dnem83ZUVKd0RLOExSajINCmMvWGNLV0V4eGNxa0xqaWVtN0NkZVBiaS94cjVqTXNQWXpPbE10Y0ZhRDN6WTloOHphYmlpR00wa0lwVDhQVkNvZmdGSk1xUWRCeXINCmdGME51aW9NekFpQ1krVzlhaWFTenF1SDlGVlZFK0M0YndzVTRsZVRrQU5ER2kwNVhCVUlZYW9jTmlsSG5VZ2hHNkR5RldTNnFZRlcNCmNVNFN2UmNONXlERFVVanJ0RkpxcDJhMkNzNzZLZ2JCcjNLUWNENDJFeXBVTDQvWlMrNy80TU40U0EwNVIvbU10bWZLNEh3QUtjQzINCmpTQjZBOTNKbW5RR0lrQWVtL2t6R2tLY2xtZkFkR2ZjNEZTKzNDbis2UT09DQo9WG1yeg0KLS0tLS1FTkQgUEdQIFNJR05BVFVSRS0tLS0tDQoNCi0tLS0tLS0tLS0tLS0tYlZLQVNxZjVHWHZjWTBCWFg3RzRDNFpsLS0NCg==", + "historyId": "1332034", + "internalDate": "1639292722000" + } +} \ No newline at end of file diff --git a/test/source/mock/google/exported-messages/message-export-17daefa0eb077da6.json b/test/source/mock/google/exported-messages/message-export-17daefa0eb077da6.json new file mode 100644 index 00000000000..efc266c0911 --- /dev/null +++ b/test/source/mock/google/exported-messages/message-export-17daefa0eb077da6.json @@ -0,0 +1,213 @@ +{ + "acctEmail": "flowcrypt.compatibility@gmail.com", + "full": { + "id": "17daefa0eb077da6", + "threadId": "17daefa0eb077da6", + "labelIds": [ + "UNREAD", + "IMPORTANT", + "CATEGORY_PERSONAL", + "Label_15", + "INBOX" + ], + "snippet": "1234", + "payload": { + "partId": "", + "mimeType": "multipart/signed", + "filename": "", + "headers": [ + { + "name": "X-Gm-Message-State", + "value": "AOAM5310rjz9iRy4imS1iiocbN/AfStgxrKPlqHb8aDoiNOPZ7mNoV7u E+0Fs6oCk4xlEtGG2QPtbqFoKhCoI+07Gg==" + }, + { + "name": "From", + "value": "some.sender@test.com" + }, + { + "name": "Date", + "value": "Sun, 12 Dec 2021 17:09:19 +0300" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:91.0) Gecko/20100101 Thunderbird/91.3.2" + }, + { + "name": "Content-Language", + "value": "en-US" + }, + { + "name": "To", + "value": "flowcrypt.compatibility@gmail.com" + }, + { + "name": "Subject", + "value": "Thunderbird 91.3.2 html-formatted signed only with key attached" + }, + { + "name": "Autocrypt", + "value": "addr=some.sender@test.com; keydata= xsBNBGG1nOIBCAC1CCjvL0wJ6bYAxP1xWCDJGe4ykbRl9RNxJ8F31baifLPvFDIJdpB7sxOF 7WtFWS6TqfZPRdRhKKAvqlGALNEMJFgi2oK7lElZxrPB8OFBii+oa6IoQn/qWK+FXAEn3k7T eiuEKjmYjMyE2DvDB5P8pMUluwZhvB8H/rmVLLfiK8qm/fnQdU6RCfQ5++IR78kMdOrfRk6e LbuM6D3cyqS+cBq4RWd+VbJ7BT6SJrRJVDQCz3DVlCROJwxW68RRpyK/LY481grXUxqzNoDE IC9gVWD5XtBD6DmZ9LeH2dEj0nmgMsLBqFIUWGccyGOpeHZgl32tDgyhJbKOjDSMwQBVABEB AAHNG0tleTEgPHNvbWUuc2VuZGVyQHRlc3QuY29tPsLAjQQQAQgAIAUCYbWc4gYLCQcIAwIE FQgKAgQWAgEAAhkBAhsDAh4BACEJECA/rnB2AFOBFiEEK7IZd28jzkjruGCcID+ucHYAU4GW ygf/SWos4aPwxX+GdDqFtGXyRLGRh7lL19SY8vZefjUW94rnl+3ATuEx5vtDIrjWlDkryYwo c64B6c86dv6nLP+WPnQDM2n6mRejvNQ+b2KGmn8HLa7mZ7I+pf4gff9T9jjfwa1TZA870L/G QHiAB+6Lf2uPuwH4wjz66JSdRSgOTnsGkgIsIrZDdVEGhqcHdNeL9+oTR5NqtBUCaS8pLPSH OqTFISJhclKtUlBpwz8XLpG3fDdZZCTGsp1dI0PK2xSZY9U/iEREeqKzyqOOoaYBdPhdpGup 3QGEFIRAm0dreMoZpvSAihAC4wN6NGccFGa9R22ytXlSfLD0oNxNeQdA/87ATQRhtZziAQgA 0mmw+727SdQwbwti7AaGQrDo7TpytFKNyrlZVMmEQ2d77KDYGklSQmY2Sg6XL7HAotfSVBnQ si/FzvwyCX2qR9UXC/Upvm4aVQWwgW3V8NeYZiR0pWzJWj61hihAWCRjKBnDdeDwOREiBaku yM+krnHkf/vd9+kh4ZZyQje74Mw69ZuZv5wkGMhaZ5RoOS+E5m1lujPNo/1atNF5pFlVuyxv 8dedFTULzPPE6RgkeZD1W+zmMIqFB7pRMy16U79xcrzrVehnh2dd4AQ4spkgnR4sQZ6XhnvV WEYPU9T59XkxLoWMldq1XIFhuf/Lt3PhsGku7a55wnpDMA50CJMPSQARAQABwsB2BBgBCAAJ BQJhtZziAhsMACEJECA/rnB2AFOBFiEEK7IZd28jzkjruGCcID+ucHYAU4H/EAf9GXlraJYm o1UqKlUR6WuFnTonNvVY6+83uekuHXHj1xBPAFYzuVFd9dnyhQ8ob5VAfSSIBz4EV243steQ L8G+zU5t0uOoSKFHiB/36a6tW2gAmrktGkyERtXENmgAUjsy2C8nuAZq9oyDCNUZT6U//Suk EzvUrkCmVZPpE/sfiEvvo+pgFtaWRGKb6iBNaa7CXosIcJDWi5+ODaM2xeWkbfZrJOBqmKAu TGqaGrN33kRES3fupYst7Q7HWJ0iuSv6qohdt/YubvjXk3yO/XnuqDKLHVrdYgSJkRnvvx3f VZI/m1sX1+3wVCPyQ9hK0ix1ZTZdGYd76dun/fEmuNQHBg==" + }, + { + "name": "Content-Type", + "value": "multipart/signed; micalg=pgp-sha256; protocol=\"application/pgp-signature\"; boundary=\"------------0uFim6rOI5fZebWt0qk8nYxd\"" + } + ], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0", + "mimeType": "multipart/mixed", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "multipart/mixed; boundary=\"------------0i0uwO075ZQ0NjkA1rJACksf\"; protected-headers=\"v1\"" + }, + { + "name": "From", + "value": "Some Sender " + }, + { + "name": "To", + "value": "flowcrypt.compatibility@gmail.com" + }, + { + "name": "Message-ID", + "value": "<0fb349f0-ebe4-a376-afe8-6676693a98f8@test.com>" + }, + { + "name": "Subject", + "value": "Thunderbird 91.3.2 html-formatted signed only with key attached" + } + ], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0.0", + "mimeType": "multipart/mixed", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "multipart/mixed; boundary=\"------------tbEnUmG6KD47ErjC30Q983TB\"" + } + ], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0.0.0", + "mimeType": "text/html", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/html; charset=UTF-8" + }, + { + "name": "Content-Transfer-Encoding", + "value": "quoted-printable" + } + ], + "body": { + "size": 166, + "data": "PGh0bWw-DQogIDxoZWFkPg0KDQogICAgPG1ldGEgaHR0cC1lcXVpdj0iY29udGVudC10eXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgiPg0KICA8L2hlYWQ-DQogIDxib2R5Pg0KICAgIDxwPjxiPjEyMzQ8L2I-PGJyPg0KICAgIDwvcD4NCiAgPC9ib2R5Pg0KPC9odG1sPg==" + } + }, + { + "partId": "0.0.1", + "mimeType": "application/pgp-keys", + "filename": "OpenPGP_0x203FAE7076005381.asc", + "headers": [ + { + "name": "Content-Type", + "value": "application/pgp-keys; name=\"OpenPGP_0x203FAE7076005381.asc\"" + }, + { + "name": "Content-Disposition", + "value": "attachment; filename=\"OpenPGP_0x203FAE7076005381.asc\"" + }, + { + "name": "Content-Description", + "value": "OpenPGP public key" + }, + { + "name": "Content-Transfer-Encoding", + "value": "quoted-printable" + } + ], + "body": { + "attachmentId": "ANGjdJ-Mddv0XnkP86jJeBDbgKp4_XEtKqMgm9Brg6OpaF868rfMNslEK-uPuLfQYbezxaK-6pVYvSW5tDwvW-4qVM4HVHxKH1vMHfVh8WCK73fZaadX4Q9mxrZE4k1RT_iwgC-xeo1m_k04MxKktX7jwQrfIYGXWmZSzdlj5qdYAJOnOfEEfG66-dADg0-56UzP8IuWiolDMlJ0dJkkR5fjUVC45AKns3DUZ4kqr2fiDqyUecln8nlJGHX_mijEC6v5dQMp4x52_BVR9Hl70Xf_G9RNSfxn-Bwsuwp5Ay740izKiQC9awwnKVSVwcGDZQaAEWO2HFLYCoBKCSO0qE_zEZ3xWOq5jD6RLeueciD3fmLCylx3mcIrqvtjCL0", + "size": 1765 + } + } + ] + } + ] + }, + { + "partId": "1", + "mimeType": "application/pgp-signature", + "filename": "OpenPGP_signature", + "headers": [ + { + "name": "Content-Type", + "value": "application/pgp-signature; name=\"OpenPGP_signature.asc\"" + }, + { + "name": "Content-Description", + "value": "OpenPGP digital signature" + }, + { + "name": "Content-Disposition", + "value": "attachment; filename=\"OpenPGP_signature\"" + } + ], + "body": { + "attachmentId": "ANGjdJ8vtcpLF81NG4tqGWpXE1jXS6kl3RLAe6neIv0cl5xncy5Jx55efH6OVofh0SKWoWJVcrByC2W6KAmO3V33Est4bibPw9tLtpP_FaMpztOpyFCDy2jB6GRVJplLSRLK4SI7G5r_tZqbgqrcPuCSMR9gjGdduoEJCbw1fwyS8z-kfmzxh_TgMJhSq72MsIkVibHmkk9WgId3yr-sfuEjcY9XdJa9rDZ6xTkfNfyY9jP-VfJisV3XP55v-JRGc3v3I-6Sr_w5ILRwii0ilF4lnYV5h0XWgJ2vmWOu9FN14xDBeFFSWbL1AwuS7ESiX2153GJFqs-xQePSa7D1U1DCT4IDzkXOKEesI2IxkAknOit7ZhqoOVxwXQhYqWs", + "size": 505 + } + } + ] + }, + "sizeEstimate": 10888, + "historyId": "1332117", + "internalDate": "1639318159000" + }, + "attachments": { + "ANGjdJ-Mddv0XnkP86jJeBDbgKp4_XEtKqMgm9Brg6OpaF868rfMNslEK-uPuLfQYbezxaK-6pVYvSW5tDwvW-4qVM4HVHxKH1vMHfVh8WCK73fZaadX4Q9mxrZE4k1RT_iwgC-xeo1m_k04MxKktX7jwQrfIYGXWmZSzdlj5qdYAJOnOfEEfG66-dADg0-56UzP8IuWiolDMlJ0dJkkR5fjUVC45AKns3DUZ4kqr2fiDqyUecln8nlJGHX_mijEC6v5dQMp4x52_BVR9Hl70Xf_G9RNSfxn-Bwsuwp5Ay740izKiQC9awwnKVSVwcGDZQaAEWO2HFLYCoBKCSO0qE_zEZ3xWOq5jD6RLeueciD3fmLCylx3mcIrqvtjCL0": { + "data": "LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tDQoNCnhzQk5CR0cxbk9JQkNBQzFDQ2p2TDB3SjZiWUF4UDF4V0NESkdlNHlrYlJsOVJOeEo4RjMxYmFpZkxQdkZESUoNCmRwQjdzeE9GN1d0RldTNlRxZlpQUmRSaEtLQXZxbEdBTE5FTUpGZ2kyb0s3bEVsWnhyUEI4T0ZCaWkrb2E2SW8NClFuL3FXSytGWEFFbjNrN1RlaXVFS2ptWWpNeUUyRHZEQjVQOHBNVWx1d1podkI4SC9ybVZMTGZpSzhxbS9mblENCmRVNlJDZlE1KytJUjc4a01kT3JmUms2ZUxidU02RDNjeXFTK2NCcTRSV2QrVmJKN0JUNlNKclJKVkRRQ3ozRFYNCmxDUk9Kd3hXNjhSUnB5Sy9MWTQ4MWdyWFV4cXpOb0RFSUM5Z1ZXRDVYdEJENkRtWjlMZUgyZEVqMG5tZ01zTEINCnFGSVVXR2NjeUdPcGVIWmdsMzJ0RGd5aEpiS09qRFNNd1FCVkFCRUJBQUhORzB0bGVURWdQSE52YldVdWMyVnUNClpHVnlRSFJsYzNRdVkyOXRQc0xBalFRUUFRZ0FJQVVDWWJXYzRnWUxDUWNJQXdJRUZRZ0tBZ1FXQWdFQUFoa0INCkFoc0RBaDRCQUNFSkVDQS9ybkIyQUZPQkZpRUVLN0laZDI4anpranJ1R0NjSUQrdWNIWUFVNEdXeWdmL1NXb3MNCjRhUHd4WCtHZERxRnRHWHlSTEdSaDdsTDE5U1k4dlplZmpVVzk0cm5sKzNBVHVFeDV2dERJcmpXbERrcnlZd28NCmM2NEI2Yzg2ZHY2bkxQK1dQblFETTJuNm1SZWp2TlErYjJLR21uOEhMYTdtWjdJK3BmNGdmZjlUOWpqZndhMVQNClpBODcwTC9HUUhpQUIrNkxmMnVQdXdINHdqejY2SlNkUlNnT1Ruc0drZ0lzSXJaRGRWRUdocWNIZE5lTDkrb1QNClI1TnF0QlVDYVM4cExQU0hPcVRGSVNKaGNsS3RVbEJwd3o4WExwRzNmRGRaWkNUR3NwMWRJMFBLMnhTWlk5VS8NCmlFUkVlcUt6eXFPT29hWUJkUGhkcEd1cDNRR0VGSVJBbTBkcmVNb1pwdlNBaWhBQzR3TjZOR2NjRkdhOVIyMnkNCnRYbFNmTEQwb054TmVRZEEvODdBVFFSaHRaemlBUWdBMG1tdys3MjdTZFF3Ynd0aTdBYUdRckRvN1RweXRGS04NCnlybFpWTW1FUTJkNzdLRFlHa2xTUW1ZMlNnNlhMN0hBb3RmU1ZCblFzaS9GenZ3eUNYMnFSOVVYQy9VcHZtNGENClZRV3dnVzNWOE5lWVppUjBwV3pKV2o2MWhpaEFXQ1JqS0JuRGRlRHdPUkVpQmFrdXlNK2tybkhrZi92ZDkra2gNCjRaWnlRamU3NE13NjladVp2NXdrR01oYVo1Um9PUytFNW0xbHVqUE5vLzFhdE5GNXBGbFZ1eXh2OGRlZEZUVUwNCnpQUEU2UmdrZVpEMVcrem1NSXFGQjdwUk15MTZVNzl4Y3J6clZlaG5oMmRkNEFRNHNwa2duUjRzUVo2WGhudlYNCldFWVBVOVQ1OVhreExvV01sZHExWElGaHVmL0x0M1Boc0drdTdhNTV3bnBETUE1MENKTVBTUUFSQVFBQndzQjINCkJCZ0JDQUFKQlFKaHRaemlBaHNNQUNFSkVDQS9ybkIyQUZPQkZpRUVLN0laZDI4anpranJ1R0NjSUQrdWNIWUENClU0SC9FQWY5R1hscmFKWW1vMVVxS2xVUjZXdUZuVG9uTnZWWTYrODN1ZWt1SFhIajF4QlBBRll6dVZGZDlkbnkNCmhROG9iNVZBZlNTSUJ6NEVWMjQzc3RlUUw4Ryt6VTV0MHVPb1NLRkhpQi8zNmE2dFcyZ0Ftcmt0R2t5RVJ0WEUNCk5tZ0FVanN5MkM4bnVBWnE5b3lEQ05VWlQ2VS8vU3VrRXp2VXJrQ21WWlBwRS9zZmlFdnZvK3BnRnRhV1JHS2INCjZpQk5hYTdDWG9zSWNKRFdpNStPRGFNMnhlV2tiZlpySk9CcW1LQXVUR3FhR3JOMzNrUkVTM2Z1cFlzdDdRN0gNCldKMGl1U3Y2cW9oZHQvWXVidmpYazN5Ty9YbnVxREtMSFZyZFlnU0prUm52dngzZlZaSS9tMXNYMSszd1ZDUHkNClE5aEswaXgxWlRaZEdZZDc2ZHVuL2ZFbXVOUUhCZz09DQo9eURCcA0KLS0tLS1FTkQgUEdQIFBVQkxJQyBLRVkgQkxPQ0stLS0tLQ", + "size": 1765 + }, + "ANGjdJ8vtcpLF81NG4tqGWpXE1jXS6kl3RLAe6neIv0cl5xncy5Jx55efH6OVofh0SKWoWJVcrByC2W6KAmO3V33Est4bibPw9tLtpP_FaMpztOpyFCDy2jB6GRVJplLSRLK4SI7G5r_tZqbgqrcPuCSMR9gjGdduoEJCbw1fwyS8z-kfmzxh_TgMJhSq72MsIkVibHmkk9WgId3yr-sfuEjcY9XdJa9rDZ6xTkfNfyY9jP-VfJisV3XP55v-JRGc3v3I-6Sr_w5ILRwii0ilF4lnYV5h0XWgJ2vmWOu9FN14xDBeFFSWbL1AwuS7ESiX2153GJFqs-xQePSa7D1U1DCT4IDzkXOKEesI2IxkAknOit7ZhqoOVxwXQhYqWs": { + "data": "LS0tLS1CRUdJTiBQR1AgU0lHTkFUVVJFLS0tLS0NCg0Kd3NCNUJBQUJDQUFqRmlFRUs3SVpkMjhqemtqcnVHQ2NJRCt1Y0hZQVU0RUZBbUcyQW84RkF3QUFBQUFBQ2drUUlEK3VjSFlBVTRHeQ0KaVFnQXB0L0tFdGFwTFNwdjQ1ZTIvQ2xKMHBGenV0RzZoQ1luSmg2dkVMVjlnSGFTNzVWdTNuQlpQVXpQVUs4NS9pSk5sbktIMjNhMQ0KNUhaV2Iwa3pVQnhOcUUzYmJGV0cvejBWMFZxdUxodldDV0hoNURwaUlSUW9uOEc2eEpTZW9XK0hydVN6RlpydXU5QlhUOVlPUmlXaQ0KWlJzMktJTVBpR0s5eHhlY3ladFU4MXVZS1ZzR2xPL0N5RS8zd1hjZUJLVGtOdCsrUVN4T2FUR3pjWk5BU1pyV2d1U3kzejVLU3NsVQ0KUlhSYmZRb0xVSDdua29RT2xmcGVZZ0ZsaWIrSzdrT05Xc0ZuOGdIUzFMTGYwa1ErZG91alpMTHU0RGw4d0FpN21ZM1prU1A5K1J5Vw0KWjY5a2tiMFJSdEUrb0ZTUnhYYnM4Q0xGRkkwbU05U054ajBDSW1XekJRPT0NCj1mdGZODQotLS0tLUVORCBQR1AgU0lHTkFUVVJFLS0tLS0NCg", + "size": 505 + } + }, + "raw": { + "id": "17daefa0eb077da6", + "threadId": "17daefa0eb077da6", + "labelIds": [ + "UNREAD", + "IMPORTANT", + "CATEGORY_PERSONAL", + "Label_15", + "INBOX" + ], + "snippet": "1234", + "sizeEstimate": 10888, + "raw": "RGVsaXZlcmVkLVRvOiBmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20NClJlY2VpdmVkOiBieSAyMDAyOmE2NzpmZDUxOjA6MDowOjA6MCB3aXRoIFNNVFAgaWQgZzE3Y3NwNDUyOTY5N3ZzcjsNCiAgICAgICAgU3VuLCAxMiBEZWMgMjAyMSAwNjowOToyMyAtMDgwMCAoUFNUKQ0KWC1SZWNlaXZlZDogYnkgMjAwMjphMmU6Yjg4Yzo6IHdpdGggU01UUCBpZCByMTJtcjIzMDg2NDEwbGpwLjIwNC4xNjM5MzE4MTYzMzM3Ow0KICAgICAgICBTdW4sIDEyIERlYyAyMDIxIDA2OjA5OjIzIC0wODAwIChQU1QpDQpBUkMtU2VhbDogaT0xOyBhPXJzYS1zaGEyNTY7IHQ9MTYzOTMxODE2MzsgY3Y9bm9uZTsNCiAgICAgICAgZD1nb29nbGUuY29tOyBzPWFyYy0yMDE2MDgxNjsNCiAgICAgICAgYj1JdVV1b0RQUUVodEprUmw5QkhXdndMdmNLblRtaEVnZ2xDMVUrL3lZMzk1K3BPNlA2UVE3NlhnWWxWSEc5TFU3ZHANCiAgICAgICAgIElPUy9FZW1YTHRGZXcyWnpXWVAzZjhJNHc4ZUZJbENOVC9MS09BeVA3VituYlczTzRuUWduR05rd3N2eGh1S1o3Q3FpDQogICAgICAgICBCaUVOUEpESkFjdEpLcFNWek0wdVNxOEF5d215WW5VUjBIZjZiUHZMaFkwbGdFNndZdVFmWVYybVA5dFoyd24vQ1N2TA0KICAgICAgICAgUWFQL0VLd0J3WWxoR1FUWUlEdnhqQ3MzRHRTanpJUWVYN2l4cWNBWldkeDlGeUxaWENOYVcxTCsra1RVOEFFQW1IUk4NCiAgICAgICAgIDRPTkJnYVlsaGJqRVd2NVVRUDA1QkRxd1VSMDNQWDF4cnFvajJyYXpqeEpIYnRaVHBsMnVSS2Zmbk4rZkMvcnE2UGVMDQogICAgICAgICBVdU53PT0NCkFSQy1NZXNzYWdlLVNpZ25hdHVyZTogaT0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOyBkPWdvb2dsZS5jb207IHM9YXJjLTIwMTYwODE2Ow0KICAgICAgICBoPWF1dG9jcnlwdDpzdWJqZWN0OnRvOmNvbnRlbnQtbGFuZ3VhZ2U6dXNlci1hZ2VudDptaW1lLXZlcnNpb246ZGF0ZQ0KICAgICAgICAgOm1lc3NhZ2UtaWQ6ZnJvbTpka2ltLXNpZ25hdHVyZTsNCiAgICAgICAgYmg9MlRRZElJak9jWTBFdHJpNUJUU0NUdHkySStlbTVnSG1jOGFXcUNFRDB4ND07DQogICAgICAgIGI9RU9kY3VNYTVPcmIyNVNEWFNpY2huV2pJM3VKeDhEOW82L1UzRlorSWVLRUxndkpiZzd4cU5aeXhocmswUTk4UG9hDQogICAgICAgICBwUE9CV3hWVWgwL2FrMkRZYVhvM2hiRVYxaXQ4d3hMZFR3dTlLeFU1ejdYdE9SVGZRa2xwY1Z5dlFuN1QxQ1JYeDFRZg0KICAgICAgICAgOGdWZzF4RWVxUHBkZWh1YldUSGdMSmdGVWVaMFY4UXZEN1V2ODFZMmlTZjhSd2QzWXlQT1lQdHQyLzYvWURHUTdBRUgNCiAgICAgICAgIHBvWTBmNW0rU0NnRVRRWVc1dlYrYkF1aGI0S3Jmb20wcm56TmR6R1dLcU4yNGVWRlZ3Szcwblcwdy9yM3pZU1pMTW4xDQogICAgICAgICBUVmE1bHpzeWlPZHFyRWZJN3VIWVRtK2RTOFJMd0Z4UzkxSEpFeFIwT1lFVlpqUFg2NXB2NFNkYXhwdUloZ20rRy9JRQ0KICAgICAgICAgWitwZz09DQpBUkMtQXV0aGVudGljYXRpb24tUmVzdWx0czogaT0xOyBteC5nb29nbGUuY29tOw0KICAgICAgIGRraW09cGFzcyBoZWFkZXIuaT1AZmxvd2NyeXB0LWRldi4yMDIxMDExMi5nYXBwc3NtdHAuY29tIGhlYWRlci5zPTIwMjEwMTEyIGhlYWRlci5iPSJkQ1MvNHg4YSI7DQogICAgICAgc3BmPW5ldXRyYWwgKGdvb2dsZS5jb206IDIwOS44NS4yMjAuNDEgaXMgbmVpdGhlciBwZXJtaXR0ZWQgbm9yIGRlbmllZCBieSBiZXN0IGd1ZXNzIHJlY29yZCBmb3IgZG9tYWluIG9mIGNpLnRlc3RzLmdtYWlsQGZsb3djcnlwdC5kZXYpIHNtdHAubWFpbGZyb209Y2kudGVzdHMuZ21haWxAZmxvd2NyeXB0LmRldg0KUmV0dXJuLVBhdGg6IDxjaS50ZXN0cy5nbWFpbEBmbG93Y3J5cHQuZGV2Pg0KUmVjZWl2ZWQ6IGZyb20gbWFpbC1zb3ItZjQxLmdvb2dsZS5jb20gKG1haWwtc29yLWY0MS5nb29nbGUuY29tLiBbMjA5Ljg1LjIyMC40MV0pDQogICAgICAgIGJ5IG14Lmdvb2dsZS5jb20gd2l0aCBTTVRQUyBpZCBwMjJzb3IzODA2NTg1bGppLjMxLjIwMjEuMTIuMTIuMDYuMDkuMjMNCiAgICAgICAgZm9yIDxmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20-DQogICAgICAgIChHb29nbGUgVHJhbnNwb3J0IFNlY3VyaXR5KTsNCiAgICAgICAgU3VuLCAxMiBEZWMgMjAyMSAwNjowOToyMyAtMDgwMCAoUFNUKQ0KUmVjZWl2ZWQtU1BGOiBuZXV0cmFsIChnb29nbGUuY29tOiAyMDkuODUuMjIwLjQxIGlzIG5laXRoZXIgcGVybWl0dGVkIG5vciBkZW5pZWQgYnkgYmVzdCBndWVzcyByZWNvcmQgZm9yIGRvbWFpbiBvZiBjaS50ZXN0cy5nbWFpbEBmbG93Y3J5cHQuZGV2KSBjbGllbnQtaXA9MjA5Ljg1LjIyMC40MTsNCkF1dGhlbnRpY2F0aW9uLVJlc3VsdHM6IG14Lmdvb2dsZS5jb207DQogICAgICAgZGtpbT1wYXNzIGhlYWRlci5pPUBmbG93Y3J5cHQtZGV2LjIwMjEwMTEyLmdhcHBzc210cC5jb20gaGVhZGVyLnM9MjAyMTAxMTIgaGVhZGVyLmI9ImRDUy80eDhhIjsNCiAgICAgICBzcGY9bmV1dHJhbCAoZ29vZ2xlLmNvbTogMjA5Ljg1LjIyMC40MSBpcyBuZWl0aGVyIHBlcm1pdHRlZCBub3IgZGVuaWVkIGJ5IGJlc3QgZ3Vlc3MgcmVjb3JkIGZvciBkb21haW4gb2YgY2kudGVzdHMuZ21haWxAZmxvd2NyeXB0LmRldikgc210cC5tYWlsZnJvbT1jaS50ZXN0cy5nbWFpbEBmbG93Y3J5cHQuZGV2DQpES0lNLVNpZ25hdHVyZTogdj0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOw0KICAgICAgICBkPWZsb3djcnlwdC1kZXYuMjAyMTAxMTIuZ2FwcHNzbXRwLmNvbTsgcz0yMDIxMDExMjsNCiAgICAgICAgaD1mcm9tOm1lc3NhZ2UtaWQ6ZGF0ZTptaW1lLXZlcnNpb246dXNlci1hZ2VudDpjb250ZW50LWxhbmd1YWdlOnRvDQogICAgICAgICA6c3ViamVjdDphdXRvY3J5cHQ7DQogICAgICAgIGJoPTJUUWRJSWpPY1kwRXRyaTVCVFNDVHR5MkkrZW01Z0htYzhhV3FDRUQweDQ9Ow0KICAgICAgICBiPWRDUy80eDhhcEtQSEhpT3JpTlpwaVFyZUlqSHk1aDhUMzhUV1dqUEw3bVhVdGZlbXl6dnN6Y0E0K08zK0F1VkIvYQ0KICAgICAgICAgcHVIQ0VWcGhianBXaFE4QXd5VExtbHdrTmFjamlBaGh2bzJFeVIzRENtZ0xtRlhGRlROYXErNnl0ajA4U1Y3emgvMGMNCiAgICAgICAgIGVRek9GL0NmT1F2Wkl3QmZVUDN2MnNDMU54MVV1NkQxRlZUUE1QMDcwVXVLaVVQNFVncFAveXlPTWpwbjN3aDRIak5ODQogICAgICAgICBLK2pBak9ZSGdUbk1HMEtaOVNCUGdlaHJaektuOVcyeXVjcXJXNFlRTGZIRlVsdSthMmZQY0xqWE5PMTVoTkZhMHNvOA0KICAgICAgICAgcUNxRXY4WURGMVl2VUx4RCtWa1NGYXJTejh6NGd2NnkzZjEyaExKc3YvWFVHTkV3L0RWZUdNenNQSjBPMkxYR0tpWEkNCiAgICAgICAgIDgyMnc9PQ0KWC1Hb29nbGUtREtJTS1TaWduYXR1cmU6IHY9MTsgYT1yc2Etc2hhMjU2OyBjPXJlbGF4ZWQvcmVsYXhlZDsNCiAgICAgICAgZD0xZTEwMC5uZXQ7IHM9MjAyMTAxMTI7DQogICAgICAgIGg9eC1nbS1tZXNzYWdlLXN0YXRlOmZyb206bWVzc2FnZS1pZDpkYXRlOm1pbWUtdmVyc2lvbjp1c2VyLWFnZW50DQogICAgICAgICA6Y29udGVudC1sYW5ndWFnZTp0bzpzdWJqZWN0OmF1dG9jcnlwdDsNCiAgICAgICAgYmg9MlRRZElJak9jWTBFdHJpNUJUU0NUdHkySStlbTVnSG1jOGFXcUNFRDB4ND07DQogICAgICAgIGI9Ry9KdUhHTDVYb3hxNnBRUERTVzE2aTR4ZHl3eUd4bjk4UUlpV3VFdHE4dUFIT0IzenE5SjRIbHVpa2xwdTVuMTJ3DQogICAgICAgICAwbEoreDNQYVkxUUJXdHBmdGVaN08wSG5OMWhBMndNNkpCdmVZMU1IWUpjMUM5T2ViQzNGeVJ2QjhNSElQVjBhajdDTA0KICAgICAgICAgNHNrZ29vMWZoRzFnK3ZBWTNkVnE5aENBeVNPcDcyekMyU25DNzdRbldmTjkyWFJZbDZhUklmc29YaERSbms2ZXdHbjUNCiAgICAgICAgIFBZYUZNY1N4WmRlMjVHN0dvbzY1bFV3eFRrWm01VStnRHlXcTk5U1g0MEVaUnZyVmhWbTlkeU41VDVzM2xxUHMzNDJPDQogICAgICAgICBQU25mTFhVWVJqZkUzRDZiMk93SzR5aGd6aXBkMVM2Y0ZlMlFJWm8wUllLVWlMcVN6WjU0blJmSFhpc0V4M0hCRFdVdA0KICAgICAgICAgS0VkUT09DQpYLUdtLU1lc3NhZ2UtU3RhdGU6IEFPQU01MzEwcmp6OWlSeTRpbVMxaWlvY2JOL0FmU3RneHJLUGxxSGI4YURvaU5PUFo3bU5vVjd1DQoJRSswRnM2b0NrNHhsRXRHRzJRUHRicUZvS2hDb0krMDdHZz09DQpYLUdvb2dsZS1TbXRwLVNvdXJjZTogQUJkaFBKeklXbEV2R09WZE4zc3JXMG1xR2p3OXVxM0VjQmltVnlOMUtMaEE0K05UbFBZT1h2aTZuMk9kaTM4ZmprZVpNL0RJOE5VZFRRPT0NClgtUmVjZWl2ZWQ6IGJ5IDIwMDI6YTJlOjcxMTQ6OiB3aXRoIFNNVFAgaWQgbTIwbXIyNDEyMzk3MGxqYy4yMjkuMTYzOTMxODE2MjMzMzsNCiAgICAgICAgU3VuLCAxMiBEZWMgMjAyMSAwNjowOToyMiAtMDgwMCAoUFNUKQ0KUmV0dXJuLVBhdGg6IDxjaS50ZXN0cy5nbWFpbEBmbG93Y3J5cHQuZGV2Pg0KUmVjZWl2ZWQ6IGZyb20gWzE5Mi4xNjguMS44XSAoMDg5MjYzMTcwNi5zdGF0aWMuY29yYmluYS5ydS4gWzc4LjEwNy4xOTUuMTEzXSkNCiAgICAgICAgYnkgc210cC5nbWFpbC5jb20gd2l0aCBFU01UUFNBIGlkIGgxMXNtMTAzNzU4MmxqYi40Mi4yMDIxLjEyLjEyLjA2LjA5LjIxDQogICAgICAgIGZvciA8Zmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tPg0KICAgICAgICAodmVyc2lvbj1UTFMxXzMgY2lwaGVyPVRMU19BRVNfMTI4X0dDTV9TSEEyNTYgYml0cz0xMjgvMTI4KTsNCiAgICAgICAgU3VuLCAxMiBEZWMgMjAyMSAwNjowOToyMSAtMDgwMCAoUFNUKQ0KRnJvbTogQ0kgVGVzdCA2OCA8Y2kudGVzdHMuZ21haWxAZmxvd2NyeXB0LmRldj4NClgtR29vZ2xlLU9yaWdpbmFsLUZyb206IENJIFRlc3QgNjggPHNvbWUuc2VuZGVyQHRlc3QuY29tPg0KTWVzc2FnZS1JRDogPDBmYjM0OWYwLWViZTQtYTM3Ni1hZmU4LTY2NzY2OTNhOThmOEB0ZXN0LmNvbT4NCkRhdGU6IFN1biwgMTIgRGVjIDIwMjEgMTc6MDk6MTkgKzAzMDANCk1JTUUtVmVyc2lvbjogMS4wDQpVc2VyLUFnZW50OiBNb3ppbGxhLzUuMCAoV2luZG93cyBOVCA2LjM7IFdpbjY0OyB4NjQ7IHJ2OjkxLjApIEdlY2tvLzIwMTAwMTAxDQogVGh1bmRlcmJpcmQvOTEuMy4yDQpDb250ZW50LUxhbmd1YWdlOiBlbi1VUw0KVG86IGZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbQ0KU3ViamVjdDogVGh1bmRlcmJpcmQgOTEuMy4yIGh0bWwtZm9ybWF0dGVkIHNpZ25lZCBvbmx5IHdpdGgga2V5IGF0dGFjaGVkDQpBdXRvY3J5cHQ6IGFkZHI9c29tZS5zZW5kZXJAdGVzdC5jb207IGtleWRhdGE9DQogeHNCTkJHRzFuT0lCQ0FDMUNDanZMMHdKNmJZQXhQMXhXQ0RKR2U0eWtiUmw5Uk54SjhGMzFiYWlmTFB2RkRJSmRwQjdzeE9GDQogN1d0RldTNlRxZlpQUmRSaEtLQXZxbEdBTE5FTUpGZ2kyb0s3bEVsWnhyUEI4T0ZCaWkrb2E2SW9Rbi9xV0srRlhBRW4zazdUDQogZWl1RUtqbVlqTXlFMkR2REI1UDhwTVVsdXdaaHZCOEgvcm1WTExmaUs4cW0vZm5RZFU2UkNmUTUrK0lSNzhrTWRPcmZSazZlDQogTGJ1TTZEM2N5cVMrY0JxNFJXZCtWYko3QlQ2U0pyUkpWRFFDejNEVmxDUk9Kd3hXNjhSUnB5Sy9MWTQ4MWdyWFV4cXpOb0RFDQogSUM5Z1ZXRDVYdEJENkRtWjlMZUgyZEVqMG5tZ01zTEJxRklVV0djY3lHT3BlSFpnbDMydERneWhKYktPakRTTXdRQlZBQkVCDQogQUFITkcwdGxlVEVnUEhOdmJXVXVjMlZ1WkdWeVFIUmxjM1F1WTI5dFBzTEFqUVFRQVFnQUlBVUNZYldjNGdZTENRY0lBd0lFDQogRlFnS0FnUVdBZ0VBQWhrQkFoc0RBaDRCQUNFSkVDQS9ybkIyQUZPQkZpRUVLN0laZDI4anpranJ1R0NjSUQrdWNIWUFVNEdXDQogeWdmL1NXb3M0YVB3eFgrR2REcUZ0R1h5UkxHUmg3bEwxOVNZOHZaZWZqVVc5NHJubCszQVR1RXg1dnRESXJqV2xEa3J5WXdvDQogYzY0QjZjODZkdjZuTFArV1BuUURNMm42bVJlanZOUStiMktHbW44SExhN21aN0krcGY0Z2ZmOVQ5ampmd2ExVFpBODcwTC9HDQogUUhpQUIrNkxmMnVQdXdINHdqejY2SlNkUlNnT1Ruc0drZ0lzSXJaRGRWRUdocWNIZE5lTDkrb1RSNU5xdEJVQ2FTOHBMUFNIDQogT3FURklTSmhjbEt0VWxCcHd6OFhMcEczZkRkWlpDVEdzcDFkSTBQSzJ4U1pZOVUvaUVSRWVxS3p5cU9Pb2FZQmRQaGRwR3VwDQogM1FHRUZJUkFtMGRyZU1vWnB2U0FpaEFDNHdONk5HY2NGR2E5UjIyeXRYbFNmTEQwb054TmVRZEEvODdBVFFSaHRaemlBUWdBDQogMG1tdys3MjdTZFF3Ynd0aTdBYUdRckRvN1RweXRGS055cmxaVk1tRVEyZDc3S0RZR2tsU1FtWTJTZzZYTDdIQW90ZlNWQm5RDQogc2kvRnp2d3lDWDJxUjlVWEMvVXB2bTRhVlFXd2dXM1Y4TmVZWmlSMHBXekpXajYxaGloQVdDUmpLQm5EZGVEd09SRWlCYWt1DQogeU0ra3JuSGtmL3ZkOStraDRaWnlRamU3NE13NjladVp2NXdrR01oYVo1Um9PUytFNW0xbHVqUE5vLzFhdE5GNXBGbFZ1eXh2DQogOGRlZEZUVUx6UFBFNlJna2VaRDFXK3ptTUlxRkI3cFJNeTE2VTc5eGNyenJWZWhuaDJkZDRBUTRzcGtnblI0c1FaNlhobnZWDQogV0VZUFU5VDU5WGt4TG9XTWxkcTFYSUZodWYvTHQzUGhzR2t1N2E1NXducERNQTUwQ0pNUFNRQVJBUUFCd3NCMkJCZ0JDQUFKDQogQlFKaHRaemlBaHNNQUNFSkVDQS9ybkIyQUZPQkZpRUVLN0laZDI4anpranJ1R0NjSUQrdWNIWUFVNEgvRUFmOUdYbHJhSlltDQogbzFVcUtsVVI2V3VGblRvbk52Vlk2KzgzdWVrdUhYSGoxeEJQQUZZenVWRmQ5ZG55aFE4b2I1VkFmU1NJQno0RVYyNDNzdGVRDQogTDhHK3pVNXQwdU9vU0tGSGlCLzM2YTZ0VzJnQW1ya3RHa3lFUnRYRU5tZ0FVanN5MkM4bnVBWnE5b3lEQ05VWlQ2VS8vU3VrDQogRXp2VXJrQ21WWlBwRS9zZmlFdnZvK3BnRnRhV1JHS2I2aUJOYWE3Q1hvc0ljSkRXaTUrT0RhTTJ4ZVdrYmZackpPQnFtS0F1DQogVEdxYUdyTjMza1JFUzNmdXBZc3Q3UTdIV0owaXVTdjZxb2hkdC9ZdWJ2alhrM3lPL1hudXFES0xIVnJkWWdTSmtSbnZ2eDNmDQogVlpJL20xc1gxKzN3VkNQeVE5aEswaXgxWlRaZEdZZDc2ZHVuL2ZFbXVOUUhCZz09DQpDb250ZW50LVR5cGU6IG11bHRpcGFydC9zaWduZWQ7IG1pY2FsZz1wZ3Atc2hhMjU2Ow0KIHByb3RvY29sPSJhcHBsaWNhdGlvbi9wZ3Atc2lnbmF0dXJlIjsNCiBib3VuZGFyeT0iLS0tLS0tLS0tLS0tMHVGaW02ck9JNWZaZWJXdDBxazhuWXhkIg0KDQpUaGlzIGlzIGFuIE9wZW5QR1AvTUlNRSBzaWduZWQgbWVzc2FnZSAoUkZDIDQ4ODAgYW5kIDMxNTYpDQotLS0tLS0tLS0tLS0tLTB1RmltNnJPSTVmWmViV3QwcWs4bll4ZA0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7IGJvdW5kYXJ5PSItLS0tLS0tLS0tLS0waTB1d08wNzVaUTBOamtBMXJKQUNrc2YiOw0KIHByb3RlY3RlZC1oZWFkZXJzPSJ2MSINCkZyb206IENJIFRlc3QgNjggPHNvbWUuc2VuZGVyQHRlc3QuY29tPg0KVG86IGZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbQ0KTWVzc2FnZS1JRDogPDBmYjM0OWYwLWViZTQtYTM3Ni1hZmU4LTY2NzY2OTNhOThmOEB0ZXN0LmNvbT4NClN1YmplY3Q6IFRodW5kZXJiaXJkIDkxLjMuMiBodG1sLWZvcm1hdHRlZCBzaWduZWQgb25seSB3aXRoIGtleSBhdHRhY2hlZA0KDQotLS0tLS0tLS0tLS0tLTBpMHV3TzA3NVpRME5qa0ExckpBQ2tzZg0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7IGJvdW5kYXJ5PSItLS0tLS0tLS0tLS10YkVuVW1HNktENDdFcmpDMzBROTgzVEIiDQoNCi0tLS0tLS0tLS0tLS0tdGJFblVtRzZLRDQ3RXJqQzMwUTk4M1RCDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD1VVEYtOA0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogcXVvdGVkLXByaW50YWJsZQ0KDQo8aHRtbD4NCiAgPGhlYWQ-DQoNCiAgICA8bWV0YSBodHRwLWVxdWl2PTNEImNvbnRlbnQtdHlwZSIgY29udGVudD0zRCJ0ZXh0L2h0bWw7IGNoYXJzZXQ9M0RVVEY9DQotOCI-DQogIDwvaGVhZD4NCiAgPGJvZHk-DQogICAgPHA-PGI-MTIzNDwvYj48YnI-DQogICAgPC9wPg0KICA8L2JvZHk-DQo8L2h0bWw-DQotLS0tLS0tLS0tLS0tLXRiRW5VbUc2S0Q0N0VyakMzMFE5ODNUQg0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wZ3Ata2V5czsgbmFtZT0iT3BlblBHUF8weDIwM0ZBRTcwNzYwMDUzODEuYXNjIg0KQ29udGVudC1EaXNwb3NpdGlvbjogYXR0YWNobWVudDsgZmlsZW5hbWU9Ik9wZW5QR1BfMHgyMDNGQUU3MDc2MDA1MzgxLmFzYyINCkNvbnRlbnQtRGVzY3JpcHRpb246IE9wZW5QR1AgcHVibGljIGtleQ0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogcXVvdGVkLXByaW50YWJsZQ0KDQotLS0tLUJFR0lOIFBHUCBQVUJMSUMgS0VZIEJMT0NLLS0tLS0NCg0KeHNCTkJHRzFuT0lCQ0FDMUNDanZMMHdKNmJZQXhQMXhXQ0RKR2U0eWtiUmw5Uk54SjhGMzFiYWlmTFB2RkRJSg0KZHBCN3N4T0Y3V3RGV1M2VHFmWlBSZFJoS0tBdnFsR0FMTkVNSkZnaTJvSzdsRWxaeHJQQjhPRkJpaStvYTZJbw0KUW4vcVdLK0ZYQUVuM2s3VGVpdUVLam1Zak15RTJEdkRCNVA4cE1VbHV3Wmh2QjhIL3JtVkxMZmlLOHFtL2ZuUQ0KZFU2UkNmUTUrK0lSNzhrTWRPcmZSazZlTGJ1TTZEM2N5cVMrY0JxNFJXZCtWYko3QlQ2U0pyUkpWRFFDejNEVg0KbENST0p3eFc2OFJScHlLL0xZNDgxZ3JYVXhxek5vREVJQzlnVldENVh0QkQ2RG1aOUxlSDJkRWowbm1nTXNMQg0KcUZJVVdHY2N5R09wZUhaZ2wzMnREZ3loSmJLT2pEU013UUJWQUJFQkFBSE5HMHRsZVRFZ1BITnZiV1V1YzJWdQ0KWkdWeVFIUmxjM1F1WTI5dFBzTEFqUVFRQVFnQUlBVUNZYldjNGdZTENRY0lBd0lFRlFnS0FnUVdBZ0VBQWhrQg0KQWhzREFoNEJBQ0VKRUNBL3JuQjJBRk9CRmlFRUs3SVpkMjhqemtqcnVHQ2NJRCt1Y0hZQVU0R1d5Z2YvU1dvcw0KNGFQd3hYK0dkRHFGdEdYeVJMR1JoN2xMMTlTWTh2WmVmalVXOTRybmwrM0FUdUV4NXZ0RElyaldsRGtyeVl3bw0KYzY0QjZjODZkdjZuTFArV1BuUURNMm42bVJlanZOUStiMktHbW44SExhN21aN0krcGY0Z2ZmOVQ5ampmd2ExVA0KWkE4NzBML0dRSGlBQis2TGYydVB1d0g0d2p6NjZKU2RSU2dPVG5zR2tnSXNJclpEZFZFR2hxY0hkTmVMOStvVA0KUjVOcXRCVUNhUzhwTFBTSE9xVEZJU0poY2xLdFVsQnB3ejhYTHBHM2ZEZFpaQ1RHc3AxZEkwUEsyeFNaWTlVLw0KaUVSRWVxS3p5cU9Pb2FZQmRQaGRwR3VwM1FHRUZJUkFtMGRyZU1vWnB2U0FpaEFDNHdONk5HY2NGR2E5UjIyeQ0KdFhsU2ZMRDBvTnhOZVFkQS84N0FUUVJodFp6aUFRZ0EwbW13KzcyN1NkUXdid3RpN0FhR1FyRG83VHB5dEZLTg0KeXJsWlZNbUVRMmQ3N0tEWUdrbFNRbVkyU2c2WEw3SEFvdGZTVkJuUXNpL0Z6dnd5Q1gycVI5VVhDL1Vwdm00YQ0KVlFXd2dXM1Y4TmVZWmlSMHBXekpXajYxaGloQVdDUmpLQm5EZGVEd09SRWlCYWt1eU0ra3JuSGtmL3ZkOStraA0KNFpaeVFqZTc0TXc2OVp1WnY1d2tHTWhhWjVSb09TK0U1bTFsdWpQTm8vMWF0TkY1cEZsVnV5eHY4ZGVkRlRVTA0KelBQRTZSZ2tlWkQxVyt6bU1JcUZCN3BSTXkxNlU3OXhjcnpyVmVobmgyZGQ0QVE0c3BrZ25SNHNRWjZYaG52Vg0KV0VZUFU5VDU5WGt4TG9XTWxkcTFYSUZodWYvTHQzUGhzR2t1N2E1NXducERNQTUwQ0pNUFNRQVJBUUFCd3NCMg0KQkJnQkNBQUpCUUpodFp6aUFoc01BQ0VKRUNBL3JuQjJBRk9CRmlFRUs3SVpkMjhqemtqcnVHQ2NJRCt1Y0hZQQ0KVTRIL0VBZjlHWGxyYUpZbW8xVXFLbFVSNld1Rm5Ub25OdlZZNis4M3Vla3VIWEhqMXhCUEFGWXp1VkZkOWRueQ0KaFE4b2I1VkFmU1NJQno0RVYyNDNzdGVRTDhHK3pVNXQwdU9vU0tGSGlCLzM2YTZ0VzJnQW1ya3RHa3lFUnRYRQ0KTm1nQVVqc3kyQzhudUFacTlveURDTlVaVDZVLy9TdWtFenZVcmtDbVZaUHBFL3NmaUV2dm8rcGdGdGFXUkdLYg0KNmlCTmFhN0NYb3NJY0pEV2k1K09EYU0yeGVXa2JmWnJKT0JxbUtBdVRHcWFHck4zM2tSRVMzZnVwWXN0N1E3SA0KV0owaXVTdjZxb2hkdC9ZdWJ2alhrM3lPL1hudXFES0xIVnJkWWdTSmtSbnZ2eDNmVlpJL20xc1gxKzN3VkNQeQ0KUTloSzBpeDFaVFpkR1lkNzZkdW4vZkVtdU5RSEJnPTNEPTNEDQo9M0R5REJwDQotLS0tLUVORCBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tDQotLS0tLS0tLS0tLS0tLXRiRW5VbUc2S0Q0N0VyakMzMFE5ODNUQi0tDQoNCg0KLS0tLS0tLS0tLS0tLS0waTB1d08wNzVaUTBOamtBMXJKQUNrc2YtLQ0KDQotLS0tLS0tLS0tLS0tLTB1RmltNnJPSTVmWmViV3QwcWs4bll4ZA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wZ3Atc2lnbmF0dXJlOyBuYW1lPSJPcGVuUEdQX3NpZ25hdHVyZS5hc2MiDQpDb250ZW50LURlc2NyaXB0aW9uOiBPcGVuUEdQIGRpZ2l0YWwgc2lnbmF0dXJlDQpDb250ZW50LURpc3Bvc2l0aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT0iT3BlblBHUF9zaWduYXR1cmUiDQoNCi0tLS0tQkVHSU4gUEdQIFNJR05BVFVSRS0tLS0tDQoNCndzQjVCQUFCQ0FBakZpRUVLN0laZDI4anpranJ1R0NjSUQrdWNIWUFVNEVGQW1HMkFvOEZBd0FBQUFBQUNna1FJRCt1Y0hZQVU0R3kNCmlRZ0FwdC9LRXRhcExTcHY0NWUyL0NsSjBwRnp1dEc2aENZbkpoNnZFTFY5Z0hhUzc1VnUzbkJaUFV6UFVLODUvaUpObG5LSDIzYTENCjVIWldiMGt6VUJ4TnFFM2JiRldHL3owVjBWcXVMaHZXQ1dIaDVEcGlJUlFvbjhHNnhKU2VvVytIcnVTekZacnV1OUJYVDlZT1JpV2kNClpSczJLSU1QaUdLOXh4ZWN5WnRVODF1WUtWc0dsTy9DeUUvM3dYY2VCS1RrTnQrK1FTeE9hVEd6Y1pOQVNacldndVN5M3o1S1NzbFUNClJYUmJmUW9MVUg3bmtvUU9sZnBlWWdGbGliK0s3a09OV3NGbjhnSFMxTExmMGtRK2RvdWpaTEx1NERsOHdBaTdtWTNaa1NQOStSeVcNClo2OWtrYjBSUnRFK29GU1J4WGJzOENMRkZJMG1NOVNOeGowQ0ltV3pCUT09DQo9ZnRmTg0KLS0tLS1FTkQgUEdQIFNJR05BVFVSRS0tLS0tDQoNCi0tLS0tLS0tLS0tLS0tMHVGaW02ck9JNWZaZWJXdDBxazhuWXhkLS0NCg==", + "historyId": "1332117", + "internalDate": "1639318159000" + } +} \ No newline at end of file diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts index 39d551417e5..7dcf1f3e5f2 100644 --- a/test/source/tests/decrypt.ts +++ b/test/source/tests/decrypt.ts @@ -331,7 +331,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te })); ava.default('decrypt - thunderbird - signedHtml verifyDetached doesn\'t duplicate PGP key section', testWithBrowser('compatibility', async (t, browser) => { - const threadId = '1754cfd1b2f1d6e5'; + const threadId = '17daefa0eb077da6'; const acctEmail = 'flowcrypt.compatibility@gmail.com'; const inboxPage = await browser.newPage(t, TestUrls.extension(`chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}&threadId=${threadId}`)); await inboxPage.waitAll('iframe'); @@ -342,7 +342,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te })); ava.default('decrypt - thunderbird - signedMsg verifyDetached doesn\'t duplicate PGP key section', testWithBrowser('compatibility', async (t, browser) => { - const threadId = '1754cfc37886899e'; + const threadId = '17dad75e63e47f97'; const acctEmail = 'flowcrypt.compatibility@gmail.com'; const inboxPage = await browser.newPage(t, TestUrls.extension(`chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}&threadId=${threadId}`)); await inboxPage.waitAll('iframe'); @@ -364,44 +364,45 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te })); ava.default('decrypt - thunderbird - signed text is recognized', testWithBrowser('compatibility', async (t, browser) => { - const threadId = '1754cfc37886899e'; + const threadId = '17dad75e63e47f97'; const acctEmail = 'flowcrypt.compatibility@gmail.com'; const inboxPage = await browser.newPage(t, TestUrls.extension(`chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}&threadId=${threadId}`)); await inboxPage.waitAll('iframe', { timeout: 2 }); const urls = await inboxPage.getFramesUrls(['/chrome/elements/pgp_block.htm'], { sleep: 10, appearIn: 20 }); expect(urls.length).to.equal(1); const url = urls[0].split('/chrome/elements/pgp_block.htm')[1]; - const signature = ['dhartley@verdoncollege.school.nz', 'matching signature']; + const signature = ['some.sender@test.com', 'matching signature']; await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params: url, content: ['1234'], signature }); })); ava.default('decrypt - fetched pubkey is automatically saved to contacts', testWithBrowser('compatibility', async (t, browser) => { - const msgId = '1754cfc37886899e'; + const msgId = '17dad75e63e47f97'; const acctEmail = 'flowcrypt.compatibility@gmail.com'; + const senderEmail = 'some.sender@test.com'; + const acctAttr = acctEmail.replace(/[\.@]/g, ''); + const senderAttr = senderEmail.replace(/[\.@]/g, ''); { const settingsPage = await browser.newPage(t, TestUrls.extensionSettings(acctEmail)); await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); const contactsFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, '@action-open-contacts-page', ['contacts.htm', 'placement=settings']); await contactsFrame.waitAll('@page-contacts'); await Util.sleep(1); - expect(await contactsFrame.isElementPresent('@action-show-email-flowcryptcompatibilitygmailcom')).to.be.true; - expect(await contactsFrame.isElementPresent('@action-show-email-dhartleyverdoncollegeschoolnz')).to.be.false; + expect(await contactsFrame.isElementPresent(`@action-show-email-${acctAttr}`)).to.be.true; + expect(await contactsFrame.isElementPresent(`@action-show-email-${senderAttr}`)).to.be.false; } - const params = `?frameId=none&acctEmail=${acctEmail}&msgId=${msgId}&signature=___cu_true___&senderEmail=dhartley@verdoncollege.school.nz`; + const params = `?frameId=none&acctEmail=${acctEmail}&msgId=${msgId}&signature=___cu_true___&senderEmail=${senderEmail}`; await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params, content: ['1234'] }); - // the fetched pubkey is saved to ContactStore asynchronously, so let's wait a little - await Util.sleep(1); { const settingsPage = await browser.newPage(t, TestUrls.extensionSettings(acctEmail)); await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); const contactsFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, '@action-open-contacts-page', ['contacts.htm', 'placement=settings']); await contactsFrame.waitAll('@page-contacts'); await Util.sleep(1); - expect(await contactsFrame.isElementPresent('@action-show-email-flowcryptcompatibilitygmailcom')).to.be.true; - expect(await contactsFrame.isElementPresent('@action-show-email-dhartleyverdoncollegeschoolnz')).to.be.true; - await contactsFrame.waitAndClick('@action-show-email-dhartleyverdoncollegeschoolnz'); - // contains newly fetched key - await contactsFrame.waitForContent('@page-contacts', 'openpgp - active - DC26 454A FB71 D18E ABBA D73D 1C7E 6D3C 5563 A941'); + expect(await contactsFrame.isElementPresent(`@action-show-email-${acctAttr}`)).to.be.true; + expect(await contactsFrame.isElementPresent(`@action-show-email-${senderAttr}`)).to.be.true; + await contactsFrame.waitAndClick(`@action-show-email-${senderAttr}`); + // contains the newly fetched key + await contactsFrame.waitForContent('@page-contacts', 'openpgp - active - 2BB2 1977 6F23 CE48 EBB8 609C 203F AE70 7600 5381'); } })); @@ -463,9 +464,9 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te })); ava.default('decrypt - re-fetch signed-only message from API on non-fatal verification error', testWithBrowser('compatibility', async (t, browser) => { - const msgId = '1754cfd1b2f1d6e5'; + const msgId = '17daefa0eb077da6'; const acctEmail = 'flowcrypt.compatibility@gmail.com'; - const signerEmail = 'dhartley@verdoncollege.school.nz'; + const signerEmail = 'some.sender@test.com'; const data = await GoogleData.withInitializedData(acctEmail); const msg = data.getMessage(msgId)!; const signature = Buf.fromBase64Str(msg!.raw!).toUtfStr() @@ -475,7 +476,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params, content: [], // todo: #4164 I would expect '1234' here - signature: ['dhartley@verdoncollege.school.nz', 'matching signature'] + signature: ['some.sender@test.com', 'matching signature'] }); })); diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 074a4e8b537..5401830d7c4 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -814,13 +814,13 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY ava.default('[unit][MsgUtil.verifyDetached] verifies Thunderbird html signed message', async t => { const data = await GoogleData.withInitializedData('flowcrypt.compatibility@gmail.com'); - const msg: GmailMsg = data.getMessage('1754cfd1b2f1d6e5')!; + const msg: GmailMsg = data.getMessage('17daefa0eb077da6')!; const msgText = Buf.fromBase64Str(msg!.raw!).toUtfStr(); const sigText = msgText .match(/\-\-\-\-\-BEGIN PGP SIGNATURE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s)![0] .replace(/=\r\n/g, '').replace(/=3D/g, '='); const plaintext = msgText - .match(/Content\-Type: multipart\/mixed; boundary="vv8xtFOOk2SxbnIpwvxkobfET7PglPfc3".*\-\-vv8xtFOOk2SxbnIpwvxkobfET7PglPfc3\-\-\r?\n/s)![0] + .match(/Content\-Type: multipart\/mixed; boundary="------------0i0uwO075ZQ0NjkA1rJACksf".*--------------0i0uwO075ZQ0NjkA1rJACksf--\r?\n/s)![0] .replace(/\r?\n/g, '\r\n')!; const pubkey = plaintext .match(/\-\-\-\-\-BEGIN PGP PUBLIC KEY BLOCK\-\-\-\-\-.*\-\-\-\-\-END PGP PUBLIC KEY BLOCK\-\-\-\-\-/s)![0] @@ -832,13 +832,13 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY ava.default('[unit][MsgUtil.verifyDetached] verifies Thunderbird text signed message', async t => { const data = await GoogleData.withInitializedData('flowcrypt.compatibility@gmail.com'); - const msg: GmailMsg = data.getMessage('1754cfc37886899e')!; + const msg: GmailMsg = data.getMessage('17dad75e63e47f97')!; const msgText = Buf.fromBase64Str(msg!.raw!).toUtfStr(); const sigText = msgText .match(/\-\-\-\-\-BEGIN PGP SIGNATURE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s)![0] .replace(/=\r\n/g, '').replace(/=3D/g, '='); const plaintext = msgText - .match(/Content\-Type: multipart\/mixed; boundary="XWwnusC4nxhk2LRvLCC6Skcb8YiKQ4Lu0".*\-\-XWwnusC4nxhk2LRvLCC6Skcb8YiKQ4Lu0\-\-\r?\n/s)![0] + .match(/Content\-Type: multipart\/mixed; boundary="------------FQ7CfxuiGriwTfTfyc4i1ppF".*-------------FQ7CfxuiGriwTfTfyc4i1ppF--\r?\n/s)![0] .replace(/\r?\n/g, '\r\n')!; const pubkey = plaintext .match(/\-\-\-\-\-BEGIN PGP PUBLIC KEY BLOCK\-\-\-\-\-.*\-\-\-\-\-END PGP PUBLIC KEY BLOCK\-\-\-\-\-/s)![0] @@ -874,26 +874,24 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY ava.default(`[unit][MsgUtil.verifyDetached] returns non-fatal error when signature doesn't match`, async t => { const sigText = Buf.fromUtfStr(`-----BEGIN PGP SIGNATURE----- -wsD5BAABCAAjFiEE3CZFSvtx0Y6rutc9HH5tPFVjqUEFAl+QotYFAwAAAAAACgkQHH5tPFVjqUF4 -xgv+MrdQ07MfCVU93ptZg+S+OOkQ1AcZxGFdiivs10KkNGtLm9s+w/iEUAySSWbtKjbLV6O3AYvC -QFKsFRFr17Ekz6mSPj99zifFMBvTOIAev/d08dmX0kGd6YlP+GyZL3Wqcgy1T1H3obgOmToDtk7R -V52Ki1aTJYH/Z7v6PsQRWn8emfH/yGYplBhzZy2XjO6UIar9T8wtAJOd6+Ii2sfyGyEPjzGckLaR -JZOxQ4jpJJUszz2WsvLNwtKoqwV15Eg3oxZzHWYE8P63xXoE4G762604SIqv/ggyQZTt/Es6Scun -A1BJflFm+cHzQTW2yQfwCCvlzEZNiNwXfwGfV99K5iG1eW3lv7sMLJnitwTidNIlD5LTNdeUnTXj -XJvkEQsyTUI4qbzzJbUNYz7lraizC2nPiwFzLv692mS0urtD3mUhOBA9hZwk3l/20GsGia0FeUIS -E1d8Vh/Ey7IJ8TXbfFrdv5ZP3HqMK0089SooZwx/GN2QIaOYQXsS0u7IFNhU\n=q5Sf +wsB5BAABCAAjFiEEK7IZd28jzkjruGCcID+ucHYAU4EFAmG1nzIFAwAAAAAACgkQID+ucHYAU4H1 +9AgAmi5QUmrzlMa/V8SeEv7VydA3v7Hca/EM18o4ot/ygQgS1BoCm9tAajOGWgzo7eEJwDK8LRj2 +c/XcKWExxcqkLjiem7CdePbi/xr5jMsPYzOlMtcFaD3zY9h8zabiiGM0kIpT8PVCofgFJMqQdByr +gF0NuioMzAiCY+W9aiaSzquH9FVVE+C4bwsU4leTkANDGi05XBUIYaocNilHnUghG6DyFWS6qYFW +cU4SvRcN5yDDUUjrtFJqp2a2Cs76KgbBr3KQcD42EypUL4/ZS+7/4MN4SA05R/mMtmfK4HwAKcC2 +jSB6A93JmnQGIkAem/kzGkKclmfAdGfc4FS+3Cn+6Q==Xmrz -----END PGP SIGNATURE-----`); const data = await GoogleData.withInitializedData('flowcrypt.compatibility@gmail.com'); - const msg = data.getMessage('1754cfc37886899e')!; + const msg = data.getMessage('17dad75e63e47f97')!; const msgText = Buf.fromBase64Str(msg!.raw!).toUtfStr(); { - const dhartleyPubkey = msgText + const pubkey = msgText .match(/\-\-\-\-\-BEGIN PGP PUBLIC KEY BLOCK\-\-\-\-\-.*\-\-\-\-\-END PGP PUBLIC KEY BLOCK\-\-\-\-\-/s)![0] .replace(/=\r\n/g, '').replace(/=3D/g, '='); const resultRightKey = await MsgUtil.verifyDetached({ plaintext: Buf.fromUtfStr('some irrelevant text'), sigText, - verificationPubs: [dhartleyPubkey] + verificationPubs: [pubkey] }); expect(resultRightKey.match).to.be.false; expect(resultRightKey.error).to.not.be.undefined; From cb682af9babd42c7800ab6decb77352ad76efae1 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 15 Dec 2021 06:57:41 +0000 Subject: [PATCH 27/31] moved key sorting to platform-independent module --- extension/js/common/core/crypto/key.ts | 9 +++++++++ extension/js/common/platform/store/contact-store.ts | 12 +----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index b58af65494c..f3b37c86859 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -429,4 +429,13 @@ export class KeyUtil { return fetched.lastModified > stored.lastModified; }; + public static sortPubkeyInfos = (pubkeyInfos: PubkeyInfo[]): PubkeyInfo[] => { + return pubkeyInfos.sort((a, b) => KeyUtil.getSortValue(b) - KeyUtil.getSortValue(a)); + }; + + private static getSortValue = (pubinfo: PubkeyInfo): number => { + const expirationSortValue = (typeof pubinfo.pubkey.expiration === 'undefined') ? Infinity : pubinfo.pubkey.expiration!; + // sort non-revoked first, then non-expired + return (pubinfo.revoked || pubinfo.pubkey.revoked) ? -Infinity : expirationSortValue; + }; } diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index db9098fad65..16546c7f6f0 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -453,16 +453,6 @@ export class ContactStore extends AbstractStore { }); }; - public static sortPubkeyInfos = (pubkeyInfos: PubkeyInfo[]): PubkeyInfo[] => { - return pubkeyInfos.sort((a, b) => ContactStore.getSortValue(b) - ContactStore.getSortValue(a)); - }; - - public static getSortValue = (pubinfo: PubkeyInfo): number => { - const expirationSortValue = (typeof pubinfo.pubkey.expiration === 'undefined') ? Infinity : pubinfo.pubkey.expiration!; - // sort non-revoked first, then non-expired - return (pubinfo.revoked || pubinfo.pubkey.revoked) ? -Infinity : expirationSortValue; - }; - private static sortKeys = async (pubkeys: Pubkey[], revocations: Revocation[]): Promise => { // parse the keys const pubkeyInfos = await Promise.all(pubkeys.map(async (pubkey) => { @@ -470,7 +460,7 @@ export class ContactStore extends AbstractStore { const revoked = pk.revoked || revocations.some(r => ContactStore.equalFingerprints(pk.id, r.fingerprint)); return { lastCheck: pubkey.lastCheck || undefined, pubkey: pk, revoked }; })); - return ContactStore.sortPubkeyInfos(pubkeyInfos); + return KeyUtil.sortPubkeyInfos(pubkeyInfos); }; private static getPubkeyId = ({ id, type }: { id: string, type: string }): string => { From e1415f039b3f2d8387c69501c1231ab90ddf11ef Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 15 Dec 2021 11:28:21 +0000 Subject: [PATCH 28/31] niceties --- test/source/tests/unit-node.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 5401830d7c4..b989345772a 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -782,29 +782,29 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY .match(/\-\-\-\-\-BEGIN PGP SIGNED MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s)![0]; const encryptedData = Buf.fromUtfStr(enc); // actual key the message was signed with - const pubkey = testConstants.pubkey2864E326A5BE488A; + const olderVersionOfPubkey = testConstants.pubkey2864E326A5BE488A; // better key - const betterKey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: FlowCrypt Email Encryption [BUILD_REPLACEABLE_VERSION]\r\nComment: Seamlessly send and receive encrypted email\r\n\r\nxjMEYZeW2RYJKwYBBAHaRw8BAQdAT5QfLVP3y1yukk3MM/oiuXLNe1f9az5M\r\nBnOlKdF0nKnNJVNvbWVib2R5IDxTYW1zNTBzYW1zNTBzZXB0QEdtYWlsLkNv\r\nbT7CjwQQFgoAIAUCYZeW2QYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJS27QEA7pFlkLfD0KFQ\r\nsH/dwb/NPzn5zCi2L9gjPAC3d8gv1fwA/0FjAy/vKct4D7QH8KwtEGQns5+D\r\nP1WxDr4YI2hp5TkAzjgEYZeW2RIKKwYBBAGXVQEFAQEHQKNLY/bXrhJMWA2+\r\nWTjk3I7KhawyZfLomJ4hovqr7UtOAwEIB8J4BBgWCAAJBQJhl5bZAhsMACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJQnpgD/c1CzfS3YzJUx\r\nnFMrhjiE0WVgqOV/3CkfI4m4RA30QUIA/ju8r4AD2h6lu3Mx/6I6PzIRZQty\r\nLvTkcu4UKodZa4kK\r\n=7C4A\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n"; + const newlyCreatedPubkey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: FlowCrypt Email Encryption [BUILD_REPLACEABLE_VERSION]\r\nComment: Seamlessly send and receive encrypted email\r\n\r\nxjMEYZeW2RYJKwYBBAHaRw8BAQdAT5QfLVP3y1yukk3MM/oiuXLNe1f9az5M\r\nBnOlKdF0nKnNJVNvbWVib2R5IDxTYW1zNTBzYW1zNTBzZXB0QEdtYWlsLkNv\r\nbT7CjwQQFgoAIAUCYZeW2QYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJS27QEA7pFlkLfD0KFQ\r\nsH/dwb/NPzn5zCi2L9gjPAC3d8gv1fwA/0FjAy/vKct4D7QH8KwtEGQns5+D\r\nP1WxDr4YI2hp5TkAzjgEYZeW2RIKKwYBBAGXVQEFAQEHQKNLY/bXrhJMWA2+\r\nWTjk3I7KhawyZfLomJ4hovqr7UtOAwEIB8J4BBgWCAAJBQJhl5bZAhsMACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJQnpgD/c1CzfS3YzJUx\r\nnFMrhjiE0WVgqOV/3CkfI4m4RA30QUIA/ju8r4AD2h6lu3Mx/6I6PzIRZQty\r\nLvTkcu4UKodZa4kK\r\n=7C4A\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n"; { - const decrypted1 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [pubkey, betterKey] }); + const decrypted1 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [olderVersionOfPubkey, newlyCreatedPubkey] }); expect(decrypted1.success).to.equal(true); const verifyRes1 = (decrypted1 as DecryptSuccess).signature!; expect(verifyRes1.match).to.be.true; } { - const decrypted2 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [betterKey, pubkey] }); + const decrypted2 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [newlyCreatedPubkey, olderVersionOfPubkey] }); expect(decrypted2.success).to.equal(true); const verifyRes2 = (decrypted2 as DecryptSuccess).signature!; expect(verifyRes2.match).to.be.true; } { - const decrypted3 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [pubkey] }); + const decrypted3 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [olderVersionOfPubkey] }); expect(decrypted3.success).to.equal(true); const verifyRes3 = (decrypted3 as DecryptSuccess).signature!; expect(verifyRes3.match).to.be.true; } { - const decrypted4 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [betterKey] }); + const decrypted4 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [newlyCreatedPubkey] }); expect(decrypted4.success).to.equal(true); const verifyRes4 = (decrypted4 as DecryptSuccess).signature!; expect(verifyRes4.match).to.not.be.true; From fe88d6277b5f97012430fa01eeda577302aa1f6b Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 15 Dec 2021 12:46:59 +0000 Subject: [PATCH 29/31] removed unnecessary optionalPwd parameter --- .../elements/pgp_block_modules/pgp-block-decrypt-module.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts index cafa84618dd..6ebbb5a3930 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts @@ -54,7 +54,7 @@ export class PgpBlockViewDecryptModule { this.isPwdMsgBasedOnMsgSnippet = isPwdMsg; this.view.renderModule.renderText('Decrypting...'); this.msgFetchedFromApi = format; - await this.decryptAndRender(Buf.fromUtfStr(armored), verificationPubs, undefined, subject); + await this.decryptAndRender(Buf.fromUtfStr(armored), verificationPubs, subject); } } } catch (e) { @@ -64,7 +64,7 @@ export class PgpBlockViewDecryptModule { public canAndShouldFetchFromApi = () => this.canReadEmails && this.msgFetchedFromApi !== 'raw'; - private decryptAndRender = async (encryptedData: Buf, verificationPubs: string[], optionalPwd?: string, plainSubject?: string) => { + private decryptAndRender = async (encryptedData: Buf, verificationPubs: string[], plainSubject?: string) => { if (!this.view.signature?.parsedSignature) { const kisWithPp = await KeyStore.getAllWithOptionalPassPhrase(this.view.acctEmail); const decrypt = async (verificationPubs: string[]) => await BrowserMsg.send.bg.await.pgpMsgDecrypt({ kisWithPp, encryptedData, verificationPubs }); @@ -97,7 +97,7 @@ export class PgpBlockViewDecryptModule { })); await PassphraseStore.waitUntilPassphraseChanged(this.view.acctEmail, result.longids.needPassphrase); this.view.renderModule.renderText('Decrypting...'); - await this.decryptAndRender(encryptedData, verificationPubs, optionalPwd); + await this.decryptAndRender(encryptedData, verificationPubs); } else { const primaryKi = await KeyStore.getFirstOptional(this.view.acctEmail); if (!result.longids.chosen && !primaryKi) { From 2df39531610a2cd28a31d4dd1cb49b6e101be2ce Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 15 Dec 2021 16:12:32 +0000 Subject: [PATCH 30/31] better naming --- test/source/tests/unit-node.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index b989345772a..c7ca8bd8305 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -782,29 +782,29 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY .match(/\-\-\-\-\-BEGIN PGP SIGNED MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s)![0]; const encryptedData = Buf.fromUtfStr(enc); // actual key the message was signed with - const olderVersionOfPubkey = testConstants.pubkey2864E326A5BE488A; + const signerPubkey = testConstants.pubkey2864E326A5BE488A; // better key - const newlyCreatedPubkey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: FlowCrypt Email Encryption [BUILD_REPLACEABLE_VERSION]\r\nComment: Seamlessly send and receive encrypted email\r\n\r\nxjMEYZeW2RYJKwYBBAHaRw8BAQdAT5QfLVP3y1yukk3MM/oiuXLNe1f9az5M\r\nBnOlKdF0nKnNJVNvbWVib2R5IDxTYW1zNTBzYW1zNTBzZXB0QEdtYWlsLkNv\r\nbT7CjwQQFgoAIAUCYZeW2QYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJS27QEA7pFlkLfD0KFQ\r\nsH/dwb/NPzn5zCi2L9gjPAC3d8gv1fwA/0FjAy/vKct4D7QH8KwtEGQns5+D\r\nP1WxDr4YI2hp5TkAzjgEYZeW2RIKKwYBBAGXVQEFAQEHQKNLY/bXrhJMWA2+\r\nWTjk3I7KhawyZfLomJ4hovqr7UtOAwEIB8J4BBgWCAAJBQJhl5bZAhsMACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJQnpgD/c1CzfS3YzJUx\r\nnFMrhjiE0WVgqOV/3CkfI4m4RA30QUIA/ju8r4AD2h6lu3Mx/6I6PzIRZQty\r\nLvTkcu4UKodZa4kK\r\n=7C4A\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n"; + const wrongPubkey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: FlowCrypt Email Encryption [BUILD_REPLACEABLE_VERSION]\r\nComment: Seamlessly send and receive encrypted email\r\n\r\nxjMEYZeW2RYJKwYBBAHaRw8BAQdAT5QfLVP3y1yukk3MM/oiuXLNe1f9az5M\r\nBnOlKdF0nKnNJVNvbWVib2R5IDxTYW1zNTBzYW1zNTBzZXB0QEdtYWlsLkNv\r\nbT7CjwQQFgoAIAUCYZeW2QYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJS27QEA7pFlkLfD0KFQ\r\nsH/dwb/NPzn5zCi2L9gjPAC3d8gv1fwA/0FjAy/vKct4D7QH8KwtEGQns5+D\r\nP1WxDr4YI2hp5TkAzjgEYZeW2RIKKwYBBAGXVQEFAQEHQKNLY/bXrhJMWA2+\r\nWTjk3I7KhawyZfLomJ4hovqr7UtOAwEIB8J4BBgWCAAJBQJhl5bZAhsMACEJ\r\nEMrSTYqLk6SUFiEEBP90ux3d6kDwDdzvytJNiouTpJQnpgD/c1CzfS3YzJUx\r\nnFMrhjiE0WVgqOV/3CkfI4m4RA30QUIA/ju8r4AD2h6lu3Mx/6I6PzIRZQty\r\nLvTkcu4UKodZa4kK\r\n=7C4A\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n"; { - const decrypted1 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [olderVersionOfPubkey, newlyCreatedPubkey] }); + const decrypted1 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [signerPubkey, wrongPubkey] }); expect(decrypted1.success).to.equal(true); const verifyRes1 = (decrypted1 as DecryptSuccess).signature!; expect(verifyRes1.match).to.be.true; } { - const decrypted2 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [newlyCreatedPubkey, olderVersionOfPubkey] }); + const decrypted2 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [wrongPubkey, signerPubkey] }); expect(decrypted2.success).to.equal(true); const verifyRes2 = (decrypted2 as DecryptSuccess).signature!; expect(verifyRes2.match).to.be.true; } { - const decrypted3 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [olderVersionOfPubkey] }); + const decrypted3 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [signerPubkey] }); expect(decrypted3.success).to.equal(true); const verifyRes3 = (decrypted3 as DecryptSuccess).signature!; expect(verifyRes3.match).to.be.true; } { - const decrypted4 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [newlyCreatedPubkey] }); + const decrypted4 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [wrongPubkey] }); expect(decrypted4.success).to.equal(true); const verifyRes4 = (decrypted4 as DecryptSuccess).signature!; expect(verifyRes4.match).to.not.be.true; From 761f9a9cdab04a9e7da73b884d430b6aaf05f3de Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 16 Dec 2021 13:49:39 +0000 Subject: [PATCH 31/31] don't refetch encrypted message on verification error --- .../pgp-block-signature-module.ts | 4 +- .../message-export-15f7f7c5979b5a26.json | 107 ++++++++++++++++++ test/source/tests/decrypt.ts | 65 +++++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 test/source/mock/google/exported-messages/message-export-15f7f7c5979b5a26.json diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index 157000d2174..3346975b5e1 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -19,10 +19,10 @@ export class PgpBlockViewSignatureModule { retryVerification?: (verificationPubs: string[]) => Promise) => { this.view.renderModule.doNotSetStateAsReadyYet = true; // so that body state is not marked as ready too soon - automated tests need to know when to check results if (verifyRes?.error) { - if (!verifyRes.isErrFatal && this.view.decryptModule.canAndShouldFetchFromApi()) { + if (this.view.signature && !verifyRes.isErrFatal && this.view.decryptModule.canAndShouldFetchFromApi()) { // Sometimes the signed content is slightly modified when parsed from DOM, // so the message should be re-fetched straight from API to make sure we get the original signed data and verify again - this.view.signature!.parsedSignature = undefined; // force to re-parse + this.view.signature.parsedSignature = undefined; // force to re-parse await this.view.decryptModule.initialize(verificationPubs, true); return; } diff --git a/test/source/mock/google/exported-messages/message-export-15f7f7c5979b5a26.json b/test/source/mock/google/exported-messages/message-export-15f7f7c5979b5a26.json new file mode 100644 index 00000000000..dd53162e03e --- /dev/null +++ b/test/source/mock/google/exported-messages/message-export-15f7f7c5979b5a26.json @@ -0,0 +1,107 @@ +{ + "acctEmail": "flowcrypt.compatibility@gmail.com", + "full": { + "id": "15f7f7c5979b5a26", + "threadId": "15f7f7c5979b5a26", + "labelIds": [ + "IMPORTANT", + "CATEGORY_PERSONAL", + "Label_4", + "INBOX" + ], + "snippet": "-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 this message is signed and was modified in transit -----BEGIN PGP SIGNATURE----- Version: FlowCrypt 5.0.4 Gmail Encryption flowcrypt.com Comment:", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [ + { + "name": "X-Gm-Message-State", + "value": "AMCzsaW+RFAVklPv6Hs+l2xafoENwylyEvCFXMMRIoBz3TKpcQySNMP7 8ckQ0bjvXRq0RMSsApXUGTrXQgpyKRe9vkBAe653qA==" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "From", + "value": "sender@domain.com" + }, + { + "name": "Date", + "value": "Thu, 2 Nov 2017 18:26:15 -0700" + }, + { + "name": "Subject", + "value": "signed message - maliciously modified - should not pass" + }, + { + "name": "To", + "value": "flowcrypt.compatibility@gmail.com" + }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=\"94eb2c07ab54b48c8c055d09fd40\"" + } + ], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/plain; charset=\"UTF-8\"" + } + ], + "body": { + "size": 1042, + "data": "LS0tLS1CRUdJTiBQR1AgU0lHTkVEIE1FU1NBR0UtLS0tLQ0KSGFzaDogU0hBMjU2DQoNCnRoaXMgbWVzc2FnZSBpcyBzaWduZWQgYW5kIHdhcyBtb2RpZmllZCBpbiB0cmFuc2l0DQotLS0tLUJFR0lOIFBHUCBTSUdOQVRVUkUtLS0tLQ0KVmVyc2lvbjogRmxvd0NyeXB0IDUuMC40IEdtYWlsIEVuY3J5cHRpb24gZmxvd2NyeXB0LmNvbQ0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kLCByZWNlaXZlIGFuZCBzZWFyY2ggZW5jcnlwdGVkIGVtYWlsDQoNCndzRmNCQUVCQ0FBUUJRSlorOFZYQ1JBR3lsVSt3a1ZkY0FBQWdSd1FBTWtjKzJXcklCZitYM1QreU5wcw0KdmE0eE1RRVZ2QkZTNEd4YlRBMXhuZG1mdW8zaHIzVmZxL3NBSTJQbG82NUQwcmQ5V2M2cDg4QXdsKzl3DQphMGw0WXgzaXNIMGptOWdjcHlwellKZjlsZWVydEJjdkl4RHZxR1pjL3hQeHlSelFLZ1hpTmt2YVpJOFUNCk5Oa29BZEN2ZXM0d3gzc2IwbEpoSUt5UmhYd0tXV3JvV0tmb0JZQXJ5S0RTRTJxOGQxeitQMG1EYm9wNw0KSjVZTHVpKzZWc0dKb3FaZE04bE1HTHAyVHV5R1Z4eTVtb0V2cklZOHp2WW02MUtlK3dHTGJMNjNCcWRnDQpaYjhwTUVwMXhkVy94UGdGVDZJcnRGZEhURWlxY3prZWlIT0c3K0Y4elBjS0Jidng2bHVGQXZGaDEwM0kNCmwwNlJSZWZlSmtwcExKMlMrQTBpRHptcURrbllpbjJIbThWQmEyaWxsL1JmSTRjMVRoek01dHJqckMxYg0Kck9NeklNWmZEZENEQ1JqZklMTnlqbmFuaHNZRVlaMzBObm1qT3JORHRoQTlWQ0VVdXBvUnZTbURiQThPDQpWdGNrWkgzWXpJaTZDc0hzOGRsMldUUS9sajF5WEtEcW5wb1d6bmR6VnJGUnRqVlc3STc1bEwvKy9NWXANCmhrdGJGQVdJa3JxcVdhcHdJVUVHZWZ6YnJ1bFJSVlVVNm02c2xiS0l1ck9na0V0K2NvSlVOTVRzdVBQWA0KYUFhNDN6ZThoVWppNUVYQUxFOUkrNDF1eitkdG5KeW1hUWhzblhMOFI4VkhBZ293cnZRZ0xldVRaTlZIDQpBZU5rYmNuZytLejNFclZrM1ErUkR0MGk1Rkwwb0l6UC9Bc3hoR2VPRzM4aitRVmU2T2dZbTdJamZhSUENCnI0OTgNCj03NnE2DQotLS0tLUVORCBQR1AgU0lHTkFUVVJFLS0tLS0NCg==" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/html; charset=\"UTF-8\"" + }, + { + "name": "Content-Transfer-Encoding", + "value": "quoted-printable" + } + ], + "body": { + "size": 3846, + "data": "PGRpdiBkaXI9Imx0ciI-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPi0tLS0tQkVHSU4gUEdQIFNJR05FRCBNRVNTQUdFLS0tLS08L3NwYW4-PGJyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-SGFzaDogU0hBMjU2PC9zcGFuPjxiciBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PGJyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-dGhpcyBtZXNzYWdlIGlzIHNpZ25lZCBhbmQgd2FzIG1vZGlmaWVkIGluIHRyYW5zaXQ8L3NwYW4-PGJyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-LS0tLS1CRUdJTiBQR1AgU0lHTkFUVVJFLS0tLS08L3NwYW4-PGJyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-VmVyc2lvbjogRmxvd0NyeXB0IDUuMC40IEdtYWlsIEVuY3J5cHRpb27CoDwvc3Bhbj48YSBocmVmPSJodHRwOi8vZmxvd2NyeXB0LmNvbS8iIHJlbD0ibm9yZWZlcnJlciIgdGFyZ2V0PSJfYmxhbmsiIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5mbG93Y3J5cHQuY29tPC9hPjxiciBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPkNvbW1lbnQ6IFNlYW1sZXNzbHkgc2VuZCwgcmVjZWl2ZSBhbmQgc2VhcmNoIGVuY3J5cHRlZCBlbWFpbDwvc3Bhbj48YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxiciBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPndzRmNCQUVCQ0FBUUJRSlorOFZYQ1JBR3lsVSs8L3NwYW4-PHdiciBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPndrVmRjQUFBZ1J3UUFNa2MrMldySUJmK1gzVCs8L3NwYW4-PHdiciBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPnlOcHM8L3NwYW4-PGJyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-dmE0eE1RRVZ2QkZTNEd4YlRBMXhuZG1mdW8zaHIzPC9zcGFuPjx3YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5WZnEvc0FJMlBsbzY1RDByZDlXYzZwODhBd2wrOXc8L3NwYW4-PGJyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-YTBsNFl4M2lzSDBqbTlnY3B5cHpZSmY5bGVlcnRCPC9zcGFuPjx3YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5jdkl4RHZxR1pjL3hQeHlSelFLZ1hpTmt2YVpJOFU8L3NwYW4-PGJyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-Tk5rb0FkQ3ZlczR3eDNzYjBsSmhJS3lSaFh3S1dXPC9zcGFuPjx3YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5yb1dLZm9CWUFyeUtEU0UycThkMXorUDBtRGJvcDc8L3NwYW4-PGJyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-SjVZTHVpKzwvc3Bhbj48d2JyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-NlZzR0pvcVpkTThsTUdMcDJUdXlHVnh5NW1vRXZyPC9zcGFuPjx3YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5JWTh6dlltNjFLZSt3R0xiTDYzQnFkZzwvc3Bhbj48YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5aYjhwTUVwMXhkVy88L3NwYW4-PHdiciBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPnhQZ0ZUNklydEZkSFRFaXFjemtlaUhPRzcrPC9zcGFuPjx3YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5GOHpQY0tCYnZ4Nmx1RkF2RmgxMDNJPC9zcGFuPjxiciBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPmwwNlJSZWZlSmtwcExKMlMrPC9zcGFuPjx3YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5BMGlEem1xRGtuWWluMkhtOFZCYTJpbGwvPC9zcGFuPjx3YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5SZkk0YzFUaHpNNXRyanJDMWI8L3NwYW4-PGJyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-ck9NeklNWmZEZENEQ1JqZklMTnlqbmFuaHNZRVlaPC9zcGFuPjx3YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij4zME5ubWpPck5EdGhBOVZDRVV1cG9SdlNtRGJBOE88L3NwYW4-PGJyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-VnRja1pIM1l6SWk2Q3NIczhkbDJXVFEvPC9zcGFuPjx3YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5sajF5WEtEcW5wb1d6bmR6VnJGUnRqVlc3STc1bEw8L3NwYW4-PHdiciBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPi8rL01ZcDwvc3Bhbj48YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5oa3RiRkFXSWtycXFXYXB3SVVFR2VmemJydWxSUlY8L3NwYW4-PHdiciBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPlVVNm02c2xiS0l1ck9na0V0K2NvSlVOTVRzdVBQWDwvc3Bhbj48YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5hQWE0M3plOGhVamk1RVhBTEU5SSs0MXV6Kzwvc3Bhbj48d2JyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-ZHRuSnltYVFoc25YTDhSOFZIQWdvd3J2UWdMZXVUPC9zcGFuPjx3YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5aTlZIPC9zcGFuPjxiciBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPkFlTmtiY25nK0t6M0VyVmszUSs8L3NwYW4-PHdiciBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPlJEdDBpNUZMMG9JelAvQXN4aEdlT0czOGorPC9zcGFuPjx3YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij5RVmU2T2dZbTdJamZhSUE8L3NwYW4-PGJyIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-cjQ5ODwvc3Bhbj48YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij49NzZxNjwvc3Bhbj48YnIgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij4tLS0tLUVORCBQR1AgU0lHTkFUVVJFLS0tLS08L3NwYW4-PGJyPjwvZGl2Pg0K" + } + } + ] + }, + "sizeEstimate": 10126, + "historyId": "1333314", + "internalDate": "1509672375000" + }, + "attachments": {}, + "raw": { + "id": "15f7f7c5979b5a26", + "threadId": "15f7f7c5979b5a26", + "labelIds": [ + "IMPORTANT", + "CATEGORY_PERSONAL", + "Label_4", + "INBOX" + ], + "snippet": "-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 this message is signed and was modified in transit -----BEGIN PGP SIGNATURE----- Version: FlowCrypt 5.0.4 Gmail Encryption flowcrypt.com Comment:", + "sizeEstimate": 10126, + "raw": "RGVsaXZlcmVkLVRvOiBmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20NClJlY2VpdmVkOiBieSAxMC4xMi4xNjcuMjUgd2l0aCBTTVRQIGlkIHUyNWNzcDI3MDg5NDhxdmE7DQogICAgICAgIFRodSwgMiBOb3YgMjAxNyAxODoyNjoxNiAtMDcwMCAoUERUKQ0KWC1SZWNlaXZlZDogYnkgMTAuMzYuMjUzLjkgd2l0aCBTTVRQIGlkIG05bXI1Mjc3NDcxaXRoLjEwNS4xNTA5NjcyMzc2NjE0Ow0KICAgICAgICBUaHUsIDAyIE5vdiAyMDE3IDE4OjI2OjE2IC0wNzAwIChQRFQpDQpBUkMtU2VhbDogaT0xOyBhPXJzYS1zaGEyNTY7IHQ9MTUwOTY3MjM3NjsgY3Y9bm9uZTsNCiAgICAgICAgZD1nb29nbGUuY29tOyBzPWFyYy0yMDE2MDgxNjsNCiAgICAgICAgYj1RNDFvK0RDLy9EN3R6bU1FeWJSTmpUVFdEZzBzTWsrdGRDblNyREgvUVQ2WXdqaE9lUk4vN1ZwOHZEV3F4MEpFdmINCiAgICAgICAgIHVDYjR4OERrVUd3RkNlaFdBL0NrTTJNbS9CTUczdkJ0SllnTWFNZkxrZ2FMQjd5UnRTY1NDYlVpRkpDMFN4K0lFQ1NXDQogICAgICAgICBFaHhPb1RTbmluc01PZmE3cFp2aWNIckZFTDU1aGxweWNxRmJiZFM3T2FveGZndGZiU0FxQTNkMWFsbzVkNzFVSXJ2WA0KICAgICAgICAgUFF1ckdqMlZPK2JPVjh1b0h1Ym1pTFkrWHlDQzRvTHYrdjZMUEg4U0t3enUrZHFOdEU0RUlQMFZEZ0dRZjZmTFFiakcNCiAgICAgICAgIDNqZjlkc3Fpd2tBR2pZL3VJRTNWWWhwYnNpMnBrMTV2ckRiRFpVYlFGODBkMzlCaGNZeUlIbjNkMzBWc3dxQjZWVFMxDQogICAgICAgICB3WmRnPT0NCkFSQy1NZXNzYWdlLVNpZ25hdHVyZTogaT0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOyBkPWdvb2dsZS5jb207IHM9YXJjLTIwMTYwODE2Ow0KICAgICAgICBoPXRvOnN1YmplY3Q6bWVzc2FnZS1pZDpkYXRlOmZyb206bWltZS12ZXJzaW9uOmRraW0tc2lnbmF0dXJlDQogICAgICAgICA6YXJjLWF1dGhlbnRpY2F0aW9uLXJlc3VsdHM7DQogICAgICAgIGJoPWFFNi9PS2hhcW02OExEelBkcGpmdHpMbTFPNkRWby82MXlubythUk16QjA9Ow0KICAgICAgICBiPU4wMkxrK1JwaG5PM2NGWGdTWTgvT0FXd29mTFl2UXA5cjdMenVNQnV4a0ovRXgwcnlTd0JHck9UdFJtV041Zk5SSQ0KICAgICAgICAgamxCOENQZmRtOVVSb2Jsa3MrVVhxOEtvcUIrVUJsVjBwWWVQaWM0b0M2K09SS2JzRUVZYTlKWFVvSFdRUVovYVlubFgNCiAgICAgICAgIEk3b0tobEVYeUIxY0duN0dPMUR1MmlFRmZ6SHV6Z0J5VXFoUWVLVUMxUTlLUVJBb3ArclNjbzM5VkJaS0srZlo1bFV5DQogICAgICAgICArRHN2WGRjZHhLNHBMLzVsVzhWUHF6aWJhMm5TcnpuNWhHcTBaeGFNaGM4YmNwQi9WKzVqeVpKNWQzcnVOb05Yb0hxUg0KICAgICAgICAgTUE2MHVQWnhMb1cxdmYveUU5THozNVk2OCtWRW5icEE4aGlGY0pXOTEweE1LSTFaVGJadnR2VjlGUWlBTmVTdUFDM2sNCiAgICAgICAgIGxCU3c9PQ0KQVJDLUF1dGhlbnRpY2F0aW9uLVJlc3VsdHM6IGk9MTsgbXguZ29vZ2xlLmNvbTsNCiAgICAgICBka2ltPXBhc3MgaGVhZGVyLmk9QGdtYWlsLmNvbSBoZWFkZXIucz0yMDE2MTAyNSBoZWFkZXIuYj1MR1A3NTFMMjsNCiAgICAgICBzcGY9cGFzcyAoZ29vZ2xlLmNvbTogZG9tYWluIG9mIGNyeXB0dXAudGVzdGVyQGdtYWlsLmNvbSBkZXNpZ25hdGVzIDIwOS44NS4yMjAuNDEgYXMgcGVybWl0dGVkIHNlbmRlcikgc210cC5tYWlsZnJvbT1jcnlwdHVwLnRlc3RlckBnbWFpbC5jb207DQogICAgICAgZG1hcmM9cGFzcyAocD1OT05FIHNwPU5PTkUgZGlzPU5PTkUpIGhlYWRlci5mcm9tPWdtYWlsLmNvbQ0KUmV0dXJuLVBhdGg6IDxjcnlwdHVwLnRlc3RlckBnbWFpbC5jb20-DQpSZWNlaXZlZDogZnJvbSBtYWlsLXNvci1mNDEuZ29vZ2xlLmNvbSAobWFpbC1zb3ItZjQxLmdvb2dsZS5jb20uIFsyMDkuODUuMjIwLjQxXSkNCiAgICAgICAgYnkgbXguZ29vZ2xlLmNvbSB3aXRoIFNNVFBTIGlkIHQxNXNvcjQ4MzM2NGl0aS42Ny4yMDE3LjExLjAyLjE4LjI2LjE2DQogICAgICAgIGZvciA8Zmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tPg0KICAgICAgICAoR29vZ2xlIFRyYW5zcG9ydCBTZWN1cml0eSk7DQogICAgICAgIFRodSwgMDIgTm92IDIwMTcgMTg6MjY6MTYgLTA3MDAgKFBEVCkNClJlY2VpdmVkLVNQRjogcGFzcyAoZ29vZ2xlLmNvbTogZG9tYWluIG9mIGNyeXB0dXAudGVzdGVyQGdtYWlsLmNvbSBkZXNpZ25hdGVzIDIwOS44NS4yMjAuNDEgYXMgcGVybWl0dGVkIHNlbmRlcikgY2xpZW50LWlwPTIwOS44NS4yMjAuNDE7DQpBdXRoZW50aWNhdGlvbi1SZXN1bHRzOiBteC5nb29nbGUuY29tOw0KICAgICAgIGRraW09cGFzcyBoZWFkZXIuaT1AZ21haWwuY29tIGhlYWRlci5zPTIwMTYxMDI1IGhlYWRlci5iPUxHUDc1MUwyOw0KICAgICAgIHNwZj1wYXNzIChnb29nbGUuY29tOiBkb21haW4gb2YgY3J5cHR1cC50ZXN0ZXJAZ21haWwuY29tIGRlc2lnbmF0ZXMgMjA5Ljg1LjIyMC40MSBhcyBwZXJtaXR0ZWQgc2VuZGVyKSBzbXRwLm1haWxmcm9tPWNyeXB0dXAudGVzdGVyQGdtYWlsLmNvbTsNCiAgICAgICBkbWFyYz1wYXNzIChwPU5PTkUgc3A9Tk9ORSBkaXM9Tk9ORSkgaGVhZGVyLmZyb209Z21haWwuY29tDQpES0lNLVNpZ25hdHVyZTogdj0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOw0KICAgICAgICBkPWdtYWlsLmNvbTsgcz0yMDE2MTAyNTsNCiAgICAgICAgaD1taW1lLXZlcnNpb246ZnJvbTpkYXRlOm1lc3NhZ2UtaWQ6c3ViamVjdDp0bzsNCiAgICAgICAgYmg9YUU2L09LaGFxbTY4TER6UGRwamZ0ekxtMU82RFZvLzYxeW5vK2FSTXpCMD07DQogICAgICAgIGI9TEdQNzUxTDJZUnhKNlMrVzV5citJN0ZQWGtNZDdPZFhzQ3J4ZGdsZnc3SFF4QlpySEdsKzdEcXBuZzNXNmR5Q3hsDQogICAgICAgICB5a2R4Y0FCUkUvRFViYzNpNnVqQnhOeUx0aGVsWCsxT2pQdHJuYjYxbUF2NmFCTmhzS1VuSVhERTBXK082K2xzaSt3Lw0KICAgICAgICAgWXVEVDhUQStpWDNMWU9RVUhIdFFodUM0Ti9JbmpUeDRKbmd3N0hwdWYxcDFJQk9qQmdmbld0Wi8zcXBYVDg3a2t4NFANCiAgICAgICAgIHZmV1JUMG8wTC9jVWlydFdCVTh0aVBiRXI4aU95RW40Nk5JWHJzUE1MbnV5dVF1OUk2eUl6bk1XRDZOUmJSeGNRMzNZDQogICAgICAgICBoa0lLNCtlaktyYnRFNjVodkE3dkVWV1NJM1ZsTStWMjFEWEdmYnExKzNzbHl3eXJLRTZrTGVQUkloMmEwUlVTZjVkRg0KICAgICAgICAgblFJQT09DQpYLUdvb2dsZS1ES0lNLVNpZ25hdHVyZTogdj0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOw0KICAgICAgICBkPTFlMTAwLm5ldDsgcz0yMDE2MTAyNTsNCiAgICAgICAgaD14LWdtLW1lc3NhZ2Utc3RhdGU6bWltZS12ZXJzaW9uOmZyb206ZGF0ZTptZXNzYWdlLWlkOnN1YmplY3Q6dG87DQogICAgICAgIGJoPWFFNi9PS2hhcW02OExEelBkcGpmdHpMbTFPNkRWby82MXlubythUk16QjA9Ow0KICAgICAgICBiPUg3MFEwYVBLRnp2Sy9qdGJkbG53bk9mdWN1TUZjUWROYlVDdUk0NWlqbkFZRTJqNzRsVFAwa0ZwdnB6U1M4UXRPRg0KICAgICAgICAgQVVYSmk5QzFjcUdwaEJSY2V0WkMvcTJydlA0TDhwR1ZFT0R0QjJMbmxqQ3F0aXFMeFkyVjVVNHYxODNxTmVLUzJvRDMNCiAgICAgICAgIHkwRnhQWFA4bmdoOHFNaFJhTFN0a1g2RkNneTJVMjNrOHdxWUhlemZ5bHFzeWoveGlMUnJ1M0dTaUhra1VRTkJoQitIDQogICAgICAgICBDbzdHSDl4aHMzejdReW0xRXYzQmdrbmw2Q3RkMDJKc2xJWnFWK3BHWkY2MndCSXpJYTc4L1BWaUVhbGhLbStvQWZpMg0KICAgICAgICAgbCtWYjJJMVRtcktPZWN3K0VycVBMc0ppYlUxSCtvTlJ6M0pxMDgxYVBzazRiZlQ0YjRIaXN2QzVrU2I4NERtN1pNcHoNCiAgICAgICAgIGFHR3c9PQ0KWC1HbS1NZXNzYWdlLVN0YXRlOiBBTUN6c2FXK1JGQVZrbFB2NkhzK2wyeGFmb0VOd3lseUV2Q0ZYTU1SSW9CejNUS3BjUXlTTk1QNw0KCThja1EwYmp2WFJxMFJNU3NBcFhVR1RyWFFncHlLUmU5dmtCQWU2NTNxQT09DQpYLUdvb2dsZS1TbXRwLVNvdXJjZTogQUJoUXArVE41U2xzY1VGVmhJQ05LcDhGZnNrV1QzTjRtWC9hVW83QnZZWnIzOVEyV1ZyZlZSVzRJZDE1Um5VMUtvOUE2N0NMeUh6SDVHNXROeE9ReFM5ZDgvMD0NClgtUmVjZWl2ZWQ6IGJ5IDEwLjM2LjE5Ny4xMzAgd2l0aCBTTVRQIGlkIGYxMjRtcjQ3MTYxNjdpdGcuOTkuMTUwOTY3MjM3NjEwODsNCiBUaHUsIDAyIE5vdiAyMDE3IDE4OjI2OjE2IC0wNzAwIChQRFQpDQpNSU1FLVZlcnNpb246IDEuMA0KUmVjZWl2ZWQ6IGJ5IDEwLjEwNy4xODIuMiB3aXRoIEhUVFA7IFRodSwgMiBOb3YgMjAxNyAxODoyNjoxNSAtMDcwMCAoUERUKQ0KRnJvbTogQ3J5cHRVcCBUZXN0ZXIgPGNyeXB0dXAudGVzdGVyQGdtYWlsLmNvbT4NCkRhdGU6IFRodSwgMiBOb3YgMjAxNyAxODoyNjoxNSAtMDcwMA0KTWVzc2FnZS1JRDogPENBTno2R3p5WjUwaURHTCtPXzdUbUF6MGhTUURqemZUN01uNm1WWUdaVXkxaStERVhlUUBtYWlsLmdtYWlsLmNvbT4NClN1YmplY3Q6IHNpZ25lZCBtZXNzYWdlIC0gbWFsaWNpb3VzbHkgbW9kaWZpZWQgLSBzaG91bGQgbm90IHBhc3MNClRvOiBmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20NCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L2FsdGVybmF0aXZlOyBib3VuZGFyeT0iOTRlYjJjMDdhYjU0YjQ4YzhjMDU1ZDA5ZmQ0MCINCg0KLS05NGViMmMwN2FiNTRiNDhjOGMwNTVkMDlmZDQwDQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9IlVURi04Ig0KDQotLS0tLUJFR0lOIFBHUCBTSUdORUQgTUVTU0FHRS0tLS0tDQpIYXNoOiBTSEEyNTYNCg0KdGhpcyBtZXNzYWdlIGlzIHNpZ25lZCBhbmQgd2FzIG1vZGlmaWVkIGluIHRyYW5zaXQNCi0tLS0tQkVHSU4gUEdQIFNJR05BVFVSRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgNS4wLjQgR21haWwgRW5jcnlwdGlvbiBmbG93Y3J5cHQuY29tDQpDb21tZW50OiBTZWFtbGVzc2x5IHNlbmQsIHJlY2VpdmUgYW5kIHNlYXJjaCBlbmNyeXB0ZWQgZW1haWwNCg0Kd3NGY0JBRUJDQUFRQlFKWis4VlhDUkFHeWxVK3drVmRjQUFBZ1J3UUFNa2MrMldySUJmK1gzVCt5TnBzDQp2YTR4TVFFVnZCRlM0R3hiVEExeG5kbWZ1bzNocjNWZnEvc0FJMlBsbzY1RDByZDlXYzZwODhBd2wrOXcNCmEwbDRZeDNpc0gwam05Z2NweXB6WUpmOWxlZXJ0QmN2SXhEdnFHWmMveFB4eVJ6UUtnWGlOa3ZhWkk4VQ0KTk5rb0FkQ3ZlczR3eDNzYjBsSmhJS3lSaFh3S1dXcm9XS2ZvQllBcnlLRFNFMnE4ZDF6K1AwbURib3A3DQpKNVlMdWkrNlZzR0pvcVpkTThsTUdMcDJUdXlHVnh5NW1vRXZySVk4enZZbTYxS2Urd0dMYkw2M0JxZGcNClpiOHBNRXAxeGRXL3hQZ0ZUNklydEZkSFRFaXFjemtlaUhPRzcrRjh6UGNLQmJ2eDZsdUZBdkZoMTAzSQ0KbDA2UlJlZmVKa3BwTEoyUytBMGlEem1xRGtuWWluMkhtOFZCYTJpbGwvUmZJNGMxVGh6TTV0cmpyQzFiDQpyT016SU1aZkRkQ0RDUmpmSUxOeWpuYW5oc1lFWVozME5ubWpPck5EdGhBOVZDRVV1cG9SdlNtRGJBOE8NClZ0Y2taSDNZeklpNkNzSHM4ZGwyV1RRL2xqMXlYS0RxbnBvV3puZHpWckZSdGpWVzdJNzVsTC8rL01ZcA0KaGt0YkZBV0lrcnFxV2Fwd0lVRUdlZnpicnVsUlJWVVU2bTZzbGJLSXVyT2drRXQrY29KVU5NVHN1UFBYDQphQWE0M3plOGhVamk1RVhBTEU5SSs0MXV6K2R0bkp5bWFRaHNuWEw4UjhWSEFnb3dydlFnTGV1VFpOVkgNCkFlTmtiY25nK0t6M0VyVmszUStSRHQwaTVGTDBvSXpQL0FzeGhHZU9HMzhqK1FWZTZPZ1ltN0lqZmFJQQ0KcjQ5OA0KPTc2cTYNCi0tLS0tRU5EIFBHUCBTSUdOQVRVUkUtLS0tLQ0KDQotLTk0ZWIyYzA3YWI1NGI0OGM4YzA1NWQwOWZkNDANCkNvbnRlbnQtVHlwZTogdGV4dC9odG1sOyBjaGFyc2V0PSJVVEYtOCINCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUNCg0KPGRpdiBkaXI9M0QibHRyIj48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij4tLS0tLUJFR0lOIFBHUCBTSUdORUQgTUVTPQ0KU0FHRS0tLS0tPC9zcGFuPjxiciBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6MTIuPQ0KOHB4Ij5IYXNoOiBTSEEyNTY8L3NwYW4-PGJyIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPjxiciBzdHlsZT0zRCJmb250LXNpPQ0KemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij50aGlzIG1lc3NhZ2UgaXMgc2lnbmVkIGFuZCB3YXMgPQ0KbW9kaWZpZWQgaW4gdHJhbnNpdDwvc3Bhbj48YnIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9uPQ0KdC1zaXplOjEyLjhweCI-LS0tLS1CRUdJTiBQR1AgU0lHTkFUVVJFLS0tLS08L3NwYW4-PGJyIHN0eWxlPTNEImZvbnQtc2l6ZToxPQ0KMi44cHgiPjxzcGFuIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPlZlcnNpb246IEZsb3dDcnlwdCA1LjAuNCBHbWFpbCBFbmNyPQ0KeXB0aW9uPUMyPUEwPC9zcGFuPjxhIGhyZWY9M0QiaHR0cDovL2Zsb3djcnlwdC5jb20vIiByZWw9M0Qibm9yZWZlcnJlciIgdGFyPQ0KZ2V0PTNEIl9ibGFuayIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-Zmxvd2NyeXB0LmNvbTwvYT48YnIgc3R5bGU9M0QiZm9uPQ0KdC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-Q29tbWVudDogU2VhbWxlc3NseSBzZW5kLCByPQ0KZWNlaXZlIGFuZCBzZWFyY2ggZW5jcnlwdGVkIGVtYWlsPC9zcGFuPjxiciBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij48YnIgPQ0Kc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-d3NGY0JBRUJDQUFRQlFKPQ0KWis4VlhDUkFHeWxVKzwvc3Bhbj48d2JyIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPTNEImZvbnQtc2l6PQ0KZToxMi44cHgiPndrVmRjQUFBZ1J3UUFNa2MrMldySUJmK1gzVCs8L3NwYW4-PHdiciBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4PQ0KIj48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij55TnBzPC9zcGFuPjxiciBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4PQ0KIj48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij52YTR4TVFFVnZCRlM0R3hiVEExeG5kbWZ1bzNocjM8L3NwYW4-PHdiPQ0KciBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij5WZnEvc0FJMlBsbzY1PQ0KRDByZDlXYzZwODhBd2wrOXc8L3NwYW4-PGJyIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPTNEImZvbnQtPQ0Kc2l6ZToxMi44cHgiPmEwbDRZeDNpc0gwam05Z2NweXB6WUpmOWxlZXJ0Qjwvc3Bhbj48d2JyIHN0eWxlPTNEImZvbnQtc2l6ZToxPQ0KMi44cHgiPjxzcGFuIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPmN2SXhEdnFHWmMveFB4eVJ6UUtnWGlOa3ZhWkk4VTwvc3BhPQ0Kbj48YnIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-Tk5rb0FkQ3ZlPQ0KczR3eDNzYjBsSmhJS3lSaFh3S1dXPC9zcGFuPjx3YnIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiPQ0KZm9udC1zaXplOjEyLjhweCI-cm9XS2ZvQllBcnlLRFNFMnE4ZDF6K1AwbURib3A3PC9zcGFuPjxiciBzdHlsZT0zRCJmb250LXNpPQ0KemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij5KNVlMdWkrPC9zcGFuPjx3YnIgc3R5bGU9M0QiZm9uPQ0KdC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-NlZzR0pvcVpkTThsTUdMcDJUdXlHVnh5NW1vPQ0KRXZyPC9zcGFuPjx3YnIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PQ0KSVk4enZZbTYxS2Urd0dMYkw2M0JxZGc8L3NwYW4-PGJyIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPTNEPQ0KImZvbnQtc2l6ZToxMi44cHgiPlpiOHBNRXAxeGRXLzwvc3Bhbj48d2JyIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuPQ0KIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPnhQZ0ZUNklydEZkSFRFaXFjemtlaUhPRzcrPC9zcGFuPjx3YnIgc3R5bGU9M0QiPQ0KZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-Rjh6UGNLQmJ2eDZsdUZBdkZoMTAzSTwvPQ0Kc3Bhbj48YnIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-bDA2UlJlPQ0KZmVKa3BwTEoyUys8L3NwYW4-PHdiciBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6PQ0KMTIuOHB4Ij5BMGlEem1xRGtuWWluMkhtOFZCYTJpbGwvPC9zcGFuPjx3YnIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwPQ0KYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-UmZJNGMxVGh6TTV0cmpyQzFiPC9zcGFuPjxiciBzdHlsZT0zRCJmb250LXNpPQ0KemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij5yT016SU1aZkRkQ0RDUmpmSUxOeWpuYW5oc1lFWVo8PQ0KL3NwYW4-PHdiciBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij4zME5uPQ0KbWpPck5EdGhBOVZDRVV1cG9SdlNtRGJBOE88L3NwYW4-PGJyIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPQ0KPTNEImZvbnQtc2l6ZToxMi44cHgiPlZ0Y2taSDNZeklpNkNzSHM4ZGwyV1RRLzwvc3Bhbj48d2JyIHN0eWxlPTNEImZvbnQtc2l6PQ0KZToxMi44cHgiPjxzcGFuIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPmxqMXlYS0RxbnBvV3puZHpWckZSdGpWVzdJNzVsTDwvPQ0Kc3Bhbj48d2JyIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPi8rL01ZPQ0KcDwvc3Bhbj48YnIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-aGt0PQ0KYkZBV0lrcnFxV2Fwd0lVRUdlZnpicnVsUlJWPC9zcGFuPjx3YnIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5PQ0KbGU9M0QiZm9udC1zaXplOjEyLjhweCI-VVU2bTZzbGJLSXVyT2drRXQrY29KVU5NVHN1UFBYPC9zcGFuPjxiciBzdHlsZT0zRCJmPQ0Kb250LXNpemU6MTIuOHB4Ij48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij5hQWE0M3plOGhVamk1RVhBTEU5SSs0MXV6PQ0KKzwvc3Bhbj48d2JyIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPmR0PQ0Kbkp5bWFRaHNuWEw4UjhWSEFnb3dydlFnTGV1VDwvc3Bhbj48d2JyIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0PQ0KeWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPlpOVkg8L3NwYW4-PGJyIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0PQ0KeWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPkFlTmtiY25nK0t6M0VyVmszUSs8L3NwYW4-PHdiciBzdHlsZT0zRCJmb250LXNpemU6PQ0KMTIuOHB4Ij48c3BhbiBzdHlsZT0zRCJmb250LXNpemU6MTIuOHB4Ij5SRHQwaTVGTDBvSXpQL0FzeGhHZU9HMzhqKzwvc3Bhbj48PQ0Kd2JyIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPjxzcGFuIHN0eWxlPTNEImZvbnQtc2l6ZToxMi44cHgiPlFWZTZPZ1ltN0lqPQ0KZmFJQTwvc3Bhbj48YnIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PQ0KcjQ5ODwvc3Bhbj48YnIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PQ0KPTNENzZxNjwvc3Bhbj48YnIgc3R5bGU9M0QiZm9udC1zaXplOjEyLjhweCI-PHNwYW4gc3R5bGU9M0QiZm9udC1zaXplOjEyLjhwPQ0KeCI-LS0tLS1FTkQgUEdQIFNJR05BVFVSRS0tLS0tPC9zcGFuPjxicj48L2Rpdj4NCg0KLS05NGViMmMwN2FiNTRiNDhjOGMwNTVkMDlmZDQwLS0NCg==", + "historyId": "1333314", + "internalDate": "1509672375000" + } +} \ No newline at end of file diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts index 7dcf1f3e5f2..a4524390f8b 100644 --- a/test/source/tests/decrypt.ts +++ b/test/source/tests/decrypt.ts @@ -260,6 +260,71 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te }); })); + ava.default(`decrypt - [security] signed message - maliciously modified - should not pass`, testWithBrowser('compatibility', async (t, browser) => { + const acctEmail = 'flowcrypt.compatibility@gmail.com'; + const msgId = '15f7f7c5979b5a26'; + const signerEmail = 'sender@domain.com'; + const params = `?frameId=none&account_email=${acctEmail}&senderEmail=${signerEmail}&msgId=${msgId}`; + await PageRecipe.addPubkey(t, browser, acctEmail, `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: FlowCrypt Email Encryption 8.2.0 +Comment: Seamlessly send and receive encrypted email + +xsFNBFj/aG8BEADO625P5MArNIVlMBPp/HM1lYD1gcVwgYl4aHuXohDMS6dv +VAlSDXMVWwbsXJ9T3AxYIL3ZoOFDc1Jy0AqBKhYoOYm5miYHpOQtP/M4V6fK +3rhmc8C1LP1JXuaEXS0w7MQig8JZC08ECUH1/Gnhm3tyacRgrAr13s591Obj +oP/kwglOUjKDYvkXXk9iwouU85sh9HKwC4wR6idFhFSnsl8xp4FI4plLQPTy +Ea1nf3l+oVqCFT5moVtsew7qUD5mWkgytEdr728Sqh5vjiO+lc6cjqb0PK77 +DAuhTel1bV5PRCtRom/qrqmOz4MbE5wd2kU/JxFPIXZ1BKyicT/Q6I9MXjni +77Bl91x0V9brnBqyhfY524Vlm/2AEb3H9I10rsTBtU4TT+SJOlwyU1V7hDkJ +Kq1zTrVjCvoPcTBXGx9xSZmJO4TI7frNZFiJ5uiYwTYPwp3Yze69y/NORwme +ZlXtXJbzpVvRzXUzex89c6pFiKE8mC5/DV/eJanBYKgSyGEiHq9U6kDJrTN4 +/fSjiIJ0fWK3bcYwyYUbf9+/JcLSo2sG259FuRF75yxIe2u2RLSh62plEsyb +cpD545pvlrKIvwg/1hio999lMnSjj+hfNQ7A+Xm5BWiSzrJ1fR1Oo5rq68kY +1C4K8FUQwP3zEF2YDoqbBEnYaxaH7HUcbc34xQARAQABzSlDcnlwdFVwIFRl +c3RlciA8Y3J5cHR1cC50ZXN0ZXJAZ21haWwuY29tPsLBfwQQAQgAKQUCWP9o +cAYLCQcIAwIJEAbKVT7CRV1wBBUIAgoDFgIBAhkBAhsDAh4BAAoJEAbKVT7C +RV1wL8EP/iGk15uGa6gNYdjfoGElIjZCyp1VWTU3VSkkQhLxzWWmB6mQyuZj +vU0SpW89OGyJXoX2M7dDFuuQJmZub7adek0810FaRb9WBmxRZKJe6kdnIc13 +Z2zgs9e9ltHCq1rvHsVa+F0dQu0elFXJJbX6LqvyRnuKQxcGLIZbi/GXswgl +g3p6OsuSSSa/fKGylrUjMNPtF6jKhbEz9/5Be+3Fn3memhO07oKtr0SFYNQr +mg2Sp6xmDwVm8GGQO69DEyxBzDZtzVhnJgOgWcgKli3u6HBvvg1pVwtgLEnF +KoNug9qZoeNPPdv4ueHnE4cM1ZrWsnFqLusexO4RKgxhnQ+UaK1SeRahDKuD +bAYreN5aFex6KNUeCFum1QDSKhRlL9FUtDAPPu3HtVDfbWgu+tn/YnUXzQWN +MovbuYaIp0qyaC5f+PPZ4cqi++B8npUoIStkLrGrxwnvQVbB0fh9JMLMwzLV +4wwSbZCkSPRXCv0H71ODr71SjTUm5M9c2l6xiNmDruKdwhyvmkApbkdz4ZXV +VEg0e8E/2rH1sTB+N47h/gtJF6J0asnu3A7Pt8IuKn6ycPxmLcAtCX82vzpc +rshPtQJVaRASle4BvuoikyJdhuQ5wTf7XX3JCzUrGA1W8u/mmVdwrVb7oX3g +IzfWJbjamWQUg6jspvPAVLBBSzncwS22zsFNBFj/aG8BEACilSpjULG6TZYb +hWcnR46n/gGgQULCW/UO8y0rlAAZgS1BvfqIUnW9bbCOTBKuy3ZLMtrBeCrG +OigR4NFSuDXbvCks3lRZYBEsos68rf2vCWnf3Wro2HSeX5YlceOl2ALlV0To +XrND5aWvGkBsFLpm1f7NiDV6qPB8A5HtFCONvpPzhtkpJIixk1NlEtzjJPOW +1qKh4vX2JJjO2EyUbenSYMI6nr3yLxBVI4d4uoqRUsKfgdbkt/0x7XP4tOus +FmcCFm9GdZ7AIVaYpC+nJGi4hIZL1BJC/5qk3yL9MCQLALEb1ymb5jvKkKyq +vFEKwA43zEj/+LHKIYrsIz0WKqbdzcqq5YgnE0VmUwS14+8NRNpuGXAHkVBR +b9S4XCz5Ed7gaJsWqCqm8E+g+uLM/ml6KSDKKXLFhX+uMxZ2AQCTe7WDpiEE +DB+WmRjVfvL+rlrz6YBMwBULrQ1Fa9rbQCH8ivhz7ue6RzgAedTfpdOHp/Vl +3lJk9XKqamlwClfXBB96EZKQUc+cGiFtS5hJVm7m4xFimXywfDYLxjLANJTK +rGmlXVdLMKHoUB7r1yEL9XngSyv7AC9/1QkrTMJFvIH2i/PmxCgyvpeCXdZo +V2vlQMs0wBLE08gGmD92NX0efeSwPGBwbH7uLoGM6nO/+9RMbxPu0vJHQb9M +DonpFrO81QARAQABwsFpBBgBCAATBQJY/2hzCRAGylU+wkVdcAIbDAAKCRAG +ylU+wkVdcMWLD/97wA3viAjYsP7zbuvfvjb9qxDvomeozrcYNPdz1Ono3mLs +czEHD4p1w+4SBAdYAN2kMFw+1EaRBQP23Laa28axhKDbsb8c/JvY5hIt/osX +sxA9seXRES8iPIYq8zSNXqx8ZADUOR9jkR1tAhqpqYHvcZmsbW+bBdhHg0EV +ge2qEPFy84k0NOVM1Fwj3nsblym9ZLrx3YWQIceVJGxl0u3UmSdNpR0JgCuC +QlItExJY8DBYMVmk8kkd/uWQSBTWq6qXf/vARKEMqp+aA5gPMFngrQfL/yNI +emIRaWAXoXwqXQcJGz4BGGgBuX8zjldvT5sOnfTEokygeSg7K95ZlbPYwdvT +QhLMOUoQF7YysI8l7qIdUW2qM8zepn3eHIhpgq7QfwdzceWpgHma683zQUVf +sU09dzg2IihGnk13oXaq8wye4P4Cw4oKBDgpxNrwmh7j5wnxtreuLMrjmS0+ ++8k3NJ4HpmP2tIiIX2JThrj1ANSb2bMZIvH+kW0niR8WqJWzqG1u2hs4EoWN +RWuEm0qwW6TtrChMDpyX3K135ID5TFJ2pvpwUerliNH4LBEAbQcXZt13pe9i +1mePDNOQzBhDMbfRA8VOnL+e77I7CUB5GK/YQw1YoeOc1VamrACkYYfMVX6D +XZ8r4OC6sguP/yozWlkG+7dDxsgKQVBENeG6Lw== +=1oxZ +-----END PGP PUBLIC KEY BLOCK-----`, 'sender@domain.com'); + // as the verification pubkey is not known, this scenario doesn't trigger message re-fetch + await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { params, content: [], signature: ["Unknown Signer", "Message digest did not match"] }); + })); + ava.default(`decrypt - [everdesk] message encrypted for sub but claims encryptedFor:primary,sub`, testWithBrowser('compatibility', async (t, browser) => { await BrowserRecipe.pgpBlockVerifyDecryptedContent(t, browser, { content: ["this is a sample for FlowCrypt compatibility"],