From 7973cb78aee3d3045bf8ca91c4718be0ca85fd1e Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 6 Jun 2022 17:13:01 +0000 Subject: [PATCH 01/26] wire EKM for live gmail test --- extension/chrome/dev/ci_unit_test.ts | 2 + extension/chrome/settings/setup.ts | 18 +--- .../chrome/settings/setup/setup-create-key.ts | 3 +- .../chrome/settings/setup/setup-import-key.ts | 5 +- .../setup/setup-key-manager-autogen.ts | 23 +---- .../settings/setup/setup-recover-key.ts | 3 +- extension/js/common/shared.ts | 41 ++++++++- .../webmail/setup-webmail-content-script.ts | 24 ++++- .../js/content_scripts/webmail/webmail.ts | 3 +- test/source/browser/browser-pool.ts | 2 +- test/source/mock.ts | 6 +- test/source/mock/all-apis-mock.ts | 9 +- .../mock/key-manager/key-manager-endpoints.ts | 25 ++++-- test/source/test.ts | 16 ++-- test/source/tests/gmail.ts | 18 ++++ test/source/tests/tooling/consts.ts | 87 ++++++++++++++++++- 16 files changed, 212 insertions(+), 73 deletions(-) diff --git a/extension/chrome/dev/ci_unit_test.ts b/extension/chrome/dev/ci_unit_test.ts index 13a79c9972f..ada27dc67fd 100644 --- a/extension/chrome/dev/ci_unit_test.ts +++ b/extension/chrome/dev/ci_unit_test.ts @@ -12,6 +12,7 @@ import { Wkd } from '../../js/common/api/key-server/wkd.js'; import { MsgUtil } from '../../js/common/core/crypto/pgp/msg-util.js'; import { Sks } from '../../js/common/api/key-server/sks.js'; import { Ui } from '../../js/common/browser/ui.js'; +import { AcctStore } from '../../js/common/platform/store/acct-store.js'; import { ContactStore } from '../../js/common/platform/store/contact-store.js'; import { Debug } from '../../js/common/platform/debug.js'; import { Catch } from '../../js/common/platform/catch.js'; @@ -33,6 +34,7 @@ const libs: any[] = [ Sks, MsgUtil, Ui, + AcctStore, ContactStore, Debug, Catch, diff --git a/extension/chrome/settings/setup.ts b/extension/chrome/settings/setup.ts index 5c1e9f80837..c01e2d7743a 100644 --- a/extension/chrome/settings/setup.ts +++ b/extension/chrome/settings/setup.ts @@ -7,7 +7,7 @@ import { Url } from '../../js/common/core/common.js'; import { ApiErr } from '../../js/common/api/shared/api-error.js'; import { Assert } from '../../js/common/assert.js'; import { Catch } from '../../js/common/platform/catch.js'; -import { Key, KeyInfoWithIdentity, KeyUtil } from '../../js/common/core/crypto/key.js'; +import { KeyInfoWithIdentity, KeyUtil } from '../../js/common/core/crypto/key.js'; import { Gmail } from '../../js/common/api/email-provider/gmail/gmail.js'; import { Google } from '../../js/common/api/email-provider/gmail/google.js'; import { KeyImportUi } from '../../js/common/ui/key-import-ui.js'; @@ -27,8 +27,6 @@ import { PubLookup } from '../../js/common/api/pub-lookup.js'; import { Scopes, AcctStoreDict, AcctStore } from '../../js/common/platform/store/acct-store.js'; import { KeyStore } from '../../js/common/platform/store/key-store.js'; import { KeyStoreUtil } from "../../js/common/core/crypto/key-store-util.js"; -import { PassphraseStore } from '../../js/common/platform/store/passphrase-store.js'; -import { ContactStore } from '../../js/common/platform/store/contact-store.js'; import { KeyManager } from '../../js/common/api/key-server/key-manager.js'; import { SetupWithEmailKeyManagerModule } from './setup/setup-key-manager-autogen.js'; import { shouldPassPhraseBeHidden } from '../../js/common/ui/passphrase-ui.js'; @@ -243,20 +241,6 @@ export class SetupView extends View { await AcctStore.set(this.acctEmail, { setup_date: Date.now(), setup_done: true, cryptup_enabled: true }); }; - public saveKeysAndPassPhrase = async (prvs: Key[], options: SetupOptions) => { - for (const prv of prvs) { - await KeyStore.add(this.acctEmail, prv); - await PassphraseStore.set((options.passphrase_save && !this.orgRules.forbidStoringPassPhrase()) ? 'local' : 'session', - this.acctEmail, { longid: KeyUtil.getPrimaryLongid(prv) }, options.passphrase); - } - const { sendAs } = await AcctStore.get(this.acctEmail, ['sendAs']); - const myOwnEmailsAddrs: string[] = [this.acctEmail].concat(Object.keys(sendAs!)); - const { full_name: name } = await AcctStore.get(this.acctEmail, ['full_name']); - for (const email of myOwnEmailsAddrs) { - await ContactStore.update(undefined, email, { name, pubkey: KeyUtil.armor(await KeyUtil.asPublicKey(prvs[0])) }); - } - }; - public shouldSubmitPubkey = (checkboxSelector: string) => { if (this.orgRules.mustSubmitToAttester() && !this.orgRules.canSubmitPubToAttester()) { throw new Error('Organisation rules are misconfigured: ENFORCE_ATTESTER_SUBMIT not compatible with NO_ATTESTER_SUBMIT'); diff --git a/extension/chrome/settings/setup/setup-create-key.ts b/extension/chrome/settings/setup/setup-create-key.ts index 129415a5a36..87e633bda2d 100644 --- a/extension/chrome/settings/setup/setup-create-key.ts +++ b/extension/chrome/settings/setup/setup-create-key.ts @@ -11,6 +11,7 @@ import { Url } from '../../../js/common/core/common.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; import { OpenPGPKey } from '../../../js/common/core/crypto/pgp/openpgp-key.js'; +import { saveKeysAndPassPhrase } from '../../../js/common/shared.js'; export class SetupCreateKeyModule { @@ -72,7 +73,7 @@ export class SetupCreateKeyModule { const expireMonths = this.view.orgRules.getEnforcedKeygenExpirationMonths(); const key = await OpenPGPKey.create(pgpUids, keyAlgo, options.passphrase, expireMonths); const prv = await KeyUtil.parse(key.private); - await this.view.saveKeysAndPassPhrase([prv], options); + await saveKeysAndPassPhrase(this.view.acctEmail, [prv], options); return { id: prv.id, family: prv.family }; }; } diff --git a/extension/chrome/settings/setup/setup-import-key.ts b/extension/chrome/settings/setup/setup-import-key.ts index b94bc87b99c..a74ed072420 100644 --- a/extension/chrome/settings/setup/setup-import-key.ts +++ b/extension/chrome/settings/setup/setup-import-key.ts @@ -11,6 +11,7 @@ import { Ui } from '../../../js/common/browser/ui.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { Key, UnexpectedKeyTypeError } from '../../../js/common/core/crypto/key.js'; import { Lang } from '../../../js/common/lang.js'; +import { saveKeysAndPassPhrase } from '../../../js/common/shared.js'; export class SetupImportKeyModule { @@ -39,7 +40,7 @@ export class SetupImportKeyModule { } } Xss.sanitizeRender('#step_2b_manual_enter .action_add_private_key', Ui.spinner('white')); - await this.view.saveKeysAndPassPhrase([checked.encrypted], options); + await saveKeysAndPassPhrase(this.view.acctEmail, [checked.encrypted], options); await this.view.submitPublicKeys(options); await this.view.finalizeSetup(); await this.view.setupRender.renderSetupDone(); @@ -70,7 +71,7 @@ export class SetupImportKeyModule { this.view.setupRender.displayBlock('step_2b_manual_enter'); return; } - await this.view.saveKeysAndPassPhrase([fixedPrv], options); + await saveKeysAndPassPhrase(this.view.acctEmail, [fixedPrv], options); await this.view.submitPublicKeys(options); await this.view.finalizeSetup(); await this.view.setupRender.renderSetupDone(); diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index d9dc7bebf9b..0a9470ed801 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -6,13 +6,13 @@ import { SetupOptions, SetupView } from '../setup.js'; import { Ui } from '../../../js/common/browser/ui.js'; import { Url } from '../../../js/common/core/common.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; -import { Buf } from '../../../js/common/core/buf.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { Api } from '../../../js/common/api/shared/api.js'; import { Settings } from '../../../js/common/settings.js'; import { KeyUtil } from '../../../js/common/core/crypto/key.js'; import { OpenPGPKey } from '../../../js/common/core/crypto/pgp/openpgp-key.js'; import { Lang } from '../../../js/common/lang.js'; +import { processAndStoreKeysFromEkmLocally, saveKeysAndPassPhrase } from '../../../js/common/shared.js'; export class SetupWithEmailKeyManagerModule { @@ -42,7 +42,7 @@ export class SetupWithEmailKeyManagerModule { const { privateKeys } = await this.view.keyManager!.getPrivateKeys(this.view.idToken!); if (privateKeys.length) { // keys already exist on keyserver, auto-import - await this.processAndStoreKeysFromEkmLocally(privateKeys, setupOptions); + await processAndStoreKeysFromEkmLocally(this.view.acctEmail, privateKeys, setupOptions); } else if (this.view.orgRules.canCreateKeys()) { // generate keys on client and store them on key manager await this.autoGenerateKeyAndStoreBothLocallyAndToEkm(setupOptions); @@ -63,23 +63,6 @@ export class SetupWithEmailKeyManagerModule { } }; - private processAndStoreKeysFromEkmLocally = async (privateKeys: { decryptedPrivateKey: string }[], setupOptions: SetupOptions) => { - const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(privateKeys.map(pk => pk.decryptedPrivateKey).join('\n'))); - if (!keys.length) { - throw new Error(`Could not parse any valid keys from Key Manager response for user ${this.view.acctEmail}`); - } - for (const prv of keys) { - if (!prv.isPrivate) { - throw new Error(`Key ${prv.id} for user ${this.view.acctEmail} is not a private key`); - } - if (!prv.fullyDecrypted) { - throw new Error(`Key ${prv.id} for user ${this.view.acctEmail} from FlowCrypt Email Key Manager is not fully decrypted`); - } - await KeyUtil.encrypt(prv, setupOptions.passphrase); - } - await this.view.saveKeysAndPassPhrase(keys, setupOptions); - }; - private autoGenerateKeyAndStoreBothLocallyAndToEkm = async (setupOptions: SetupOptions) => { const keygenAlgo = this.view.orgRules.getEnforcedKeygenAlgo(); if (!keygenAlgo) { @@ -99,7 +82,7 @@ export class SetupWithEmailKeyManagerModule { const pubArmor = KeyUtil.armor(await KeyUtil.asPublicKey(decryptablePrv)); const storePrvOnKm = async () => this.view.keyManager!.storePrivateKey(this.view.idToken!, KeyUtil.armor(decryptablePrv), pubArmor); await Settings.retryUntilSuccessful(storePrvOnKm, 'Failed to store newly generated key on FlowCrypt Email Key Manager', Lang.general.contactIfNeedAssistance(this.view.isFesUsed())); - await this.view.saveKeysAndPassPhrase([await KeyUtil.parse(generated.private)], setupOptions); // store encrypted key + pass phrase locally + await saveKeysAndPassPhrase(this.view.acctEmail, [await KeyUtil.parse(generated.private)], setupOptions); // store encrypted key + pass phrase locally }; } diff --git a/extension/chrome/settings/setup/setup-recover-key.ts b/extension/chrome/settings/setup/setup-recover-key.ts index 4be82f1f2ed..1aa21ddf8a1 100644 --- a/extension/chrome/settings/setup/setup-recover-key.ts +++ b/extension/chrome/settings/setup/setup-recover-key.ts @@ -12,6 +12,7 @@ import { Url } from '../../../js/common/core/common.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; +import { saveKeysAndPassPhrase } from '../../../js/common/shared.js'; export class SetupRecoverKeyModule { @@ -63,7 +64,7 @@ export class SetupRecoverKeyModule { passphrase_save: true, // todo - reevaluate saving passphrase when recovering recovered: true, }; - await this.view.saveKeysAndPassPhrase(newlyMatchingKeys, options); + await saveKeysAndPassPhrase(this.view.acctEmail, newlyMatchingKeys, options); const { setup_done } = await AcctStore.get(this.view.acctEmail, ['setup_done']); if (!setup_done) { // normal situation - fresh setup await this.view.submitPublicKeys(options); diff --git a/extension/js/common/shared.ts b/extension/js/common/shared.ts index b65d4e24854..9d616d02606 100644 --- a/extension/js/common/shared.ts +++ b/extension/js/common/shared.ts @@ -2,9 +2,15 @@ 'use strict'; +import { SetupOptions } from '../../chrome/settings/setup.js'; +import { Buf } from './core/buf.js'; import { EmailParts } from './core/common.js'; -import { KeyUtil, PubkeyInfo } from './core/crypto/key.js'; +import { Key, KeyUtil, PubkeyInfo } from './core/crypto/key.js'; +import { OrgRules } from './org-rules.js'; +import { AcctStore } from './platform/store/acct-store.js'; import { ContactStore } from './platform/store/contact-store.js'; +import { KeyStore } from './platform/store/key-store.js'; +import { PassphraseStore } from './platform/store/passphrase-store.js'; /** * Save fetched keys if they are newer versions of public keys we already have (compared by fingerprint) @@ -35,3 +41,36 @@ export const saveFetchedPubkeysIfNewerThanInStorage = async ({ email, pubkeys }: return await compareAndSavePubkeysToStorage({ email }, pubkeys, storedContact?.sortedPubkeys ?? []); }; +// todo: where to take acctEmail and orgRules +export const saveKeysAndPassPhrase = async (acctEmail: string, prvs: Key[], options: SetupOptions) => { + for (const prv of prvs) { + await KeyStore.add(acctEmail, prv); + const orgRules = await OrgRules.newInstance(acctEmail); + await PassphraseStore.set((options.passphrase_save && !orgRules.forbidStoringPassPhrase()) ? 'local' : 'session', + acctEmail, { longid: KeyUtil.getPrimaryLongid(prv) }, options.passphrase); + } + const { sendAs } = await AcctStore.get(acctEmail, ['sendAs']); + const myOwnEmailsAddrs: string[] = [acctEmail].concat(Object.keys(sendAs!)); + const { full_name: name } = await AcctStore.get(acctEmail, ['full_name']); + for (const email of myOwnEmailsAddrs) { + await ContactStore.update(undefined, email, { name, pubkey: KeyUtil.armor(await KeyUtil.asPublicKey(prvs[0])) }); + } +}; + +// todo: where to take acctEmail? +export const processAndStoreKeysFromEkmLocally = async (acctEmail: string, privateKeys: { decryptedPrivateKey: string }[], setupOptions: SetupOptions) => { + const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(privateKeys.map(pk => pk.decryptedPrivateKey).join('\n'))); + if (!keys.length) { + throw new Error(`Could not parse any valid keys from Key Manager response for user ${acctEmail}`); + } + for (const prv of keys) { + if (!prv.isPrivate) { + throw new Error(`Key ${prv.id} for user ${acctEmail} is not a private key`); + } + if (!prv.fullyDecrypted) { + throw new Error(`Key ${prv.id} for user ${acctEmail} from FlowCrypt Email Key Manager is not fully decrypted`); + } + await KeyUtil.encrypt(prv, setupOptions.passphrase); + } + await saveKeysAndPassPhrase(acctEmail, keys, setupOptions); +}; diff --git a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts index 107042cdfe4..1350752103d 100644 --- a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts +++ b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts @@ -12,9 +12,12 @@ import { Injector } from '../../common/inject.js'; import { Notifications } from '../../common/notifications.js'; import Swal from 'sweetalert2'; import { Ui } from '../../common/browser/ui.js'; -import { VERSION } from '../../common/core/const.js'; +import { InMemoryStoreKeys, VERSION } from '../../common/core/const.js'; import { AcctStore } from '../../common/platform/store/acct-store.js'; import { GlobalStore } from '../../common/platform/store/global-store.js'; +import { OrgRules } from '../../common/org-rules.js'; +import { KeyManager } from '../../common/api/key-server/key-manager.js'; +import { InMemoryStore } from '../../common/platform/store/in-memory-store.js'; export type WebmailVariantObject = { newDataLayer: undefined | boolean, newUi: undefined | boolean, email: undefined | string, gmailVariant: WebmailVariantString }; export type IntervalFunction = { interval: number, handler: () => void }; @@ -24,7 +27,7 @@ type WebmailSpecificInfo = { getUserAccountEmail: () => string | undefined; getUserFullName: () => string | undefined; getReplacer: () => WebmailElementReplacer; - start: (acctEmail: string, inject: Injector, notifications: Notifications, factory: XssSafeFactory, notifyMurdered: () => void) => Promise; + start: (acctEmail: string, orgRules: OrgRules, inject: Injector, notifications: Notifications, factory: XssSafeFactory, notifyMurdered: () => void) => Promise; }; export interface WebmailElementReplacer { getIntervalFunctions: () => Array; @@ -232,13 +235,28 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi notifEl.appendChild(div); }; + const startPullingKeysFromEkm = async (acctEmail: string, orgRules: OrgRules) => { + if (orgRules.usesKeyManager()) { + const idToken = await InMemoryStore.get(acctEmail, InMemoryStoreKeys.ID_TOKEN); + if (idToken) { + const keyManager = new KeyManager(orgRules.getKeyManagerUrlForPrivateKeys()!); + Catch.setHandledTimeout(async () => { + const { privateKeys } = await keyManager.getPrivateKeys(idToken); + console.log(privateKeys); // processAndStoreKeysFromEkmLocally + }, 0); + } + } + }; + const entrypoint = async () => { try { const acctEmail = await waitForAcctEmail(); const { tabId, notifications, factory, inject } = await initInternalVars(acctEmail); await showNotificationsAndWaitTilAcctSetUp(acctEmail, notifications); browserMsgListen(acctEmail, tabId, inject, factory, notifications); - await webmailSpecific.start(acctEmail, inject, notifications, factory, notifyMurdered); + const orgRules = await OrgRules.newInstance(acctEmail); + await startPullingKeysFromEkm(acctEmail, orgRules); + await webmailSpecific.start(acctEmail, orgRules, inject, notifications, factory, notifyMurdered); } catch (e) { if (e instanceof TabIdRequiredError) { console.error(`FlowCrypt cannot start: ${String(e)}`); diff --git a/extension/js/content_scripts/webmail/webmail.ts b/extension/js/content_scripts/webmail/webmail.ts index 8670c537dd1..6e4895a2618 100644 --- a/extension/js/content_scripts/webmail/webmail.ts +++ b/extension/js/content_scripts/webmail/webmail.ts @@ -81,10 +81,9 @@ Catch.try(async () => { return insights; }; - const start = async (acctEmail: string, injector: Injector, notifications: Notifications, factory: XssSafeFactory, notifyMurdered: () => void) => { + const start = async (acctEmail: string, orgRules: OrgRules, injector: Injector, notifications: Notifications, factory: XssSafeFactory, notifyMurdered: () => void) => { hijackGmailHotkeys(); const storage = await AcctStore.get(acctEmail, ['sendAs', 'full_name']); - const orgRules = await OrgRules.newInstance(acctEmail); if (!storage.sendAs) { storage.sendAs = {}; storage.sendAs[acctEmail] = { name: storage.full_name, isPrimary: true }; diff --git a/test/source/browser/browser-pool.ts b/test/source/browser/browser-pool.ts index 10917a35608..900363447e1 100644 --- a/test/source/browser/browser-pool.ts +++ b/test/source/browser/browser-pool.ts @@ -40,8 +40,8 @@ export class BrowserPool { ]; if (this.isMock) { args.push('--ignore-certificate-errors'); - args.push('--allow-insecure-localhost'); } + args.push('--allow-insecure-localhost'); const slowMo = this.isMock ? 60 : 60; const browser = await puppeteer.launch({ args, ignoreHTTPSErrors: this.isMock, headless: false, devtools: false, slowMo }); const handle = new BrowserHandle(browser, this.semaphore, this.height, this.width); diff --git a/test/source/mock.ts b/test/source/mock.ts index 085aa54a589..ba529646559 100644 --- a/test/source/mock.ts +++ b/test/source/mock.ts @@ -2,12 +2,12 @@ import { startAllApisMock } from './mock/all-apis-mock'; -export const mock = async (logger: (line: string) => void) => { - return await startAllApisMock(logger); +export const mock = async (isMock: boolean, logger: (line: string) => void) => { + return await startAllApisMock(isMock, logger); }; if (require.main === module) { - mock(msgLog => console.log(msgLog)).catch(e => { + mock(true, msgLog => console.log(msgLog)).catch(e => { console.error(e); process.exit(1); }); diff --git a/test/source/mock/all-apis-mock.ts b/test/source/mock/all-apis-mock.ts index dea60cbc6d7..f1146f1592a 100644 --- a/test/source/mock/all-apis-mock.ts +++ b/test/source/mock/all-apis-mock.ts @@ -7,14 +7,14 @@ import * as http from 'http'; import { mockAttesterEndpoints } from './attester/attester-endpoints'; import { mockBackendEndpoints } from './backend/backend-endpoints'; import { mockGoogleEndpoints } from './google/google-endpoints'; -import { mockKeyManagerEndpoints } from './key-manager/key-manager-endpoints'; +import { liveKeyManagerEndpoints, mockKeyManagerEndpoints } from './key-manager/key-manager-endpoints'; import { mockWkdEndpoints } from './wkd/wkd-endpoints'; import { mockSksEndpoints } from './sks/sks-endpoints'; import { mockFesEndpoints } from './fes/fes-endpoints'; export type HandlersDefinition = Handlers<{ query: { [k: string]: string; }; body?: unknown; }, unknown>; -export const startAllApisMock = async (logger: (line: string) => void) => { +export const startAllApisMock = async (isMock: boolean, logger: (line: string) => void) => { class LoggedApi extends Api { protected throttleChunkMsUpload = 15; protected throttleChunkMsDownload = 50; @@ -24,7 +24,7 @@ export const startAllApisMock = async (logger: (line: string) => void) => { } }; } - const api = new LoggedApi<{ query: { [k: string]: string }, body?: unknown }, unknown>('google-mock', { + const handlers = isMock ? { ...mockGoogleEndpoints, ...mockBackendEndpoints, ...mockAttesterEndpoints, @@ -33,7 +33,8 @@ export const startAllApisMock = async (logger: (line: string) => void) => { ...mockSksEndpoints, ...mockFesEndpoints, '/favicon.ico': async () => '', - }); + } : { ...liveKeyManagerEndpoints }; + const api = new LoggedApi<{ query: { [k: string]: string }, body?: unknown }, unknown>('google-mock', handlers); await api.listen(8001); return api; }; diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index 5419fbf83b9..21499c6d304 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -14,9 +14,6 @@ import { testConstants } from '../../tests/tooling/consts'; // tslint:disable:no-unused-expression /* eslint-disable no-unused-expressions */ -// longid: 00B0115807969D75 -const existingPrv = '-----BEGIN PGP PRIVATE KEY BLOCK-----\r\nVersion: FlowCrypt 7.6.2 Gmail Encryption\r\nComment: Seamlessly send and receive encrypted email\r\n\r\n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\r\n=tqvP\r\n-----END PGP PRIVATE KEY BLOCK-----\r\n'; - const twoKeys1 = `-----BEGIN PGP PRIVATE KEY BLOCK----- xcLYBF+RzZUBCADHT42w0/fMBIEjNZhIgl3bVDXPoX9FYmrROXN2nOy+mEhB @@ -209,6 +206,20 @@ yeSm0uVPwODhwX7ezB9jW6uVt0R8S8iM3rQdEMsA/jDep5LNn47K6o8VrDt0zYo6 export const MOCK_KM_LAST_INSERTED_KEY: { [acct: string]: { decryptedPrivateKey: string, publicKey: string } } = {}; // accessed from test runners +export const LIVE_KM_RESPONSE: { privateKeys: { decryptedPrivateKey: string }[] } = { privateKeys: [] }; + +export const liveKeyManagerEndpoints: HandlersDefinition = { + '/flowcrypt-email-key-manager/keys/private': async ({ }, req) => { + if (isGet(req)) { + return LIVE_KM_RESPONSE; + } + if (isPut(req)) { + throw new HttpClientErr(`Unexpectedly calling liveKeyManagerEndpoints:/keys/private PUT`); + } + throw new HttpClientErr(`Unknown method: ${req.method}`); + } +}; + export const mockKeyManagerEndpoints: HandlersDefinition = { '/flowcrypt-email-key-manager/keys/private': async ({ body }, req) => { const acctEmail = oauth.checkAuthorizationHeaderWithIdToken(req.headers.authorization); @@ -217,7 +228,7 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { return { privateKeys: [{ decryptedPrivateKey: testConstants.wkdAtgooglemockflowcryptlocalcom8001Private }] }; } if (acctEmail === 'get.key@key-manager-autogen.flowcrypt.test') { - return { privateKeys: [{ decryptedPrivateKey: existingPrv }] }; + return { privateKeys: [{ decryptedPrivateKey: testConstants.existingPrv }] }; } if (acctEmail === 'get.key@no-submit-org-rule.key-manager-autogen.flowcrypt.test') { return { privateKeys: [{ decryptedPrivateKey: prvNoSubmit }] }; @@ -226,13 +237,13 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { return { privateKeys: [{ decryptedPrivateKey: twoKeys1 }, { decryptedPrivateKey: twoKeys2 }] }; } if (acctEmail === 'user@key-manager-no-pub-lookup.flowcrypt.test') { - return { privateKeys: [{ decryptedPrivateKey: existingPrv }] }; + return { privateKeys: [{ decryptedPrivateKey: testConstants.existingPrv }] }; } if (acctEmail === 'get.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test') { - return { privateKeys: [{ decryptedPrivateKey: existingPrv }] }; + return { privateKeys: [{ decryptedPrivateKey: testConstants.existingPrv }] }; } if (acctEmail === 'get.key@key-manager-choose-passphrase.flowcrypt.test') { - return { privateKeys: [{ decryptedPrivateKey: existingPrv }] }; + return { privateKeys: [{ decryptedPrivateKey: testConstants.existingPrv }] }; } if (acctEmail === 'get.key@key-manager-autoimport-no-prv-create.flowcrypt.test') { return { privateKeys: [] }; diff --git a/test/source/test.ts b/test/source/test.ts index 815bf11b106..24cb271b6ec 100644 --- a/test/source/test.ts +++ b/test/source/test.ts @@ -54,15 +54,13 @@ ava.default.before('set config and mock api', async t => { standaloneTestTimeout(t, consts.TIMEOUT_EACH_RETRY, t.title); Config.extensionId = await browserPool.getExtensionId(t); console.info(`Extension url: chrome-extension://${Config.extensionId}`); - if (isMock) { - const mockApi = await mock(line => { - if (DEBUG_MOCK_LOG) { - console.log(line); - } - mockApiLogs.push(line); - }); - closeMockApi = mockApi.close; - } + const mockApi = await mock(isMock, line => { + if (DEBUG_MOCK_LOG) { + console.log(line); + } + mockApiLogs.push(line); + }); + closeMockApi = mockApi.close; t.pass(); }); diff --git a/test/source/tests/gmail.ts b/test/source/tests/gmail.ts index c74e9419867..bafc7bdf6b8 100644 --- a/test/source/tests/gmail.ts +++ b/test/source/tests/gmail.ts @@ -15,6 +15,8 @@ import { TestWithBrowser } from './../test'; import { expect } from 'chai'; import { OauthPageRecipe } from './page-recipe/oauth-page-recipe'; import { SetupPageRecipe } from './page-recipe/setup-page-recipe'; +import { LIVE_KM_RESPONSE } from '../mock/key-manager/key-manager-endpoints'; +import { testConstants } from './tooling/consts'; /** * All tests that use mail.google.com or have to operate without a Gmail API mock should go here @@ -391,6 +393,22 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test await pageHasSecureReplyContainer(t, browser, gmailPage); })); + ava.default('mail.google.com - fetching new private key -- asking for pass phrase', testWithBrowser('ci.tests.gmail', async (t, browser) => { + const dbPage = await browser.newPage(t, TestUrls.extension('chrome/dev/ci_unit_test.htm')); + // forge OrgRules to wire the key manager + await dbPage.page.evaluate(async () => { + await (window as any).AcctStore.set('ci.tests.gmail@flowcrypt.dev', { + flags: ['PRV_AUTOIMPORT_OR_AUTOGEN'], // todo: ATTESTER_SUBMIT, FORBID_STORING_PASSPHRASE + rules: { key_manager_url: 'https://localhost:8001/flowcrypt-email-key-manager' } + }); + }); + LIVE_KM_RESPONSE.privateKeys = [{ decryptedPrivateKey: testConstants.existingPrv }]; + const gmailPage = await openGmailPage(t, browser); + await Util.sleep(10); + // todo: + await gmailPage.close(); + })); + // ava.default('mail.google.com - reauth after uuid change', testWithBrowser('ci.tests.gmail', async (t, browser) => { // const acct = 'ci.tests.gmail@flowcrypt.dev'; // const settingsPage = await browser.newPage(t, TestUrls.extensionSettings(acct)); diff --git a/test/source/tests/tooling/consts.ts b/test/source/tests/tooling/consts.ts index c3a1431faf0..630b802b28c 100644 --- a/test/source/tests/tooling/consts.ts +++ b/test/source/tests/tooling/consts.ts @@ -1436,7 +1436,71 @@ aXvUekkHcZ2P6wjh6mYZgFKJxwUW6Y3lCBPAHSWLAFKw5xp8EPq1Q3h84NsNE6Te 4SNUmgtls3XYfGQ9ZKUUAQTDqLeLYTIadYq8sD9bthjUIIo0Hz2R1F6kCgDN5NnK 0+tURVqcqBi8Azs3HACQsbhb32RlotA= =GE+P ------END PGP PRIVATE KEY BLOCK-----` +-----END PGP PRIVATE KEY BLOCK-----`, + existingPrv: // longid: 00B0115807969D75 + `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: FlowCrypt 7.6.2 Gmail Encryption +Comment: Seamlessly send and receive encrypted email + +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 +=tqvP +-----END PGP PRIVATE KEY BLOCK-----`, + }; /* eslint-disable max-len */ @@ -1446,7 +1510,26 @@ export const testKeyConstants = { { "title": "ci.tests.gmail", "passphrase": "citstonfcdevdomain", - "armored": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: FlowCrypt Email Encryption 7.8.9\nComment: Seamlessly send and receive encrypted email\n\nxYYEXzq9RBYJKwYBBAHaRw8BAQdAYB2/hjjJaVZbDboWislVE88A5Bi7CEHV\n1PeNfGEM3dj+CQMIqZRTqDEC2Z7gOBJuZ5sRHi6hGUCAFLCGpVKZO9mCv/7g\nv8BTSC59djP9ezSiVT7JremTjf3fmNKoPeE4y+tobSb0rqUyecvT4PASY1iC\nDM0sR21haWwgQ0kgVGVzdCA8Y2kudGVzdHMuZ21haWxAZmxvd2NyeXB0LmRl\ndj7CjwQQFgoAIAUCXzq9RAYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJ\nEAdIHIrPnUn+FiEEm6Oc4HXwg5swNA/IB0gcis+dSf42VgD+LuUQu/B0g+ll\nqtzegLKUGX/CDLijJm3fOPFKW7l3T3ABALQGMqXca1jpuMERQdq+dE5yxhqS\nHIqX4yYCamljOaUNx4sEXzq9RBIKKwYBBAGXVQEFAQEHQILCd69DwnEpYMCg\n7rqcZvFbOzdVDo/V7hSape+EVPwRAwEIB/4JAwiOx5ib9VjVguB1bR/LEg9K\nMZVpoJek5xRwgJYUrP0R+FSCq3qQgu0DDJMTepmA+Ks/pVSe8bjLXp6OzhLD\ni9wrQsIltClLzgn5IqoSVHGAwngEGBYIAAkFAl86vUQCGwwAIQkQB0gcis+d\nSf4WIQSbo5zgdfCDmzA0D8gHSByKz51J/jLcAP0dmYzcO3JGCvFRpXDeX7Bs\neB0Dxje8Q1w52uHm4BYZgwD/fFYiASnRQzCHwTpwyk110W+jPt+rNZ6OIRZH\n0d++GQ0=\n=rX/o\n-----END PGP PRIVATE KEY BLOCK-----\n", /// + "armored": `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: FlowCrypt Email Encryption 7.8.9 +Comment: Seamlessly send and receive encrypted email + +xYYEXzq9RBYJKwYBBAHaRw8BAQdAYB2/hjjJaVZbDboWislVE88A5Bi7CEHV +1PeNfGEM3dj+CQMIqZRTqDEC2Z7gOBJuZ5sRHi6hGUCAFLCGpVKZO9mCv/7g +v8BTSC59djP9ezSiVT7JremTjf3fmNKoPeE4y+tobSb0rqUyecvT4PASY1iC +DM0sR21haWwgQ0kgVGVzdCA8Y2kudGVzdHMuZ21haWxAZmxvd2NyeXB0LmRl +dj7CjwQQFgoAIAUCXzq9RAYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJ +EAdIHIrPnUn+FiEEm6Oc4HXwg5swNA/IB0gcis+dSf42VgD+LuUQu/B0g+ll +qtzegLKUGX/CDLijJm3fOPFKW7l3T3ABALQGMqXca1jpuMERQdq+dE5yxhqS +HIqX4yYCamljOaUNx4sEXzq9RBIKKwYBBAGXVQEFAQEHQILCd69DwnEpYMCg +7rqcZvFbOzdVDo/V7hSape+EVPwRAwEIB/4JAwiOx5ib9VjVguB1bR/LEg9K +MZVpoJek5xRwgJYUrP0R+FSCq3qQgu0DDJMTepmA+Ks/pVSe8bjLXp6OzhLD +i9wrQsIltClLzgn5IqoSVHGAwngEGBYIAAkFAl86vUQCGwwAIQkQB0gcis+d +Sf4WIQSbo5zgdfCDmzA0D8gHSByKz51J/jLcAP0dmYzcO3JGCvFRpXDeX7Bs +eB0Dxje8Q1w52uHm4BYZgwD/fFYiASnRQzCHwTpwyk110W+jPt+rNZ6OIRZH +0d++GQ0= +=rX/o +-----END PGP PRIVATE KEY BLOCK-----`, "longid": null // tslint:disable-line:no-null-keyword }, { From 26155d27e2a6cf3fae1db688e853ec9ff7863483 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 6 Jun 2022 17:27:18 +0000 Subject: [PATCH 02/26] fix tooling scripts --- tooling/bundle-content-scripts.ts | 28 ++++++++++++++-------------- tooling/fill-values.ts | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tooling/bundle-content-scripts.ts b/tooling/bundle-content-scripts.ts index 9f7bd76b06a..d0457fb244a 100644 --- a/tooling/bundle-content-scripts.ts +++ b/tooling/bundle-content-scripts.ts @@ -32,19 +32,19 @@ mkdirSync(OUT_DIR); // webmail buildContentScript(([] as string[]).concat( - getFilesInDir(`${sourceDir}/common/platform`, /\.js$/, false), - getFilesInDir(`${sourceDir}/common/platform/store`, /\.js$/, false), - getFilesInDir(`${sourceDir}/common/core`, /\.js$/, false), - getFilesInDir(`${sourceDir}/common/core/crypto`, /\.js$/, false), - getFilesInDir(`${sourceDir}/common/core/crypto/pgp`, /\.js$/, false), - getFilesInDir(`${sourceDir}/common/core/crypto/smime`, /\.js$/, false), - getFilesInDir(`${sourceDir}/common/api/shared`, /\.js$/, false), - getFilesInDir(`${sourceDir}/common/api/key-server`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common/platform`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common/platform/store`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common/core`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common/core/crypto`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common/core/crypto/pgp`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common/core/crypto/smime`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common/api/shared`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common/api/key-server`, /\.js$/, false), // getFilesInDir(`${sourceDir}/common/api/account-server`, /\.js$/, false), // not used by content scripts yet - getFilesInDir(`${sourceDir}/common/api/email-provider`, /\.js$/, false), - getFilesInDir(`${sourceDir}/common/api/email-provider/gmail`, /\.js$/, false), - getFilesInDir(`${sourceDir}/common/api`, /\.js$/, false), - getFilesInDir(`${sourceDir}/common/browser`, /\.js$/, false), - getFilesInDir(`${sourceDir}/common`, /\.js$/, false), - getFilesInDir(`${sourceDir}/content_scripts/webmail`, /\.js$/), + getFilesInDir(`${sourceDir}/js/common/api/email-provider`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common/api/email-provider/gmail`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common/api`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common/browser`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/common`, /\.js$/, false), + getFilesInDir(`${sourceDir}/js/content_scripts/webmail`, /\.js$/), ), 'webmail_bundle.js'); diff --git a/tooling/fill-values.ts b/tooling/fill-values.ts index b99014cf1c0..06c469bf112 100644 --- a/tooling/fill-values.ts +++ b/tooling/fill-values.ts @@ -25,7 +25,7 @@ const replaceables: { needle: RegExp, val: string }[] = [ const paths = [ `${targetDirExtension}/js/common/core/const.js`, - `./build/${targetDirContentScripts}/common/core/const.js`, + `./build/${targetDirContentScripts}/js/common/core/const.js`, ]; for (const path of paths) { From bc092e796f7a9b60e30d2cc091e1993a519b0791 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 13 Jun 2022 06:29:57 +0000 Subject: [PATCH 03/26] finished merge and refactoring --- extension/chrome/settings/setup.ts | 18 +------- .../chrome/settings/setup/setup-create-key.ts | 2 +- .../chrome/settings/setup/setup-import-key.ts | 2 +- .../setup/setup-key-manager-autogen.ts | 2 +- .../settings/setup/setup-recover-key.ts | 2 +- extension/js/common/helpers.ts | 42 ++++++++++++++++++- extension/js/common/shared.ts | 42 +------------------ .../js/content_scripts/webmail/webmail.ts | 8 +++- 8 files changed, 54 insertions(+), 64 deletions(-) diff --git a/extension/chrome/settings/setup.ts b/extension/chrome/settings/setup.ts index a2c71327e3b..7bc1c557c02 100644 --- a/extension/chrome/settings/setup.ts +++ b/extension/chrome/settings/setup.ts @@ -7,7 +7,7 @@ import { Url } from '../../js/common/core/common.js'; import { ApiErr } from '../../js/common/api/shared/api-error.js'; import { Assert } from '../../js/common/assert.js'; import { Catch } from '../../js/common/platform/catch.js'; -import { Key, KeyInfoWithIdentity, KeyUtil } from '../../js/common/core/crypto/key.js'; +import { KeyInfoWithIdentity, KeyUtil } from '../../js/common/core/crypto/key.js'; import { Gmail } from '../../js/common/api/email-provider/gmail/gmail.js'; import { Google } from '../../js/common/api/email-provider/gmail/google.js'; import { KeyImportUi } from '../../js/common/ui/key-import-ui.js'; @@ -31,8 +31,6 @@ import { KeyManager } from '../../js/common/api/key-server/key-manager.js'; import { SetupWithEmailKeyManagerModule } from './setup/setup-key-manager-autogen.js'; import { shouldPassPhraseBeHidden } from '../../js/common/ui/passphrase-ui.js'; import Swal from 'sweetalert2'; -import { PassphraseStore } from '../../js/common/platform/store/passphrase-store.js'; -import { ContactStore } from '../../js/common/platform/store/contact-store.js'; export interface SetupOptions { passphrase: string; @@ -243,20 +241,6 @@ export class SetupView extends View { await AcctStore.set(this.acctEmail, { setup_date: Date.now(), setup_done: true, cryptup_enabled: true }); }; - public saveKeysAndPassPhrase = async (prvs: Key[], options: SetupOptions) => { - for (const prv of prvs) { - await KeyStore.add(this.acctEmail, prv); - await PassphraseStore.set((options.passphrase_save && !this.clientConfiguration.forbidStoringPassPhrase()) ? 'local' : 'session', - this.acctEmail, { longid: KeyUtil.getPrimaryLongid(prv) }, options.passphrase); - } - const { sendAs } = await AcctStore.get(this.acctEmail, ['sendAs']); - const myOwnEmailsAddrs: string[] = [this.acctEmail].concat(Object.keys(sendAs!)); - const { full_name: name } = await AcctStore.get(this.acctEmail, ['full_name']); - for (const email of myOwnEmailsAddrs) { - await ContactStore.update(undefined, email, { name, pubkey: KeyUtil.armor(await KeyUtil.asPublicKey(prvs[0])) }); - } - }; - public shouldSubmitPubkey = (checkboxSelector: string) => { if (this.clientConfiguration.mustSubmitToAttester() && !this.clientConfiguration.canSubmitPubToAttester()) { throw new Error('Organisation rules are misconfigured: ENFORCE_ATTESTER_SUBMIT not compatible with NO_ATTESTER_SUBMIT'); diff --git a/extension/chrome/settings/setup/setup-create-key.ts b/extension/chrome/settings/setup/setup-create-key.ts index c8f2e49e6a4..12da8d227c2 100644 --- a/extension/chrome/settings/setup/setup-create-key.ts +++ b/extension/chrome/settings/setup/setup-create-key.ts @@ -11,7 +11,7 @@ import { Url } from '../../../js/common/core/common.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; import { OpenPGPKey } from '../../../js/common/core/crypto/pgp/openpgp-key.js'; -import { saveKeysAndPassPhrase } from '../../../js/common/shared.js'; +import { saveKeysAndPassPhrase } from '../../../js/common/helpers.js'; export class SetupCreateKeyModule { diff --git a/extension/chrome/settings/setup/setup-import-key.ts b/extension/chrome/settings/setup/setup-import-key.ts index a74ed072420..0681c20ef8c 100644 --- a/extension/chrome/settings/setup/setup-import-key.ts +++ b/extension/chrome/settings/setup/setup-import-key.ts @@ -11,7 +11,7 @@ import { Ui } from '../../../js/common/browser/ui.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { Key, UnexpectedKeyTypeError } from '../../../js/common/core/crypto/key.js'; import { Lang } from '../../../js/common/lang.js'; -import { saveKeysAndPassPhrase } from '../../../js/common/shared.js'; +import { saveKeysAndPassPhrase } from '../../../js/common/helpers.js'; export class SetupImportKeyModule { diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index 5f0060aa964..fdc41cb666e 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -12,7 +12,7 @@ import { Settings } from '../../../js/common/settings.js'; import { KeyUtil } from '../../../js/common/core/crypto/key.js'; import { OpenPGPKey } from '../../../js/common/core/crypto/pgp/openpgp-key.js'; import { Lang } from '../../../js/common/lang.js'; -import { processAndStoreKeysFromEkmLocally, saveKeysAndPassPhrase } from '../../../js/common/shared.js'; +import { processAndStoreKeysFromEkmLocally, saveKeysAndPassPhrase } from '../../../js/common/helpers.js'; export class SetupWithEmailKeyManagerModule { diff --git a/extension/chrome/settings/setup/setup-recover-key.ts b/extension/chrome/settings/setup/setup-recover-key.ts index 1aa21ddf8a1..0480cd550ae 100644 --- a/extension/chrome/settings/setup/setup-recover-key.ts +++ b/extension/chrome/settings/setup/setup-recover-key.ts @@ -12,7 +12,7 @@ import { Url } from '../../../js/common/core/common.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; -import { saveKeysAndPassPhrase } from '../../../js/common/shared.js'; +import { saveKeysAndPassPhrase } from '../../../js/common/helpers.js'; export class SetupRecoverKeyModule { diff --git a/extension/js/common/helpers.ts b/extension/js/common/helpers.ts index 066962223ec..f6495840bc7 100644 --- a/extension/js/common/helpers.ts +++ b/extension/js/common/helpers.ts @@ -3,8 +3,48 @@ 'use strict'; import { AcctStore } from './platform/store/acct-store.js'; - +import { SetupOptions } from '../../chrome/settings/setup.js'; +import { Buf } from './core/buf.js'; +import { Key, KeyUtil } from './core/crypto/key.js'; +import { ClientConfiguration } from './client-configuration.js'; +import { ContactStore } from './platform/store/contact-store.js'; +import { KeyStore } from './platform/store/key-store.js'; +import { PassphraseStore } from './platform/store/passphrase-store.js'; export const isFesUsed = async (acctEmail: string) => { const { fesUrl } = await AcctStore.get(acctEmail, ['fesUrl']); return Boolean(fesUrl); }; + +// todo: where to take acctEmail and clientConfiguration +export const saveKeysAndPassPhrase = async (acctEmail: string, prvs: Key[], options: SetupOptions) => { + for (const prv of prvs) { + await KeyStore.add(acctEmail, prv); + const clientConfiguration = await ClientConfiguration.newInstance(acctEmail); + await PassphraseStore.set((options.passphrase_save && !clientConfiguration.forbidStoringPassPhrase()) ? 'local' : 'session', + acctEmail, { longid: KeyUtil.getPrimaryLongid(prv) }, options.passphrase); + } + const { sendAs } = await AcctStore.get(acctEmail, ['sendAs']); + const myOwnEmailsAddrs: string[] = [acctEmail].concat(Object.keys(sendAs!)); + const { full_name: name } = await AcctStore.get(acctEmail, ['full_name']); + for (const email of myOwnEmailsAddrs) { + await ContactStore.update(undefined, email, { name, pubkey: KeyUtil.armor(await KeyUtil.asPublicKey(prvs[0])) }); + } +}; + +// todo: where to take acctEmail? +export const processAndStoreKeysFromEkmLocally = async (acctEmail: string, privateKeys: { decryptedPrivateKey: string }[], setupOptions: SetupOptions) => { + const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(privateKeys.map(pk => pk.decryptedPrivateKey).join('\n'))); + if (!keys.length) { + throw new Error(`Could not parse any valid keys from Key Manager response for user ${acctEmail}`); + } + for (const prv of keys) { + if (!prv.isPrivate) { + throw new Error(`Key ${prv.id} for user ${acctEmail} is not a private key`); + } + if (!prv.fullyDecrypted) { + throw new Error(`Key ${prv.id} for user ${acctEmail} from FlowCrypt Email Key Manager is not fully decrypted`); + } + await KeyUtil.encrypt(prv, setupOptions.passphrase); + } + await saveKeysAndPassPhrase(acctEmail, keys, setupOptions); +}; diff --git a/extension/js/common/shared.ts b/extension/js/common/shared.ts index 3efd98354a4..ddfb0a463aa 100644 --- a/extension/js/common/shared.ts +++ b/extension/js/common/shared.ts @@ -2,15 +2,9 @@ 'use strict'; -import { SetupOptions } from '../../chrome/settings/setup.js'; -import { Buf } from './core/buf.js'; import { EmailParts } from './core/common.js'; -import { Key, KeyUtil, PubkeyInfo } from './core/crypto/key.js'; -import { ClientConfiguration } from './client-configuration.js'; -import { AcctStore } from './platform/store/acct-store.js'; +import { KeyUtil, PubkeyInfo } from './core/crypto/key.js'; import { ContactStore } from './platform/store/contact-store.js'; -import { KeyStore } from './platform/store/key-store.js'; -import { PassphraseStore } from './platform/store/passphrase-store.js'; /** * Save fetched keys if they are newer versions of public keys we already have (compared by fingerprint) @@ -40,37 +34,3 @@ export const saveFetchedPubkeysIfNewerThanInStorage = async ({ email, pubkeys }: const storedContact = await ContactStore.getOneWithAllPubkeys(undefined, email); return await compareAndSavePubkeysToStorage({ email }, pubkeys, storedContact?.sortedPubkeys ?? []); }; - -// todo: where to take acctEmail and clientConfiguration -export const saveKeysAndPassPhrase = async (acctEmail: string, prvs: Key[], options: SetupOptions) => { - for (const prv of prvs) { - await KeyStore.add(acctEmail, prv); - const clientConfiguration = await ClientConfiguration.newInstance(acctEmail); - await PassphraseStore.set((options.passphrase_save && !clientConfiguration.forbidStoringPassPhrase()) ? 'local' : 'session', - acctEmail, { longid: KeyUtil.getPrimaryLongid(prv) }, options.passphrase); - } - const { sendAs } = await AcctStore.get(acctEmail, ['sendAs']); - const myOwnEmailsAddrs: string[] = [acctEmail].concat(Object.keys(sendAs!)); - const { full_name: name } = await AcctStore.get(acctEmail, ['full_name']); - for (const email of myOwnEmailsAddrs) { - await ContactStore.update(undefined, email, { name, pubkey: KeyUtil.armor(await KeyUtil.asPublicKey(prvs[0])) }); - } -}; - -// todo: where to take acctEmail? -export const processAndStoreKeysFromEkmLocally = async (acctEmail: string, privateKeys: { decryptedPrivateKey: string }[], setupOptions: SetupOptions) => { - const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(privateKeys.map(pk => pk.decryptedPrivateKey).join('\n'))); - if (!keys.length) { - throw new Error(`Could not parse any valid keys from Key Manager response for user ${acctEmail}`); - } - for (const prv of keys) { - if (!prv.isPrivate) { - throw new Error(`Key ${prv.id} for user ${acctEmail} is not a private key`); - } - if (!prv.fullyDecrypted) { - throw new Error(`Key ${prv.id} for user ${acctEmail} from FlowCrypt Email Key Manager is not fully decrypted`); - } - await KeyUtil.encrypt(prv, setupOptions.passphrase); - } - await saveKeysAndPassPhrase(acctEmail, keys, setupOptions); -}; diff --git a/extension/js/content_scripts/webmail/webmail.ts b/extension/js/content_scripts/webmail/webmail.ts index b33b489a1f7..48d980381b4 100644 --- a/extension/js/content_scripts/webmail/webmail.ts +++ b/extension/js/content_scripts/webmail/webmail.ts @@ -81,7 +81,13 @@ Catch.try(async () => { return insights; }; - const start = async (acctEmail: string, clientConfiguration: ClientConfiguration, injector: Injector, notifications: Notifications, factory: XssSafeFactory, notifyMurdered: () => void) => { + const start = async (acctEmail: string, + clientConfiguration: ClientConfiguration, + injector: Injector, + notifications: Notifications, + factory: XssSafeFactory, + notifyMurdered: () => void + ) => { hijackGmailHotkeys(); const storage = await AcctStore.get(acctEmail, ['sendAs', 'full_name']); if (!storage.sendAs) { From b8ed25f5456e0826380ab26c614789e094ff58d0 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 18 Jun 2022 16:36:20 +0000 Subject: [PATCH 04/26] support and implementation of mocked test for setup-webmail-content-script --- extension/chrome/settings/setup.ts | 5 +- .../setup/setup-key-manager-autogen.ts | 3 +- extension/js/common/browser/browser-msg.ts | 9 ++- extension/js/common/core/crypto/key.ts | 2 +- .../js/common/core/crypto/pgp/openpgp-key.ts | 2 +- extension/js/common/helpers.ts | 56 +++++++++++++++---- .../webmail/setup-webmail-content-script.ts | 2 +- test/source/browser/browser-handle.ts | 5 +- test/source/mock/google/google-data.ts | 11 ++++ test/source/mock/google/google-endpoints.ts | 8 +++ .../mock/key-manager/key-manager-endpoints.ts | 10 ++++ test/source/tests/setup.ts | 24 ++++++++ test/source/tests/tooling/browser-recipe.ts | 4 +- tooling/build-types-and-manifests.ts | 3 + 14 files changed, 122 insertions(+), 22 deletions(-) diff --git a/extension/chrome/settings/setup.ts b/extension/chrome/settings/setup.ts index 7bc1c557c02..d1e36f6ac3b 100644 --- a/extension/chrome/settings/setup.ts +++ b/extension/chrome/settings/setup.ts @@ -32,9 +32,12 @@ import { SetupWithEmailKeyManagerModule } from './setup/setup-key-manager-autoge import { shouldPassPhraseBeHidden } from '../../js/common/ui/passphrase-ui.js'; import Swal from 'sweetalert2'; -export interface SetupOptions { +export interface PassphraseOptions { passphrase: string; passphrase_save: boolean; +} + +export interface SetupOptions extends PassphraseOptions { submit_main: boolean; submit_all: boolean; recovered?: boolean; diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index fdc41cb666e..c2c70d10b18 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -42,7 +42,8 @@ export class SetupWithEmailKeyManagerModule { const { privateKeys } = await this.view.keyManager!.getPrivateKeys(this.view.idToken!); if (privateKeys.length) { // keys already exist on keyserver, auto-import - await processAndStoreKeysFromEkmLocally(this.view.acctEmail, privateKeys, setupOptions); + // todo: do we need to submit on auto-update? + await processAndStoreKeysFromEkmLocally({ acctEmail: this.view.acctEmail, privateKeys, options: setupOptions }); } else if (this.view.clientConfiguration.canCreateKeys()) { // generate keys on client and store them on key manager await this.autoGenerateKeyAndStoreBothLocallyAndToEkm(setupOptions); diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index 8664500a7c8..f7abf4279c8 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 { saveFetchedPubkeysIfNewerThanInStorage } from '../shared.js'; +import { processAndStoreKeysFromEkmLocally } from '../helpers.js'; export type GoogleAuthWindowResult$result = 'Success' | 'Denied' | 'Error' | 'Closed'; @@ -65,6 +66,7 @@ export namespace Bm { export type ShowAttachmentPreview = { iframeUrl: string }; export type ReRenderRecipient = { email: string }; export type SaveFetchedPubkeys = { email: string, pubkeys: string[] }; + export type ProcessKeysFromEkm = { acctEmail: string, privateKeys: { decryptedPrivateKey: string }[] }; export namespace Res { export type GetActiveTabInfo = { provider: 'gmail' | undefined, acctEmail: string | undefined, sameWorld: boolean | undefined }; @@ -83,13 +85,14 @@ export namespace Bm { export type AjaxGmailAttachmentGetChunk = { chunk: Buf }; export type _tab_ = { tabId: string | null | undefined }; export type SaveFetchedPubkeys = boolean; + export type ProcessKeysFromEkm = void; 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 | SaveFetchedPubkeys; + | AjaxGmailAttachmentGetChunk | SaveFetchedPubkeys | ProcessKeysFromEkm; } export type AnyRequest = PassphraseEntry | OpenPage | OpenGoogleAuthDialog | Redirect | Reload | @@ -98,7 +101,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 | SaveFetchedPubkeys; + ShowAttachmentPreview | ReRenderRecipient | SaveFetchedPubkeys | ProcessKeysFromEkm; // export type RawResponselessHandler = (req: AnyRequest) => Promise; // export type RawRespoHandler = (req: AnyRequest) => Promise; @@ -146,6 +149,7 @@ export class BrowserMsg { 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, + processKeysFromEkm: (bm: Bm.ProcessKeysFromEkm) => BrowserMsg.sendAwait(undefined, 'processKeysFromEkm', bm, true) as Promise, }, }, passphraseEntry: (dest: Bm.Dest, bm: Bm.PassphraseEntry) => BrowserMsg.sendCatch(dest, 'passphrase_entry', bm), @@ -242,6 +246,7 @@ export class BrowserMsg { BrowserMsg.bgAddListener('pgpMsgVerifyDetached', MsgUtil.verifyDetached); BrowserMsg.bgAddListener('pgpMsgType', MsgUtil.type); BrowserMsg.bgAddListener('saveFetchedPubkeys', saveFetchedPubkeysIfNewerThanInStorage); + BrowserMsg.bgAddListener('processKeysFromEkm', processAndStoreKeysFromEkmLocally); }; public static addListener = (name: string, handler: Handler) => { diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index a250437d4be..b4ebc013fdd 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -363,7 +363,7 @@ export class KeyUtil { } }; - public static reformatKey = async (privateKey: Key, passphrase: string, userIds: { email: string | undefined; name: string }[], expireSeconds: number) => { + public static reformatKey = async (privateKey: Key, passphrase: string | undefined, userIds: { email: string | undefined; name: string }[], expireSeconds: number) => { if (privateKey.family === 'openpgp') { return await OpenPGPKey.reformatKey(privateKey, passphrase, userIds, expireSeconds); } else { diff --git a/extension/js/common/core/crypto/pgp/openpgp-key.ts b/extension/js/common/core/crypto/pgp/openpgp-key.ts index 08e2d4396c0..257e5fc3cc0 100644 --- a/extension/js/common/core/crypto/pgp/openpgp-key.ts +++ b/extension/js/common/core/crypto/pgp/openpgp-key.ts @@ -151,7 +151,7 @@ export class OpenPGPKey { return await Catch.doesReject(opgpPrv.verifyPrimaryKey(), ['No self-certifications']); }; - public static reformatKey = async (privateKey: Key, passphrase: string, userIds: { email: string | undefined; name: string }[], expireSeconds: number) => { + public static reformatKey = async (privateKey: Key, passphrase: string | undefined, userIds: { email: string | undefined; name: string }[], expireSeconds: number) => { const opgpPrv = OpenPGPKey.extractExternalLibraryObjFromKey(privateKey); const keyPair = await opgp.reformatKey({ privateKey: opgpPrv, passphrase, userIds, keyExpirationTime: expireSeconds }); return await OpenPGPKey.convertExternalLibraryObjToKey(keyPair.key); diff --git a/extension/js/common/helpers.ts b/extension/js/common/helpers.ts index f6495840bc7..4b9b40358f7 100644 --- a/extension/js/common/helpers.ts +++ b/extension/js/common/helpers.ts @@ -3,7 +3,7 @@ 'use strict'; import { AcctStore } from './platform/store/acct-store.js'; -import { SetupOptions } from '../../chrome/settings/setup.js'; +import { PassphraseOptions } from '../../chrome/settings/setup.js'; import { Buf } from './core/buf.js'; import { Key, KeyUtil } from './core/crypto/key.js'; import { ClientConfiguration } from './client-configuration.js'; @@ -16,27 +16,38 @@ export const isFesUsed = async (acctEmail: string) => { }; // todo: where to take acctEmail and clientConfiguration -export const saveKeysAndPassPhrase = async (acctEmail: string, prvs: Key[], options: SetupOptions) => { +export const saveKeysAndPassPhrase = async (acctEmail: string, prvs: Key[], options?: PassphraseOptions) => { for (const prv of prvs) { await KeyStore.add(acctEmail, prv); - const clientConfiguration = await ClientConfiguration.newInstance(acctEmail); - await PassphraseStore.set((options.passphrase_save && !clientConfiguration.forbidStoringPassPhrase()) ? 'local' : 'session', - acctEmail, { longid: KeyUtil.getPrimaryLongid(prv) }, options.passphrase); + if (options !== undefined) { + const clientConfiguration = await ClientConfiguration.newInstance(acctEmail); + await PassphraseStore.set((options.passphrase_save && !clientConfiguration.forbidStoringPassPhrase()) ? 'local' : 'session', + acctEmail, { longid: KeyUtil.getPrimaryLongid(prv) }, options.passphrase); + } } - const { sendAs } = await AcctStore.get(acctEmail, ['sendAs']); + const { sendAs, full_name: name } = await AcctStore.get(acctEmail, ['sendAs', 'full_name']); const myOwnEmailsAddrs: string[] = [acctEmail].concat(Object.keys(sendAs!)); - const { full_name: name } = await AcctStore.get(acctEmail, ['full_name']); for (const email of myOwnEmailsAddrs) { - await ContactStore.update(undefined, email, { name, pubkey: KeyUtil.armor(await KeyUtil.asPublicKey(prvs[0])) }); + if (options !== undefined) { + // first run, update name + // todo: refactor? + await ContactStore.update(undefined, email, { name }); + } + for (const prv of prvs) { + await ContactStore.update(undefined, email, { pubkey: KeyUtil.armor(await KeyUtil.asPublicKey(prv)) }); + } } }; -// todo: where to take acctEmail? -export const processAndStoreKeysFromEkmLocally = async (acctEmail: string, privateKeys: { decryptedPrivateKey: string }[], setupOptions: SetupOptions) => { +export const processAndStoreKeysFromEkmLocally = async ( + { acctEmail, privateKeys, options }: { acctEmail: string, privateKeys: { decryptedPrivateKey: string }[], options?: PassphraseOptions } +) => { const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(privateKeys.map(pk => pk.decryptedPrivateKey).join('\n'))); if (!keys.length) { throw new Error(`Could not parse any valid keys from Key Manager response for user ${acctEmail}`); } + const existingKeys = await KeyStore.get(acctEmail); + const keysToSave: Key[] = []; for (const prv of keys) { if (!prv.isPrivate) { throw new Error(`Key ${prv.id} for user ${acctEmail} is not a private key`); @@ -44,7 +55,28 @@ export const processAndStoreKeysFromEkmLocally = async (acctEmail: string, priva if (!prv.fullyDecrypted) { throw new Error(`Key ${prv.id} for user ${acctEmail} from FlowCrypt Email Key Manager is not fully decrypted`); } - await KeyUtil.encrypt(prv, setupOptions.passphrase); + if (options === undefined) { + // updating here + // todo: refactor? + const longid = KeyUtil.getPrimaryLongid(prv); + const keyToUpdate = existingKeys.filter(ki => ki.longid === longid); + if (keyToUpdate.length !== 1) { + throw new Error('Not supported yet.'); + } + const oldKey = await KeyUtil.parse(keyToUpdate[0].private); + if (!oldKey.lastModified || !prv.lastModified || oldKey.lastModified === prv.lastModified) { + continue; + } + const passphrase = await PassphraseStore.get(acctEmail, { longid }); + if (passphrase === undefined) { + throw new Error('Not supported yet.'); + } + console.log(`passphrase is "${passphrase}"`); + await KeyUtil.encrypt(prv, passphrase); + } else { + await KeyUtil.encrypt(prv, options.passphrase); + } + keysToSave.push(prv); } - await saveKeysAndPassPhrase(acctEmail, keys, setupOptions); + await saveKeysAndPassPhrase(acctEmail, keysToSave, options); }; diff --git a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts index 555492d4516..134bb67a959 100644 --- a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts +++ b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts @@ -242,7 +242,7 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi const keyManager = new KeyManager(clientConfiguration.getKeyManagerUrlForPrivateKeys()!); Catch.setHandledTimeout(async () => { const { privateKeys } = await keyManager.getPrivateKeys(idToken); - console.log(privateKeys); // processAndStoreKeysFromEkmLocally + await BrowserMsg.send.bg.await.processKeysFromEkm({ acctEmail, privateKeys }); }, 0); } } diff --git a/test/source/browser/browser-handle.ts b/test/source/browser/browser-handle.ts index 4598741a18c..20dac2a88ea 100644 --- a/test/source/browser/browser-handle.ts +++ b/test/source/browser/browser-handle.ts @@ -20,8 +20,11 @@ export class BrowserHandle { this.viewport = { height, width }; } - public newPage = async (t: AvaContext, url?: string, initialScript?: EvaluateFn): Promise => { + public newPage = async (t: AvaContext, url?: string, initialScript?: EvaluateFn, extraHeaders?: Record): Promise => { const page = await this.browser.newPage(); + if (extraHeaders !== undefined) { + await page.setExtraHTTPHeaders(extraHeaders); + } await page.setViewport(this.viewport); const controllablePage = new ControllablePage(t, page); if (url) { diff --git a/test/source/mock/google/google-data.ts b/test/source/mock/google/google-data.ts index ddddb3e7615..1bbff85512b 100644 --- a/test/source/mock/google/google-data.ts +++ b/test/source/mock/google/google-data.ts @@ -162,6 +162,17 @@ export class GoogleData { return msgCopy; }; + public static getMockGmailPage = (acct: string) => ` + +
+
${acct}
+
+
+
Full Name
+
+ + `; + private static msgSubject = (m: GmailMsg): string => { const subjectHeader = m.payload && m.payload.headers && m.payload.headers.find(h => h.name === 'Subject'); return (subjectHeader && subjectHeader.value) || ''; diff --git a/test/source/mock/google/google-endpoints.ts b/test/source/mock/google/google-endpoints.ts index 4a9fa6b2502..4c717f954c5 100644 --- a/test/source/mock/google/google-endpoints.ts +++ b/test/source/mock/google/google-endpoints.ts @@ -93,6 +93,14 @@ export const mockGoogleEndpoints: HandlersDefinition = { return empty; } }, + '/gmail': async (_parsedReq, req) => { + if (isGet(req)) { + await Util.sleep(2); // necessary to activate setup web content script? + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); + return GoogleData.getMockGmailPage(acct); + } + throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); + }, '/gmail/v1/users/me/settings/sendAs': async (parsedReq, req) => { const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); if (isGet(req)) { diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index 970d91336a1..3381c858fd5 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -230,6 +230,16 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { if (acctEmail === 'get.key@key-manager-autogen.flowcrypt.test') { return { privateKeys: [{ decryptedPrivateKey: testConstants.existingPrv }] }; } + if (acctEmail === 'get.updating.key@key-manager-autogen.flowcrypt.test') { + if (!LIVE_KM_RESPONSE.privateKeys.length) { + LIVE_KM_RESPONSE.privateKeys = [{ decryptedPrivateKey: testConstants.updatingPrv }]; + } else { + const key = await KeyUtil.parse(LIVE_KM_RESPONSE.privateKeys[0].decryptedPrivateKey); + const updatedKey = await KeyUtil.reformatKey(key, undefined, [{ name: 'Full Name', email: key.emails[0] }], 6000); + LIVE_KM_RESPONSE.privateKeys = [{ decryptedPrivateKey: KeyUtil.armor(updatedKey) }]; + } + return LIVE_KM_RESPONSE; // todo: rename + } if (acctEmail === 'get.key@no-submit-client-configuration.key-manager-autogen.flowcrypt.test') { return { privateKeys: [{ decryptedPrivateKey: prvNoSubmit }] }; } diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index 9b1f3a06962..39b1e9cfd38 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -554,6 +554,30 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== await securityFrame.notPresent(['@action-change-passphrase-begin', '@action-test-passphrase-begin', '@action-forget-pp']); })); + ava.default('get.updating.key@key-manager-autogen.flowcrypt.test - automatic update of key found on key manager', testWithBrowser(undefined, async (t, browser) => { + const acct = 'get.updating.key@key-manager-autogen.flowcrypt.test'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); + await SetupPageRecipe.autoSetupWithEKM(settingsPage); + const { cryptup_getupdatingkeykeymanagerautogenflowcrypttest_keys: oldKeys } = await settingsPage.getFromLocalStorage([ + 'cryptup_getupdatingkeykeymanagerautogenflowcrypttest_keys' + ]); + const oldKi = oldKeys as KeyInfoWithIdentity[]; + expect(oldKi.length).to.equal(1); + const oldLastModified = (await KeyUtil.parse(oldKi[0].private)).lastModified!; + const accessToken = await BrowserRecipe.getGoogleAccessToken(settingsPage, acct); + const gmailPage = await browser.newPage(t, "https://localhost:8001/gmail", undefined, { Authorization: `Bearer ${accessToken}` }); + await Util.sleep(3); + const { cryptup_getupdatingkeykeymanagerautogenflowcrypttest_keys: newKeys } = await settingsPage.getFromLocalStorage([ + 'cryptup_getupdatingkeykeymanagerautogenflowcrypttest_keys' + ]); + const newKi = newKeys as KeyInfoWithIdentity[]; + expect(newKi.length).to.equal(1); + const newLastModified = (await KeyUtil.parse(newKi[0].private)).lastModified!; + expect(newLastModified !== oldLastModified).to.be.true; + // todo: passphrase checks? + await gmailPage.close(); + })); + ava.default('get.key@key-manager-choose-passphrase.flowcrypt.test - passphrase chosen by user with key found on key manager', testWithBrowser(undefined, async (t, browser) => { const acct = 'get.key@key-manager-choose-passphrase.flowcrypt.test'; const passphrase = 'Long and complicated pass PHRASE'; diff --git a/test/source/tests/tooling/browser-recipe.ts b/test/source/tests/tooling/browser-recipe.ts index 3cb1ec22644..946febef2c6 100644 --- a/test/source/tests/tooling/browser-recipe.ts +++ b/test/source/tests/tooling/browser-recipe.ts @@ -129,9 +129,9 @@ export class BrowserRecipe { const pgpHostPage = await browser.newPage(t, `chrome/dev/ci_pgp_host_page.htm${m.params}`); const pgpBlockPage = await pgpHostPage.getFrame(['pgp_block.htm']); if (m.expectPercentageProgress) { - await pgpBlockPage.waitForContent('@pgp-block-content', /Retrieving message... \d+%/, 20, 10); + await pgpBlockPage.waitForContent('@pgp-block-content', /Retrieving message... \d+%/, 200000, 10); } - await pgpBlockPage.waitForSelTestState('ready', 100); + await pgpBlockPage.waitForSelTestState('ready', 100000); await Util.sleep(1); if (m.quoted) { await pgpBlockPage.waitAndClick('@action-show-quoted-content'); diff --git a/tooling/build-types-and-manifests.ts b/tooling/build-types-and-manifests.ts index 7007f01f585..a5326e14507 100644 --- a/tooling/build-types-and-manifests.ts +++ b/tooling/build-types-and-manifests.ts @@ -123,6 +123,9 @@ const makeMockBuild = (sourceBuildType: string) => { edit(`${buildDir(mockBuildType)}/js/common/core/const.js`, editor); edit(`${buildDir(mockBuildType)}/js/common/platform/catch.js`, editor); edit(`${buildDir(mockBuildType)}/js/content_scripts/webmail_bundle.js`, editor); + edit(`${buildDir(mockBuildType)}/manifest.json`, (code) => + code.replace(/(\"matches\":\s*\[\s*)(\"https:\/\/mail.google.com\/\*")(\s*\])/gm, '$1"https://localhost:8001/gmail"$3') + ); }; const makeLocalFesBuild = (sourceBuildType: string) => { From 049a790f87a9d37cf01cee94cc159ad37bad2450 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 18 Jun 2022 16:39:26 +0000 Subject: [PATCH 05/26] Added base private key for auto-updates --- test/source/tests/tooling/consts.ts | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/source/tests/tooling/consts.ts b/test/source/tests/tooling/consts.ts index 7aa4fc1b23c..cea6eb3a5c5 100644 --- a/test/source/tests/tooling/consts.ts +++ b/test/source/tests/tooling/consts.ts @@ -1500,7 +1500,70 @@ ZFAUVxQrW2a7PVytQbF4Jn8oasXvCzGOicXkK+K5Qtwbu+mK3tVWxlWncnHz jqbTjQE5H6b0hHiDw2XnI5+UEt/QdNVudMmWRYQOofPRXOgW13Q= =tqvP -----END PGP PRIVATE KEY BLOCK-----`, + updatingPrv: // get.updating.key@key-manager-autogen.flowcrypt.test + `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: FlowCrypt Email Encryption [BUILD_REPLACEABLE_VERSION] +Comment: Seamlessly send and receive encrypted email +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 +=3FoO +-----END PGP PRIVATE KEY BLOCK-----` }; /* eslint-disable max-len */ From 068155d7620faa22af5e970d0db29448aa3d9c52 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 19 Jun 2022 08:52:08 +0000 Subject: [PATCH 06/26] fix --- extension/js/background_page/background_page.ts | 2 ++ extension/js/common/browser/browser-msg.ts | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/js/background_page/background_page.ts b/extension/js/background_page/background_page.ts index e2f552366f1..0f6c2aa7be1 100644 --- a/extension/js/background_page/background_page.ts +++ b/extension/js/background_page/background_page.ts @@ -16,6 +16,7 @@ import { ContactStore } from '../common/platform/store/contact-store.js'; import { AcctStore } from '../common/platform/store/acct-store.js'; import { ExpirationCache } from '../common/core/expiration-cache.js'; import { emailKeyIndex } from '../common/core/common.js'; +import { processAndStoreKeysFromEkmLocally } from '../common/helpers.js'; console.info('background_process.js starting'); @@ -61,6 +62,7 @@ opgp.initWorker({ path: '/lib/openpgp.worker.js' }); BrowserMsg.bgAddListener('storeGlobalSet', (r: Bm.StoreGlobalSet) => GlobalStore.set(r.values)); BrowserMsg.bgAddListener('storeAcctGet', (r: Bm.StoreAcctGet) => AcctStore.get(r.acctEmail, r.keys)); BrowserMsg.bgAddListener('storeAcctSet', (r: Bm.StoreAcctSet) => AcctStore.set(r.acctEmail, r.values)); + BrowserMsg.bgAddListener('processKeysFromEkm', processAndStoreKeysFromEkmLocally); BrowserMsg.addPgpListeners(); // todo - remove https://github.com/FlowCrypt/flowcrypt-browser/issues/2560 fixed diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index f7abf4279c8..69e5f684880 100644 --- a/extension/js/common/browser/browser-msg.ts +++ b/extension/js/common/browser/browser-msg.ts @@ -18,7 +18,6 @@ import { Ui } from './ui.js'; import { GlobalStoreDict, GlobalIndex } from '../platform/store/global-store.js'; import { AcctStoreDict, AccountIndex } from '../platform/store/acct-store.js'; import { saveFetchedPubkeysIfNewerThanInStorage } from '../shared.js'; -import { processAndStoreKeysFromEkmLocally } from '../helpers.js'; export type GoogleAuthWindowResult$result = 'Success' | 'Denied' | 'Error' | 'Closed'; @@ -246,7 +245,6 @@ export class BrowserMsg { BrowserMsg.bgAddListener('pgpMsgVerifyDetached', MsgUtil.verifyDetached); BrowserMsg.bgAddListener('pgpMsgType', MsgUtil.type); BrowserMsg.bgAddListener('saveFetchedPubkeys', saveFetchedPubkeysIfNewerThanInStorage); - BrowserMsg.bgAddListener('processKeysFromEkm', processAndStoreKeysFromEkmLocally); }; public static addListener = (name: string, handler: Handler) => { From 2de153bd37acca9abdbcb3e45dbceb02239d637e Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 28 Jun 2022 10:28:41 +0000 Subject: [PATCH 07/26] removed live test --- test/source/browser/browser-pool.ts | 2 +- test/source/mock.ts | 6 +++--- test/source/mock/all-apis-mock.ts | 11 +++++------ .../mock/key-manager/key-manager-endpoints.ts | 12 ------------ test/source/test.ts | 16 +++++++++------- test/source/tests/gmail.ts | 16 ---------------- 6 files changed, 18 insertions(+), 45 deletions(-) diff --git a/test/source/browser/browser-pool.ts b/test/source/browser/browser-pool.ts index 900363447e1..10917a35608 100644 --- a/test/source/browser/browser-pool.ts +++ b/test/source/browser/browser-pool.ts @@ -40,8 +40,8 @@ export class BrowserPool { ]; if (this.isMock) { args.push('--ignore-certificate-errors'); + args.push('--allow-insecure-localhost'); } - args.push('--allow-insecure-localhost'); const slowMo = this.isMock ? 60 : 60; const browser = await puppeteer.launch({ args, ignoreHTTPSErrors: this.isMock, headless: false, devtools: false, slowMo }); const handle = new BrowserHandle(browser, this.semaphore, this.height, this.width); diff --git a/test/source/mock.ts b/test/source/mock.ts index ba529646559..085aa54a589 100644 --- a/test/source/mock.ts +++ b/test/source/mock.ts @@ -2,12 +2,12 @@ import { startAllApisMock } from './mock/all-apis-mock'; -export const mock = async (isMock: boolean, logger: (line: string) => void) => { - return await startAllApisMock(isMock, logger); +export const mock = async (logger: (line: string) => void) => { + return await startAllApisMock(logger); }; if (require.main === module) { - mock(true, msgLog => console.log(msgLog)).catch(e => { + mock(msgLog => console.log(msgLog)).catch(e => { console.error(e); process.exit(1); }); diff --git a/test/source/mock/all-apis-mock.ts b/test/source/mock/all-apis-mock.ts index f1146f1592a..43434afa2c2 100644 --- a/test/source/mock/all-apis-mock.ts +++ b/test/source/mock/all-apis-mock.ts @@ -7,14 +7,14 @@ import * as http from 'http'; import { mockAttesterEndpoints } from './attester/attester-endpoints'; import { mockBackendEndpoints } from './backend/backend-endpoints'; import { mockGoogleEndpoints } from './google/google-endpoints'; -import { liveKeyManagerEndpoints, mockKeyManagerEndpoints } from './key-manager/key-manager-endpoints'; +import { mockKeyManagerEndpoints } from './key-manager/key-manager-endpoints'; import { mockWkdEndpoints } from './wkd/wkd-endpoints'; import { mockSksEndpoints } from './sks/sks-endpoints'; import { mockFesEndpoints } from './fes/fes-endpoints'; export type HandlersDefinition = Handlers<{ query: { [k: string]: string; }; body?: unknown; }, unknown>; -export const startAllApisMock = async (isMock: boolean, logger: (line: string) => void) => { +export const startAllApisMock = async (logger: (line: string) => void) => { class LoggedApi extends Api { protected throttleChunkMsUpload = 15; protected throttleChunkMsDownload = 50; @@ -24,7 +24,7 @@ export const startAllApisMock = async (isMock: boolean, logger: (line: string) = } }; } - const handlers = isMock ? { + const api = new LoggedApi<{ query: { [k: string]: string }, body?: unknown }, unknown>('google-mock', { ...mockGoogleEndpoints, ...mockBackendEndpoints, ...mockAttesterEndpoints, @@ -33,8 +33,7 @@ export const startAllApisMock = async (isMock: boolean, logger: (line: string) = ...mockSksEndpoints, ...mockFesEndpoints, '/favicon.ico': async () => '', - } : { ...liveKeyManagerEndpoints }; - const api = new LoggedApi<{ query: { [k: string]: string }, body?: unknown }, unknown>('google-mock', handlers); + }); await api.listen(8001); return api; -}; +}; \ No newline at end of file diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index 3381c858fd5..5bd433de4a7 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -208,18 +208,6 @@ export const MOCK_KM_LAST_INSERTED_KEY: { [acct: string]: { decryptedPrivateKey: export const LIVE_KM_RESPONSE: { privateKeys: { decryptedPrivateKey: string }[] } = { privateKeys: [] }; -export const liveKeyManagerEndpoints: HandlersDefinition = { - '/flowcrypt-email-key-manager/keys/private': async ({ }, req) => { - if (isGet(req)) { - return LIVE_KM_RESPONSE; - } - if (isPut(req)) { - throw new HttpClientErr(`Unexpectedly calling liveKeyManagerEndpoints:/keys/private PUT`); - } - throw new HttpClientErr(`Unknown method: ${req.method}`); - } -}; - export const mockKeyManagerEndpoints: HandlersDefinition = { '/flowcrypt-email-key-manager/v1/keys/private': async ({ body }, req) => { const acctEmail = oauth.checkAuthorizationHeaderWithIdToken(req.headers.authorization); diff --git a/test/source/test.ts b/test/source/test.ts index 24cb271b6ec..815bf11b106 100644 --- a/test/source/test.ts +++ b/test/source/test.ts @@ -54,13 +54,15 @@ ava.default.before('set config and mock api', async t => { standaloneTestTimeout(t, consts.TIMEOUT_EACH_RETRY, t.title); Config.extensionId = await browserPool.getExtensionId(t); console.info(`Extension url: chrome-extension://${Config.extensionId}`); - const mockApi = await mock(isMock, line => { - if (DEBUG_MOCK_LOG) { - console.log(line); - } - mockApiLogs.push(line); - }); - closeMockApi = mockApi.close; + if (isMock) { + const mockApi = await mock(line => { + if (DEBUG_MOCK_LOG) { + console.log(line); + } + mockApiLogs.push(line); + }); + closeMockApi = mockApi.close; + } t.pass(); }); diff --git a/test/source/tests/gmail.ts b/test/source/tests/gmail.ts index 5bfb544e25a..261c097c927 100644 --- a/test/source/tests/gmail.ts +++ b/test/source/tests/gmail.ts @@ -393,22 +393,6 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test await pageHasSecureReplyContainer(t, browser, gmailPage); })); - ava.default('mail.google.com - fetching new private key -- asking for pass phrase', testWithBrowser('ci.tests.gmail', async (t, browser) => { - const dbPage = await browser.newPage(t, TestUrls.extension('chrome/dev/ci_unit_test.htm')); - // forge ClientConfiguration to wire the key manager - await dbPage.page.evaluate(async () => { - await (window as any).AcctStore.set('ci.tests.gmail@flowcrypt.dev', { - flags: ['PRV_AUTOIMPORT_OR_AUTOGEN'], // todo: ATTESTER_SUBMIT, FORBID_STORING_PASSPHRASE - rules: { key_manager_url: 'https://localhost:8001/flowcrypt-email-key-manager' } - }); - }); - LIVE_KM_RESPONSE.privateKeys = [{ decryptedPrivateKey: testConstants.existingPrv }]; - const gmailPage = await openGmailPage(t, browser); - await Util.sleep(10); - // todo: - await gmailPage.close(); - })); - // ava.default('mail.google.com - reauth after uuid change', testWithBrowser('ci.tests.gmail', async (t, browser) => { // const acct = 'ci.tests.gmail@flowcrypt.dev'; // const settingsPage = await browser.newPage(t, TestUrls.extensionSettings(acct)); From 6cc4d64947a25708ef616b965d83e9b4ecf89a53 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 28 Jun 2022 10:32:03 +0000 Subject: [PATCH 08/26] reverted debug constants --- test/source/tests/tooling/browser-recipe.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/source/tests/tooling/browser-recipe.ts b/test/source/tests/tooling/browser-recipe.ts index 946febef2c6..3cb1ec22644 100644 --- a/test/source/tests/tooling/browser-recipe.ts +++ b/test/source/tests/tooling/browser-recipe.ts @@ -129,9 +129,9 @@ export class BrowserRecipe { const pgpHostPage = await browser.newPage(t, `chrome/dev/ci_pgp_host_page.htm${m.params}`); const pgpBlockPage = await pgpHostPage.getFrame(['pgp_block.htm']); if (m.expectPercentageProgress) { - await pgpBlockPage.waitForContent('@pgp-block-content', /Retrieving message... \d+%/, 200000, 10); + await pgpBlockPage.waitForContent('@pgp-block-content', /Retrieving message... \d+%/, 20, 10); } - await pgpBlockPage.waitForSelTestState('ready', 100000); + await pgpBlockPage.waitForSelTestState('ready', 100); await Util.sleep(1); if (m.quoted) { await pgpBlockPage.waitAndClick('@action-show-quoted-content'); From f49b12a2dc541d31bbdbd88903d8c9a5c23e5c45 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 28 Jun 2022 12:38:13 +0000 Subject: [PATCH 09/26] tidying up --- extension/js/common/shared.ts | 1 + test/source/mock/all-apis-mock.ts | 2 +- test/source/tests/gmail.ts | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/extension/js/common/shared.ts b/extension/js/common/shared.ts index ddfb0a463aa..b65d4e24854 100644 --- a/extension/js/common/shared.ts +++ b/extension/js/common/shared.ts @@ -34,3 +34,4 @@ export const saveFetchedPubkeysIfNewerThanInStorage = async ({ email, pubkeys }: const storedContact = await ContactStore.getOneWithAllPubkeys(undefined, email); return await compareAndSavePubkeysToStorage({ email }, pubkeys, storedContact?.sortedPubkeys ?? []); }; + diff --git a/test/source/mock/all-apis-mock.ts b/test/source/mock/all-apis-mock.ts index 43434afa2c2..dea60cbc6d7 100644 --- a/test/source/mock/all-apis-mock.ts +++ b/test/source/mock/all-apis-mock.ts @@ -36,4 +36,4 @@ export const startAllApisMock = async (logger: (line: string) => void) => { }); await api.listen(8001); return api; -}; \ No newline at end of file +}; diff --git a/test/source/tests/gmail.ts b/test/source/tests/gmail.ts index 261c097c927..c74e9419867 100644 --- a/test/source/tests/gmail.ts +++ b/test/source/tests/gmail.ts @@ -15,8 +15,6 @@ import { TestWithBrowser } from './../test'; import { expect } from 'chai'; import { OauthPageRecipe } from './page-recipe/oauth-page-recipe'; import { SetupPageRecipe } from './page-recipe/setup-page-recipe'; -import { LIVE_KM_RESPONSE } from '../mock/key-manager/key-manager-endpoints'; -import { testConstants } from './tooling/consts'; /** * All tests that use mail.google.com or have to operate without a Gmail API mock should go here From 78421b5a31d9bf2994ade2ee394f58bafd51571e Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 28 Jun 2022 15:03:05 +0000 Subject: [PATCH 10/26] use https://gmail.localhost:8001/gmail to avoid interference with Chrome authorization --- test/source/tests/setup.ts | 2 +- tooling/build-types-and-manifests.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index 39b1e9cfd38..a76349a631d 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -565,7 +565,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== expect(oldKi.length).to.equal(1); const oldLastModified = (await KeyUtil.parse(oldKi[0].private)).lastModified!; const accessToken = await BrowserRecipe.getGoogleAccessToken(settingsPage, acct); - const gmailPage = await browser.newPage(t, "https://localhost:8001/gmail", undefined, { Authorization: `Bearer ${accessToken}` }); + const gmailPage = await browser.newPage(t, "https://gmail.localhost:8001/gmail", undefined, { Authorization: `Bearer ${accessToken}` }); await Util.sleep(3); const { cryptup_getupdatingkeykeymanagerautogenflowcrypttest_keys: newKeys } = await settingsPage.getFromLocalStorage([ 'cryptup_getupdatingkeykeymanagerautogenflowcrypttest_keys' diff --git a/tooling/build-types-and-manifests.ts b/tooling/build-types-and-manifests.ts index a5326e14507..cb4da42e952 100644 --- a/tooling/build-types-and-manifests.ts +++ b/tooling/build-types-and-manifests.ts @@ -124,7 +124,7 @@ const makeMockBuild = (sourceBuildType: string) => { edit(`${buildDir(mockBuildType)}/js/common/platform/catch.js`, editor); edit(`${buildDir(mockBuildType)}/js/content_scripts/webmail_bundle.js`, editor); edit(`${buildDir(mockBuildType)}/manifest.json`, (code) => - code.replace(/(\"matches\":\s*\[\s*)(\"https:\/\/mail.google.com\/\*")(\s*\])/gm, '$1"https://localhost:8001/gmail"$3') + code.replace(/(\"matches\":\s*\[\s*)(\"https:\/\/mail.google.com\/\*")(\s*\])/gm, '$1"https://gmail.localhost:8001/gmail"$3') ); }; From e9f2d5fe0144d6ba8196a6c8b8430716911e3edb Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 29 Jun 2022 10:45:00 +0000 Subject: [PATCH 11/26] Renamed the object for test KM's updating key --- .../source/mock/key-manager/key-manager-endpoints.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index 5bd433de4a7..a1477ed12a0 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -206,7 +206,7 @@ yeSm0uVPwODhwX7ezB9jW6uVt0R8S8iM3rQdEMsA/jDep5LNn47K6o8VrDt0zYo6 export const MOCK_KM_LAST_INSERTED_KEY: { [acct: string]: { decryptedPrivateKey: string, publicKey: string } } = {}; // accessed from test runners -export const LIVE_KM_RESPONSE: { privateKeys: { decryptedPrivateKey: string }[] } = { privateKeys: [] }; +export const MOCK_KM_UPDATING_KEY: { privateKeys: { decryptedPrivateKey: string }[] } = { privateKeys: [] }; export const mockKeyManagerEndpoints: HandlersDefinition = { '/flowcrypt-email-key-manager/v1/keys/private': async ({ body }, req) => { @@ -219,14 +219,14 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { return { privateKeys: [{ decryptedPrivateKey: testConstants.existingPrv }] }; } if (acctEmail === 'get.updating.key@key-manager-autogen.flowcrypt.test') { - if (!LIVE_KM_RESPONSE.privateKeys.length) { - LIVE_KM_RESPONSE.privateKeys = [{ decryptedPrivateKey: testConstants.updatingPrv }]; + if (!MOCK_KM_UPDATING_KEY.privateKeys.length) { + MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: testConstants.updatingPrv }]; } else { - const key = await KeyUtil.parse(LIVE_KM_RESPONSE.privateKeys[0].decryptedPrivateKey); + const key = await KeyUtil.parse(MOCK_KM_UPDATING_KEY.privateKeys[0].decryptedPrivateKey); const updatedKey = await KeyUtil.reformatKey(key, undefined, [{ name: 'Full Name', email: key.emails[0] }], 6000); - LIVE_KM_RESPONSE.privateKeys = [{ decryptedPrivateKey: KeyUtil.armor(updatedKey) }]; + MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: KeyUtil.armor(updatedKey) }]; } - return LIVE_KM_RESPONSE; // todo: rename + return MOCK_KM_UPDATING_KEY; } if (acctEmail === 'get.key@no-submit-client-configuration.key-manager-autogen.flowcrypt.test') { return { privateKeys: [{ decryptedPrivateKey: prvNoSubmit }] }; From 39228cb6c2a986b3af98517083007872f2df2626 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 3 Jul 2022 11:11:13 +0000 Subject: [PATCH 12/26] wip --- extension/chrome/elements/passphrase.ts | 12 +++- .../setup/setup-key-manager-autogen.ts | 6 +- extension/js/common/browser/browser-msg.ts | 4 +- extension/js/common/helpers.ts | 64 +++++++++++------ .../common/platform/store/passphrase-store.ts | 33 +++++---- extension/js/common/xss-safe-factory.ts | 2 +- .../webmail/setup-webmail-content-script.ts | 40 +++++++++-- .../mock/key-manager/key-manager-endpoints.ts | 2 +- test/source/tests/setup.ts | 71 ++++++++++++++----- test/source/tests/tooling/consts.ts | 2 +- tooling/build-types-and-manifests.ts | 2 +- 11 files changed, 169 insertions(+), 69 deletions(-) diff --git a/extension/chrome/elements/passphrase.ts b/extension/chrome/elements/passphrase.ts index 05e00198fc4..7068ff86344 100644 --- a/extension/chrome/elements/passphrase.ts +++ b/extension/chrome/elements/passphrase.ts @@ -34,8 +34,10 @@ View.run(class PassphraseView extends View { const uncheckedUrlParams = Url.parse(['acctEmail', 'parentTabId', 'longids', 'type', 'initiatorFrameId']); this.acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail'); this.parentTabId = Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId'); - this.longids = Assert.urlParamRequire.string(uncheckedUrlParams, 'longids').split(','); - this.type = Assert.urlParamRequire.oneof(uncheckedUrlParams, 'type', ['embedded', 'sign', 'message', 'draft', 'attachment', 'quote', 'backup']); + const longidsParam = Assert.urlParamRequire.string(uncheckedUrlParams, 'longids'); + this.longids = longidsParam ? longidsParam.split(',') : []; + this.type = Assert.urlParamRequire.oneof(uncheckedUrlParams, 'type', + ['embedded', 'sign', 'message', 'draft', 'attachment', 'quote', 'backup', 'update_key']); this.initiatorFrameId = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'initiatorFrameId'); } @@ -55,6 +57,9 @@ View.run(class PassphraseView extends View { } await initPassphraseToggle(['passphrase']); const allPrivateKeys = await KeyStore.get(this.acctEmail); + if (this.longids.length === 0) { + this.longids.push(...allPrivateKeys.map(ki => ki.longid)); + } this.keysWeNeedPassPhraseFor = allPrivateKeys.filter(ki => this.longids.includes(ki.longid)); if (this.type === 'embedded') { $('h1').parent().css('display', 'none'); @@ -71,6 +76,8 @@ View.run(class PassphraseView extends View { $('h1').text('Enter FlowCrypt pass phrase to load quoted content'); } else if (this.type === 'backup') { $('h1').text('Enter FlowCrypt pass phrase to back up'); + } else if (this.type === 'update_key') { + $('h1').text('Enter FlowCrypt pass phrase to keep your account keys up to date'); } $('#passphrase').focus(); if (allPrivateKeys.length > 1) { @@ -183,6 +190,7 @@ View.run(class PassphraseView extends View { Ui.toast(`${unlockCount} of ${allPrivateKeys.length} keys ${(unlockCount > 1) ? 'were' : 'was'} unlocked by this pass phrase`); } if (atLeastOneMatched) { + console.log('At least one matched'); this.closeDialog(true, this.initiatorFrameId); } else { this.renderFailedEntryPpPrompt(); diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index f087fd40891..d95e38e6ed7 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -43,7 +43,11 @@ export class SetupWithEmailKeyManagerModule { if (privateKeys.length) { // keys already exist on keyserver, auto-import // todo: do we need to submit on auto-update? - await processAndStoreKeysFromEkmLocally({ acctEmail: this.view.acctEmail, privateKeys, options: setupOptions }); + await processAndStoreKeysFromEkmLocally({ + acctEmail: this.view.acctEmail, + decryptedPrivateKeys: privateKeys.map(entry => entry.decryptedPrivateKey), + options: setupOptions + }); } else if (this.view.clientConfiguration.canCreateKeys()) { // generate keys on client and store them on key manager await this.autoGenerateKeyAndStoreBothLocallyAndToEkm(setupOptions); diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index 69e5f684880..8953fbbf793 100644 --- a/extension/js/common/browser/browser-msg.ts +++ b/extension/js/common/browser/browser-msg.ts @@ -65,7 +65,7 @@ export namespace Bm { export type ShowAttachmentPreview = { iframeUrl: string }; export type ReRenderRecipient = { email: string }; export type SaveFetchedPubkeys = { email: string, pubkeys: string[] }; - export type ProcessKeysFromEkm = { acctEmail: string, privateKeys: { decryptedPrivateKey: string }[] }; + export type ProcessKeysFromEkm = { acctEmail: string, decryptedPrivateKeys: string[] }; export namespace Res { export type GetActiveTabInfo = { provider: 'gmail' | undefined, acctEmail: string | undefined, sameWorld: boolean | undefined }; @@ -84,7 +84,7 @@ export namespace Bm { export type AjaxGmailAttachmentGetChunk = { chunk: Buf }; export type _tab_ = { tabId: string | null | undefined }; export type SaveFetchedPubkeys = boolean; - export type ProcessKeysFromEkm = void; + export type ProcessKeysFromEkm = { unencryptedKeysToSave: string[] }; export type Db = any; // not included in Any below export type Ajax = any; // not included in Any below diff --git a/extension/js/common/helpers.ts b/extension/js/common/helpers.ts index 4b9b40358f7..d9fd4bd455a 100644 --- a/extension/js/common/helpers.ts +++ b/extension/js/common/helpers.ts @@ -10,6 +10,7 @@ import { ClientConfiguration } from './client-configuration.js'; import { ContactStore } from './platform/store/contact-store.js'; import { KeyStore } from './platform/store/key-store.js'; import { PassphraseStore } from './platform/store/passphrase-store.js'; +import { Bm } from './browser/browser-msg.js'; export const isFesUsed = async (acctEmail: string) => { const { fesUrl } = await AcctStore.get(acctEmail, ['fesUrl']); return Boolean(fesUrl); @@ -40,15 +41,22 @@ export const saveKeysAndPassPhrase = async (acctEmail: string, prvs: Key[], opti }; export const processAndStoreKeysFromEkmLocally = async ( - { acctEmail, privateKeys, options }: { acctEmail: string, privateKeys: { decryptedPrivateKey: string }[], options?: PassphraseOptions } -) => { - const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(privateKeys.map(pk => pk.decryptedPrivateKey).join('\n'))); + { acctEmail, decryptedPrivateKeys, options }: { acctEmail: string, decryptedPrivateKeys: string[], options?: PassphraseOptions } +): Promise => { + const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(decryptedPrivateKeys.join('\n'))); if (!keys.length) { throw new Error(`Could not parse any valid keys from Key Manager response for user ${acctEmail}`); } + let unencryptedKeysToSave: Key[] = []; const existingKeys = await KeyStore.get(acctEmail); - const keysToSave: Key[] = []; + let passphrase = options?.passphrase; + if (passphrase === undefined && !existingKeys.length) { + return { unencryptedKeysToSave: [] }; // return success as we can't possibly validate a passphrase + // this can only happen on misconfiguration + // todo: or should we throw? + } for (const prv of keys) { + // todo: should we still process remaining correct keys? if (!prv.isPrivate) { throw new Error(`Key ${prv.id} for user ${acctEmail} is not a private key`); } @@ -59,24 +67,38 @@ export const processAndStoreKeysFromEkmLocally = async ( // updating here // todo: refactor? const longid = KeyUtil.getPrimaryLongid(prv); - const keyToUpdate = existingKeys.filter(ki => ki.longid === longid); - if (keyToUpdate.length !== 1) { - throw new Error('Not supported yet.'); - } - const oldKey = await KeyUtil.parse(keyToUpdate[0].private); - if (!oldKey.lastModified || !prv.lastModified || oldKey.lastModified === prv.lastModified) { - continue; + const keyToUpdate = existingKeys.filter(ki => ki.longid === longid && ki.family === prv.family); + if (keyToUpdate.length === 1) { + const oldKey = await KeyUtil.parse(keyToUpdate[0].private); + if (!oldKey.lastModified || !prv.lastModified || oldKey.lastModified === prv.lastModified) { + continue; + } + } else if (keyToUpdate.length > 1) { + throw new Error(`Unexpected error: key search by longid=${longid} yielded ${keyToUpdate.length} results`); } - const passphrase = await PassphraseStore.get(acctEmail, { longid }); - if (passphrase === undefined) { - throw new Error('Not supported yet.'); - } - console.log(`passphrase is "${passphrase}"`); - await KeyUtil.encrypt(prv, passphrase); - } else { - await KeyUtil.encrypt(prv, options.passphrase); } - keysToSave.push(prv); + unencryptedKeysToSave.push(prv); + } + let encryptedKeys: Key[] = []; + if (unencryptedKeysToSave.length) { + if (passphrase === undefined) { + // trying to find a passphrase that unlocks at least one key + const passphrases = await PassphraseStore.getMany(acctEmail, existingKeys); + passphrase = passphrases.find(pp => pp !== undefined); + } + if (passphrase !== undefined) { + const pp = passphrase; + // todo: some more fancy conversion, preserving a passphrase for a particual longid? + await Promise.all(unencryptedKeysToSave.map(prv => KeyUtil.encrypt(prv, pp))); + encryptedKeys = unencryptedKeysToSave; + unencryptedKeysToSave = []; + } + } + if (encryptedKeys.length) { + // also updates `name`, todo: refactor? + await saveKeysAndPassPhrase(acctEmail, encryptedKeys, options); + return { unencryptedKeysToSave: [] }; + } else { + return { unencryptedKeysToSave: unencryptedKeysToSave.map(KeyUtil.armor) }; } - await saveKeysAndPassPhrase(acctEmail, keysToSave, options); }; diff --git a/extension/js/common/platform/store/passphrase-store.ts b/extension/js/common/platform/store/passphrase-store.ts index 19e658ca245..0dd56386feb 100644 --- a/extension/js/common/platform/store/passphrase-store.ts +++ b/extension/js/common/platform/store/passphrase-store.ts @@ -13,8 +13,13 @@ export class PassphraseStore extends AbstractStore { // if we implement (and migrate) password storage to use KeyIdentity instead of longid, we'll have `keyInfo: KeyIdentity` here public static get = async (acctEmail: string, keyInfo: { longid: string }, ignoreSession: boolean = false): Promise => { - const storageIndex = PassphraseStore.getIndex(keyInfo.longid); - return await PassphraseStore.getByIndex(acctEmail, storageIndex, ignoreSession); + return (await PassphraseStore.getMany(acctEmail, [keyInfo], ignoreSession))[0]; + }; + + // if we implement (and migrate) password storage to use KeyIdentity instead of longid, we'll have `keyInfo: KeyIdentity` here + public static getMany = async (acctEmail: string, keyInfos: { longid: string }[], ignoreSession: boolean = false): Promise<(string | undefined)[]> => { + const storageIndexes = keyInfos.map(keyInfo => PassphraseStore.getIndex(keyInfo.longid)); + return await PassphraseStore.getByIndexes(acctEmail, storageIndexes, ignoreSession); }; // if we implement (and migrate) password storage to use KeyIdentity instead of longid, we'll have `keyInfo: KeyIdentity` here @@ -50,17 +55,19 @@ export class PassphraseStore extends AbstractStore { return `passphrase_${longid}` as unknown as AccountIndex; }; - private static getByIndex = async (acctEmail: string, storageIndex: AccountIndex, ignoreSession: boolean = false): Promise => { - const storage = await AcctStore.get(acctEmail, [storageIndex]); - const found = storage[storageIndex]; - if (typeof found === 'string') { - return found; - } - if (ignoreSession) { - return undefined; - } - const res = await InMemoryStore.get(acctEmail, storageIndex) ?? undefined; - return res; + private static getByIndexes = async (acctEmail: string, storageIndexes: AccountIndex[], ignoreSession: boolean = false): Promise<(string | undefined)[]> => { + const storage = await AcctStore.get(acctEmail, storageIndexes); + const results = await Promise.all(storageIndexes.map(async storageIndex => { + const found = storage[storageIndex]; + if (typeof found === 'string') { + return found; + } + if (ignoreSession) { + return undefined; + } + return await InMemoryStore.get(acctEmail, storageIndex) ?? undefined; + })); + return results; }; private static setByIndex = async (storageType: StorageType, acctEmail: string, storageIndex: AccountIndex, passphrase: string | undefined): Promise => { diff --git a/extension/js/common/xss-safe-factory.ts b/extension/js/common/xss-safe-factory.ts index 420c103f9e8..9b247116b55 100644 --- a/extension/js/common/xss-safe-factory.ts +++ b/extension/js/common/xss-safe-factory.ts @@ -19,7 +19,7 @@ import { SendAsAlias } from './platform/store/acct-store.js'; type Placement = 'settings' | 'settings_compose' | 'default' | 'dialog' | 'gmail' | 'embedded' | 'compose'; export type WebmailVariantString = undefined | 'html' | 'standard' | 'new'; -export type PassphraseDialogType = 'embedded' | 'message' | 'attachment' | 'draft' | 'sign' | `quote` | `backup`; +export type PassphraseDialogType = 'embedded' | 'message' | 'attachment' | 'draft' | 'sign' | `quote` | `backup` | 'update_key'; export type FactoryReplyParams = { replyMsgId?: string, draftId?: string, diff --git a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts index 134bb67a959..2cbd156571e 100644 --- a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts +++ b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts @@ -119,7 +119,7 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi } }; - const browserMsgListen = (acctEmail: string, tabId: string, inject: Injector, factory: XssSafeFactory, notifications: Notifications) => { + const browserMsgListen = (acctEmail: string, tabId: string, inject: Injector, factory: XssSafeFactory, notifications: Notifications, ppEvent: { entered?: boolean }) => { BrowserMsg.addListener('set_active_window', async ({ frameId }: Bm.ComposeWindow) => { if ($(`.secure_compose_window.active[data-frame-id="${frameId}"]`).length) { return; // already active @@ -177,8 +177,11 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi BrowserMsg.addListener('scroll_to_cursor_in_reply_box', async ({ replyMsgId, cursorOffsetTop }: Bm.ScrollToCursorInReplyBox) => { webmailSpecific.getReplacer().scrollToCursorInReplyBox(replyMsgId, cursorOffsetTop); }); - BrowserMsg.addListener('passphrase_dialog', async ({ longids, type, initiatorFrameId }: Bm.PassphraseDialog) => { - await factory.showPassphraseDialog(longids, type, initiatorFrameId); + BrowserMsg.addListener('passphrase_dialog', async (args: Bm.PassphraseDialog) => { + await showPassphraseDialog(factory, args); + }); + BrowserMsg.addListener('passphrase_entry', async ({ entered }: Bm.PassphraseEntry) => { + ppEvent.entered = entered; }); BrowserMsg.addListener('add_pubkey_dialog', async ({ emails }: Bm.AddPubkeyDialog) => { await factory.showAddPubkeyDialog(emails); @@ -235,14 +238,36 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi notifEl.appendChild(div); }; - const startPullingKeysFromEkm = async (acctEmail: string, clientConfiguration: ClientConfiguration) => { + const showPassphraseDialog = async (factory: XssSafeFactory, { longids, type, initiatorFrameId }: Bm.PassphraseDialog) => { + await factory.showPassphraseDialog(longids, type, initiatorFrameId); + }; + + const processKeysFromEkm = async (acctEmail: string, decryptedPrivateKeys: string[], factory: XssSafeFactory, ppEvent: { entered?: boolean }) => { + const { unencryptedKeysToSave } = await BrowserMsg.send.bg.await.processKeysFromEkm({ acctEmail, decryptedPrivateKeys }); + if (unencryptedKeysToSave.length) { + ppEvent.entered = undefined; + await showPassphraseDialog(factory, { longids: [], type: 'update_key' }); + while (ppEvent.entered === undefined) { + await Ui.time.sleep(100); + } + if (ppEvent.entered) { + await processKeysFromEkm(acctEmail, unencryptedKeysToSave, factory, ppEvent); + } else { + return; // todo: alert + } + } + }; + + const startPullingKeysFromEkm = async (acctEmail: string, clientConfiguration: ClientConfiguration, factory: XssSafeFactory, ppEvent: { entered?: boolean }) => { if (clientConfiguration.usesKeyManager()) { const idToken = await InMemoryStore.get(acctEmail, InMemoryStoreKeys.ID_TOKEN); if (idToken) { const keyManager = new KeyManager(clientConfiguration.getKeyManagerUrlForPrivateKeys()!); Catch.setHandledTimeout(async () => { const { privateKeys } = await keyManager.getPrivateKeys(idToken); - await BrowserMsg.send.bg.await.processKeysFromEkm({ acctEmail, privateKeys }); + if (privateKeys.length) { + await processKeysFromEkm(acctEmail, privateKeys.map(entry => entry.decryptedPrivateKey), factory, ppEvent); + } }, 0); } } @@ -253,9 +278,10 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi const acctEmail = await waitForAcctEmail(); const { tabId, notifications, factory, inject } = await initInternalVars(acctEmail); await showNotificationsAndWaitTilAcctSetUp(acctEmail, notifications); - browserMsgListen(acctEmail, tabId, inject, factory, notifications); + const ppEvent: { entered?: boolean } = {}; + browserMsgListen(acctEmail, tabId, inject, factory, notifications, ppEvent); const clientConfiguration = await ClientConfiguration.newInstance(acctEmail); - await startPullingKeysFromEkm(acctEmail, clientConfiguration); + await startPullingKeysFromEkm(acctEmail, clientConfiguration, factory, ppEvent); await webmailSpecific.start(acctEmail, clientConfiguration, inject, notifications, factory, notifyMurdered); } catch (e) { if (e instanceof TabIdRequiredError) { diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index d6aeec7ab77..ed0dfdbfeab 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -218,7 +218,7 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { if (acctEmail === 'get.key@key-manager-autogen.flowcrypt.test') { return { privateKeys: [{ decryptedPrivateKey: testConstants.existingPrv }] }; } - if (acctEmail === 'get.updating.key@key-manager-autogen.flowcrypt.test') { + if (acctEmail === 'get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test') { if (!MOCK_KM_UPDATING_KEY.privateKeys.length) { MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: testConstants.updatingPrv }]; } else { diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index d42a6cca520..ec03102da84 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -14,6 +14,8 @@ import { MOCK_ATTESTER_LAST_INSERTED_PUB } from './../mock/attester/attester-end import { BrowserRecipe } from './tooling/browser-recipe'; import { KeyInfoWithIdentity, KeyUtil } from '../core/crypto/key'; import { testConstants } from './tooling/consts'; +import { TestUrls } from '../browser/test-urls'; +import { InboxPageRecipe } from './page-recipe/inbox-page-recipe'; // tslint:disable:no-blank-lines-func // tslint:disable:no-unused-expression @@ -554,27 +556,58 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== await securityFrame.notPresent(['@action-change-passphrase-begin', '@action-test-passphrase-begin', '@action-forget-pp']); })); - ava.default('get.updating.key@key-manager-autogen.flowcrypt.test - automatic update of key found on key manager', testWithBrowser(undefined, async (t, browser) => { - const acct = 'get.updating.key@key-manager-autogen.flowcrypt.test'; + ava.default('get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test - automatic update of key found on key manager', testWithBrowser(undefined, async (t, browser) => { + const acct = 'get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test'; const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); - await SetupPageRecipe.autoSetupWithEKM(settingsPage); - const { cryptup_getupdatingkeykeymanagerautogenflowcrypttest_keys: oldKeys } = await settingsPage.getFromLocalStorage([ - 'cryptup_getupdatingkeykeymanagerautogenflowcrypttest_keys' - ]); - const oldKi = oldKeys as KeyInfoWithIdentity[]; - expect(oldKi.length).to.equal(1); - const oldLastModified = (await KeyUtil.parse(oldKi[0].private)).lastModified!; + const oldPassphrase = 'long enough to suit requirements'; + await SetupPageRecipe.autoSetupWithEKM(settingsPage, { + enterPp: { + passphrase: oldPassphrase, + checks: { isSavePassphraseChecked: false, isSavePassphraseHidden: true } + } + }); + const { cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys: keyset1 } + = await settingsPage.getFromLocalStorage(['cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys']); + const ki1 = keyset1 as KeyInfoWithIdentity[]; + expect(ki1.length).to.equal(1); + const prv1 = await KeyUtil.parse(ki1[0].private); + const prv1LastModified = prv1.lastModified!; + expect(prv1.fullyEncrypted).to.be.true; + expect(await KeyUtil.decrypt(prv1, oldPassphrase as string, undefined, undefined)).to.be.true; const accessToken = await BrowserRecipe.getGoogleAccessToken(settingsPage, acct); - const gmailPage = await browser.newPage(t, "https://gmail.localhost:8001/gmail", undefined, { Authorization: `Bearer ${accessToken}` }); - await Util.sleep(3); - const { cryptup_getupdatingkeykeymanagerautogenflowcrypttest_keys: newKeys } = await settingsPage.getFromLocalStorage([ - 'cryptup_getupdatingkeykeymanagerautogenflowcrypttest_keys' - ]); - const newKi = newKeys as KeyInfoWithIdentity[]; - expect(newKi.length).to.equal(1); - const newLastModified = (await KeyUtil.parse(newKi[0].private)).lastModified!; - expect(newLastModified !== oldLastModified).to.be.true; - // todo: passphrase checks? + let gmailPage = await browser.newPage(t, "https://gmail.localhost:8001/gmail", undefined, { Authorization: `Bearer ${accessToken}` }); + await Util.sleep(3); // todo: wait for notification + const { cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys: keyset2 } + = await settingsPage.getFromLocalStorage(['cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys']); + const ki2 = keyset2 as KeyInfoWithIdentity[]; + expect(ki2.length).to.equal(1); + const prv2 = await KeyUtil.parse(ki2[0].private); + const prv2LastModified = prv2.lastModified!; + expect(prv2LastModified).to.not.equal(prv1LastModified); // an update happened + expect(prv2.fullyEncrypted).to.be.true; + expect(await KeyUtil.decrypt(prv2, oldPassphrase as string, undefined, undefined)).to.be.true; + await gmailPage.close(); + // forget the passphrase + const inboxPage = await browser.newPage(t, TestUrls.extension(`chrome/settings/inbox/inbox.htm?acctEmail=${acct}`)); + await InboxPageRecipe.finishSessionOnInboxPage(inboxPage); + gmailPage = await browser.newPage(t, "https://gmail.localhost:8001/gmail", undefined, { Authorization: `Bearer ${accessToken}` }); + await gmailPage.waitAll('@dialog-passphrase'); + const passphraseDialog = await gmailPage.getFrame(['passphrase.htm']); + await passphraseDialog.waitForContent('@passphrase-text', 'Enter FlowCrypt pass phrase to keep your account keys up to date'); + // todo: await ComposePageRecipe.cancelPassphraseDialog(inboxPage, inputMethod); + await passphraseDialog.waitAndType('@input-pass-phrase', oldPassphrase); + await passphraseDialog.waitAndClick('@action-confirm-pass-phrase-entry'); + await inboxPage.waitTillGone('@dialog-passphrase'); + await Util.sleep(3); // todo: wait for notification + const { cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys: keyset3 + } = await settingsPage.getFromLocalStorage(['cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys']); + const ki3 = keyset3 as KeyInfoWithIdentity[]; + expect(ki3.length).to.equal(1); + const prv3 = await KeyUtil.parse(ki3[0].private); + const prv3LastModified = prv3.lastModified!; + expect(prv3LastModified).to.not.equal(prv2LastModified); // an update happened + expect(prv3.fullyEncrypted).to.be.true; + expect(await KeyUtil.decrypt(prv3, oldPassphrase as string, undefined, undefined)).to.be.true; await gmailPage.close(); })); diff --git a/test/source/tests/tooling/consts.ts b/test/source/tests/tooling/consts.ts index cea6eb3a5c5..6a35676d96e 100644 --- a/test/source/tests/tooling/consts.ts +++ b/test/source/tests/tooling/consts.ts @@ -1500,7 +1500,7 @@ ZFAUVxQrW2a7PVytQbF4Jn8oasXvCzGOicXkK+K5Qtwbu+mK3tVWxlWncnHz jqbTjQE5H6b0hHiDw2XnI5+UEt/QdNVudMmWRYQOofPRXOgW13Q= =tqvP -----END PGP PRIVATE KEY BLOCK-----`, - updatingPrv: // get.updating.key@key-manager-autogen.flowcrypt.test + updatingPrv: // get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test `-----BEGIN PGP PRIVATE KEY BLOCK----- Version: FlowCrypt Email Encryption [BUILD_REPLACEABLE_VERSION] Comment: Seamlessly send and receive encrypted email diff --git a/tooling/build-types-and-manifests.ts b/tooling/build-types-and-manifests.ts index cb4da42e952..7a5f448ffb9 100644 --- a/tooling/build-types-and-manifests.ts +++ b/tooling/build-types-and-manifests.ts @@ -124,7 +124,7 @@ const makeMockBuild = (sourceBuildType: string) => { edit(`${buildDir(mockBuildType)}/js/common/platform/catch.js`, editor); edit(`${buildDir(mockBuildType)}/js/content_scripts/webmail_bundle.js`, editor); edit(`${buildDir(mockBuildType)}/manifest.json`, (code) => - code.replace(/(\"matches\":\s*\[\s*)(\"https:\/\/mail.google.com\/\*")(\s*\])/gm, '$1"https://gmail.localhost:8001/gmail"$3') + code.replace(/https:\/\/mail.google.com/g, 'https://gmail.localhost:8001') ); }; From 7a4fe50a0146731f3d3a837aa222880a9f61d57b Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 3 Jul 2022 14:17:47 +0000 Subject: [PATCH 13/26] fixed regexes --- tooling/build-types-and-manifests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tooling/build-types-and-manifests.ts b/tooling/build-types-and-manifests.ts index 7a5f448ffb9..22e934499b1 100644 --- a/tooling/build-types-and-manifests.ts +++ b/tooling/build-types-and-manifests.ts @@ -118,13 +118,13 @@ const makeMockBuild = (sourceBuildType: string) => { .replace(/const (OAUTH_GOOGLE_API_HOST|GMAIL_GOOGLE_API_HOST|PEOPLE_GOOGLE_API_HOST|GOOGLE_OAUTH_SCREEN_HOST) = [^;]+;/g, `const $1 = '${MOCK_HOST[sourceBuildType]}';`) .replace(/const (BACKEND_API_HOST) = [^;]+;/g, `const $1 = 'https://localhost:8001/api/';`) .replace(/const (ATTESTER_API_HOST) = [^;]+;/g, `const $1 = 'https://localhost:8001/attester/';`) - .replace(/https:\/\/flowcrypt.com\/api\/help\/error/g, 'https://localhost:8001/api/help/error'); + .replace(/https:\/\/flowcrypt\.com\/api\/help\/error/g, 'https://localhost:8001/api/help/error'); }; edit(`${buildDir(mockBuildType)}/js/common/core/const.js`, editor); edit(`${buildDir(mockBuildType)}/js/common/platform/catch.js`, editor); edit(`${buildDir(mockBuildType)}/js/content_scripts/webmail_bundle.js`, editor); edit(`${buildDir(mockBuildType)}/manifest.json`, (code) => - code.replace(/https:\/\/mail.google.com/g, 'https://gmail.localhost:8001') + code.replace(/https:\/\/mail\.google\.com/g, 'https://gmail.localhost:8001') ); }; From c2fef29a28c98b4b864202440980b64deba6d683 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 4 Jul 2022 09:14:36 +0000 Subject: [PATCH 14/26] Displaying 'Account keys updated' toast --- extension/js/common/browser/browser-msg.ts | 2 +- extension/js/common/helpers.ts | 6 +++--- .../content_scripts/webmail/setup-webmail-content-script.ts | 5 ++++- test/source/tests/setup.ts | 5 +++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index 8953fbbf793..9e77029849b 100644 --- a/extension/js/common/browser/browser-msg.ts +++ b/extension/js/common/browser/browser-msg.ts @@ -84,7 +84,7 @@ export namespace Bm { export type AjaxGmailAttachmentGetChunk = { chunk: Buf }; export type _tab_ = { tabId: string | null | undefined }; export type SaveFetchedPubkeys = boolean; - export type ProcessKeysFromEkm = { unencryptedKeysToSave: string[] }; + export type ProcessKeysFromEkm = { unencryptedKeysToSave: string[], updateCount: number }; export type Db = any; // not included in Any below export type Ajax = any; // not included in Any below diff --git a/extension/js/common/helpers.ts b/extension/js/common/helpers.ts index d9fd4bd455a..bafbf6e43a4 100644 --- a/extension/js/common/helpers.ts +++ b/extension/js/common/helpers.ts @@ -51,7 +51,7 @@ export const processAndStoreKeysFromEkmLocally = async ( const existingKeys = await KeyStore.get(acctEmail); let passphrase = options?.passphrase; if (passphrase === undefined && !existingKeys.length) { - return { unencryptedKeysToSave: [] }; // return success as we can't possibly validate a passphrase + return { unencryptedKeysToSave: [], updateCount: 0 }; // return success as we can't possibly validate a passphrase // this can only happen on misconfiguration // todo: or should we throw? } @@ -97,8 +97,8 @@ export const processAndStoreKeysFromEkmLocally = async ( if (encryptedKeys.length) { // also updates `name`, todo: refactor? await saveKeysAndPassPhrase(acctEmail, encryptedKeys, options); - return { unencryptedKeysToSave: [] }; + return { unencryptedKeysToSave: [], updateCount: encryptedKeys.length }; } else { - return { unencryptedKeysToSave: unencryptedKeysToSave.map(KeyUtil.armor) }; + return { unencryptedKeysToSave: unencryptedKeysToSave.map(KeyUtil.armor), updateCount: 0 }; } }; diff --git a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts index 2cbd156571e..edf7bc93b71 100644 --- a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts +++ b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts @@ -243,9 +243,10 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi }; const processKeysFromEkm = async (acctEmail: string, decryptedPrivateKeys: string[], factory: XssSafeFactory, ppEvent: { entered?: boolean }) => { - const { unencryptedKeysToSave } = await BrowserMsg.send.bg.await.processKeysFromEkm({ acctEmail, decryptedPrivateKeys }); + const { unencryptedKeysToSave, updateCount } = await BrowserMsg.send.bg.await.processKeysFromEkm({ acctEmail, decryptedPrivateKeys }); if (unencryptedKeysToSave.length) { ppEvent.entered = undefined; + // todo: we need to think about possible collision with a pass phrase dialog activated by a compose frame await showPassphraseDialog(factory, { longids: [], type: 'update_key' }); while (ppEvent.entered === undefined) { await Ui.time.sleep(100); @@ -255,6 +256,8 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi } else { return; // todo: alert } + } else if (updateCount > 0) { + Ui.toast('Account keys updated'); } }; diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index ec03102da84..9cb112d3288 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -16,6 +16,7 @@ import { KeyInfoWithIdentity, KeyUtil } from '../core/crypto/key'; import { testConstants } from './tooling/consts'; import { TestUrls } from '../browser/test-urls'; import { InboxPageRecipe } from './page-recipe/inbox-page-recipe'; +import { PageRecipe } from './page-recipe/abstract-page-recipe'; // tslint:disable:no-blank-lines-func // tslint:disable:no-unused-expression @@ -576,7 +577,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== expect(await KeyUtil.decrypt(prv1, oldPassphrase as string, undefined, undefined)).to.be.true; const accessToken = await BrowserRecipe.getGoogleAccessToken(settingsPage, acct); let gmailPage = await browser.newPage(t, "https://gmail.localhost:8001/gmail", undefined, { Authorization: `Bearer ${accessToken}` }); - await Util.sleep(3); // todo: wait for notification + await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); const { cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys: keyset2 } = await settingsPage.getFromLocalStorage(['cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys']); const ki2 = keyset2 as KeyInfoWithIdentity[]; @@ -598,7 +599,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== await passphraseDialog.waitAndType('@input-pass-phrase', oldPassphrase); await passphraseDialog.waitAndClick('@action-confirm-pass-phrase-entry'); await inboxPage.waitTillGone('@dialog-passphrase'); - await Util.sleep(3); // todo: wait for notification + await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); const { cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys: keyset3 } = await settingsPage.getFromLocalStorage(['cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys']); const ki3 = keyset3 as KeyInfoWithIdentity[]; From aef6a30311e54c103286488f415a4e5c07100240 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 5 Jul 2022 16:33:59 +0000 Subject: [PATCH 15/26] more thorough test control --- extension/chrome/elements/passphrase.ts | 1 - extension/js/common/helpers.ts | 2 +- .../mock/key-manager/key-manager-endpoints.ts | 7 -- .../tests/page-recipe/compose-page-recipe.ts | 2 +- test/source/tests/setup.ts | 118 +++++++++++------- 5 files changed, 78 insertions(+), 52 deletions(-) diff --git a/extension/chrome/elements/passphrase.ts b/extension/chrome/elements/passphrase.ts index 7068ff86344..0ec7ad37017 100644 --- a/extension/chrome/elements/passphrase.ts +++ b/extension/chrome/elements/passphrase.ts @@ -190,7 +190,6 @@ View.run(class PassphraseView extends View { Ui.toast(`${unlockCount} of ${allPrivateKeys.length} keys ${(unlockCount > 1) ? 'were' : 'was'} unlocked by this pass phrase`); } if (atLeastOneMatched) { - console.log('At least one matched'); this.closeDialog(true, this.initiatorFrameId); } else { this.renderFailedEntryPpPrompt(); diff --git a/extension/js/common/helpers.ts b/extension/js/common/helpers.ts index bafbf6e43a4..c69cfd3ae4c 100644 --- a/extension/js/common/helpers.ts +++ b/extension/js/common/helpers.ts @@ -70,7 +70,7 @@ export const processAndStoreKeysFromEkmLocally = async ( const keyToUpdate = existingKeys.filter(ki => ki.longid === longid && ki.family === prv.family); if (keyToUpdate.length === 1) { const oldKey = await KeyUtil.parse(keyToUpdate[0].private); - if (!oldKey.lastModified || !prv.lastModified || oldKey.lastModified === prv.lastModified) { + if (!oldKey.lastModified || !prv.lastModified || oldKey.lastModified >= prv.lastModified) { continue; } } else if (keyToUpdate.length > 1) { diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index ed0dfdbfeab..da33faa2fb5 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -219,13 +219,6 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { return { privateKeys: [{ decryptedPrivateKey: testConstants.existingPrv }] }; } if (acctEmail === 'get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test') { - if (!MOCK_KM_UPDATING_KEY.privateKeys.length) { - MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: testConstants.updatingPrv }]; - } else { - const key = await KeyUtil.parse(MOCK_KM_UPDATING_KEY.privateKeys[0].decryptedPrivateKey); - const updatedKey = await KeyUtil.reformatKey(key, undefined, [{ name: 'Full Name', email: key.emails[0] }], 6000); - MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: KeyUtil.armor(updatedKey) }]; - } return MOCK_KM_UPDATING_KEY; } if (acctEmail === 'get.key@no-submit-client-configuration.key-manager-autogen.flowcrypt.test') { diff --git a/test/source/tests/page-recipe/compose-page-recipe.ts b/test/source/tests/page-recipe/compose-page-recipe.ts index c6d377f2044..5e16003ea2b 100644 --- a/test/source/tests/page-recipe/compose-page-recipe.ts +++ b/test/source/tests/page-recipe/compose-page-recipe.ts @@ -170,7 +170,7 @@ export class ComposePageRecipe extends PageRecipe { } else if (inputMethod === 'keyboard') { await page.press('Escape'); } - await page.waitTillGone('@dialog'); + await page.waitTillGone('@dialog-passphrase'); expect(passPhraseFrame.frame.isDetached()).to.equal(true); }; } diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index 9cb112d3288..1d0e143b14b 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -9,12 +9,11 @@ import { expect } from 'chai'; import { SettingsPageRecipe } from './page-recipe/settings-page-recipe'; import { ComposePageRecipe } from './page-recipe/compose-page-recipe'; import { Str } from './../core/common'; -import { MOCK_KM_LAST_INSERTED_KEY } from './../mock/key-manager/key-manager-endpoints'; +import { MOCK_KM_LAST_INSERTED_KEY, MOCK_KM_UPDATING_KEY } from './../mock/key-manager/key-manager-endpoints'; import { MOCK_ATTESTER_LAST_INSERTED_PUB } from './../mock/attester/attester-endpoints'; import { BrowserRecipe } from './tooling/browser-recipe'; -import { KeyInfoWithIdentity, KeyUtil } from '../core/crypto/key'; +import { Key, KeyInfoWithIdentity, KeyUtil } from '../core/crypto/key'; import { testConstants } from './tooling/consts'; -import { TestUrls } from '../browser/test-urls'; import { InboxPageRecipe } from './page-recipe/inbox-page-recipe'; import { PageRecipe } from './page-recipe/abstract-page-recipe'; @@ -558,57 +557,92 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== })); ava.default('get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test - automatic update of key found on key manager', testWithBrowser(undefined, async (t, browser) => { + MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: testConstants.updatingPrv }]; const acct = 'get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test'; const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); - const oldPassphrase = 'long enough to suit requirements'; + const passphrase = 'long enough to suit requirements'; await SetupPageRecipe.autoSetupWithEKM(settingsPage, { - enterPp: { - passphrase: oldPassphrase, - checks: { isSavePassphraseChecked: false, isSavePassphraseHidden: true } - } + enterPp: { passphrase, checks: { isSavePassphraseChecked: false, isSavePassphraseHidden: true } } }); - const { cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys: keyset1 } - = await settingsPage.getFromLocalStorage(['cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys']); - const ki1 = keyset1 as KeyInfoWithIdentity[]; - expect(ki1.length).to.equal(1); - const prv1 = await KeyUtil.parse(ki1[0].private); - const prv1LastModified = prv1.lastModified!; - expect(prv1.fullyEncrypted).to.be.true; - expect(await KeyUtil.decrypt(prv1, oldPassphrase as string, undefined, undefined)).to.be.true; + const retrieveAndCheckKeys = async (expectedKeyCount: number) => { + const { cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys: keyset } + = await settingsPage.getFromLocalStorage(['cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys']); + const kis = keyset as KeyInfoWithIdentity[]; + expect(kis.length).to.equal(expectedKeyCount); + return await Promise.all(kis.map(async ki => { + const prv = await KeyUtil.parse(ki.private); + expect(prv.fullyEncrypted).to.be.true; + expect(await KeyUtil.decrypt(prv, passphrase as string, undefined, undefined)).to.be.true; + expect(prv.lastModified).to.not.be.an.undefined; + return { prv, lastModified: prv.lastModified! }; + })); + }; + const updateAndArmorKey = async (prv: Key) => { + return KeyUtil.armor(await KeyUtil.reformatKey(prv, undefined, [{ name: 'Full Name', email: acct }], 6000)); + }; + const set1 = await retrieveAndCheckKeys(1); const accessToken = await BrowserRecipe.getGoogleAccessToken(settingsPage, acct); - let gmailPage = await browser.newPage(t, "https://gmail.localhost:8001/gmail", undefined, { Authorization: `Bearer ${accessToken}` }); + const dummyGmailUrl = 'https://gmail.localhost:8001/gmail'; + const extraAuthHeaders = { Authorization: `Bearer ${accessToken}` }; + // 1. EKM returns the same key, no update, no toast + let gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); + await PageRecipe.noToastAppears(gmailPage); + await gmailPage.notPresent('@dialog-passphrase'); + const set2 = await retrieveAndCheckKeys(1); + expect(set2[0].lastModified).to.equal(set1[0].lastModified); // no update + await gmailPage.close(); + // 2. EKM returns a newer version of the existing key + const someOlderVersion = await updateAndArmorKey(set2[0].prv); + MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: someOlderVersion }]; + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); - const { cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys: keyset2 } - = await settingsPage.getFromLocalStorage(['cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys']); - const ki2 = keyset2 as KeyInfoWithIdentity[]; - expect(ki2.length).to.equal(1); - const prv2 = await KeyUtil.parse(ki2[0].private); - const prv2LastModified = prv2.lastModified!; - expect(prv2LastModified).to.not.equal(prv1LastModified); // an update happened - expect(prv2.fullyEncrypted).to.be.true; - expect(await KeyUtil.decrypt(prv2, oldPassphrase as string, undefined, undefined)).to.be.true; + const set3 = await retrieveAndCheckKeys(1); + expect(set3[0].lastModified).to.not.equal(set2[0].lastModified); // an update happened + await gmailPage.close(); + // 3. EKM returns the same version of the existing key, no toast, no update + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); + await PageRecipe.noToastAppears(gmailPage); + await gmailPage.notPresent('@dialog-passphrase'); + const set4 = await retrieveAndCheckKeys(1); + expect(set4[0].lastModified).to.equal(set3[0].lastModified); // no update + // 4. Forget the passphrase, EKM the same version of the existing key, no prompt + await InboxPageRecipe.finishSessionOnInboxPage(gmailPage); await gmailPage.close(); - // forget the passphrase - const inboxPage = await browser.newPage(t, TestUrls.extension(`chrome/settings/inbox/inbox.htm?acctEmail=${acct}`)); - await InboxPageRecipe.finishSessionOnInboxPage(inboxPage); - gmailPage = await browser.newPage(t, "https://gmail.localhost:8001/gmail", undefined, { Authorization: `Bearer ${accessToken}` }); + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); + await PageRecipe.noToastAppears(gmailPage); + await gmailPage.notPresent('@dialog-passphrase'); + const set5 = await retrieveAndCheckKeys(1); + expect(set5[0].lastModified).to.equal(set4[0].lastModified); // no update + await gmailPage.close(); + // 5. EKM returns a newer version of the existing key, canceling passphrase prompt, no update + MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: await updateAndArmorKey(set5[0].prv) }]; + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); + await gmailPage.waitAll('@dialog-passphrase'); + // todo: why ComposePageRecipe? + await ComposePageRecipe.cancelPassphraseDialog(gmailPage, 'keyboard'); + await PageRecipe.noToastAppears(gmailPage); + const set6 = await retrieveAndCheckKeys(1); + expect(set6[0].lastModified).to.equal(set5[0].lastModified); // no update + await gmailPage.close(); + // 6. EKM returns a newer version of the existing key, entering the passphrase, update toast + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await gmailPage.waitAll('@dialog-passphrase'); const passphraseDialog = await gmailPage.getFrame(['passphrase.htm']); await passphraseDialog.waitForContent('@passphrase-text', 'Enter FlowCrypt pass phrase to keep your account keys up to date'); - // todo: await ComposePageRecipe.cancelPassphraseDialog(inboxPage, inputMethod); - await passphraseDialog.waitAndType('@input-pass-phrase', oldPassphrase); + await passphraseDialog.waitAndType('@input-pass-phrase', passphrase); await passphraseDialog.waitAndClick('@action-confirm-pass-phrase-entry'); - await inboxPage.waitTillGone('@dialog-passphrase'); + await gmailPage.waitTillGone('@dialog-passphrase'); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); - const { cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys: keyset3 - } = await settingsPage.getFromLocalStorage(['cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys']); - const ki3 = keyset3 as KeyInfoWithIdentity[]; - expect(ki3.length).to.equal(1); - const prv3 = await KeyUtil.parse(ki3[0].private); - const prv3LastModified = prv3.lastModified!; - expect(prv3LastModified).to.not.equal(prv2LastModified); // an update happened - expect(prv3.fullyEncrypted).to.be.true; - expect(await KeyUtil.decrypt(prv3, oldPassphrase as string, undefined, undefined)).to.be.true; + const set7 = await retrieveAndCheckKeys(1); + expect(set7[0].lastModified).to.not.equal(set6[0].lastModified); // an update happened + await gmailPage.close(); + // 7. EKM returns an older version of the existing key, no toast, no update + MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: someOlderVersion }]; + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); + await PageRecipe.noToastAppears(gmailPage); + await gmailPage.notPresent('@dialog-passphrase'); + const set8 = await retrieveAndCheckKeys(1); + expect(set8[0].lastModified).to.equal(set7[0].lastModified); // no update await gmailPage.close(); })); From c87b39128d52a80655b1f4cd35eef51ad2dd0601 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 5 Jul 2022 19:00:21 +0000 Subject: [PATCH 16/26] more test cases --- test/source/tests/setup.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index 1d0e143b14b..f137f5b0e1a 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -597,7 +597,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); const set3 = await retrieveAndCheckKeys(1); - expect(set3[0].lastModified).to.not.equal(set2[0].lastModified); // an update happened + expect(set3[0].lastModified).to.be.greaterThan(set2[0].lastModified); // an update happened await gmailPage.close(); // 3. EKM returns the same version of the existing key, no toast, no update gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); @@ -634,7 +634,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== await gmailPage.waitTillGone('@dialog-passphrase'); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); const set7 = await retrieveAndCheckKeys(1); - expect(set7[0].lastModified).to.not.equal(set6[0].lastModified); // an update happened + expect(set7[0].lastModified).to.be.greaterThan(set6[0].lastModified); // an update happened await gmailPage.close(); // 7. EKM returns an older version of the existing key, no toast, no update MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: someOlderVersion }]; @@ -644,6 +644,28 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== const set8 = await retrieveAndCheckKeys(1); expect(set8[0].lastModified).to.equal(set7[0].lastModified); // no update await gmailPage.close(); + // 8. EKM returns an older version of the existing key, and a new key, toast, new key gets added encrypted with the same passphrase + MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: someOlderVersion }, { decryptedPrivateKey: testConstants.existingPrv }]; + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); + await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); + await gmailPage.notPresent('@dialog-passphrase'); + const set9 = await retrieveAndCheckKeys(2); + const mainKey9 = KeyUtil.filterKeysByIdentity(set9.map(ki => ki.prv), [{ family: 'openpgp', id: '392FB1E9FF4184659AB6A246835C0141B9ECF536' }]); + expect(mainKey9.length).to.equal(1); + expect(KeyUtil.filterKeysByIdentity(set9.map(ki => ki.prv), [{ family: 'openpgp', id: 'FAFB7D675AC74E87F84D169F00B0115807969D75' }]).length).to.equal(1); + expect(mainKey9[0].lastModified!).to.equal(set8[0].lastModified); // no update + await gmailPage.close(); + // 9. EKM returns a newer version of one key, fully omitting the other one, a toast, and update, no removal + MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: await updateAndArmorKey(mainKey9[0]) }]; + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); + await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); + await gmailPage.notPresent('@dialog-passphrase'); + const set10 = await retrieveAndCheckKeys(2); + const mainKey10 = KeyUtil.filterKeysByIdentity(set10.map(ki => ki.prv), [{ family: 'openpgp', id: '392FB1E9FF4184659AB6A246835C0141B9ECF536' }]); + expect(mainKey10.length).to.equal(1); + expect(KeyUtil.filterKeysByIdentity(set10.map(ki => ki.prv), [{ family: 'openpgp', id: 'FAFB7D675AC74E87F84D169F00B0115807969D75' }]).length).to.equal(1); + expect(mainKey10[0].lastModified!).to.be.greaterThan(mainKey9[0].lastModified!); // updated this key + await gmailPage.close(); })); ava.default('get.key@key-manager-choose-passphrase.flowcrypt.test - passphrase chosen by user with key found on key manager', testWithBrowser(undefined, async (t, browser) => { From 1bd33a3dad44a153541364fdf2061b36aeb99ffa Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Fri, 8 Jul 2022 14:39:37 +0000 Subject: [PATCH 17/26] More test cases and EKM failure test --- .../webmail/setup-webmail-content-script.ts | 33 +++++----- test/source/test.ts | 3 + test/source/tests/setup.ts | 63 ++++++++++++++++--- 3 files changed, 78 insertions(+), 21 deletions(-) diff --git a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts index edf7bc93b71..eadfb15d727 100644 --- a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts +++ b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts @@ -243,21 +243,26 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi }; const processKeysFromEkm = async (acctEmail: string, decryptedPrivateKeys: string[], factory: XssSafeFactory, ppEvent: { entered?: boolean }) => { - const { unencryptedKeysToSave, updateCount } = await BrowserMsg.send.bg.await.processKeysFromEkm({ acctEmail, decryptedPrivateKeys }); - if (unencryptedKeysToSave.length) { - ppEvent.entered = undefined; - // todo: we need to think about possible collision with a pass phrase dialog activated by a compose frame - await showPassphraseDialog(factory, { longids: [], type: 'update_key' }); - while (ppEvent.entered === undefined) { - await Ui.time.sleep(100); - } - if (ppEvent.entered) { - await processKeysFromEkm(acctEmail, unencryptedKeysToSave, factory, ppEvent); - } else { - return; // todo: alert + try { + const { unencryptedKeysToSave, updateCount } = await BrowserMsg.send.bg.await.processKeysFromEkm({ acctEmail, decryptedPrivateKeys }); + if (unencryptedKeysToSave.length) { + ppEvent.entered = undefined; + // todo: we need to think about possible collision with a pass phrase dialog activated by a compose frame + await showPassphraseDialog(factory, { longids: [], type: 'update_key' }); + while (ppEvent.entered === undefined) { + await Ui.time.sleep(100); + } + if (ppEvent.entered) { + await processKeysFromEkm(acctEmail, unencryptedKeysToSave, factory, ppEvent); + } else { + return; + } + } else if (updateCount > 0) { + Ui.toast('Account keys updated'); } - } else if (updateCount > 0) { - Ui.toast('Account keys updated'); + } catch (e) { + Catch.reportErr(e); + Ui.toast(`Could not update keys from EKM due to error: ${e instanceof Error ? e.message : String(e)}`); } }; diff --git a/test/source/test.ts b/test/source/test.ts index 815bf11b106..07021fb2cab 100644 --- a/test/source/test.ts +++ b/test/source/test.ts @@ -122,6 +122,9 @@ ava.default.after.always('evaluate Catch.reportErr errors', async t => { // our S/MIME implementation is still early so it throws "reportable" errors like this during tests const usefulErrors = mockBackendData.reportedErrors .filter(e => e.message !== 'Too few bytes to read ASN.1 value.') + // below for test "get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test - automatic update of key found on key manager" + .filter(e => !e.trace.includes('Could not parse any valid keys from Key Manager response for user ' + + 'get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test')) // below for test "no.fes@example.com - skip FES on consumer, show friendly message on enterprise" .filter(e => !e.trace.includes('-1 when GET-ing https://fes.example.com')) // todo - ideally mock tests would never call this. But we do tests with human@flowcrypt.com so it's calling here diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index f137f5b0e1a..71d756cb09f 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -564,6 +564,9 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== await SetupPageRecipe.autoSetupWithEKM(settingsPage, { enterPp: { passphrase, checks: { isSavePassphraseChecked: false, isSavePassphraseHidden: true } } }); + const accessToken = await BrowserRecipe.getGoogleAccessToken(settingsPage, acct); + const dummyGmailUrl = 'https://gmail.localhost:8001/gmail'; + const extraAuthHeaders = { Authorization: `Bearer ${accessToken}` }; const retrieveAndCheckKeys = async (expectedKeyCount: number) => { const { cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys: keyset } = await settingsPage.getFromLocalStorage(['cryptup_getupdatingkeykeymanagerchoosepassphraseforbidstoringflowcrypttest_keys']); @@ -581,9 +584,6 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== return KeyUtil.armor(await KeyUtil.reformatKey(prv, undefined, [{ name: 'Full Name', email: acct }], 6000)); }; const set1 = await retrieveAndCheckKeys(1); - const accessToken = await BrowserRecipe.getGoogleAccessToken(settingsPage, acct); - const dummyGmailUrl = 'https://gmail.localhost:8001/gmail'; - const extraAuthHeaders = { Authorization: `Bearer ${accessToken}` }; // 1. EKM returns the same key, no update, no toast let gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await PageRecipe.noToastAppears(gmailPage); @@ -627,10 +627,12 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== // 6. EKM returns a newer version of the existing key, entering the passphrase, update toast gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await gmailPage.waitAll('@dialog-passphrase'); - const passphraseDialog = await gmailPage.getFrame(['passphrase.htm']); - await passphraseDialog.waitForContent('@passphrase-text', 'Enter FlowCrypt pass phrase to keep your account keys up to date'); - await passphraseDialog.waitAndType('@input-pass-phrase', passphrase); - await passphraseDialog.waitAndClick('@action-confirm-pass-phrase-entry'); + { + const passphraseDialog = await gmailPage.getFrame(['passphrase.htm']); + await passphraseDialog.waitForContent('@passphrase-text', 'Enter FlowCrypt pass phrase to keep your account keys up to date'); + await passphraseDialog.waitAndType('@input-pass-phrase', passphrase); + await passphraseDialog.waitAndClick('@action-confirm-pass-phrase-entry'); + } await gmailPage.waitTillGone('@dialog-passphrase'); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); const set7 = await retrieveAndCheckKeys(1); @@ -665,6 +667,53 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== expect(mainKey10.length).to.equal(1); expect(KeyUtil.filterKeysByIdentity(set10.map(ki => ki.prv), [{ family: 'openpgp', id: 'FAFB7D675AC74E87F84D169F00B0115807969D75' }]).length).to.equal(1); expect(mainKey10[0].lastModified!).to.be.greaterThan(mainKey9[0].lastModified!); // updated this key + // 10. Forget the passphrase, EKM returns a third key, we enter a passphrase that doesn't match any of the existing keys, no update + await InboxPageRecipe.finishSessionOnInboxPage(gmailPage); + await gmailPage.close(); + MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: testConstants.unprotectedPrvKey }]; + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); + await gmailPage.waitAll('@dialog-passphrase'); + { + const passphraseDialog = await gmailPage.getFrame(['passphrase.htm']); + await passphraseDialog.waitAndType('@input-pass-phrase', 'g00D_pa$$worD-But_Different'); + await passphraseDialog.waitAndClick('@action-confirm-pass-phrase-entry'); + // todo: how to wait properly + await passphraseDialog.waitForContent('@input-pass-phrase', /^$/); + expect(await passphraseDialog.attr('@input-pass-phrase', 'placeholder')).to.eq('Please try again'); + } + await ComposePageRecipe.cancelPassphraseDialog(gmailPage, 'keyboard'); + await PageRecipe.noToastAppears(gmailPage); + const set11 = await retrieveAndCheckKeys(2); + expect(set11.map(entry => entry.prv.id)).to.eql(['392FB1E9FF4184659AB6A246835C0141B9ECF536', 'FAFB7D675AC74E87F84D169F00B0115807969D75']); + await gmailPage.close(); + // 11. EKM returns a new third key, we enter a passphrase matching an existing key, update happens + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); + await gmailPage.waitAll('@dialog-passphrase'); + { + const passphraseDialog = await gmailPage.getFrame(['passphrase.htm']); + await passphraseDialog.waitForContent('@passphrase-text', 'Enter FlowCrypt pass phrase to keep your account keys up to date'); + await passphraseDialog.waitAndType('@input-pass-phrase', passphrase); + await passphraseDialog.waitAndClick('@action-confirm-pass-phrase-entry'); + } + await gmailPage.waitTillGone('@dialog-passphrase'); + await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); + const set12 = await retrieveAndCheckKeys(3); + expect(set12.map(entry => entry.prv.id)).to.eql([ + '392FB1E9FF4184659AB6A246835C0141B9ECF536', + 'FAFB7D675AC74E87F84D169F00B0115807969D75', + '277D1ADA213881F4ABE0415395E783DC0289E2E2' + ]); + await InboxPageRecipe.finishSessionOnInboxPage(gmailPage); + await gmailPage.close(); + // 12. Forget the passphrase, EKM sends a broken key + // todo: we should probably update the valid keys? + MOCK_KM_UPDATING_KEY.privateKeys = [ + { decryptedPrivateKey: await updateAndArmorKey(mainKey10[0]) }, // update the main key + // only include a half of another armored key + { decryptedPrivateKey: testConstants.unprotectedPrvKey.substring(0, testConstants.unprotectedPrvKey.length / 2) } + ]; + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); + await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Could not update keys from EKM due to error:'); await gmailPage.close(); })); From 46a5872d69323be630a632b3cb88c91ebf6e220d Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 9 Jul 2022 18:20:00 +0000 Subject: [PATCH 18/26] refactorings and better test setup --- .../setup/setup-key-manager-autogen.ts | 14 ++-- extension/js/common/helpers.ts | 81 +++++++++++-------- test/source/test.ts | 3 +- test/source/tests/setup.ts | 9 ++- 4 files changed, 64 insertions(+), 43 deletions(-) diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index 6e321372587..726209415a7 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -56,11 +56,15 @@ export class SetupWithEmailKeyManagerModule { if (privateKeys.length) { // keys already exist on keyserver, auto-import // todo: do we need to submit on auto-update? - await processAndStoreKeysFromEkmLocally({ - acctEmail: this.view.acctEmail, - decryptedPrivateKeys: privateKeys.map(entry => entry.decryptedPrivateKey), - options: setupOptions - }); + try { + await processAndStoreKeysFromEkmLocally({ + acctEmail: this.view.acctEmail, + decryptedPrivateKeys: privateKeys.map(entry => entry.decryptedPrivateKey), + options: setupOptions + }); + } catch (e) { + throw new Error(`Could not store keys from EKM due to error: ${e instanceof Error ? e.message : String(e)}`); + } } else if (this.view.clientConfiguration.canCreateKeys()) { // generate keys on client and store them on key manager await this.autoGenerateKeyAndStoreBothLocallyAndToEkm(setupOptions); diff --git a/extension/js/common/helpers.ts b/extension/js/common/helpers.ts index c69cfd3ae4c..20a328788c2 100644 --- a/extension/js/common/helpers.ts +++ b/extension/js/common/helpers.ts @@ -5,7 +5,7 @@ import { AcctStore } from './platform/store/acct-store.js'; import { PassphraseOptions } from '../../chrome/settings/setup.js'; import { Buf } from './core/buf.js'; -import { Key, KeyUtil } from './core/crypto/key.js'; +import { Key, KeyInfoWithIdentity, KeyUtil } from './core/crypto/key.js'; import { ClientConfiguration } from './client-configuration.js'; import { ContactStore } from './platform/store/contact-store.js'; import { KeyStore } from './platform/store/key-store.js'; @@ -16,12 +16,11 @@ export const isFesUsed = async (acctEmail: string) => { return Boolean(fesUrl); }; -// todo: where to take acctEmail and clientConfiguration export const saveKeysAndPassPhrase = async (acctEmail: string, prvs: Key[], options?: PassphraseOptions) => { + const clientConfiguration = await ClientConfiguration.newInstance(acctEmail); for (const prv of prvs) { await KeyStore.add(acctEmail, prv); if (options !== undefined) { - const clientConfiguration = await ClientConfiguration.newInstance(acctEmail); await PassphraseStore.set((options.passphrase_save && !clientConfiguration.forbidStoringPassPhrase()) ? 'local' : 'session', acctEmail, { longid: KeyUtil.getPrimaryLongid(prv) }, options.passphrase); } @@ -40,14 +39,55 @@ export const saveKeysAndPassPhrase = async (acctEmail: string, prvs: Key[], opti } }; +const parseAndCheckPrivateKeys = async (decryptedPrivateKeys: string[]) => { + const unencryptedPrvs: Key[] = []; + // parse and check that all the keys are valid + for (const entry of decryptedPrivateKeys) { + const { keys, errs } = await KeyUtil.readMany(Buf.fromUtfStr(entry)); + if (errs.length) { + throw new Error(`Some keys could not be parsed`); + } + if (!keys.length) { + throw new Error(`Could not parse any valid keys`); + } + for (const prv of keys) { + if (!prv.isPrivate) { + throw new Error(`Key ${prv.id} is not a private key`); + } + if (!prv.fullyDecrypted) { + throw new Error(`Key ${prv.id} is not fully decrypted`); + } + } + unencryptedPrvs.push(...keys); + } + return { unencryptedPrvs }; +}; + +const filterKeysToSave = async (candidateKeys: Key[], existingKeys: KeyInfoWithIdentity[]) => { + if (!existingKeys.length) { + return candidateKeys; + } + const result: Key[] = []; + for (const candidate of candidateKeys) { + const longid = KeyUtil.getPrimaryLongid(candidate); + const keyToUpdate = existingKeys.filter(ki => ki.longid === longid && ki.family === candidate.family); + if (keyToUpdate.length === 1) { + const oldKey = await KeyUtil.parse(keyToUpdate[0].private); + if (!oldKey.lastModified || !candidate.lastModified || oldKey.lastModified >= candidate.lastModified) { + continue; + } + } else if (keyToUpdate.length > 1) { + throw new Error(`Unexpected error: key search by longid=${longid} yielded ${keyToUpdate.length} results`); + } + result.push(candidate); + }; + return result; +}; + export const processAndStoreKeysFromEkmLocally = async ( { acctEmail, decryptedPrivateKeys, options }: { acctEmail: string, decryptedPrivateKeys: string[], options?: PassphraseOptions } ): Promise => { - const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(decryptedPrivateKeys.join('\n'))); - if (!keys.length) { - throw new Error(`Could not parse any valid keys from Key Manager response for user ${acctEmail}`); - } - let unencryptedKeysToSave: Key[] = []; + const { unencryptedPrvs } = await parseAndCheckPrivateKeys(decryptedPrivateKeys); const existingKeys = await KeyStore.get(acctEmail); let passphrase = options?.passphrase; if (passphrase === undefined && !existingKeys.length) { @@ -55,30 +95,7 @@ export const processAndStoreKeysFromEkmLocally = async ( // this can only happen on misconfiguration // todo: or should we throw? } - for (const prv of keys) { - // todo: should we still process remaining correct keys? - if (!prv.isPrivate) { - throw new Error(`Key ${prv.id} for user ${acctEmail} is not a private key`); - } - if (!prv.fullyDecrypted) { - throw new Error(`Key ${prv.id} for user ${acctEmail} from FlowCrypt Email Key Manager is not fully decrypted`); - } - if (options === undefined) { - // updating here - // todo: refactor? - const longid = KeyUtil.getPrimaryLongid(prv); - const keyToUpdate = existingKeys.filter(ki => ki.longid === longid && ki.family === prv.family); - if (keyToUpdate.length === 1) { - const oldKey = await KeyUtil.parse(keyToUpdate[0].private); - if (!oldKey.lastModified || !prv.lastModified || oldKey.lastModified >= prv.lastModified) { - continue; - } - } else if (keyToUpdate.length > 1) { - throw new Error(`Unexpected error: key search by longid=${longid} yielded ${keyToUpdate.length} results`); - } - } - unencryptedKeysToSave.push(prv); - } + let unencryptedKeysToSave = await filterKeysToSave(unencryptedPrvs, existingKeys); let encryptedKeys: Key[] = []; if (unencryptedKeysToSave.length) { if (passphrase === undefined) { diff --git a/test/source/test.ts b/test/source/test.ts index 5ee8e375b7b..faeed7aa87e 100644 --- a/test/source/test.ts +++ b/test/source/test.ts @@ -123,8 +123,7 @@ ava.default.after.always('evaluate Catch.reportErr errors', async t => { const usefulErrors = mockBackendData.reportedErrors .filter(e => e.message !== 'Too few bytes to read ASN.1 value.') // below for test "get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test - automatic update of key found on key manager" - .filter(e => !e.trace.includes('Could not parse any valid keys from Key Manager response for user ' - + 'get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test')) + .filter(e => e.message !== 'BrowserMsg(processKeysFromEkm) sendRawResponse::Error: Some keys could not be parsed') // below for test "user4@standardsubdomainfes.test:8001 - PWD encrypted message with FES web portal - a send fails with gateway update error" .filter(e => !e.message.includes('Test error')) // below for test "no.fes@example.com - skip FES on consumer, show friendly message on enterprise" diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index 71d756cb09f..f949097c2dd 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -703,17 +703,18 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== 'FAFB7D675AC74E87F84D169F00B0115807969D75', '277D1ADA213881F4ABE0415395E783DC0289E2E2' ]); - await InboxPageRecipe.finishSessionOnInboxPage(gmailPage); - await gmailPage.close(); // 12. Forget the passphrase, EKM sends a broken key // todo: we should probably update the valid keys? + await InboxPageRecipe.finishSessionOnInboxPage(gmailPage); + await gmailPage.close(); MOCK_KM_UPDATING_KEY.privateKeys = [ - { decryptedPrivateKey: await updateAndArmorKey(mainKey10[0]) }, // update the main key + { decryptedPrivateKey: await updateAndArmorKey(set2[0].prv) }, // update the main key // only include a half of another armored key { decryptedPrivateKey: testConstants.unprotectedPrvKey.substring(0, testConstants.unprotectedPrvKey.length / 2) } ]; gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); - await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Could not update keys from EKM due to error:'); + await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, + 'Could not update keys from EKM due to error: BrowserMsg(processKeysFromEkm) sendRawResponse::Error: Some keys could not be parsed'); await gmailPage.close(); })); From 33adeb77a17adb605d7dba4a8543a546ab0e5efe Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 9 Jul 2022 18:23:19 +0000 Subject: [PATCH 19/26] lint fix --- extension/js/common/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/js/common/helpers.ts b/extension/js/common/helpers.ts index 20a328788c2..2d42d9514ee 100644 --- a/extension/js/common/helpers.ts +++ b/extension/js/common/helpers.ts @@ -80,7 +80,7 @@ const filterKeysToSave = async (candidateKeys: Key[], existingKeys: KeyInfoWithI throw new Error(`Unexpected error: key search by longid=${longid} yielded ${keyToUpdate.length} results`); } result.push(candidate); - }; + } return result; }; From 229673722ca7816990533b0de032b4dbf16c52a5 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 10 Jul 2022 08:57:55 +0000 Subject: [PATCH 20/26] added test for EKM down --- .../mock/key-manager/key-manager-endpoints.ts | 9 ++- test/source/test.ts | 5 +- test/source/tests/setup.ts | 69 ++++++++++++------- 3 files changed, 56 insertions(+), 27 deletions(-) diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index da33faa2fb5..2c4713e5ba1 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -1,6 +1,6 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ -import { HttpClientErr } from '../lib/api'; +import { HttpClientErr, Status } from '../lib/api'; import { HandlersDefinition } from '../all-apis-mock'; import { isPut, isGet } from '../lib/mock-util'; import { oauth } from '../lib/oauth'; @@ -206,7 +206,7 @@ yeSm0uVPwODhwX7ezB9jW6uVt0R8S8iM3rQdEMsA/jDep5LNn47K6o8VrDt0zYo6 export const MOCK_KM_LAST_INSERTED_KEY: { [acct: string]: { privateKey: string } } = {}; // accessed from test runners -export const MOCK_KM_UPDATING_KEY: { privateKeys: { decryptedPrivateKey: string }[] } = { privateKeys: [] }; +export const MOCK_KM_UPDATING_KEY: { response?: { privateKeys: { decryptedPrivateKey: string }[] }, badRequestError?: string } = {}; export const mockKeyManagerEndpoints: HandlersDefinition = { '/flowcrypt-email-key-manager/v1/keys/private': async ({ body }, req) => { @@ -219,7 +219,10 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { return { privateKeys: [{ decryptedPrivateKey: testConstants.existingPrv }] }; } if (acctEmail === 'get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test') { - return MOCK_KM_UPDATING_KEY; + if (MOCK_KM_UPDATING_KEY.response !== undefined && MOCK_KM_UPDATING_KEY.badRequestError === undefined) { + return MOCK_KM_UPDATING_KEY.response; + } + throw new HttpClientErr(MOCK_KM_UPDATING_KEY.badRequestError || 'Vague error', Status.BAD_REQUEST); } if (acctEmail === 'get.key@no-submit-client-configuration.key-manager-autogen.flowcrypt.test') { return { privateKeys: [{ decryptedPrivateKey: prvNoSubmit }] }; diff --git a/test/source/test.ts b/test/source/test.ts index faeed7aa87e..c4b7260156d 100644 --- a/test/source/test.ts +++ b/test/source/test.ts @@ -123,7 +123,10 @@ ava.default.after.always('evaluate Catch.reportErr errors', async t => { const usefulErrors = mockBackendData.reportedErrors .filter(e => e.message !== 'Too few bytes to read ASN.1 value.') // below for test "get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test - automatic update of key found on key manager" - .filter(e => e.message !== 'BrowserMsg(processKeysFromEkm) sendRawResponse::Error: Some keys could not be parsed') + .filter(e => ![ + 'BrowserMsg(processKeysFromEkm) sendRawResponse::Error: Some keys could not be parsed', + 'BrowserMsg(ajax) Bad Request: 400 when GET-ing https://localhost:8001/flowcrypt-email-key-manager/v1/keys/private (no body): -> RequestTimeout' + ].includes(e.message)) // below for test "user4@standardsubdomainfes.test:8001 - PWD encrypted message with FES web portal - a send fails with gateway update error" .filter(e => !e.message.includes('Test error')) // below for test "no.fes@example.com - skip FES on consumer, show friendly message on enterprise" diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index f949097c2dd..1536aca1c1f 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -557,7 +557,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== })); ava.default('get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test - automatic update of key found on key manager', testWithBrowser(undefined, async (t, browser) => { - MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: testConstants.updatingPrv }]; + MOCK_KM_UPDATING_KEY.response = { privateKeys: [{ decryptedPrivateKey: testConstants.updatingPrv }] }; const acct = 'get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test'; const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); const passphrase = 'long enough to suit requirements'; @@ -577,7 +577,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== expect(prv.fullyEncrypted).to.be.true; expect(await KeyUtil.decrypt(prv, passphrase as string, undefined, undefined)).to.be.true; expect(prv.lastModified).to.not.be.an.undefined; - return { prv, lastModified: prv.lastModified! }; + return prv; })); }; const updateAndArmorKey = async (prv: Key) => { @@ -592,12 +592,12 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== expect(set2[0].lastModified).to.equal(set1[0].lastModified); // no update await gmailPage.close(); // 2. EKM returns a newer version of the existing key - const someOlderVersion = await updateAndArmorKey(set2[0].prv); - MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: someOlderVersion }]; + const someOlderVersion = await updateAndArmorKey(set2[0]); + MOCK_KM_UPDATING_KEY.response = { privateKeys: [{ decryptedPrivateKey: someOlderVersion }] }; gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); const set3 = await retrieveAndCheckKeys(1); - expect(set3[0].lastModified).to.be.greaterThan(set2[0].lastModified); // an update happened + expect(set3[0].lastModified!).to.be.greaterThan(set2[0].lastModified!); // an update happened await gmailPage.close(); // 3. EKM returns the same version of the existing key, no toast, no update gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); @@ -615,10 +615,9 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== expect(set5[0].lastModified).to.equal(set4[0].lastModified); // no update await gmailPage.close(); // 5. EKM returns a newer version of the existing key, canceling passphrase prompt, no update - MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: await updateAndArmorKey(set5[0].prv) }]; + MOCK_KM_UPDATING_KEY.response = { privateKeys: [{ decryptedPrivateKey: await updateAndArmorKey(set5[0]) }] }; gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await gmailPage.waitAll('@dialog-passphrase'); - // todo: why ComposePageRecipe? await ComposePageRecipe.cancelPassphraseDialog(gmailPage, 'keyboard'); await PageRecipe.noToastAppears(gmailPage); const set6 = await retrieveAndCheckKeys(1); @@ -636,10 +635,10 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== await gmailPage.waitTillGone('@dialog-passphrase'); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); const set7 = await retrieveAndCheckKeys(1); - expect(set7[0].lastModified).to.be.greaterThan(set6[0].lastModified); // an update happened + expect(set7[0].lastModified!).to.be.greaterThan(set6[0].lastModified!); // an update happened await gmailPage.close(); // 7. EKM returns an older version of the existing key, no toast, no update - MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: someOlderVersion }]; + MOCK_KM_UPDATING_KEY.response = { privateKeys: [{ decryptedPrivateKey: someOlderVersion }] }; gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await PageRecipe.noToastAppears(gmailPage); await gmailPage.notPresent('@dialog-passphrase'); @@ -647,30 +646,30 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== expect(set8[0].lastModified).to.equal(set7[0].lastModified); // no update await gmailPage.close(); // 8. EKM returns an older version of the existing key, and a new key, toast, new key gets added encrypted with the same passphrase - MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: someOlderVersion }, { decryptedPrivateKey: testConstants.existingPrv }]; + MOCK_KM_UPDATING_KEY.response = { privateKeys: [{ decryptedPrivateKey: someOlderVersion }, { decryptedPrivateKey: testConstants.existingPrv }] }; gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); await gmailPage.notPresent('@dialog-passphrase'); const set9 = await retrieveAndCheckKeys(2); - const mainKey9 = KeyUtil.filterKeysByIdentity(set9.map(ki => ki.prv), [{ family: 'openpgp', id: '392FB1E9FF4184659AB6A246835C0141B9ECF536' }]); + const mainKey9 = KeyUtil.filterKeysByIdentity(set9, [{ family: 'openpgp', id: '392FB1E9FF4184659AB6A246835C0141B9ECF536' }]); expect(mainKey9.length).to.equal(1); - expect(KeyUtil.filterKeysByIdentity(set9.map(ki => ki.prv), [{ family: 'openpgp', id: 'FAFB7D675AC74E87F84D169F00B0115807969D75' }]).length).to.equal(1); - expect(mainKey9[0].lastModified!).to.equal(set8[0].lastModified); // no update + expect(KeyUtil.filterKeysByIdentity(set9, [{ family: 'openpgp', id: 'FAFB7D675AC74E87F84D169F00B0115807969D75' }]).length).to.equal(1); + expect(mainKey9[0].lastModified).to.equal(set8[0].lastModified); // no update await gmailPage.close(); // 9. EKM returns a newer version of one key, fully omitting the other one, a toast, and update, no removal - MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: await updateAndArmorKey(mainKey9[0]) }]; + MOCK_KM_UPDATING_KEY.response = { privateKeys: [{ decryptedPrivateKey: await updateAndArmorKey(mainKey9[0]) }] }; gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); await gmailPage.notPresent('@dialog-passphrase'); const set10 = await retrieveAndCheckKeys(2); - const mainKey10 = KeyUtil.filterKeysByIdentity(set10.map(ki => ki.prv), [{ family: 'openpgp', id: '392FB1E9FF4184659AB6A246835C0141B9ECF536' }]); + const mainKey10 = KeyUtil.filterKeysByIdentity(set10, [{ family: 'openpgp', id: '392FB1E9FF4184659AB6A246835C0141B9ECF536' }]); expect(mainKey10.length).to.equal(1); - expect(KeyUtil.filterKeysByIdentity(set10.map(ki => ki.prv), [{ family: 'openpgp', id: 'FAFB7D675AC74E87F84D169F00B0115807969D75' }]).length).to.equal(1); + expect(KeyUtil.filterKeysByIdentity(set10, [{ family: 'openpgp', id: 'FAFB7D675AC74E87F84D169F00B0115807969D75' }]).length).to.equal(1); expect(mainKey10[0].lastModified!).to.be.greaterThan(mainKey9[0].lastModified!); // updated this key // 10. Forget the passphrase, EKM returns a third key, we enter a passphrase that doesn't match any of the existing keys, no update await InboxPageRecipe.finishSessionOnInboxPage(gmailPage); await gmailPage.close(); - MOCK_KM_UPDATING_KEY.privateKeys = [{ decryptedPrivateKey: testConstants.unprotectedPrvKey }]; + MOCK_KM_UPDATING_KEY.response = { privateKeys: [{ decryptedPrivateKey: testConstants.unprotectedPrvKey }] }; gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await gmailPage.waitAll('@dialog-passphrase'); { @@ -684,7 +683,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== await ComposePageRecipe.cancelPassphraseDialog(gmailPage, 'keyboard'); await PageRecipe.noToastAppears(gmailPage); const set11 = await retrieveAndCheckKeys(2); - expect(set11.map(entry => entry.prv.id)).to.eql(['392FB1E9FF4184659AB6A246835C0141B9ECF536', 'FAFB7D675AC74E87F84D169F00B0115807969D75']); + expect(set11.map(entry => entry.id)).to.eql(['392FB1E9FF4184659AB6A246835C0141B9ECF536', 'FAFB7D675AC74E87F84D169F00B0115807969D75']); await gmailPage.close(); // 11. EKM returns a new third key, we enter a passphrase matching an existing key, update happens gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); @@ -698,23 +697,47 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== await gmailPage.waitTillGone('@dialog-passphrase'); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Account keys updated'); const set12 = await retrieveAndCheckKeys(3); - expect(set12.map(entry => entry.prv.id)).to.eql([ + expect(set12.map(entry => entry.id)).to.eql([ '392FB1E9FF4184659AB6A246835C0141B9ECF536', 'FAFB7D675AC74E87F84D169F00B0115807969D75', '277D1ADA213881F4ABE0415395E783DC0289E2E2' ]); - // 12. Forget the passphrase, EKM sends a broken key - // todo: we should probably update the valid keys? + // 12. Forget the passphrase, EKM sends a broken key, no passphrase dialog, no updates await InboxPageRecipe.finishSessionOnInboxPage(gmailPage); await gmailPage.close(); - MOCK_KM_UPDATING_KEY.privateKeys = [ - { decryptedPrivateKey: await updateAndArmorKey(set2[0].prv) }, // update the main key + MOCK_KM_UPDATING_KEY.response.privateKeys = [ + { decryptedPrivateKey: await updateAndArmorKey(set2[0]) }, // update the main key // only include a half of another armored key { decryptedPrivateKey: testConstants.unprotectedPrvKey.substring(0, testConstants.unprotectedPrvKey.length / 2) } ]; gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, 'Could not update keys from EKM due to error: BrowserMsg(processKeysFromEkm) sendRawResponse::Error: Some keys could not be parsed'); + await gmailPage.notPresent('@dialog-passphrase'); + const set13 = await retrieveAndCheckKeys(3); + expect(set13.map(entry => entry.id)).to.eql([ + '392FB1E9FF4184659AB6A246835C0141B9ECF536', + 'FAFB7D675AC74E87F84D169F00B0115807969D75', + '277D1ADA213881F4ABE0415395E783DC0289E2E2' + ]); + const mainKey13 = KeyUtil.filterKeysByIdentity(set13, [{ family: 'openpgp', id: '392FB1E9FF4184659AB6A246835C0141B9ECF536' }]); + expect(mainKey13.length).to.equal(1); + expect(mainKey13[0].lastModified).to.equal(mainKey10[0].lastModified); // no update + await gmailPage.close(); + // 13. EKM down, no toast, no passphrase dialog, no updates + MOCK_KM_UPDATING_KEY.badRequestError = 'RequestTimeout'; + gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); + await PageRecipe.noToastAppears(gmailPage); + await gmailPage.notPresent('@dialog-passphrase'); + const set14 = await retrieveAndCheckKeys(3); + expect(set14.map(entry => entry.id)).to.eql([ + '392FB1E9FF4184659AB6A246835C0141B9ECF536', + 'FAFB7D675AC74E87F84D169F00B0115807969D75', + '277D1ADA213881F4ABE0415395E783DC0289E2E2' + ]); + const mainKey14 = KeyUtil.filterKeysByIdentity(set14.map(ki => ki), [{ family: 'openpgp', id: '392FB1E9FF4184659AB6A246835C0141B9ECF536' }]); + expect(mainKey14.length).to.equal(1); + expect(mainKey14[0].lastModified).to.equal(mainKey13[0].lastModified); // no update await gmailPage.close(); })); From b0d83162c4436ea765a221ea661e0d1fffa41ef0 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 11 Jul 2022 12:37:44 +0000 Subject: [PATCH 21/26] niceties --- extension/chrome/settings/setup/setup-key-manager-autogen.ts | 1 - test/source/mock/google/google-endpoints.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index 726209415a7..0edf392c3bf 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -55,7 +55,6 @@ export class SetupWithEmailKeyManagerModule { const { privateKeys } = await this.view.keyManager!.getPrivateKeys(this.view.idToken!); if (privateKeys.length) { // keys already exist on keyserver, auto-import - // todo: do we need to submit on auto-update? try { await processAndStoreKeysFromEkmLocally({ acctEmail: this.view.acctEmail, diff --git a/test/source/mock/google/google-endpoints.ts b/test/source/mock/google/google-endpoints.ts index 7da3c373d8f..881725b0588 100644 --- a/test/source/mock/google/google-endpoints.ts +++ b/test/source/mock/google/google-endpoints.ts @@ -96,7 +96,6 @@ export const mockGoogleEndpoints: HandlersDefinition = { }, '/gmail': async (_parsedReq, req) => { if (isGet(req)) { - await Util.sleep(2); // necessary to activate setup web content script? const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); return GoogleData.getMockGmailPage(acct); } From dbd86b3a85169df870bfc3a7bdf79868a1a60e6a Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 16 Jul 2022 10:54:07 -0400 Subject: [PATCH 22/26] refactoring --- extension/js/background_page/background_page.ts | 2 +- extension/js/common/browser/browser-msg.ts | 11 ++++++----- extension/js/common/helpers.ts | 8 ++++---- .../webmail/setup-webmail-content-script.ts | 6 +++--- test/source/test.ts | 2 +- test/source/tests/setup.ts | 2 +- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/extension/js/background_page/background_page.ts b/extension/js/background_page/background_page.ts index 0f6c2aa7be1..9930d238f6f 100644 --- a/extension/js/background_page/background_page.ts +++ b/extension/js/background_page/background_page.ts @@ -62,7 +62,7 @@ opgp.initWorker({ path: '/lib/openpgp.worker.js' }); BrowserMsg.bgAddListener('storeGlobalSet', (r: Bm.StoreGlobalSet) => GlobalStore.set(r.values)); BrowserMsg.bgAddListener('storeAcctGet', (r: Bm.StoreAcctGet) => AcctStore.get(r.acctEmail, r.keys)); BrowserMsg.bgAddListener('storeAcctSet', (r: Bm.StoreAcctSet) => AcctStore.set(r.acctEmail, r.values)); - BrowserMsg.bgAddListener('processKeysFromEkm', processAndStoreKeysFromEkmLocally); + BrowserMsg.bgAddListener('processAndStoreKeysFromEkmLocally', processAndStoreKeysFromEkmLocally); BrowserMsg.addPgpListeners(); // todo - remove https://github.com/FlowCrypt/flowcrypt-browser/issues/2560 fixed diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index 490e6be2617..5a9a31e202f 100644 --- a/extension/js/common/browser/browser-msg.ts +++ b/extension/js/common/browser/browser-msg.ts @@ -65,7 +65,7 @@ export namespace Bm { export type ShowAttachmentPreview = { iframeUrl: string }; export type ReRenderRecipient = { email: string }; export type SaveFetchedPubkeys = { email: string, pubkeys: string[] }; - export type ProcessKeysFromEkm = { acctEmail: string, decryptedPrivateKeys: string[] }; + export type ProcessAndStoreKeysFromEkmLocally = { acctEmail: string, decryptedPrivateKeys: string[] }; export namespace Res { export type GetActiveTabInfo = { provider: 'gmail' | undefined, acctEmail: string | undefined, sameWorld: boolean | undefined }; @@ -84,14 +84,14 @@ export namespace Bm { export type AjaxGmailAttachmentGetChunk = { chunk: Buf }; export type _tab_ = { tabId: string | null | undefined }; export type SaveFetchedPubkeys = boolean; - export type ProcessKeysFromEkm = { unencryptedKeysToSave: string[], updateCount: number }; + export type ProcessAndStoreKeysFromEkmLocally = { needPassphrase: boolean, updateCount: number }; 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 | SaveFetchedPubkeys | ProcessKeysFromEkm; + | AjaxGmailAttachmentGetChunk | SaveFetchedPubkeys | ProcessAndStoreKeysFromEkmLocally; } export type AnyRequest = PassphraseEntry | OpenPage | OpenGoogleAuthDialog | Redirect | Reload | @@ -100,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 | SaveFetchedPubkeys | ProcessKeysFromEkm; + ShowAttachmentPreview | ReRenderRecipient | SaveFetchedPubkeys | ProcessAndStoreKeysFromEkmLocally; // export type RawResponselessHandler = (req: AnyRequest) => Promise; // export type RawRespoHandler = (req: AnyRequest) => Promise; @@ -148,7 +148,8 @@ export class BrowserMsg { 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, - processKeysFromEkm: (bm: Bm.ProcessKeysFromEkm) => BrowserMsg.sendAwait(undefined, 'processKeysFromEkm', bm, true) as Promise, + processAndStoreKeysFromEkmLocally: + (bm: Bm.ProcessAndStoreKeysFromEkmLocally) => BrowserMsg.sendAwait(undefined, 'processAndStoreKeysFromEkmLocally', bm, true) as Promise, }, }, passphraseEntry: (dest: Bm.Dest, bm: Bm.PassphraseEntry) => BrowserMsg.sendCatch(dest, 'passphrase_entry', bm), diff --git a/extension/js/common/helpers.ts b/extension/js/common/helpers.ts index 2d42d9514ee..53bd562da16 100644 --- a/extension/js/common/helpers.ts +++ b/extension/js/common/helpers.ts @@ -86,12 +86,12 @@ const filterKeysToSave = async (candidateKeys: Key[], existingKeys: KeyInfoWithI export const processAndStoreKeysFromEkmLocally = async ( { acctEmail, decryptedPrivateKeys, options }: { acctEmail: string, decryptedPrivateKeys: string[], options?: PassphraseOptions } -): Promise => { +): Promise => { const { unencryptedPrvs } = await parseAndCheckPrivateKeys(decryptedPrivateKeys); const existingKeys = await KeyStore.get(acctEmail); let passphrase = options?.passphrase; if (passphrase === undefined && !existingKeys.length) { - return { unencryptedKeysToSave: [], updateCount: 0 }; // return success as we can't possibly validate a passphrase + return { needPassphrase: false, updateCount: 0 }; // return success as we can't possibly validate a passphrase // this can only happen on misconfiguration // todo: or should we throw? } @@ -114,8 +114,8 @@ export const processAndStoreKeysFromEkmLocally = async ( if (encryptedKeys.length) { // also updates `name`, todo: refactor? await saveKeysAndPassPhrase(acctEmail, encryptedKeys, options); - return { unencryptedKeysToSave: [], updateCount: encryptedKeys.length }; + return { needPassphrase: false, updateCount: encryptedKeys.length }; } else { - return { unencryptedKeysToSave: unencryptedKeysToSave.map(KeyUtil.armor), updateCount: 0 }; + return { needPassphrase: unencryptedKeysToSave.length > 0, updateCount: 0 }; } }; diff --git a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts index eadfb15d727..2bad36a67b8 100644 --- a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts +++ b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts @@ -244,8 +244,8 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi const processKeysFromEkm = async (acctEmail: string, decryptedPrivateKeys: string[], factory: XssSafeFactory, ppEvent: { entered?: boolean }) => { try { - const { unencryptedKeysToSave, updateCount } = await BrowserMsg.send.bg.await.processKeysFromEkm({ acctEmail, decryptedPrivateKeys }); - if (unencryptedKeysToSave.length) { + const { needPassphrase, updateCount } = await BrowserMsg.send.bg.await.processAndStoreKeysFromEkmLocally({ acctEmail, decryptedPrivateKeys }); + if (needPassphrase) { ppEvent.entered = undefined; // todo: we need to think about possible collision with a pass phrase dialog activated by a compose frame await showPassphraseDialog(factory, { longids: [], type: 'update_key' }); @@ -253,7 +253,7 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi await Ui.time.sleep(100); } if (ppEvent.entered) { - await processKeysFromEkm(acctEmail, unencryptedKeysToSave, factory, ppEvent); + await processKeysFromEkm(acctEmail, decryptedPrivateKeys, factory, ppEvent); } else { return; } diff --git a/test/source/test.ts b/test/source/test.ts index 5b17eee561e..60192d77faf 100644 --- a/test/source/test.ts +++ b/test/source/test.ts @@ -124,7 +124,7 @@ ava.default.after.always('evaluate Catch.reportErr errors', async t => { .filter(e => e.message !== 'Too few bytes to read ASN.1 value.') // below for test "get.updating.key@key-manager-choose-passphrase-forbid-storing.flowcrypt.test - automatic update of key found on key manager" .filter(e => ![ - 'BrowserMsg(processKeysFromEkm) sendRawResponse::Error: Some keys could not be parsed', + 'BrowserMsg(processAndStoreKeysFromEkmLocally) sendRawResponse::Error: Some keys could not be parsed', 'BrowserMsg(ajax) Bad Request: 400 when GET-ing https://localhost:8001/flowcrypt-email-key-manager/v1/keys/private (no body): -> RequestTimeout' ].includes(e.message)) // below for test "user4@standardsubdomainfes.test:8001 - PWD encrypted message with FES web portal - a send fails with gateway update error" diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index 1536aca1c1f..6d143cb3df5 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -712,7 +712,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== ]; gmailPage = await browser.newPage(t, dummyGmailUrl, undefined, extraAuthHeaders); await PageRecipe.waitForToastToAppearAndDisappear(gmailPage, - 'Could not update keys from EKM due to error: BrowserMsg(processKeysFromEkm) sendRawResponse::Error: Some keys could not be parsed'); + 'Could not update keys from EKM due to error: BrowserMsg(processAndStoreKeysFromEkmLocally) sendRawResponse::Error: Some keys could not be parsed'); await gmailPage.notPresent('@dialog-passphrase'); const set13 = await retrieveAndCheckKeys(3); expect(set13.map(entry => entry.id)).to.eql([ From e40a41430a1b8b1a68256183a3bf66a397ae7148 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 16 Jul 2022 12:24:15 -0400 Subject: [PATCH 23/26] updated comments --- extension/js/common/helpers.ts | 5 ++--- .../content_scripts/webmail/setup-webmail-content-script.ts | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/extension/js/common/helpers.ts b/extension/js/common/helpers.ts index 53bd562da16..246d795d20c 100644 --- a/extension/js/common/helpers.ts +++ b/extension/js/common/helpers.ts @@ -29,8 +29,7 @@ export const saveKeysAndPassPhrase = async (acctEmail: string, prvs: Key[], opti const myOwnEmailsAddrs: string[] = [acctEmail].concat(Object.keys(sendAs!)); for (const email of myOwnEmailsAddrs) { if (options !== undefined) { - // first run, update name - // todo: refactor? + // first run, update `name`, todo: refactor in #4545 await ContactStore.update(undefined, email, { name }); } for (const prv of prvs) { @@ -112,7 +111,7 @@ export const processAndStoreKeysFromEkmLocally = async ( } } if (encryptedKeys.length) { - // also updates `name`, todo: refactor? + // also updates `name`, todo: refactor in #4545 await saveKeysAndPassPhrase(acctEmail, encryptedKeys, options); return { needPassphrase: false, updateCount: encryptedKeys.length }; } else { diff --git a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts index 2bad36a67b8..802c5e33207 100644 --- a/extension/js/content_scripts/webmail/setup-webmail-content-script.ts +++ b/extension/js/content_scripts/webmail/setup-webmail-content-script.ts @@ -247,7 +247,6 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi const { needPassphrase, updateCount } = await BrowserMsg.send.bg.await.processAndStoreKeysFromEkmLocally({ acctEmail, decryptedPrivateKeys }); if (needPassphrase) { ppEvent.entered = undefined; - // todo: we need to think about possible collision with a pass phrase dialog activated by a compose frame await showPassphraseDialog(factory, { longids: [], type: 'update_key' }); while (ppEvent.entered === undefined) { await Ui.time.sleep(100); From b110e9f965148dbddd19264a312e10917a6b88dd Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 16 Jul 2022 12:43:59 -0400 Subject: [PATCH 24/26] renamed `options` to `ppOptions` --- .../settings/setup/setup-key-manager-autogen.ts | 2 +- extension/js/common/helpers.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index 0edf392c3bf..16cc72c5857 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -59,7 +59,7 @@ export class SetupWithEmailKeyManagerModule { await processAndStoreKeysFromEkmLocally({ acctEmail: this.view.acctEmail, decryptedPrivateKeys: privateKeys.map(entry => entry.decryptedPrivateKey), - options: setupOptions + ppOptions: setupOptions }); } catch (e) { throw new Error(`Could not store keys from EKM due to error: ${e instanceof Error ? e.message : String(e)}`); diff --git a/extension/js/common/helpers.ts b/extension/js/common/helpers.ts index 246d795d20c..a79b2751a0a 100644 --- a/extension/js/common/helpers.ts +++ b/extension/js/common/helpers.ts @@ -16,19 +16,19 @@ export const isFesUsed = async (acctEmail: string) => { return Boolean(fesUrl); }; -export const saveKeysAndPassPhrase = async (acctEmail: string, prvs: Key[], options?: PassphraseOptions) => { +export const saveKeysAndPassPhrase = async (acctEmail: string, prvs: Key[], ppOptions?: PassphraseOptions) => { const clientConfiguration = await ClientConfiguration.newInstance(acctEmail); for (const prv of prvs) { await KeyStore.add(acctEmail, prv); - if (options !== undefined) { - await PassphraseStore.set((options.passphrase_save && !clientConfiguration.forbidStoringPassPhrase()) ? 'local' : 'session', - acctEmail, { longid: KeyUtil.getPrimaryLongid(prv) }, options.passphrase); + if (ppOptions !== undefined) { + await PassphraseStore.set((ppOptions.passphrase_save && !clientConfiguration.forbidStoringPassPhrase()) ? 'local' : 'session', + acctEmail, { longid: KeyUtil.getPrimaryLongid(prv) }, ppOptions.passphrase); } } const { sendAs, full_name: name } = await AcctStore.get(acctEmail, ['sendAs', 'full_name']); const myOwnEmailsAddrs: string[] = [acctEmail].concat(Object.keys(sendAs!)); for (const email of myOwnEmailsAddrs) { - if (options !== undefined) { + if (ppOptions !== undefined) { // first run, update `name`, todo: refactor in #4545 await ContactStore.update(undefined, email, { name }); } @@ -84,11 +84,11 @@ const filterKeysToSave = async (candidateKeys: Key[], existingKeys: KeyInfoWithI }; export const processAndStoreKeysFromEkmLocally = async ( - { acctEmail, decryptedPrivateKeys, options }: { acctEmail: string, decryptedPrivateKeys: string[], options?: PassphraseOptions } + { acctEmail, decryptedPrivateKeys, ppOptions }: { acctEmail: string, decryptedPrivateKeys: string[], ppOptions?: PassphraseOptions } ): Promise => { const { unencryptedPrvs } = await parseAndCheckPrivateKeys(decryptedPrivateKeys); const existingKeys = await KeyStore.get(acctEmail); - let passphrase = options?.passphrase; + let passphrase = ppOptions?.passphrase; if (passphrase === undefined && !existingKeys.length) { return { needPassphrase: false, updateCount: 0 }; // return success as we can't possibly validate a passphrase // this can only happen on misconfiguration @@ -112,7 +112,7 @@ export const processAndStoreKeysFromEkmLocally = async ( } if (encryptedKeys.length) { // also updates `name`, todo: refactor in #4545 - await saveKeysAndPassPhrase(acctEmail, encryptedKeys, options); + await saveKeysAndPassPhrase(acctEmail, encryptedKeys, ppOptions); return { needPassphrase: false, updateCount: encryptedKeys.length }; } else { return { needPassphrase: unencryptedKeysToSave.length > 0, updateCount: 0 }; From 1acca8fc6419238d54e852d75af0307a64fa2687 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 16 Jul 2022 12:51:58 -0400 Subject: [PATCH 25/26] allow overwriting oldKey with undefined lastModified property with a key with valid lastModified --- extension/js/common/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/js/common/helpers.ts b/extension/js/common/helpers.ts index a79b2751a0a..b67fc651d50 100644 --- a/extension/js/common/helpers.ts +++ b/extension/js/common/helpers.ts @@ -72,7 +72,7 @@ const filterKeysToSave = async (candidateKeys: Key[], existingKeys: KeyInfoWithI const keyToUpdate = existingKeys.filter(ki => ki.longid === longid && ki.family === candidate.family); if (keyToUpdate.length === 1) { const oldKey = await KeyUtil.parse(keyToUpdate[0].private); - if (!oldKey.lastModified || !candidate.lastModified || oldKey.lastModified >= candidate.lastModified) { + if (!candidate.lastModified || (oldKey.lastModified && oldKey.lastModified >= candidate.lastModified)) { continue; } } else if (keyToUpdate.length > 1) { From d1f00af1380fbe30e01e122690ce22848d56a677 Mon Sep 17 00:00:00 2001 From: Tom J Date: Wed, 10 Aug 2022 14:32:45 +0000 Subject: [PATCH 26/26] fix --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b64b74682f..f8ddf9cd166 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6800,9 +6800,9 @@ "dev": true }, "node_modules/moment": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", - "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", "dev": true, "optional": true, "engines": { @@ -15473,9 +15473,9 @@ "dev": true }, "moment": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", - "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", "dev": true, "optional": true },