From e1a5cad28cf3fd646e61b88fd4206dafc016544e Mon Sep 17 00:00:00 2001 From: Tom J Date: Wed, 26 Feb 2020 19:47:24 +0000 Subject: [PATCH 01/35] issue 2590 km integration --- extension/js/common/rules.ts | 88 ++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/extension/js/common/rules.ts b/extension/js/common/rules.ts index 7fd7c8f124d..3b9bc205d7f 100644 --- a/extension/js/common/rules.ts +++ b/extension/js/common/rules.ts @@ -5,15 +5,21 @@ import { Str } from './core/common.js'; import { AcctStore } from './platform/store/acct-store.js'; -type DomainRules$flag = 'NO_PRV_CREATE' | 'NO_PRV_BACKUP' | +type DomainRules$flag = 'NO_PRV_CREATE' | 'NO_PRV_BACKUP' | 'PRV_AUTOIMPORT_OR_AUTOGEN' | 'PASS_PHRASE_QUIET_AUTOGEN' | 'ENFORCE_ATTESTER_SUBMIT' | 'NO_ATTESTER_SUBMIT' | 'DEFAULT_REMEMBER_PASS_PHRASE'; + export type DomainRules = { flags: DomainRules$flag[], custom_keyserver_url?: string, + private_key_manager_url?: string, disallow_attester_search_for_domains?: string[], }; +/** + * Organisational rules, set domain-wide, and delivered from FlowCrypt Backend + * These either enforce, alter or forbid various behavior to fit customer needs + */ export class Rules { private static readonly default = { flags: [] }; @@ -33,31 +39,93 @@ export class Rules { protected constructor(private domainRules: DomainRules) { } - public canCreateKeys = () => { + // optional urls + + /** + * Internal company SKS-like public key server to trust above Attester + */ + public getCustomKeyserver = (): string | undefined => { + return this.domainRules.custom_keyserver_url; + } + + /** + * an internal org FlowCrypt Email Key Manager instance + */ + public getPrivateKeyManagerUrl = (): string | undefined => { + return this.domainRules.private_key_manager_url; + } + + // bools + + /** + * Some orgs expect 100% of their private keys to be imported from elsewhere (and forbid keygen in the extension) + */ + public canCreateKeys = (): boolean => { return !this.domainRules.flags.includes('NO_PRV_CREATE'); } - public canBackupKeys = () => { + /** + * Some orgs want to forbid backing up of public keys (such as inbox or other methods) + */ + public canBackupKeys = (): boolean => { return !this.domainRules.flags.includes('NO_PRV_BACKUP'); } - public mustSubmitToAttester = () => { + /** + * (normally, during setup, if a public key is submitted to Attester and there is + * a conflicting key already submitted, the issue will be skipped) + * Some orgs want to make sure that their public key gets submitted to attester and conflict errors are NOT ignored: + */ + public mustSubmitToAttester = (): boolean => { return this.domainRules.flags.includes('ENFORCE_ATTESTER_SUBMIT'); } - public rememberPassPhraseByDefault = () => { - return this.domainRules.flags.includes('DEFAULT_REMEMBER_PASS_PHRASE'); + /** + * Normally, during setup, "remember pass phrase" is unchecked + * This option will cause "remember pass phrase" option to be checked by default + * This behavior is also enabled as a byproduct of PASS_PHRASE_QUIET_AUTOGEN + */ + public rememberPassPhraseByDefault = (): boolean => { + return this.domainRules.flags.includes('DEFAULT_REMEMBER_PASS_PHRASE') || this.mustAutogenPassPhraseQuietly(); } - public getCustomKeyserver = (): string | undefined => { - return this.domainRules.custom_keyserver_url; + /** + * This is to be used for customers who run their own FlowCrypt Email Key Manager + * If a key can be found on FEKM, it will be auto imported + * If not, it will be autogenerated and stored there + */ + public mustAutoImportOrAutogenPrvWithKeyManager = (): boolean => { + if (!this.domainRules.flags.includes('PRV_AUTOIMPORT_OR_AUTOGEN')) { + return false; + } + if (!this.getPrivateKeyManagerUrl()) { + throw new Error('Wrong org rules config: using PRV_AUTOIMPORT_OR_AUTOGEN without private_key_manager_url'); + } + return true; + } + + /** + * When generating keys, user will not be prompted to choose a pass phrase + * Instead a pass phrase will be automatically generated, and stored locally + * The pass phrase will NOT be displayed to user, and it will never be asked of the user + * This creates the smoothest user experience, for organisations that use full-disk-encryption and don't need pass phrase protection + */ + public mustAutogenPassPhraseQuietly = (): boolean => { + return this.domainRules.flags.includes('PASS_PHRASE_QUIET_AUTOGEN'); } - public canSubmitPubToAttester = () => { + /** + * Some orgs prefer to forbid publishing public keys publicly + */ + public canSubmitPubToAttester = (): boolean => { return !this.domainRules.flags.includes('NO_ATTESTER_SUBMIT'); } - public canLookupThisRecipientOnAttester = (emailAddr: string) => { + /** + * Some orgs have a list of email domains where they do NOT want such emails to be looked up on public sources (such as Attester) + * This is because they already have other means to obtain public keys for these domains, such as from their own internal keyserver + */ + public canLookupThisRecipientOnAttester = (emailAddr: string): boolean => { return !(this.domainRules.disallow_attester_search_for_domains || []).includes(emailAddr.split('@')[1] || 'NONE'); } From b0e639ce2a191888585ebc3e6873401c341d2bce Mon Sep 17 00:00:00 2001 From: Tom J Date: Wed, 26 Feb 2020 20:34:14 +0000 Subject: [PATCH 02/35] prepare test email --- extension/js/common/api/key-manager.ts | 35 ++++++++++++++++++++++++ test/source/mock.ts | 1 + test/source/mock/backend/backend-data.ts | 35 +++++++++++++++++++++--- 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 extension/js/common/api/key-manager.ts diff --git a/extension/js/common/api/key-manager.ts b/extension/js/common/api/key-manager.ts new file mode 100644 index 00000000000..937b4f54fd5 --- /dev/null +++ b/extension/js/common/api/key-manager.ts @@ -0,0 +1,35 @@ +/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ + +// tslint:disable:oneliner-object-literal +// tslint:disable:no-null-keyword + +'use strict'; + +import { Api, ReqMethod } from './api.js'; +import { Dict } from '../core/common.js'; + +type LoadPrvRes = { keys: string[] }; + +export class KeyManager extends Api { + + constructor( + private url: string, + private idToken: string + ) { + super(); + this.url = this.url.replace(/\/$/, ''); // remove trailing space + } + + public getPrivateKeys = async (): Promise => { + return await this.request('GET', '/keys/private') as LoadPrvRes; + } + + public storePrivateKey = async (armoredPrv: string, longid: string): Promise => { + return await this.request('PUT', '/keys/private', { key: armoredPrv, longid }); + } + + private request = async (method: ReqMethod, path: string, vals?: Dict): Promise => { + return await Api.apiCall(this.url, path, vals, 'JSON', undefined, { Authorization: `Bearer ${this.idToken}` }, undefined, method); + } + +} diff --git a/test/source/mock.ts b/test/source/mock.ts index 634fbde9485..f4a26b4d343 100644 --- a/test/source/mock.ts +++ b/test/source/mock.ts @@ -14,6 +14,7 @@ export const acctsWithoutMockData = [ 'no.pub@org-rules-test.flowcrypt.com', 'user@no-submit-org-rule.flowcrypt.com', 'user@no-search-domains-org-rule.flowcrypt.com', + "user@key-manager-autogen.flowcrypt.com", ]; export const mock = async (logger: (line: string) => void) => { diff --git a/test/source/mock/backend/backend-data.ts b/test/source/mock/backend/backend-data.ts index f09de9adca7..b2484fecc6f 100644 --- a/test/source/mock/backend/backend-data.ts +++ b/test/source/mock/backend/backend-data.ts @@ -51,15 +51,42 @@ export class BackendData { public getOrgRules = (acct: string) => { const domain = acct.split('@')[1]; if (domain === 'org-rules-test.flowcrypt.com') { - return { "flags": ["NO_PRV_CREATE", "NO_PRV_BACKUP", "ENFORCE_ATTESTER_SUBMIT"] }; + return { + "flags": [ + "NO_PRV_CREATE", + "NO_PRV_BACKUP", + "ENFORCE_ATTESTER_SUBMIT" + ] + }; } if (domain === 'no-submit-org-rule.flowcrypt.com') { - return { "flags": ["NO_ATTESTER_SUBMIT"] }; + return { + "flags": [ + "NO_ATTESTER_SUBMIT" + ] + }; } if (domain === 'no-search-domains-org-rule.flowcrypt.com') { - return { "flags": [], "disallow_attester_search_for_domains": ["flowcrypt.com"] }; + return { + "flags": [], + "disallow_attester_search_for_domains": ["flowcrypt.com"] + }; } - return { 'flags': [] }; + if (domain === 'key-manager-autogen.flowcrypt.com') { + return { + "flags": [ + "NO_PRV_BACKUP", + "ENFORCE_ATTESTER_SUBMIT", + "PRV_AUTOIMPORT_OR_AUTOGEN", + "PASS_PHRASE_QUIET_AUTOGEN", + "DEFAULT_REMEMBER_PASS_PHRASE" + ], + "private_key_manager_url": "http://localhost:8001/flowcrypt-email-key-manager" + }; + } + return { + "flags": [] + }; } } From 19bb85f83c7116afb497af253305a5533f9a6b4a Mon Sep 17 00:00:00 2001 From: Tom J Date: Wed, 26 Feb 2020 21:22:20 +0000 Subject: [PATCH 03/35] prepare for tests --- extension/js/common/api/key-manager.ts | 4 +- extension/js/common/rules.ts | 11 +++++ test/source/ci-initialize.ts | 4 +- test/source/mock.ts | 4 +- test/source/mock/all-apis-mock.ts | 2 + test/source/mock/backend/backend-data.ts | 3 +- .../mock/key-manager/key-manager-endpoints.ts | 44 +++++++++++++++++++ .../tests/page-recipe/oauth-page-recipe.ts | 13 ++---- test/source/util/index.ts | 21 ++++++--- 9 files changed, 85 insertions(+), 21 deletions(-) create mode 100644 test/source/mock/key-manager/key-manager-endpoints.ts diff --git a/extension/js/common/api/key-manager.ts b/extension/js/common/api/key-manager.ts index 937b4f54fd5..8b08c735770 100644 --- a/extension/js/common/api/key-manager.ts +++ b/extension/js/common/api/key-manager.ts @@ -24,8 +24,8 @@ export class KeyManager extends Api { return await this.request('GET', '/keys/private') as LoadPrvRes; } - public storePrivateKey = async (armoredPrv: string, longid: string): Promise => { - return await this.request('PUT', '/keys/private', { key: armoredPrv, longid }); + public storePrivateKey = async (decryptedKey: string, longid: string): Promise => { + return await this.request('PUT', '/keys/private', { decryptedKey, longid }); } private request = async (method: ReqMethod, path: string, vals?: Dict): Promise => { diff --git a/extension/js/common/rules.ts b/extension/js/common/rules.ts index 3b9bc205d7f..e93b7eafdac 100644 --- a/extension/js/common/rules.ts +++ b/extension/js/common/rules.ts @@ -4,6 +4,7 @@ import { Str } from './core/common.js'; import { AcctStore } from './platform/store/acct-store.js'; +import { KeyAlgo } from './core/pgp-key.js'; type DomainRules$flag = 'NO_PRV_CREATE' | 'NO_PRV_BACKUP' | 'PRV_AUTOIMPORT_OR_AUTOGEN' | 'PASS_PHRASE_QUIET_AUTOGEN' | 'ENFORCE_ATTESTER_SUBMIT' | 'NO_ATTESTER_SUBMIT' | @@ -14,6 +15,7 @@ export type DomainRules = { custom_keyserver_url?: string, private_key_manager_url?: string, disallow_attester_search_for_domains?: string[], + enforce_keygen_algo?: string, }; /** @@ -55,6 +57,15 @@ export class Rules { return this.domainRules.private_key_manager_url; } + // optional vars + + /** + * Enforce a key algo for keygen, eg rsa2048,rsa4096,ecc25519 + */ + public getEnforcedKeygenAlgo = (): KeyAlgo | undefined => { + return this.domainRules.enforce_keygen_algo as KeyAlgo | undefined; + } + // bools /** diff --git a/test/source/ci-initialize.ts b/test/source/ci-initialize.ts index dea89d4a67b..d13d1f3c1b3 100644 --- a/test/source/ci-initialize.ts +++ b/test/source/ci-initialize.ts @@ -4,12 +4,12 @@ import { Config } from './util'; import { FlowCryptApi } from './tests/api'; (async () => { // disabled in ci settings - for (const { email, password, backup } of Config.secrets.auth.google) { + for (const { email, password } of Config.secrets.auth.google) { if (email && password) { const e = email.replace(/gmail|flowcrypt|test|com|@|\.|org/g, ''); try { console.info(`[${e}] Initializing CI`); - await FlowCryptApi.ciInitialize(email, password, backup || 'none'); + await FlowCryptApi.ciInitialize(email, password, 'none'); } catch (e) { // do not fail whole process - the rest of tests may work without this console.error(`[${e}] ${String(e)}`); } diff --git a/test/source/mock.ts b/test/source/mock.ts index f4a26b4d343..cc1ef72f282 100644 --- a/test/source/mock.ts +++ b/test/source/mock.ts @@ -3,7 +3,6 @@ import * as request from 'fc-node-requests'; import { existsSync, writeFileSync } from 'fs'; - import { Config } from './util'; import { opgp } from './core/pgp'; import { startAllApisMock } from './mock/all-apis-mock'; @@ -14,7 +13,8 @@ export const acctsWithoutMockData = [ 'no.pub@org-rules-test.flowcrypt.com', 'user@no-submit-org-rule.flowcrypt.com', 'user@no-search-domains-org-rule.flowcrypt.com', - "user@key-manager-autogen.flowcrypt.com", + "get.key@key-manager-autogen.flowcrypt.com", + "put.key@key-manager-autogen.flowcrypt.com", ]; export const mock = async (logger: (line: string) => void) => { diff --git a/test/source/mock/all-apis-mock.ts b/test/source/mock/all-apis-mock.ts index 38bf7b9ba1e..78324456c7e 100644 --- a/test/source/mock/all-apis-mock.ts +++ b/test/source/mock/all-apis-mock.ts @@ -8,6 +8,7 @@ import { Api, Handlers } from './lib/api'; import { mockBackendEndpoints } from './backend/backend-endpoints'; import { mockGoogleEndpoints } from './google/google-endpoints'; +import { mockKeyManagerEndpoints } from './key-manager/key-manager-endpoints'; export type HandlersDefinition = Handlers<{ query: { [k: string]: string; }; body?: unknown; }, unknown>; @@ -23,6 +24,7 @@ export const startAllApisMock = async (logger: (line: string) => void) => { const api = new LoggedApi<{ query: { [k: string]: string }, body?: unknown }, unknown>('google-mock', { ...mockGoogleEndpoints, ...mockBackendEndpoints, + ...mockKeyManagerEndpoints, '/favicon.ico': async () => '', }); await api.listen(8001); diff --git a/test/source/mock/backend/backend-data.ts b/test/source/mock/backend/backend-data.ts index b2484fecc6f..314dad002ac 100644 --- a/test/source/mock/backend/backend-data.ts +++ b/test/source/mock/backend/backend-data.ts @@ -81,7 +81,8 @@ export class BackendData { "PASS_PHRASE_QUIET_AUTOGEN", "DEFAULT_REMEMBER_PASS_PHRASE" ], - "private_key_manager_url": "http://localhost:8001/flowcrypt-email-key-manager" + "private_key_manager_url": "http://localhost:8001/flowcrypt-email-key-manager", + "enforce_keygen_algo": "rsa2048", }; } return { diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts new file mode 100644 index 00000000000..00219c81fc4 --- /dev/null +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -0,0 +1,44 @@ +/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ + +import { HttpClientErr } from '../lib/api'; +import { HandlersDefinition } from '../all-apis-mock'; +import { isPut, isGet } from '../lib/mock-util'; +import { oauth } from '../lib/oauth'; +import { PgpKey } from '../../core/pgp-key'; +import { Dict } from '../../core/common'; +import { expect } from 'chai'; + +// tslint:disable:max-line-length +/* eslint-disable max-len */ + +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'; + +export const mockKeyManagerEndpoints: HandlersDefinition = { + '/keys/private': async ({ body }, req) => { + const acctEmail = oauth.checkAuthorizationHeader(req.headers.authorization); + if (isGet(req)) { + if (acctEmail === 'get.key@key-manager-autogen.flowcrypt.com') { + return { keys: [existingPrv] }; + } + if (acctEmail === 'put.key@key-manager-autogen.flowcrypt.com') { + return { keys: [] }; + } + throw new HttpClientErr(`Unexpectedly calling mockKeyManagerEndpoints:/keys/private GET with acct ${acctEmail}`); + } + if (isPut(req)) { + const { decryptedKey, longid } = body as Dict; + if (acctEmail === 'put.key@key-manager-autogen.flowcrypt.com') { + const details = await PgpKey.parseDetails(decryptedKey); + expect(details.keys).to.have.length(1); + expect(details.keys[0].algo.bits).to.equal(2048); + expect(details.keys[0].ids[0].longid).to.equal(longid); + expect(details.keys[0].users).to.have.length(1); + expect(details.keys[0].users[0]).to.equal('put.key@key-manager-autogen.flowcrypt.com'); + expect(details.keys[0].isFullyDecrypted).to.be.true; + return {}; + } + throw new HttpClientErr(`Unexpectedly calling mockKeyManagerEndpoints:/keys/private PUT with acct ${acctEmail}`); + } + throw new HttpClientErr(`Unknown method: ${req.method}`); + }, +}; diff --git a/test/source/tests/page-recipe/oauth-page-recipe.ts b/test/source/tests/page-recipe/oauth-page-recipe.ts index cc7fcaaecd3..66df1243197 100644 --- a/test/source/tests/page-recipe/oauth-page-recipe.ts +++ b/test/source/tests/page-recipe/oauth-page-recipe.ts @@ -17,7 +17,6 @@ export class OauthPageRecipe extends PageRecipe { const isMock = oauthPage.target.url().includes('localhost'); const auth = Config.secrets.auth.google.find(a => a.email === acctEmail)!; const selectors = { - backup_email_verification_choice: "//div[@class='vdE7Oc' and text() = 'Confirm your recovery email']", approve_button: '#submit_approve_access', pwd_input: 'input[type="password"]', // pwd_input: '.zHQkBf', pwd_confirm_btn: '.CwaK9', @@ -25,7 +24,7 @@ export class OauthPageRecipe extends PageRecipe { }; const enterPwdAndConfirm = async () => { await Util.sleep(isMock ? 0 : OauthPageRecipe.oauthPwdDelay); - await oauthPage.waitAndType(selectors.pwd_input, auth.password, { delay: isMock ? 0 : OauthPageRecipe.oauthPwdDelay }); + await oauthPage.waitAndType(selectors.pwd_input, auth.password!, { delay: isMock ? 0 : OauthPageRecipe.oauthPwdDelay }); await oauthPage.waitAndClick(selectors.pwd_confirm_btn, { delay: isMock ? 0 : 1 }); // confirm password await oauthPage.waitForNavigationIfAny(); }; @@ -37,7 +36,7 @@ export class OauthPageRecipe extends PageRecipe { await oauthPage.waitAndClick('#next'); await oauthPage.waitForNavigationIfAny(); await Util.sleep(isMock ? 0 : OauthPageRecipe.oauthPwdDelay); - await oauthPage.waitAndType('#Passwd', auth.password, { delay: isMock ? 0 : OauthPageRecipe.oauthPwdDelay }); + await oauthPage.waitAndType('#Passwd', auth.password!, { delay: isMock ? 0 : OauthPageRecipe.oauthPwdDelay }); await oauthPage.waitForNavigationIfAny(); await oauthPage.waitAndClick('#signIn', { delay: isMock ? 0 : 1 }); await oauthPage.waitForNavigationIfAny(); @@ -69,13 +68,9 @@ export class OauthPageRecipe extends PageRecipe { } throw new Error('Oauth page didnt close after login. Should increase timeout or await close event'); } - const element = await oauthPage.waitAny([selectors.approve_button, selectors.backup_email_verification_choice, selectors.pwd_input, selectors.secret_2fa]); + await oauthPage.waitAny([selectors.approve_button, selectors.pwd_input, selectors.secret_2fa]); await Util.sleep(isMock ? 0 : 1); - if (await oauthPage.isElementPresent(selectors.backup_email_verification_choice)) { // asks for registered backup email - await element.click(); - await oauthPage.waitAndType('#knowledge-preregistered-email-response', auth.backup, { delay: isMock ? 0 : 2 }); - await oauthPage.waitAndClick('#next', { delay: isMock ? 0 : 2 }); - } else if (await oauthPage.isElementPresent(selectors.pwd_input)) { + if (await oauthPage.isElementPresent(selectors.pwd_input)) { await enterPwdAndConfirm(); // unsure why it requires a password second time, but sometimes happens } else if (await oauthPage.isElementPresent(selectors.secret_2fa)) { if (!auth.secret_2fa) { diff --git a/test/source/util/index.ts b/test/source/util/index.ts index 7a11ec3a63e..47abf3cf7c7 100644 --- a/test/source/util/index.ts +++ b/test/source/util/index.ts @@ -36,16 +36,12 @@ export type TestMessage = { signature?: string[], }; -interface TestConfigInterface { - messages: TestMessage[]; -} - interface TestSecretsInterface { ci_admin_token: string; ci_dev_account: string; data_encryption_password: string; proxy?: { enabled: boolean, server: string, auth: { username: string, password: string } }; - auth: { google: { email: string, password: string, backup: string, secret_2fa: string | undefined }[], }; + auth: { google: { email: string, password?: string, secret_2fa?: string }[], }; keys: { title: string, passphrase: string, armored: string | null, longid: string | null }[]; keyInfo: Array<{ email: string, key: KeyInfo[] }>; } @@ -62,6 +58,21 @@ export class Config { } +Config.secrets.auth.google.push( // these don't contain any secrets, so not worth syncing through secrets file + { "email": "flowcrypt.test.key.used.pgp@gmail.com" }, + { "email": "flowcrypt.test.key.imported@gmail.com" }, + { "email": "flowcrypt.test.key.import.naked@gmail.com" }, + { "email": "flowcrypt.test.key.recovered@gmail.com" }, + { "email": "flowcrypt.test.key.new.manual@gmail.com" }, + { "email": "flowcrypt.test.key.multibackup@gmail.com" }, + { "email": "has.pub@org-rules-test.flowcrypt.com" }, + { "email": "no.pub@org-rules-test.flowcrypt.com" }, + { "email": "user@no-submit-org-rule.flowcrypt.com" }, + { "email": "user@no-search-domains-org-rule.flowcrypt.com" }, + { "email": "get.key@key-manager-autogen.flowcrypt.com" }, + { "email": "put.key@key-manager-autogen.flowcrypt.com" }, +); + export class Util { public static sleep = async (seconds: number) => { From 54615588201dc9b892956f021bb283646c6b2cee Mon Sep 17 00:00:00 2001 From: Tom J Date: Wed, 26 Feb 2020 21:39:27 +0000 Subject: [PATCH 04/35] fix code style --- test/source/mock/key-manager/key-manager-endpoints.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index 00219c81fc4..d0ca9798ec9 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -34,7 +34,7 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { expect(details.keys[0].ids[0].longid).to.equal(longid); expect(details.keys[0].users).to.have.length(1); expect(details.keys[0].users[0]).to.equal('put.key@key-manager-autogen.flowcrypt.com'); - expect(details.keys[0].isFullyDecrypted).to.be.true; + expect(details.keys[0].isFullyDecrypted).to.be.true('key must be decrypted'); return {}; } throw new HttpClientErr(`Unexpectedly calling mockKeyManagerEndpoints:/keys/private PUT with acct ${acctEmail}`); From 2eb60abd9380115a288ce9e22d705131ba18cddf Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 07:44:40 +0000 Subject: [PATCH 05/35] code style --- test/source/mock/backend/backend-data.ts | 1 + test/source/tests/tests/setup.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/test/source/mock/backend/backend-data.ts b/test/source/mock/backend/backend-data.ts index 314dad002ac..c99e6882c12 100644 --- a/test/source/mock/backend/backend-data.ts +++ b/test/source/mock/backend/backend-data.ts @@ -5,6 +5,7 @@ import { HttpAuthErr } from '../lib/api'; import { OauthMock } from '../lib/oauth'; // tslint:disable:no-null-keyword +// tslint:disable:oneliner-object-literal export class BackendData { public reportedErrors: { name: string, message: string, url: string, line: number, col: number, trace: string, version: string, environmane: string }[] = []; diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index e8f40198770..e7a7b4e293c 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -10,6 +10,7 @@ import { TestWithBrowser } from '../../test'; import { expect } from 'chai'; import { SettingsPageRecipe } from '../page-recipe/settings-page-recipe'; import { ComposePageRecipe } from '../page-recipe/compose-page-recipe'; +import { TestUrls } from '../../browser/test-urls'; // tslint:disable:no-blank-lines-func @@ -171,6 +172,18 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test await composePage.waitAll('@input-password'); })); + ava.default.skip('get.key@key-manager-autogen.flowcrypt.com - automatic setup with key found on key manager', testWithBrowser(undefined, async (t, browser) => { + const acct = 'get.key@key-manager-autogen.flowcrypt.com'; + const setupPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); + await setupPage.target.waitForNavigation(); + expect(setupPage.target.url()).contain('https://mail.google.com'); // auto-redirect to gmail + await setupPage.close(); + const settingsPage = await browser.newPage(t, TestUrls.extensionSettings('flowcrypt.compatibility@gmail.com')); + await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); + // todo - check imported key + // todo - check that it does not offer to forget pass phrase + })); + } }; From 47b701b780a54ffe17bab956cf38a37b6c490dfc Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 07:55:11 +0000 Subject: [PATCH 06/35] added test for get.key@key-manager-autogen (will fail) --- test/source/tests/tests/setup.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index e7a7b4e293c..2b5cd8f6a92 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -11,6 +11,7 @@ import { expect } from 'chai'; import { SettingsPageRecipe } from '../page-recipe/settings-page-recipe'; import { ComposePageRecipe } from '../page-recipe/compose-page-recipe'; import { TestUrls } from '../../browser/test-urls'; +import { Str } from '../../core/common'; // tslint:disable:no-blank-lines-func @@ -172,7 +173,7 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test await composePage.waitAll('@input-password'); })); - ava.default.skip('get.key@key-manager-autogen.flowcrypt.com - automatic setup with key found on key manager', testWithBrowser(undefined, async (t, browser) => { + ava.default('get.key@key-manager-autogen.flowcrypt.com - automatic setup with key found on key manager', testWithBrowser(undefined, async (t, browser) => { const acct = 'get.key@key-manager-autogen.flowcrypt.com'; const setupPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); await setupPage.target.waitForNavigation(); @@ -180,8 +181,16 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test await setupPage.close(); const settingsPage = await browser.newPage(t, TestUrls.extensionSettings('flowcrypt.compatibility@gmail.com')); await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); - // todo - check imported key - // todo - check that it does not offer to forget pass phrase + // check imported key + const myKeyFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, `@action-show-key-1`, ['my_key.htm', 'placement=settings']); + await Util.sleep(1); + await myKeyFrame.waitAll('@content-longid'); + expect(await myKeyFrame.read('@content-longid')).to.equal(Str.spaced('unknown')); + await SettingsPageRecipe.closeDialog(settingsPage); + // todo - check that it does not offer any pass phrase options + const securityFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, '@action-open-security-page', ['security.htm', 'placement=settings']); + await Util.sleep(1); + await securityFrame.notPresent(['@action-change-passphrase-begin', '@action-test-passphrase-begin', '@action-forget-pp']); })); } From 02611db48997ec33cdbf97fab81c3f123203a953 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 10:20:44 +0000 Subject: [PATCH 07/35] basic implementation / not done --- .../chrome/settings/modules/debug_api.ts | 2 +- .../chrome/settings/modules/experimental.ts | 2 +- extension/chrome/settings/setup.ts | 19 +++--- .../chrome/settings/setup/setup-create-key.ts | 2 - .../chrome/settings/setup/setup-import-key.ts | 1 - .../setup/setup-key-manager-autogen.ts | 61 +++++++++++++++++++ .../settings/setup/setup-recover-key.ts | 1 - .../chrome/settings/setup/setup-render.ts | 6 +- extension/js/common/api/key-manager.ts | 4 +- .../js/common/platform/store/acct-store.ts | 3 +- test/source/tests/tests/setup.ts | 10 +-- 11 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 extension/chrome/settings/setup/setup-key-manager-autogen.ts diff --git a/extension/chrome/settings/modules/debug_api.ts b/extension/chrome/settings/modules/debug_api.ts index 3d0c5bbb2b2..23e8307f580 100644 --- a/extension/chrome/settings/modules/debug_api.ts +++ b/extension/chrome/settings/modules/debug_api.ts @@ -39,7 +39,7 @@ View.run(class DebugApiView extends View { } else if (this.which === 'local_store') { const storage = await AcctStore.get(this.acctEmail, [ 'notification_setup_needed_dismissed', 'email_provider', 'google_token_scopes', 'hide_message_password', 'sendAs', 'outgoing_language', - 'full_name', 'cryptup_enabled', 'setup_done', 'is_newly_created_key', + 'full_name', 'cryptup_enabled', 'setup_done', 'successfully_received_at_leat_one_message', 'notification_setup_done_seen', 'openid', 'rules', 'subscription', 'use_rich_text', ]); diff --git a/extension/chrome/settings/modules/experimental.ts b/extension/chrome/settings/modules/experimental.ts index eb2b23e8124..ec9681b257b 100644 --- a/extension/chrome/settings/modules/experimental.ts +++ b/extension/chrome/settings/modules/experimental.ts @@ -129,7 +129,7 @@ View.run(class ExperimentalView extends View { 'acctEmail: ' + this.acctEmail, ]; const globalStorage = await GlobalStore.get(['version']); - const acctStorage = await AcctStore.get(this.acctEmail, ['is_newly_created_key', 'setup_date', 'full_name']); + const acctStorage = await AcctStore.get(this.acctEmail, ['setup_date', 'full_name']); text.push('global_storage: ' + JSON.stringify(globalStorage)); text.push('account_storage: ' + JSON.stringify(acctStorage)); text.push(''); diff --git a/extension/chrome/settings/setup.ts b/extension/chrome/settings/setup.ts index 75447bee9f3..babc4c02bcd 100644 --- a/extension/chrome/settings/setup.ts +++ b/extension/chrome/settings/setup.ts @@ -28,6 +28,8 @@ import { Scopes, AcctStoreDict, AcctStore } from '../../js/common/platform/store import { KeyStore } from '../../js/common/platform/store/key-store.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-manager.js'; +import { SetupKeyManagerAutogenModule } from './setup/setup-key-manager-autogen.js'; export interface SetupOptions { passphrase: string; @@ -35,7 +37,6 @@ export interface SetupOptions { submit_main: boolean; submit_all: boolean; recovered?: boolean; - is_newly_created_key: boolean; } export class SetupView extends View { @@ -43,6 +44,7 @@ export class SetupView extends View { public readonly acctEmail: string; public readonly parentTabId: string | undefined; public readonly action: 'add_key' | 'finalize' | undefined; + public readonly idToken: string; public readonly keyImportUi = new KeyImportUi({ checkEncryption: true }); public readonly gmail: Gmail; @@ -50,12 +52,14 @@ export class SetupView extends View { public readonly setupCreateKey: SetupCreateKeyModule; public readonly setupImportKey: SetupImportKeyModule; public readonly setupRender: SetupRenderModule; + public readonly setupKeyManagerAutogen: SetupKeyManagerAutogenModule; public tabId!: string; public scopes!: Scopes; public storage!: AcctStoreDict; public rules!: Rules; public keyserver!: Keyserver; + public keyManager: KeyManager | undefined; // not set if no url in org rules public acctEmailAttesterLongid: string | undefined; public fetchedKeyBackups: KeyInfo[] = []; @@ -66,8 +70,9 @@ export class SetupView extends View { constructor() { super(); - const uncheckedUrlParams = Url.parse(['acctEmail', 'action', 'parentTabId']); + const uncheckedUrlParams = Url.parse(['acctEmail', 'action', 'idToken', 'parentTabId']); this.acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail'); + this.idToken = Assert.urlParamRequire.string(uncheckedUrlParams, 'idToken'); this.action = Assert.urlParamRequire.oneof(uncheckedUrlParams, 'action', ['add_key', 'finalize', undefined]) as 'add_key' | 'finalize' | undefined; if (this.action === 'add_key') { this.parentTabId = Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId'); @@ -87,6 +92,7 @@ export class SetupView extends View { this.setupCreateKey = new SetupCreateKeyModule(this); this.setupImportKey = new SetupImportKeyModule(this); this.setupRender = new SetupRenderModule(this); + this.setupKeyManagerAutogen = new SetupKeyManagerAutogenModule(this); } public render = async () => { @@ -97,6 +103,9 @@ export class SetupView extends View { this.storage.email_provider = this.storage.email_provider || 'gmail'; this.rules = await Rules.newInstance(this.acctEmail); this.keyserver = new Keyserver(this.rules); + if (this.rules.getPrivateKeyManagerUrl()) { + this.keyManager = new KeyManager(this.rules.getPrivateKeyManagerUrl()!, this.idToken); + } if (!this.rules.canCreateKeys()) { const forbidden = `${Lang.setup.creatingKeysNotAllowedPleaseImport} Back`; Xss.sanitizeRender('#step_2a_manual_create, #step_2_easy_generating', `
${forbidden}
`); @@ -164,11 +173,7 @@ export class SetupView extends View { } public preFinalizeSetup = async (options: SetupOptions): Promise => { - await AcctStore.set(this.acctEmail, { - tmp_submit_main: options.submit_main, - tmp_submit_all: options.submit_all, - is_newly_created_key: options.is_newly_created_key, - }); + await AcctStore.set(this.acctEmail, { tmp_submit_main: options.submit_main, tmp_submit_all: options.submit_all }); } public finalizeSetup = async ({ submit_main, submit_all }: { submit_main: boolean, submit_all: boolean }): Promise => { diff --git a/extension/chrome/settings/setup/setup-create-key.ts b/extension/chrome/settings/setup/setup-create-key.ts index 4d55835a764..1083cfeaa6d 100644 --- a/extension/chrome/settings/setup/setup-create-key.ts +++ b/extension/chrome/settings/setup/setup-create-key.ts @@ -33,7 +33,6 @@ export class SetupCreateKeyModule { submit_main: this.view.shouldSubmitPubkey('#step_2a_manual_create .input_submit_key'), submit_all: this.view.shouldSubmitPubkey('#step_2a_manual_create .input_submit_all'), recovered: false, - is_newly_created_key: true, }; const keyAlgo = $('#step_2a_manual_create .key_type').val() as KeyAlgo; const action = $('#step_2a_manual_create .input_backup_inbox').prop('checked') ? 'setup_automatic' : 'setup_manual'; @@ -67,7 +66,6 @@ export class SetupCreateKeyModule { const { full_name } = await AcctStore.get(this.view.acctEmail, ['full_name']); try { const key = await PgpKey.create([{ name: full_name || '', email: this.view.acctEmail }], keyAlgo, options.passphrase); // todo - add all addresses? - options.is_newly_created_key = true; const prv = await PgpKey.read(key.private); await this.view.saveKeys([prv], options); } catch (e) { diff --git a/extension/chrome/settings/setup/setup-import-key.ts b/extension/chrome/settings/setup/setup-import-key.ts index 3dafb674ea6..0ed04b18128 100644 --- a/extension/chrome/settings/setup/setup-import-key.ts +++ b/extension/chrome/settings/setup/setup-import-key.ts @@ -25,7 +25,6 @@ export class SetupImportKeyModule { submit_main: this.view.shouldSubmitPubkey('#step_2b_manual_enter .input_submit_key'), submit_all: this.view.shouldSubmitPubkey('#step_2b_manual_enter .input_submit_all'), passphrase_save: Boolean($('#step_2b_manual_enter .input_passphrase_save').prop('checked')), - is_newly_created_key: false, recovered: false, }; try { diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts new file mode 100644 index 00000000000..24c728b693e --- /dev/null +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -0,0 +1,61 @@ +/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ + +'use strict'; + +import { SetupOptions, SetupView } from '../setup.js'; + +import { PgpKey } from '../../../js/common/core/pgp-key.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 { PgpPwd } from '../../../js/common/core/pgp-password.js'; + +export class SetupKeyManagerAutogenModule { + + constructor(private view: SetupView) { + } + + public getKeyFromKeyManagerOrAutogenAndStoreItThenRenderSetupDone = async () => { + if (!this.view.rules.mustAutogenPassPhraseQuietly()) { + const notSupportedErr = 'Combination of org rules not yet supported: PRV_AUTOIMPORT_OR_AUTOGEN cannot yet be used without PASS_PHRASE_QUIET_AUTOGEN.'; + await Ui.modal.error(`${notSupportedErr}\n\nPlease write human@flowcrypt.com to add support.`); + window.location.href = Url.create('index.htm', { acctEmail: this.view.acctEmail }); + return; + } + const keygenAlgo = this.view.rules.getEnforcedKeygenAlgo(); + if (!keygenAlgo) { + const notSupportedErr = 'Combination of org rules not yet supported: PRV_AUTOIMPORT_OR_AUTOGEN cannot yet be used without enforce_keygen_algo.'; + await Ui.modal.error(`${notSupportedErr}\n\nPlease write human@flowcrypt.com to add support.`); + window.location.href = Url.create('index.htm', { acctEmail: this.view.acctEmail }); + return; + } + const passphrase = PgpPwd.random(); // mustAutogenPassPhraseQuietly + const opts: SetupOptions = { passphrase_save: true, submit_main: true, submit_all: true, passphrase }; + const { keys } = await this.view.keyManager!.getPrivateKeys(); + if (keys.length) { // keys already exist on keyserver, auto-import + const { keys: prvs } = await PgpKey.readMany(Buf.fromUtfStr(keys.join('\n'))); + for (const prv of prvs) { + if (!prv.isPrivate()) { + throw new Error(`Key ${await PgpKey.longid(prv)} for user ${this.view.acctEmail} is not a private key`); + } + if (!prv.isFullyDecrypted()) { + throw new Error(`Key ${await PgpKey.longid(prv)} for user ${this.view.acctEmail} from FlowCrypt Email Key Manager is not fully decrypted`); + } + await PgpKey.encrypt(prv, passphrase); + } + await this.view.saveKeys(prvs, { passphrase_save: true, submit_all: true, submit_main: true, passphrase }); + } else { // generate keys and store them on key manager + const { full_name } = await AcctStore.get(this.view.acctEmail, ['full_name']); + const generated = await PgpKey.create([{ name: full_name || '', email: this.view.acctEmail }], keygenAlgo, passphrase); + const decryptablePrv = await PgpKey.read(generated.private); + const generatedKeyLongid = await PgpKey.longid(decryptablePrv); + await PgpKey.decrypt(decryptablePrv, passphrase); + await this.view.keyManager!.storePrivateKey(decryptablePrv.armor(), decryptablePrv.toPublic().armor(), generatedKeyLongid!); // store decrypted key on KM + await this.view.saveKeys([await PgpKey.read(generated.private)], opts); // store encrypted key + pass phrase locally + } + await this.view.finalizeSetup(opts); + await this.view.setupRender.renderSetupDone(); + } + +} diff --git a/extension/chrome/settings/setup/setup-recover-key.ts b/extension/chrome/settings/setup/setup-recover-key.ts index b05b6c03e71..a33338bc0f6 100644 --- a/extension/chrome/settings/setup/setup-recover-key.ts +++ b/extension/chrome/settings/setup/setup-recover-key.ts @@ -63,7 +63,6 @@ export class SetupRecoverKeyModule { passphrase, passphrase_save: true, // todo - reevaluate saving passphrase when recovering recovered: true, - is_newly_created_key: false, }; await this.view.saveKeys(newlyMatchingKeys, options); const { setup_done } = await AcctStore.get(this.view.acctEmail, ['setup_done']); diff --git a/extension/chrome/settings/setup/setup-render.ts b/extension/chrome/settings/setup/setup-render.ts index a75b1b22243..122f7db2e40 100644 --- a/extension/chrome/settings/setup/setup-render.ts +++ b/extension/chrome/settings/setup/setup-render.ts @@ -19,7 +19,7 @@ export class SetupRenderModule { } public renderInitial = async (): Promise => { - $('h1').text('Set Up FlowCrypt'); + $('h1').text(this.view.rules.mustAutoImportOrAutogenPrvWithKeyManager() ? 'Setting up FlowCrypt, please wait...' : 'Set Up FlowCrypt'); $('.email-address').text(this.view.acctEmail); $('.back').css('visibility', 'hidden'); if (this.view.storage!.email_provider === 'gmail') { // show alternative account addresses in setup form + save them for later @@ -35,6 +35,8 @@ export class SetupRenderModule { if (this.view.storage!.setup_done) { if (this.view.action !== 'add_key') { await this.renderSetupDone(); + } else if (this.view.rules.mustAutoImportOrAutogenPrvWithKeyManager()) { + // todo - handle autogen case } else { await this.view.setupRecoverKey.renderAddKeyFromBackup(); } @@ -46,6 +48,8 @@ export class SetupRenderModule { } await this.view.finalizeSetup({ submit_all: tmp_submit_all, submit_main: tmp_submit_main }); await this.renderSetupDone(); + } else if (this.view.rules.mustAutoImportOrAutogenPrvWithKeyManager()) { + await this.view.setupKeyManagerAutogen.getKeyFromKeyManagerOrAutogenAndStoreItThenRenderSetupDone(); } else { await this.renderSetupDialog(); } diff --git a/extension/js/common/api/key-manager.ts b/extension/js/common/api/key-manager.ts index 8b08c735770..f6d64a9121f 100644 --- a/extension/js/common/api/key-manager.ts +++ b/extension/js/common/api/key-manager.ts @@ -24,8 +24,8 @@ export class KeyManager extends Api { return await this.request('GET', '/keys/private') as LoadPrvRes; } - public storePrivateKey = async (decryptedKey: string, longid: string): Promise => { - return await this.request('PUT', '/keys/private', { decryptedKey, longid }); + public storePrivateKey = async (decryptedKey: string, publicKey: string, longid: string): Promise => { + return await this.request('PUT', '/keys/private', { decryptedKey, publicKey, longid }); } private request = async (method: ReqMethod, path: string, vals?: Dict): Promise => { diff --git a/extension/js/common/platform/store/acct-store.ts b/extension/js/common/platform/store/acct-store.ts index 4eb9e1f13b5..07298cc889a 100644 --- a/extension/js/common/platform/store/acct-store.ts +++ b/extension/js/common/platform/store/acct-store.ts @@ -32,7 +32,7 @@ export type Scopes = { export type AccountIndex = 'keys' | 'notification_setup_needed_dismissed' | 'email_provider' | 'google_token_access' | 'google_token_expires' | 'google_token_scopes' | 'google_token_refresh' | 'hide_message_password' | 'sendAs' | 'drafts_reply' | 'drafts_compose' | - 'pubkey_sent_to' | 'full_name' | 'cryptup_enabled' | 'setup_done' | 'is_newly_created_key' | + 'pubkey_sent_to' | 'full_name' | 'cryptup_enabled' | 'setup_done' | 'successfully_received_at_leat_one_message' | 'notification_setup_done_seen' | 'picture' | 'outgoing_language' | 'setup_date' | 'openid' | 'tmp_submit_main' | 'tmp_submit_all' | 'subscription' | 'uuid' | 'use_rich_text' | 'rules'; @@ -60,7 +60,6 @@ export type AcctStoreDict = { full_name?: string; cryptup_enabled?: boolean; setup_done?: boolean; - is_newly_created_key?: boolean; successfully_received_at_leat_one_message?: boolean; notification_setup_done_seen?: boolean; picture?: string; // google image diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index 2b5cd8f6a92..3b5e6cd83d2 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -174,12 +174,12 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test })); ava.default('get.key@key-manager-autogen.flowcrypt.com - automatic setup with key found on key manager', testWithBrowser(undefined, async (t, browser) => { + // todo, create SetupPageRecipe.autoKeygen const acct = 'get.key@key-manager-autogen.flowcrypt.com'; - const setupPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); - await setupPage.target.waitForNavigation(); - expect(setupPage.target.url()).contain('https://mail.google.com'); // auto-redirect to gmail - await setupPage.close(); - const settingsPage = await browser.newPage(t, TestUrls.extensionSettings('flowcrypt.compatibility@gmail.com')); + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); + await settingsPage.target.waitForNavigation(); + await settingsPage.waitAndClick('@action-step4done-account-settings'); + await SettingsPageRecipe.ready(settingsPage); await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); // check imported key const myKeyFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, `@action-show-key-1`, ['my_key.htm', 'placement=settings']); From 6ad601deb7d60808c8104005a0c773e628042c90 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 10:27:05 +0000 Subject: [PATCH 08/35] expect pubkey be submitted to km --- .../mock/key-manager/key-manager-endpoints.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index d0ca9798ec9..0173cdb12b7 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -26,15 +26,23 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { throw new HttpClientErr(`Unexpectedly calling mockKeyManagerEndpoints:/keys/private GET with acct ${acctEmail}`); } if (isPut(req)) { - const { decryptedKey, longid } = body as Dict; + const { decryptedKey, publicKey, longid } = body as Dict; if (acctEmail === 'put.key@key-manager-autogen.flowcrypt.com') { - const details = await PgpKey.parseDetails(decryptedKey); - expect(details.keys).to.have.length(1); - expect(details.keys[0].algo.bits).to.equal(2048); - expect(details.keys[0].ids[0].longid).to.equal(longid); - expect(details.keys[0].users).to.have.length(1); - expect(details.keys[0].users[0]).to.equal('put.key@key-manager-autogen.flowcrypt.com'); - expect(details.keys[0].isFullyDecrypted).to.be.true('key must be decrypted'); + const prvDetails = await PgpKey.parseDetails(decryptedKey); + expect(prvDetails.keys).to.have.length(1); + expect(prvDetails.keys[0].algo.bits).to.equal(2048); + expect(prvDetails.keys[0].ids[0].longid).to.equal(longid); + expect(prvDetails.keys[0].users).to.have.length(1); + expect(prvDetails.keys[0].users[0]).to.equal('put.key@key-manager-autogen.flowcrypt.com'); + expect(prvDetails.keys[0].private).to.exist('key must be private'); + expect(prvDetails.keys[0].isFullyDecrypted).to.be.true('key must be decrypted'); + const pubDetails = await PgpKey.parseDetails(publicKey); + expect(pubDetails.keys).to.have.length(1); + expect(pubDetails.keys[0].algo.bits).to.equal(2048); + expect(pubDetails.keys[0].ids[0].longid).to.equal(longid); + expect(pubDetails.keys[0].users).to.have.length(1); + expect(pubDetails.keys[0].users[0]).to.equal('put.key@key-manager-autogen.flowcrypt.com'); + expect(pubDetails.keys[0].private).to.not.exist('key must be public'); return {}; } throw new HttpClientErr(`Unexpectedly calling mockKeyManagerEndpoints:/keys/private PUT with acct ${acctEmail}`); From af8ed0b8b204959804c885903f75e637946eb880 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 10:29:22 +0000 Subject: [PATCH 09/35] hide pass phrase settings --- extension/chrome/settings/modules/security.htm | 4 ++-- extension/chrome/settings/modules/security.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/extension/chrome/settings/modules/security.htm b/extension/chrome/settings/modules/security.htm index 512d2d36ce5..adc825f8926 100644 --- a/extension/chrome/settings/modules/security.htm +++ b/extension/chrome/settings/modules/security.htm @@ -20,10 +20,10 @@

Security

-
+

Private key pass phrase

-
+
diff --git a/extension/chrome/settings/modules/security.ts b/extension/chrome/settings/modules/security.ts index 057c27ad331..f6a2ccb4263 100644 --- a/extension/chrome/settings/modules/security.ts +++ b/extension/chrome/settings/modules/security.ts @@ -16,6 +16,7 @@ import { initPassphraseToggle } from '../../../js/common/ui/passphrase-ui.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; +import { Rules } from '../../../js/common/rules.js'; View.run(class SecurityView extends View { @@ -23,6 +24,7 @@ View.run(class SecurityView extends View { private readonly parentTabId: string; private primaryKi: KeyInfo | undefined; private authInfo: FcUuidAuth | undefined; + private rules!: Rules; constructor() { super(); @@ -37,10 +39,14 @@ View.run(class SecurityView extends View { Assert.abortAndRenderErrorIfKeyinfoEmpty(this.primaryKi); this.authInfo = await AcctStore.authInfo(this.acctEmail); const storage = await AcctStore.get(this.acctEmail, ['hide_message_password', 'outgoing_language']); + this.rules = await Rules.newInstance(this.acctEmail); $('#hide_message_password').prop('checked', storage.hide_message_password === true); $('.password_message_language').val(storage.outgoing_language || 'EN'); await this.renderPassPhraseOptionsIfStoredPermanently(); await this.loadAndRenderPwdEncryptedMsgSettings(); + if (this.rules.mustAutogenPassPhraseQuietly()) { + $('.hide_if_pass_phrase_not_user_configurable').hide(); + } } public setHandlers = () => { From 50dc4a4cfc79c364ffaae92d5a525445b75e1a40 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 10:34:52 +0000 Subject: [PATCH 10/35] throw if idToken missing only on initial setup --- extension/chrome/settings/setup.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/extension/chrome/settings/setup.ts b/extension/chrome/settings/setup.ts index babc4c02bcd..44ec19624ae 100644 --- a/extension/chrome/settings/setup.ts +++ b/extension/chrome/settings/setup.ts @@ -44,7 +44,7 @@ export class SetupView extends View { public readonly acctEmail: string; public readonly parentTabId: string | undefined; public readonly action: 'add_key' | 'finalize' | undefined; - public readonly idToken: string; + public readonly idToken: string | undefined; // only needed for initial setup, not for add_key or 'finalize' public readonly keyImportUi = new KeyImportUi({ checkEncryption: true }); public readonly gmail: Gmail; @@ -72,11 +72,14 @@ export class SetupView extends View { super(); const uncheckedUrlParams = Url.parse(['acctEmail', 'action', 'idToken', 'parentTabId']); this.acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail'); - this.idToken = Assert.urlParamRequire.string(uncheckedUrlParams, 'idToken'); + this.idToken = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'idToken'); this.action = Assert.urlParamRequire.oneof(uncheckedUrlParams, 'action', ['add_key', 'finalize', undefined]) as 'add_key' | 'finalize' | undefined; if (this.action === 'add_key') { this.parentTabId = Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId'); } + if (this.action !== 'add_key' && this.action !== 'finalize') { + Assert.urlParamRequire.string(uncheckedUrlParams, 'idToken'); // will render error if missing + } if (this.acctEmail) { BrowserMsg.send.bg.updateUninstallUrl(); } else { @@ -103,7 +106,7 @@ export class SetupView extends View { this.storage.email_provider = this.storage.email_provider || 'gmail'; this.rules = await Rules.newInstance(this.acctEmail); this.keyserver = new Keyserver(this.rules); - if (this.rules.getPrivateKeyManagerUrl()) { + if (this.rules.getPrivateKeyManagerUrl() && this.idToken) { this.keyManager = new KeyManager(this.rules.getPrivateKeyManagerUrl()!, this.idToken); } if (!this.rules.canCreateKeys()) { From 29326c47152aaf50370e43eb2498ad8825a859cb Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 10:35:37 +0000 Subject: [PATCH 11/35] add idToken to url params --- extension/js/common/settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/js/common/settings.ts b/extension/js/common/settings.ts index 527ca5ee9e2..94098e2c4d4 100644 --- a/extension/js/common/settings.ts +++ b/extension/js/common/settings.ts @@ -270,7 +270,7 @@ export class Settings { window.location.href = Url.create('/chrome/settings/index.htm', { acctEmail: response.acctEmail }); } else { await AcctStore.set(response.acctEmail, { email_provider: 'gmail' }); - window.location.href = Url.create('/chrome/settings/setup.htm', { acctEmail: response.acctEmail }); + window.location.href = Url.create('/chrome/settings/setup.htm', { acctEmail: response.acctEmail, idToken: response.id_token }); } } else if (response.result === 'Denied' || response.result === 'Closed') { if (settingsTabId) { From d48fc8076b4586f8f2c93840720ab6a748bb1eff Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 10:57:33 +0000 Subject: [PATCH 12/35] fix tests --- extension/js/common/api/key-manager.ts | 2 +- test/source/mock/google/google-endpoints.ts | 22 +++++++++---------- .../mock/key-manager/key-manager-endpoints.ts | 4 ++-- test/source/mock/lib/oauth.ts | 20 ++++++++++++++++- test/source/tests/tests/setup.ts | 6 +++-- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/extension/js/common/api/key-manager.ts b/extension/js/common/api/key-manager.ts index f6d64a9121f..54bf3d918dc 100644 --- a/extension/js/common/api/key-manager.ts +++ b/extension/js/common/api/key-manager.ts @@ -29,7 +29,7 @@ export class KeyManager extends Api { } private request = async (method: ReqMethod, path: string, vals?: Dict): Promise => { - return await Api.apiCall(this.url, path, vals, 'JSON', undefined, { Authorization: `Bearer ${this.idToken}` }, undefined, method); + return await Api.apiCall(this.url, path, vals, vals ? 'JSON' : undefined, undefined, { Authorization: `Bearer ${this.idToken}` }, undefined, method); } } diff --git a/test/source/mock/google/google-endpoints.ts b/test/source/mock/google/google-endpoints.ts index 0adbe2dbc6b..39362b138e2 100644 --- a/test/source/mock/google/google-endpoints.ts +++ b/test/source/mock/google/google-endpoints.ts @@ -38,14 +38,14 @@ export const mockGoogleEndpoints: HandlersDefinition = { throw new Error(`Method not implemented for ${req.url}: ${req.method}`); }, '/oauth2/v1/tokeninfo': async ({ query: { access_token } }, req) => { - oauth.checkAuthorizationHeader(`Bearer ${access_token}`); + oauth.checkAuthorizationHeaderWithAccessToken(`Bearer ${access_token}`); if (isGet(req)) { return { issued_to: 'issued_to', audience: 'audience', scope: 'scope', expires_in: oauth.expiresIn, access_type: 'offline' }; } throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); }, '/m8/feeds/contacts/default/thin': async (parsedReq, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); if (isGet(req) && acct === 'test.ci.compose@org.flowcrypt.com') { return { feed: { @@ -58,7 +58,7 @@ export const mockGoogleEndpoints: HandlersDefinition = { throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); }, '/gmail/v1/users/me/settings/sendAs': async (parsedReq, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); if (isGet(req)) { const sendAs = [{ sendAsEmail: acct, @@ -90,7 +90,7 @@ export const mockGoogleEndpoints: HandlersDefinition = { throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); }, '/gmail/v1/users/me/messages': async ({ query: { q } }, req) => { // search messages - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); if (isGet(req) && q) { const msgs = new GoogleData(acct).searchMessages(q); return { messages: msgs.map(({ id, threadId }) => ({ id, threadId })), resultSizeEstimate: msgs.length }; @@ -98,7 +98,7 @@ export const mockGoogleEndpoints: HandlersDefinition = { throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); }, '/gmail/v1/users/me/messages/?': async ({ query: { format } }, req) => { // get msg or attachment - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); if (isGet(req)) { const id = parseResourceId(req.url!); const data = new GoogleData(acct); @@ -118,14 +118,14 @@ export const mockGoogleEndpoints: HandlersDefinition = { throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); }, '/gmail/v1/users/me/labels': async (parsedReq, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); if (isGet(req)) { return { labels: new GoogleData(acct).getLabels() }; } throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); }, '/gmail/v1/users/me/threads': async ({ query: { labelIds, includeSpamTrash } }, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); if (isGet(req)) { const threads = new GoogleData(acct).getThreads(); return { threads, resultSizeEstimate: threads.length }; @@ -133,7 +133,7 @@ export const mockGoogleEndpoints: HandlersDefinition = { throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); }, '/gmail/v1/users/me/threads/?': async ({ query: { format } }, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); if (isGet(req) && (format === 'metadata' || format === 'full')) { const id = parseResourceId(req.url!); const msgs = new GoogleData(acct).getMessagesByThread(id); @@ -145,7 +145,7 @@ export const mockGoogleEndpoints: HandlersDefinition = { } }, '/upload/gmail/v1/users/me/messages/send?uploadType=multipart': async (parsedReq, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); if (isPost(req)) { if (parsedReq.body && typeof parsedReq.body === 'string') { const parseResult = await parseMultipartDataAsMimeMsg(parsedReq.body); @@ -165,7 +165,7 @@ export const mockGoogleEndpoints: HandlersDefinition = { }, '/gmail/v1/users/me/drafts': async (parsedReq, req) => { if (isPost(req)) { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); const body = parsedReq.body as DraftSaveModel; if (body && body.message && body.message.raw && typeof body.message.raw === 'string') { @@ -184,7 +184,7 @@ export const mockGoogleEndpoints: HandlersDefinition = { throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); }, '/gmail/v1/users/me/drafts/?': async (parsedReq, req) => { - const acct = oauth.checkAuthorizationHeader(req.headers.authorization); + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); if (isGet(req)) { const id = parseResourceId(req.url!); const data = new GoogleData(acct); diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index 0173cdb12b7..0927c4b84f9 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -14,8 +14,8 @@ import { expect } from 'chai'; 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'; export const mockKeyManagerEndpoints: HandlersDefinition = { - '/keys/private': async ({ body }, req) => { - const acctEmail = oauth.checkAuthorizationHeader(req.headers.authorization); + '/flowcrypt-email-key-manager/keys/private': async ({ body }, req) => { + const acctEmail = oauth.checkAuthorizationHeaderWithIdToken(req.headers.authorization); if (isGet(req)) { if (acctEmail === 'get.key@key-manager-autogen.flowcrypt.com') { return { keys: [existingPrv] }; diff --git a/test/source/mock/lib/oauth.ts b/test/source/mock/lib/oauth.ts index aca18ce3336..f6adc0d804d 100644 --- a/test/source/mock/lib/oauth.ts +++ b/test/source/mock/lib/oauth.ts @@ -18,6 +18,7 @@ export class OauthMock { private refreshTokenByAuthCode: { [authCode: string]: string } = {}; private accessTokenByRefreshToken: { [refreshToken: string]: string } = {}; private acctByAccessToken: { [acct: string]: string } = {}; + private acctByIdToken: { [acct: string]: string } = {}; private issuedIdTokensByAcct: { [acct: string]: string[] } = {}; public consentChooseAccountPage = (url: string) => { @@ -68,7 +69,7 @@ export class OauthMock { } } - public checkAuthorizationHeader = (authorization: string | undefined) => { + public checkAuthorizationHeaderWithAccessToken = (authorization: string | undefined) => { if (!authorization) { throw new HttpClientErr('Missing mock bearer authorization header', Status.UNAUTHORIZED); } @@ -81,6 +82,22 @@ export class OauthMock { return acct; } + /** + * As if a 3rd party was evaluating it, such as key manager + */ + public checkAuthorizationHeaderWithIdToken = (authorization: string | undefined) => { + if (!authorization) { + throw new HttpClientErr('Missing mock bearer authorization header', Status.UNAUTHORIZED); + } + const accessToken = authorization.replace(/^Bearer /, ''); + const acct = this.acctByIdToken[accessToken]; + if (!acct) { + throw new HttpClientErr('Invalid idToken token', Status.UNAUTHORIZED); + } + this.checkKnownAcct(acct); + return acct; + } + public isIdTokenValid = (idToken: string) => { // we verify mock idToken by checking if we ever issued it const [header, data, sig] = idToken.split('.'); const claims = JSON.parse(Buf.fromBase64UrlStr(data).toUtfStr()); @@ -126,6 +143,7 @@ export class OauthMock { this.issuedIdTokensByAcct[email] = []; } this.issuedIdTokensByAcct[email].push(newIdToken); + this.acctByIdToken[newIdToken] = email; return newIdToken; } diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index 3b5e6cd83d2..fb4c6663421 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -182,12 +182,14 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test await SettingsPageRecipe.ready(settingsPage); await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); // check imported key - const myKeyFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, `@action-show-key-1`, ['my_key.htm', 'placement=settings']); + const myKeyFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, `@action-show-key-0`, ['my_key.htm', 'placement=settings']); await Util.sleep(1); await myKeyFrame.waitAll('@content-longid'); - expect(await myKeyFrame.read('@content-longid')).to.equal(Str.spaced('unknown')); + expect(await myKeyFrame.read('@content-longid')).to.equal('00B0 1158 0796 9D75'); await SettingsPageRecipe.closeDialog(settingsPage); + await Util.sleep(2); // todo - check that it does not offer any pass phrase options + await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); const securityFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, '@action-open-security-page', ['security.htm', 'placement=settings']); await Util.sleep(1); await securityFrame.notPresent(['@action-change-passphrase-begin', '@action-test-passphrase-begin', '@action-forget-pp']); From 75ba5cf43707022e28bf07500078311f56c81cb2 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 11:32:34 +0000 Subject: [PATCH 13/35] fix conflicting html attributes --- extension/chrome/settings/modules/security.htm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/chrome/settings/modules/security.htm b/extension/chrome/settings/modules/security.htm index adc825f8926..e82e9e46139 100644 --- a/extension/chrome/settings/modules/security.htm +++ b/extension/chrome/settings/modules/security.htm @@ -20,10 +20,10 @@

Security

-
+

Private key pass phrase

-
+
From 1806b924f792ca3b42754026a3dd29ead52c7564 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 11:39:36 +0000 Subject: [PATCH 14/35] fix checking security frame + fix test --- test/source/tests/tests/setup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index fb4c6663421..f512eb85273 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -188,8 +188,8 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test expect(await myKeyFrame.read('@content-longid')).to.equal('00B0 1158 0796 9D75'); await SettingsPageRecipe.closeDialog(settingsPage); await Util.sleep(2); - // todo - check that it does not offer any pass phrase options - await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); + // check that it does not offer any pass phrase options + await SettingsPageRecipe.toggleScreen(settingsPage, 'basic'); const securityFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, '@action-open-security-page', ['security.htm', 'placement=settings']); await Util.sleep(1); await securityFrame.notPresent(['@action-change-passphrase-begin', '@action-test-passphrase-begin', '@action-forget-pp']); From 274f9304cb676e9fc8c37819de336f030a6f32d5 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 11:41:45 +0000 Subject: [PATCH 15/35] create SetupPageRecipe.autoKeygen --- test/source/tests/page-recipe/setup-page-recipe.ts | 5 +++++ test/source/tests/tests/setup.ts | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/test/source/tests/page-recipe/setup-page-recipe.ts b/test/source/tests/page-recipe/setup-page-recipe.ts index ab91808baea..b70a47092a5 100644 --- a/test/source/tests/page-recipe/setup-page-recipe.ts +++ b/test/source/tests/page-recipe/setup-page-recipe.ts @@ -191,6 +191,11 @@ export class SetupPageRecipe extends PageRecipe { } } + public static autoKeygen = async (settingsPage: ControllablePage): Promise => { + await settingsPage.waitAndClick('@action-step4done-account-settings'); + await SettingsPageRecipe.ready(settingsPage); + } + private static createBegin = async (settingsPage: ControllablePage, keyTitle: string, { usedPgpBefore = false }: { usedPgpBefore?: boolean } = {}) => { const k = Config.key(keyTitle); if (usedPgpBefore) { diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index f512eb85273..35fe59370bd 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -174,12 +174,9 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test })); ava.default('get.key@key-manager-autogen.flowcrypt.com - automatic setup with key found on key manager', testWithBrowser(undefined, async (t, browser) => { - // todo, create SetupPageRecipe.autoKeygen const acct = 'get.key@key-manager-autogen.flowcrypt.com'; const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); - await settingsPage.target.waitForNavigation(); - await settingsPage.waitAndClick('@action-step4done-account-settings'); - await SettingsPageRecipe.ready(settingsPage); + await SetupPageRecipe.autoKeygen(settingsPage); await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); // check imported key const myKeyFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, `@action-show-key-0`, ['my_key.htm', 'placement=settings']); From e2341a486111dbf8e49f742d6b400ba91d71405c Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 15:20:46 +0000 Subject: [PATCH 16/35] added test for put.key --- .../mock/key-manager/key-manager-endpoints.ts | 13 ++++++----- .../tests/page-recipe/oauth-page-recipe.ts | 1 + test/source/tests/tests/setup.ts | 22 +++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index 0927c4b84f9..74fafd97a40 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -13,6 +13,8 @@ import { expect } from 'chai'; 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'; +export const MOCK_KM_LAST_INSERTED_KEY: { [acct: string]: { decryptedKey: string, publicKey: string, longid: string } } = {}; // accessed from test runners + export const mockKeyManagerEndpoints: HandlersDefinition = { '/flowcrypt-email-key-manager/keys/private': async ({ body }, req) => { const acctEmail = oauth.checkAuthorizationHeaderWithIdToken(req.headers.authorization); @@ -33,16 +35,17 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { expect(prvDetails.keys[0].algo.bits).to.equal(2048); expect(prvDetails.keys[0].ids[0].longid).to.equal(longid); expect(prvDetails.keys[0].users).to.have.length(1); - expect(prvDetails.keys[0].users[0]).to.equal('put.key@key-manager-autogen.flowcrypt.com'); - expect(prvDetails.keys[0].private).to.exist('key must be private'); - expect(prvDetails.keys[0].isFullyDecrypted).to.be.true('key must be decrypted'); + expect(prvDetails.keys[0].users[0]).to.equal('First Last '); + expect(prvDetails.keys[0].private).to.exist; + expect(prvDetails.keys[0].isFullyDecrypted).to.be.true; const pubDetails = await PgpKey.parseDetails(publicKey); expect(pubDetails.keys).to.have.length(1); expect(pubDetails.keys[0].algo.bits).to.equal(2048); expect(pubDetails.keys[0].ids[0].longid).to.equal(longid); expect(pubDetails.keys[0].users).to.have.length(1); - expect(pubDetails.keys[0].users[0]).to.equal('put.key@key-manager-autogen.flowcrypt.com'); - expect(pubDetails.keys[0].private).to.not.exist('key must be public'); + expect(pubDetails.keys[0].users[0]).to.equal('First Last '); + expect(pubDetails.keys[0].private).to.not.exist; + MOCK_KM_LAST_INSERTED_KEY[acctEmail] = { decryptedKey, publicKey, longid }; return {}; } throw new HttpClientErr(`Unexpectedly calling mockKeyManagerEndpoints:/keys/private PUT with acct ${acctEmail}`); diff --git a/test/source/tests/page-recipe/oauth-page-recipe.ts b/test/source/tests/page-recipe/oauth-page-recipe.ts index 66df1243197..56490d1f1ac 100644 --- a/test/source/tests/page-recipe/oauth-page-recipe.ts +++ b/test/source/tests/page-recipe/oauth-page-recipe.ts @@ -7,6 +7,7 @@ import { ControllablePage } from '../../browser'; import { FlowCryptApi } from '../api'; import { PageRecipe } from './abstract-page-recipe'; import { totp as produce2faToken } from 'speakeasy'; +import { expect } from 'chai'; export class OauthPageRecipe extends PageRecipe { diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index 35fe59370bd..6aa7f435226 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -12,6 +12,7 @@ import { SettingsPageRecipe } from '../page-recipe/settings-page-recipe'; import { ComposePageRecipe } from '../page-recipe/compose-page-recipe'; import { TestUrls } from '../../browser/test-urls'; import { Str } from '../../core/common'; +import { MOCK_KM_LAST_INSERTED_KEY } from '../../mock/key-manager/key-manager-endpoints'; // tslint:disable:no-blank-lines-func @@ -192,6 +193,27 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test await securityFrame.notPresent(['@action-change-passphrase-begin', '@action-test-passphrase-begin', '@action-forget-pp']); })); + ava.default('put.key@key-manager-autogen.flowcrypt.com - automatic setup with key not found on key manager, then generated', testWithBrowser(undefined, async (t, browser) => { + const acct = 'put.key@key-manager-autogen.flowcrypt.com'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); + await SetupPageRecipe.autoKeygen(settingsPage); + await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); + // check imported key + const myKeyFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, `@action-show-key-0`, ['my_key.htm', 'placement=settings']); + await Util.sleep(1); + await myKeyFrame.waitAll('@content-longid'); + const fromKm = MOCK_KM_LAST_INSERTED_KEY[acct]; + expect(fromKm).to.exist; + expect(await myKeyFrame.read('@content-longid')).to.equal(Str.spaced(fromKm.longid)); + await SettingsPageRecipe.closeDialog(settingsPage); + await Util.sleep(2); + // check that it does not offer any pass phrase options + await SettingsPageRecipe.toggleScreen(settingsPage, 'basic'); + const securityFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, '@action-open-security-page', ['security.htm', 'placement=settings']); + await Util.sleep(1); + await securityFrame.notPresent(['@action-change-passphrase-begin', '@action-test-passphrase-begin', '@action-forget-pp']); + })); + } }; From 602d761a3b6d0c72793fc8a22140ec8aa9c6a443 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 15:36:31 +0000 Subject: [PATCH 17/35] honor getEnforcedKeygenAlgo --- extension/chrome/settings/setup.ts | 3 +++ extension/chrome/settings/setup/setup-create-key.ts | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/extension/chrome/settings/setup.ts b/extension/chrome/settings/setup.ts index 44ec19624ae..97b222f1ab5 100644 --- a/extension/chrome/settings/setup.ts +++ b/extension/chrome/settings/setup.ts @@ -121,6 +121,9 @@ export class SetupView extends View { $('#step_2a_manual_create .input_passphrase_save').prop('checked', true); $('#step_2b_manual_enter .input_passphrase_save').prop('checked', true); } + if (this.rules.getEnforcedKeygenAlgo()) { + $('.key_type').val(this.rules.getEnforcedKeygenAlgo()!).prop('disabled', true); + } this.tabId = await BrowserMsg.requiredTabId(); await this.setupRender.renderInitial(); } diff --git a/extension/chrome/settings/setup/setup-create-key.ts b/extension/chrome/settings/setup/setup-create-key.ts index 1083cfeaa6d..7ec19df1f7e 100644 --- a/extension/chrome/settings/setup/setup-create-key.ts +++ b/extension/chrome/settings/setup/setup-create-key.ts @@ -3,7 +3,6 @@ 'use strict'; import { SetupOptions, SetupView } from '../setup.js'; - import { Catch } from '../../../js/common/platform/catch.js'; import { Lang } from '../../../js/common/lang.js'; import { PgpKey, KeyAlgo } from '../../../js/common/core/pgp-key.js'; @@ -34,7 +33,7 @@ export class SetupCreateKeyModule { submit_all: this.view.shouldSubmitPubkey('#step_2a_manual_create .input_submit_all'), recovered: false, }; - const keyAlgo = $('#step_2a_manual_create .key_type').val() as KeyAlgo; + const keyAlgo = this.view.rules.getEnforcedKeygenAlgo() || $('#step_2a_manual_create .key_type').val() as KeyAlgo; const action = $('#step_2a_manual_create .input_backup_inbox').prop('checked') ? 'setup_automatic' : 'setup_manual'; await this.createSaveKeyPair(options, keyAlgo); await this.view.preFinalizeSetup(options); From a256636a4ea687cf7edc27f18975d0a667be0e65 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 15:40:32 +0000 Subject: [PATCH 18/35] fix code style --- test/source/mock/key-manager/key-manager-endpoints.ts | 1 + test/source/tests/tests/setup.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index 74fafd97a40..322b7a7fa4a 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -10,6 +10,7 @@ import { expect } from 'chai'; // tslint:disable:max-line-length /* eslint-disable max-len */ +/* eslint-disable no-unused-expressions */ 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'; diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index 6aa7f435226..0cc414298a1 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -15,6 +15,7 @@ import { Str } from '../../core/common'; import { MOCK_KM_LAST_INSERTED_KEY } from '../../mock/key-manager/key-manager-endpoints'; // tslint:disable:no-blank-lines-func +/* eslint-disable no-unused-expressions */ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: TestWithBrowser) => { From 24e1ceff68b2322de8998fad66a50e40fb6ee32b Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 16:53:52 +0000 Subject: [PATCH 19/35] hide idToken in err msgs --- .../js/common/api/error/api-error-types.ts | 20 ++---------------- extension/js/common/browser/ui.ts | 2 +- extension/js/common/platform/catch.ts | 21 ++++++++++++++++++- .../tests/page-recipe/setup-page-recipe.ts | 1 + 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/extension/js/common/api/error/api-error-types.ts b/extension/js/common/api/error/api-error-types.ts index 9ac5d5a97d0..c16f4b9cd87 100644 --- a/extension/js/common/api/error/api-error-types.ts +++ b/extension/js/common/api/error/api-error-types.ts @@ -27,25 +27,9 @@ export class BackendAuthErr extends AuthErr { } abstract class ApiCallErr extends Error { - protected static censoredUrl = (url: string | undefined): string => { - if (!url) { - return '(unknown url)'; - } - if (url.indexOf('refreshToken=') !== -1) { - return `${url.split('?')[0]}~censored:refreshToken`; - } - if (url.indexOf('token=') !== -1) { - return `${url.split('?')[0]}~censored:token`; - } - if (url.indexOf('code=') !== -1) { - return `${url.split('?')[0]}~censored:code`; - } - return url; - } - protected static describeApiAction = (req: JQueryAjaxSettings) => { const describeBody = typeof req.data === 'undefined' ? '(no body)' : typeof req.data; - return `${req.method || 'GET'}-ing ${ApiCallErr.censoredUrl(req.url)} ${describeBody}: ${ApiCallErr.getPayloadStructure(req)}`; + return `${req.method || 'GET'}-ing ${Catch.censoredUrl(req.url)} ${describeBody}: ${ApiCallErr.getPayloadStructure(req)}`; } private static getPayloadStructure = (req: JQueryAjaxSettings): string => { @@ -94,7 +78,7 @@ export class AjaxErr extends ApiCallErr { stack += `\n\nresponseText(0, 1000):\n${responseText.substr(0, 1000)}\n\npayload(0, 1000):\n${Catch.stringify(req.data).substr(0, 1000)}`; } const message = `${String(xhr.statusText || '(no status text)')}: ${String(xhr.status || -1)} when ${ApiCallErr.describeApiAction(req)}`; - return new AjaxErr(message, stack, status, AjaxErr.censoredUrl(req.url), responseText, xhr.statusText || '(no status text)'); + return new AjaxErr(message, stack, status, Catch.censoredUrl(req.url), responseText, xhr.statusText || '(no status text)'); } constructor(message: string, public stack: string, public status: number, public url: string, public responseText: string, public statusText: string) { diff --git a/extension/js/common/browser/ui.ts b/extension/js/common/browser/ui.ts index efaae8a44a7..3f817d5904a 100644 --- a/extension/js/common/browser/ui.ts +++ b/extension/js/common/browser/ui.ts @@ -243,7 +243,7 @@ export class Ui { const formattedBtns = Object.keys(btns).map(formatBtn).join('     '); if (details) { const a = `Show technical details`; - details = `${a}
${details.replace(/\n/g, '
')}
`; + details = `${a}
${details.replace(/\n/g, '
')}
`; } Xss.sanitizeAppend('body', `
diff --git a/extension/js/common/platform/catch.ts b/extension/js/common/platform/catch.ts index d229992814c..22ad2c63395 100644 --- a/extension/js/common/platform/catch.ts +++ b/extension/js/common/platform/catch.ts @@ -219,11 +219,30 @@ export class Catch { Catch.test(); } catch (e) { // return stack after removing first 3 lines plus url - return `${((e as Error).stack || '').split('\n').splice(3).join('\n')}\n\nurl: ${window.location.href}\n`; + return `${((e as Error).stack || '').split('\n').splice(3).join('\n')}\n\nurl: ${Catch.censoredUrl(window.location.href)}\n`; } return ''; // make ts happy - this will never happen } + public static censoredUrl = (url: string | undefined): string => { + if (!url) { + return '(unknown url)'; + } + if (url.indexOf('refreshToken=') !== -1) { + return `${url.split('?')[0]}~censored:refreshToken`; + } + if (url.indexOf('token=') !== -1) { + return `${url.split('?')[0]}~censored:token`; + } + if (url.indexOf('code=') !== -1) { + return `${url.split('?')[0]}~censored:code`; + } + if (url.indexOf('idToken=') !== -1) { + return `${url.split('?')[0]}~censored:idToken`; + } + return url; + } + public static onUnhandledRejectionInternalHandler = (e: any) => { if (Catch.isPromiseRejectionEvent(e)) { Catch.reportErr(e.reason); diff --git a/test/source/tests/page-recipe/setup-page-recipe.ts b/test/source/tests/page-recipe/setup-page-recipe.ts index b70a47092a5..f57f9dfeca5 100644 --- a/test/source/tests/page-recipe/setup-page-recipe.ts +++ b/test/source/tests/page-recipe/setup-page-recipe.ts @@ -146,6 +146,7 @@ export class SetupPageRecipe extends PageRecipe { await settingsPage.waitAll('@container-overlay-details'); await Util.sleep(0.5); expect(await settingsPage.read('@container-overlay-details')).to.contain('Error stack'); + expect(await settingsPage.read('@container-overlay-details')).to.contain('censored:idToken'); await settingsPage.page.setOfflineMode(false); // back online await settingsPage.click('@action-overlay-retry'); // after retry, the rest should continue as usual below From 3e8177bff976c4541eff6219575bacd0199a2a9a Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 16:56:19 +0000 Subject: [PATCH 20/35] don't show mobile prompt --- extension/chrome/settings/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extension/chrome/settings/index.ts b/extension/chrome/settings/index.ts index 514045eea19..0da1755a6bb 100644 --- a/extension/chrome/settings/index.ts +++ b/extension/chrome/settings/index.ts @@ -236,10 +236,11 @@ View.run(class SettingsView extends View { $('.auth_denied_warning').removeClass('hidden'); } const globalStorage = await GlobalStore.get(['install_mobile_app_notification_dismissed']); - if (!globalStorage.install_mobile_app_notification_dismissed && rules.canBackupKeys() && rules.canCreateKeys()) { + if (!globalStorage.install_mobile_app_notification_dismissed && rules.canBackupKeys() && rules.canCreateKeys() && !rules.getPrivateKeyManagerUrl()) { // only show this notification if user is allowed to: // - backup keys: when not allowed, company typically has other forms of backup // - create keys: when not allowed, key must have been imported from some other system that already takes care of backups + // and doesn't use custom key manager, because backups are then taken care of $('.install_app_notification').removeClass('hidden'); } $('.dismiss_install_app_notification').click(this.setHandler(async () => { From 44927626797ffc9adc289da122a79d89d6261cc4 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 17:29:13 +0000 Subject: [PATCH 21/35] fix code style --- test/source/mock/key-manager/key-manager-endpoints.ts | 1 + test/source/tests/tests/compose.ts | 2 +- test/source/tests/tests/setup.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index 322b7a7fa4a..d5175c1a6b1 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -10,6 +10,7 @@ import { expect } from 'chai'; // tslint:disable:max-line-length /* eslint-disable max-len */ +// tslint:disable:no-unused-expressions /* eslint-disable no-unused-expressions */ 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\nxcLXBF5W35YBCACtst5TeyEEOVWX1tO3a6Z16tygpmIhQmKDakCj1XlGgSJu\r\nOqeIFW7aAIjwPoYjtjJOaR9Tw5mVJvyV439SUlyt0XwVGu+2tWJrDV+kpOVa\r\nzKOOlV6OoUihduKI00UPU5Gyi1DEoBBsBzfv+NNPErSYGVGpYg50L8CRe6LN\r\nYi2WYuJ5/ShqIMJDkKi5lGMomP9ngh/mlqI12iJ8QTLaJeGw683fsTQeILIs\r\n4qxXGBVkRnvfdYz2kGupB8aCI0z+1V6hhSZBrybkz9Z6/OSDW6tE8dHHZwDC\r\nkM/F7FeDV2wr4DrLwRA4cVQIBLYAjzMmZgLf45dnPLKXMIdHHcGCoijlABEB\r\nAAEAB/iifpjFjl7XJVofL5UnqqekdFeE37Cc7K9nA6fFnOz/tTJdDazNxYsW\r\nrYEiIrEc2LH5kPE7QzCEgF8cHDUHPaugwNaFiSsbmZkJPGRJG1jA1B1UMMJ2\r\nlGaiuY3/NMJkEceX2in1ClcQM/LuwqbY58DzKoOb4Xdv8wzbkbQFaq/Ej2bc\r\naxS9XOld3utjtbMt3471diEHcjgyRd0eG/NjRjDa3tXShV+rtU9fZUuRTFh5\r\n1H96m78CXAGjCnOOayVHKM5r2fFtp0KnnkkIJMD4TcEDMoJyEO7hPzP05Um5\r\n2ODIDDpT+LeS1F6YhnlnWkrE1JnfhXoRsqtUYy/oqBGUv9EEANisakoeZgZz\r\nYB2ieH/Rq1GES/klJEJRpjnBQ8Pc0I5YobcOE8jctlAbU5q2JLuJm/o0+7g1\r\nRaqoEmw3tV9dBI5RNNOOb37/uUo4rBolU4XWcO3yRPDKRw8kcUUjCNzfLAv2\r\n7AMUEpgfkQeZAkdyK8IVjzJTIKZc9skFLwak8EwxBADNOai+l+bGPMLCu7IT\r\nqMg14ZEsnX9pQCYvrJ40m3GWjLr5pIHSqHhji8L9ehybkV/+/CVIf/ljqfpQ\r\n5KI1fveNOyH6sG8DB1q6FSPbcCHgx+GEFFZIZ2xa1bGaZZYTvWCq3JOjxioz\r\nBotC9BUos4hLspLFbGgkgaAOVCccUhOe9QP+LQASmk13+FbClAckhyRBTwc7\r\no/6i99juJkajwmqRtXCrRBdnZ+GN6a6Hyi2t4mL1XeFkoq9DiPWVGqpxwN/B\r\n6ESkIMpLOpSPXtQ1Bjb+oMnzUv9Rx35U59NQeU/iySR85ePLCGXFHBwX0rZb\r\nsR1DSq3LZq2YOsZ+UkJVc1vM0txA4c1AS2V5IEtleSBGcm9tIE1hbmFnZXIg\r\nPGdldC5rZXlAa2V5LW1hbmFnZXItYXV0b2dlbi5mbG93Y3J5cHQuY29tPsLA\r\ndQQQAQgAHwUCXlbflgYLCQcIAwIEFQgKAgMWAgECGQECGwMCHgEACgkQALAR\r\nWAeWnXU/VAgAkj7+A2SoPwDLtprMOsyicF559/HTzNNPhq+xytj+wcNIodlo\r\nFfvejiwT5BhIEERf9beIh31NZ6xhcgOu43i8Vn5s13aBasixfTfRwWPyJZO6\r\nFTLW5iE39hgHuqp2jkV7yS5fHhGdRD/8j3UHhZ4ynIHe8BTWlDfkqt6vttff\r\nn6wx5MwEdearV2mJkyV+C6IjquWURHr32U5o+7Dm4xED4awZYzvmoUYWylVk\r\nC9EEx7qRKfbQDhVAb2uxcScaS454E8WK6UTiyqCkV3BeuhnFiSu8M8sNMgLS\r\nB+WpWG9c3WWWBKk0X3QdcKEJyillMXJx/rWSR9ihYNknYWm/FdHG38fC2ARe\r\nVt+WAQgAuW+RHmyaYzntZD8GlEGBk5GsAkeAfDLK3H4QIh+hQnXVKa9D2zY+\r\nTTiX8eiuZ+LyRHFrfhWDxM63yq+Q0djAmBRHGEAG8BfrXziQraPxswaVA78O\r\nYGh7n1YwQ2oI7wRVohTCE7Nl6h80DlsohcxSxxDLN3OLrGBa06zh9ey/xFBX\r\nTTDcT1vLqdgMeTElqgKVXjVtmtrp630Xs46a018BIHDICjp46FhQ/lVStwdS\r\ni2pfFtU/va+Cfu+x/ValilXFNEryOwtMWi6Lqm2UKoedfCpDly/INzjml179\r\nWFXC3a7g748z1hvNgh8u7RwFgmqcqLFgQdqKJ4wBsbW/Zh0LwwARAQABAAf9\r\nGON7YqN990UeR9RZEYtXP74PYauaIvv9k/7hiHWercO7TWHQ5Y+6vfTow+xl\r\n2DCy2/KDKcRBJX2qAmzHrw/8uDdkhsHf4dgRXHxEQuIHE0RrZLopzPvJDTeY\r\nDmb2yv8rGoWBulDbpBhLDYWOWKL2FfIllww4RMsWoGQ1sc3Ju/3ibEjGsIVN\r\nm31LzJkQTmJBCeuxxXSxzFMq7vZRVIYjqUNPzlSuRJ+BMDd1N68VZBLDSttg\r\nm0R47zG2E8sdsg5fk90/V3RNHMWDU1ROxMdU1zlpFYPx/0zvptYvITg1AMJ4\r\njpea2H4KLfX0BToV5b79iLXJUOD50qDfdL12zLsEjQQA8HPHT5xLGoqR7OZ+\r\nDTAmAg2lHl8CzpsVtUazq0ry9XZBwOiHMhm/Zz/fgXRCcfXYzWEDAzLndePQ\r\nUYMq/qLj6ZMXx1eEwJNE/IHPdfrbmees0kYPzSNAIVKmtH7563Eas4dvbLPf\r\n8/wnpLgZ/ugRTjS5o28PZUWjPGloJ/pSET8EAMVtGPZo3GiNtdV6eFGglkho\r\nHIZmbHgp5VyNXcbieC0mu7joIQI/4UPlRno956OrtQavC3v71P/TGpsNi6LL\r\no53HFBQzldt+lNh17C94ovWUYECiM6oEcmpOk4IgskcMDD6k2BQAz8h39Zwh\r\nL273647yEHIekOCvU43YtSzCrWB9BACxBZUL7WDkjOS0JsGSkc4nzPf+JY2c\r\nZwx9a8Gx2NWFxdLvtTI9ojwAx/X5dLpwBqUaXK66xxUuXdEJcOg0ur5kDwqL\r\nOnPeb5l6TS8feNsUheGrBybANEBEH8CIWPKFc1xH75BoO/F6JG6opJHbfra5\r\n1sZAkxTiQkb+aPEvEmDabEhqwsBfBBgBCAAJBQJeVt+WAhsMAAoJEACwEVgH\r\nlp11mxsH/1lS6Qg+/vod+IAsFFRF6+KwyIC+OVjMrZx9VmuGzZiFOyLTsa2A\r\n7tv2E8Y7KTOmQOFlPOnyFnBYqdTH0dYygltb33e0555u9c7OyHUoanU+1T7i\r\nUh2RqBDOYox3s7aSUHTFnSzwYte8lexmxs9qAlQPKLCAnUsMaH5MAl8KpLHo\r\nZFAUVxQrW2a7PVytQbF4Jn8oasXvCzGOicXkK+K5Qtwbu+mK3tVWxlWncnHz\r\n6FezUembPlBD1jgy+cJqXawxhYNz197XTjgJtL5HBvWconj7JiWJHTUaNsx+\r\njqbTjQE5H6b0hHiDw2XnI5+UEt/QdNVudMmWRYQOofPRXOgW13Q=\r\n=tqvP\r\n-----END PGP PRIVATE KEY BLOCK-----\r\n'; diff --git a/test/source/tests/tests/compose.ts b/test/source/tests/tests/compose.ts index a7fdd0da9d8..dcf4d05a092 100644 --- a/test/source/tests/tests/compose.ts +++ b/test/source/tests/tests/compose.ts @@ -19,6 +19,7 @@ import { TestWithBrowser } from '../../test'; import { expect } from "chai"; // tslint:disable:no-blank-lines-func +// tslint:disable:no-unused-expressions export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: TestWithBrowser) => { @@ -301,7 +302,6 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te await inboxPage.waitTillGone('@dialog'); await replyFrame.waitAll(['@action-expand-quoted-text']); const inputBody = await replyFrame.read('@input-body'); - // tslint:disable: no-unused-expression expect(inputBody.trim()).to.be.empty; await clickTripleDotAndExpectQuoteToLoad(replyFrame, [ 'On 2019-06-14 at 23:24, flowcrypt.compatibility@gmail.com wrote:', diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index 0cc414298a1..89baa1039a2 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -15,6 +15,7 @@ import { Str } from '../../core/common'; import { MOCK_KM_LAST_INSERTED_KEY } from '../../mock/key-manager/key-manager-endpoints'; // tslint:disable:no-blank-lines-func +// tslint:disable:no-unused-expressions /* eslint-disable no-unused-expressions */ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: TestWithBrowser) => { From d592714bc4bfc4b25b2ec11b8be89e9f64feb77d Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 17:30:23 +0000 Subject: [PATCH 22/35] hide add_key --- extension/chrome/settings/index.htm | 4 ++-- extension/chrome/settings/index.ts | 3 +++ extension/css/settings.css | 2 +- test/source/tests/tests/setup.ts | 5 ++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/extension/chrome/settings/index.htm b/extension/chrome/settings/index.htm index f2b509342d8..05872ad4376 100644 --- a/extension/chrome/settings/index.htm +++ b/extension/chrome/settings/index.htm @@ -235,8 +235,8 @@

FlowCrypt Settings

key icon My Keys - - Add Key + + Add Key
diff --git a/extension/chrome/settings/index.ts b/extension/chrome/settings/index.ts index 0da1755a6bb..0100bdf65d8 100644 --- a/extension/chrome/settings/index.ts +++ b/extension/chrome/settings/index.ts @@ -66,6 +66,9 @@ View.run(class SettingsView extends View { if (this.rules && !this.rules.canSubmitPubToAttester()) { $('.public_profile_indicator_container').hide(); // contact page is useless if user cannot submit to attester } + if (this.rules && this.rules.getPrivateKeyManagerUrl()) { + $(".add_key").hide(); // users which a key manager should not be adding keys manually + } $.get('/changelog.txt', data => ($('#status-row #status_v') as any as JQS).featherlight(String(data).replace(/\n/g, '
')), 'html'); await this.initialize(); await Assert.abortAndRenderErrOnUnprotectedKey(this.acctEmail, this.tabId); diff --git a/extension/css/settings.css b/extension/css/settings.css index 10488b891ea..be3a6a03eea 100644 --- a/extension/css/settings.css +++ b/extension/css/settings.css @@ -442,7 +442,7 @@ a.gray_link { width: 100%; } -.abs-row span.change-passphrase { +.abs-row span.keys-link-top-right-container { display: inline-block; position: absolute; right: 10px; diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index 89baa1039a2..ea2dc8231e0 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -10,7 +10,6 @@ import { TestWithBrowser } from '../../test'; import { expect } from 'chai'; import { SettingsPageRecipe } from '../page-recipe/settings-page-recipe'; import { ComposePageRecipe } from '../page-recipe/compose-page-recipe'; -import { TestUrls } from '../../browser/test-urls'; import { Str } from '../../core/common'; import { MOCK_KM_LAST_INSERTED_KEY } from '../../mock/key-manager/key-manager-endpoints'; @@ -181,6 +180,8 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); await SetupPageRecipe.autoKeygen(settingsPage); await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); + // check no "add key" + await settingsPage.notPresent('@action-open-add-key-page'); // check imported key const myKeyFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, `@action-show-key-0`, ['my_key.htm', 'placement=settings']); await Util.sleep(1); @@ -200,6 +201,8 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); await SetupPageRecipe.autoKeygen(settingsPage); await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); + // check no "add key" + await settingsPage.notPresent('@action-open-add-key-page'); // check imported key const myKeyFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, `@action-show-key-0`, ['my_key.htm', 'placement=settings']); await Util.sleep(1); From a2f11cd4ef8cbcdb5c29cfd092830fcc27187f46 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 18:24:39 +0000 Subject: [PATCH 23/35] ajaxErrorDetails --- .../compose-modules/compose-err-module.ts | 5 ++-- .../js/common/api/error/api-error-types.ts | 27 ++++++++++++------- extension/js/common/browser/browser-msg.ts | 4 +-- extension/js/common/view.ts | 10 ++++++- test/source/mock.ts | 2 ++ .../mock/key-manager/key-manager-endpoints.ts | 9 +++++++ .../tests/page-recipe/setup-page-recipe.ts | 13 ++++++--- test/source/tests/tests/setup.ts | 13 +++++++++ test/source/util/index.ts | 2 ++ 9 files changed, 67 insertions(+), 18 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-err-module.ts b/extension/chrome/elements/compose-modules/compose-err-module.ts index d5d18246dba..7e116c0a784 100644 --- a/extension/chrome/elements/compose-modules/compose-err-module.ts +++ b/extension/chrome/elements/compose-modules/compose-err-module.ts @@ -74,13 +74,12 @@ export class ComposeErrModule extends ViewModule { } else if (ApiErr.isReqTooLarge(e)) { await Ui.modal.error(`Could not send: message or attachments too large.`); } else if (ApiErr.isBadReq(e)) { - const errMsg = e.parseErrResMsg('google'); - if (errMsg === e.STD_ERR_MSGS.GOOGLE_INVALID_TO_HEADER || errMsg === e.STD_ERR_MSGS.GOOGLE_RECIPIENT_ADDRESS_REQUIRED) { + if (e.parsedErrMsg === e.STD_ERR_MSGS.GOOGLE_INVALID_TO_HEADER || e.parsedErrMsg === e.STD_ERR_MSGS.GOOGLE_RECIPIENT_ADDRESS_REQUIRED) { await Ui.modal.error('Error from google: Invalid recipients\n\nPlease remove recipients, add them back and re-send the message.'); } else { if (await Ui.modal.confirm(`Google returned an error when sending message. Please help us improve FlowCrypt by reporting the error to us.`)) { const page = '/chrome/settings/modules/help.htm'; - const pageUrlParams = { bugReport: BrowserExtension.prepareBugReport(`composer: send: bad request (errMsg: ${errMsg})`, {}, e) }; + const pageUrlParams = { bugReport: BrowserExtension.prepareBugReport(`composer: send: bad request (errMsg: ${e.parsedErrMsg})`, {}, e) }; BrowserMsg.send.bg.settings({ acctEmail: this.view.acctEmail, page, pageUrlParams }); } } diff --git a/extension/js/common/api/error/api-error-types.ts b/extension/js/common/api/error/api-error-types.ts index c16f4b9cd87..07d3d7ddbf0 100644 --- a/extension/js/common/api/error/api-error-types.ts +++ b/extension/js/common/api/error/api-error-types.ts @@ -77,18 +77,15 @@ export class AjaxErr extends ApiCallErr { // RawAjaxErr with status 200 can happen when it fails to parse response - eg non-json result stack += `\n\nresponseText(0, 1000):\n${responseText.substr(0, 1000)}\n\npayload(0, 1000):\n${Catch.stringify(req.data).substr(0, 1000)}`; } - const message = `${String(xhr.statusText || '(no status text)')}: ${String(xhr.status || -1)} when ${ApiCallErr.describeApiAction(req)}`; - return new AjaxErr(message, stack, status, Catch.censoredUrl(req.url), responseText, xhr.statusText || '(no status text)'); + const errMsg = AjaxErr.parseErrMsg(responseText, 'JSON[error][message]'); + const message = `${String(xhr.statusText || '(no status text)')}: ${String(xhr.status || -1)} when ${ApiCallErr.describeApiAction(req)} -> ${errMsg}`; + return new AjaxErr(message, stack, status, Catch.censoredUrl(req.url), responseText, xhr.statusText || '(no status text)', errMsg); } - constructor(message: string, public stack: string, public status: number, public url: string, public responseText: string, public statusText: string) { - super(message); - } - - public parseErrResMsg = (format: 'google') => { + private static parseErrMsg = (responseText: string, format: 'JSON[error][message]') => { try { - if (format === 'google') { - const errMsg = ((JSON.parse(this.responseText) as any).error as any).message as string; // catching all errs below + if (format === 'JSON[error][message]') { + const errMsg = ((JSON.parse(responseText) as any).error as any).message as string; // catching all errs below if (typeof errMsg === 'string') { return errMsg; } @@ -99,4 +96,16 @@ export class AjaxErr extends ApiCallErr { return undefined; } + constructor( + message: string, + public stack: string, + public status: number, + public url: string, + public responseText: string, + public statusText: string, + public parsedErrMsg: string | undefined + ) { + super(message); + } + } diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index f3eb0d2a9a2..1cfdc4f335a 100644 --- a/extension/js/common/browser/browser-msg.ts +++ b/extension/js/common/browser/browser-msg.ts @@ -106,7 +106,7 @@ export namespace Bm { export type ErrAsJson = { stack?: string; message: string, errorConstructor: 'Error' } | - { stack?: string; message: string, errorConstructor: 'AjaxErr', ajaxErrorDetails: { status: number, url: string, responseText: string, statusText: string } }; + { stack?: string; message: string, errorConstructor: 'AjaxErr', ajaxErrorDetails: { status: number, url: string, responseText: string, statusText: string, parsedErrMsg: string | undefined } }; } type Handler = Bm.AsyncRespondingHandler | Bm.AsyncResponselessHandler; @@ -463,7 +463,7 @@ export class BrowserMsg { const stackInfo = `\n\n[callerStack]\n${msg.stack}\n[/callerStack]\n\n[responderStack]\n${errAsJson.stack}\n[/responderStack]\n`; if (errAsJson.errorConstructor === 'AjaxErr') { const { status, url, responseText, statusText } = errAsJson.ajaxErrorDetails; - return new AjaxErr(`BrowserMsg(${name}) ${errAsJson.message}`, stackInfo, status, url, responseText, statusText); + return new AjaxErr(`BrowserMsg(${name}) ${errAsJson.message}`, stackInfo, status, url, responseText, statusText, ''); } const e = new Error(`BrowserMsg(${name}) ${errAsJson.message}`); e.stack += stackInfo; diff --git a/extension/js/common/view.ts b/extension/js/common/view.ts index 995364a2450..a5787eb5850 100644 --- a/extension/js/common/view.ts +++ b/extension/js/common/view.ts @@ -24,7 +24,15 @@ export abstract class View { private static reportAndRenderErr = (e: any) => { ApiErr.reportIfSignificant(e); - Xss.sanitizeRender('body', `${ApiErr.eli5(e)}
${String(e)}

${Ui.retryLink()}`); + Xss.sanitizeRender('body', ` +
+
${ApiErr.eli5(e)}
+
+
${String(e)}
+
+
+ ${Ui.retryLink()} + `); } public abstract async render(): Promise; diff --git a/test/source/mock.ts b/test/source/mock.ts index cc1ef72f282..40e7b74ee47 100644 --- a/test/source/mock.ts +++ b/test/source/mock.ts @@ -15,6 +15,8 @@ export const acctsWithoutMockData = [ 'user@no-search-domains-org-rule.flowcrypt.com', "get.key@key-manager-autogen.flowcrypt.com", "put.key@key-manager-autogen.flowcrypt.com", + "get.error@key-manager-autogen.flowcrypt.com", + "put.error@key-manager-autogen.flowcrypt.com", ]; export const mock = async (logger: (line: string) => void) => { diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index d5175c1a6b1..e260c51db10 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -27,6 +27,12 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { if (acctEmail === 'put.key@key-manager-autogen.flowcrypt.com') { return { keys: [] }; } + if (acctEmail === 'put.error@key-manager-autogen.flowcrypt.com') { + return { keys: [] }; + } + if (acctEmail === 'get.error@key-manager-autogen.flowcrypt.com') { + throw new Error('Intentional error for get.error to test client behavior'); + } throw new HttpClientErr(`Unexpectedly calling mockKeyManagerEndpoints:/keys/private GET with acct ${acctEmail}`); } if (isPut(req)) { @@ -50,6 +56,9 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { MOCK_KM_LAST_INSERTED_KEY[acctEmail] = { decryptedKey, publicKey, longid }; return {}; } + if (acctEmail === 'put.error@key-manager-autogen.flowcrypt.com') { + throw new Error('Intentional error for put.error user to test client behavior'); + } throw new HttpClientErr(`Unexpectedly calling mockKeyManagerEndpoints:/keys/private PUT with acct ${acctEmail}`); } throw new HttpClientErr(`Unknown method: ${req.method}`); diff --git a/test/source/tests/page-recipe/setup-page-recipe.ts b/test/source/tests/page-recipe/setup-page-recipe.ts index f57f9dfeca5..2fe94fe81f7 100644 --- a/test/source/tests/page-recipe/setup-page-recipe.ts +++ b/test/source/tests/page-recipe/setup-page-recipe.ts @@ -192,9 +192,16 @@ export class SetupPageRecipe extends PageRecipe { } } - public static autoKeygen = async (settingsPage: ControllablePage): Promise => { - await settingsPage.waitAndClick('@action-step4done-account-settings'); - await SettingsPageRecipe.ready(settingsPage); + public static autoKeygen = async (settingsPage: ControllablePage, { expectErr }: { expectErr?: { title: string, text: string } } = {}): Promise => { + if (expectErr) { + await settingsPage.waitAll(['@container-err-title', '@container-err-text', '@action-retry-by-reloading']); + expect(await settingsPage.read('@container-err-title')).to.contain(expectErr.title); + await Util.sleep(20); + expect(await settingsPage.read('@container-err-text')).to.contain(expectErr.text); + } else { + await settingsPage.waitAndClick('@action-step4done-account-settings'); + await SettingsPageRecipe.ready(settingsPage); + } } private static createBegin = async (settingsPage: ControllablePage, keyTitle: string, { usedPgpBefore = false }: { usedPgpBefore?: boolean } = {}) => { diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index ea2dc8231e0..93b7da8ec14 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -219,6 +219,19 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test await securityFrame.notPresent(['@action-change-passphrase-begin', '@action-test-passphrase-begin', '@action-forget-pp']); })); + ava.default.only('get.error@key-manager-autogen.flowcrypt.com - automatic setup with key not found on key manager, then generated', testWithBrowser(undefined, async (t, browser) => { + const acct = 'get.error@key-manager-autogen.flowcrypt.com'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); + await SetupPageRecipe.autoKeygen(settingsPage, { expectErr: { title: 'Server responded with an unexpected error.', text: 'dunno' } }); + })); + + ava.default.skip('put.error@key-manager-autogen.flowcrypt.com - automatic setup with key not found on key manager, then generated', testWithBrowser(undefined, async (t, browser) => { + const acct = 'put.error@key-manager-autogen.flowcrypt.com'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); + await SetupPageRecipe.autoKeygen(settingsPage); + await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); + })); + } }; diff --git a/test/source/util/index.ts b/test/source/util/index.ts index 47abf3cf7c7..fae09a10f2f 100644 --- a/test/source/util/index.ts +++ b/test/source/util/index.ts @@ -71,6 +71,8 @@ Config.secrets.auth.google.push( // these don't contain any secrets, so not wort { "email": "user@no-search-domains-org-rule.flowcrypt.com" }, { "email": "get.key@key-manager-autogen.flowcrypt.com" }, { "email": "put.key@key-manager-autogen.flowcrypt.com" }, + { "email": "get.error@key-manager-autogen.flowcrypt.com" }, + { "email": "put.error@key-manager-autogen.flowcrypt.com" }, ); export class Util { From 275f5a299471cffdad3597fe40628e47f8c75bea Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 18:35:47 +0000 Subject: [PATCH 24/35] test get.error and put.error --- extension/js/common/browser/browser-msg.ts | 10 +++++----- extension/js/common/view.ts | 9 ++++----- test/source/tests/tests/setup.ts | 21 ++++++++++++++++----- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index 1cfdc4f335a..120d349b820 100644 --- a/extension/js/common/browser/browser-msg.ts +++ b/extension/js/common/browser/browser-msg.ts @@ -106,7 +106,7 @@ export namespace Bm { export type ErrAsJson = { stack?: string; message: string, errorConstructor: 'Error' } | - { stack?: string; message: string, errorConstructor: 'AjaxErr', ajaxErrorDetails: { status: number, url: string, responseText: string, statusText: string, parsedErrMsg: string | undefined } }; + { stack?: string; message: string, errorConstructor: 'AjaxErr', ajaxErrorDetails: { status: number, url: string, responseText: string, statusText: string, parsedErrMsg?: string } }; } type Handler = Bm.AsyncRespondingHandler | Bm.AsyncResponselessHandler; @@ -452,8 +452,8 @@ export class BrowserMsg { private static errToJson = (e: any): Bm.ErrAsJson => { if (e instanceof AjaxErr) { - const { message, stack, status, url, responseText, statusText } = e; - return { stack, message, errorConstructor: 'AjaxErr', ajaxErrorDetails: { status, url, responseText, statusText } }; + const { message, stack, status, url, responseText, statusText, parsedErrMsg } = e; + return { stack, message, errorConstructor: 'AjaxErr', ajaxErrorDetails: { status, url, responseText, statusText, parsedErrMsg } }; } const { stack, message } = Catch.rewrapErr(e, 'sendRawResponse'); return { stack, message, errorConstructor: 'Error' }; @@ -462,8 +462,8 @@ export class BrowserMsg { private static jsonToErr = (errAsJson: Bm.ErrAsJson, msg: Bm.Raw) => { const stackInfo = `\n\n[callerStack]\n${msg.stack}\n[/callerStack]\n\n[responderStack]\n${errAsJson.stack}\n[/responderStack]\n`; if (errAsJson.errorConstructor === 'AjaxErr') { - const { status, url, responseText, statusText } = errAsJson.ajaxErrorDetails; - return new AjaxErr(`BrowserMsg(${name}) ${errAsJson.message}`, stackInfo, status, url, responseText, statusText, ''); + const { status, url, responseText, statusText, parsedErrMsg } = errAsJson.ajaxErrorDetails; + return new AjaxErr(`BrowserMsg(${name}) ${errAsJson.message}`, stackInfo, status, url, responseText, statusText, parsedErrMsg); } const e = new Error(`BrowserMsg(${name}) ${errAsJson.message}`); e.stack += stackInfo; diff --git a/extension/js/common/view.ts b/extension/js/common/view.ts index a5787eb5850..cbbd37ad41a 100644 --- a/extension/js/common/view.ts +++ b/extension/js/common/view.ts @@ -26,11 +26,10 @@ export abstract class View { ApiErr.reportIfSignificant(e); Xss.sanitizeRender('body', `
-
${ApiErr.eli5(e)}
-
-
${String(e)}
-
-
+
${ApiErr.eli5(e)}
+

+
${String(e)}
+

${Ui.retryLink()} `); } diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index 93b7da8ec14..c96abf9da9f 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -16,6 +16,7 @@ import { MOCK_KM_LAST_INSERTED_KEY } from '../../mock/key-manager/key-manager-en // tslint:disable:no-blank-lines-func // tslint:disable:no-unused-expressions /* eslint-disable no-unused-expressions */ +/* eslint-disable max-len */ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: TestWithBrowser) => { @@ -219,17 +220,27 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test await securityFrame.notPresent(['@action-change-passphrase-begin', '@action-test-passphrase-begin', '@action-forget-pp']); })); - ava.default.only('get.error@key-manager-autogen.flowcrypt.com - automatic setup with key not found on key manager, then generated', testWithBrowser(undefined, async (t, browser) => { + ava.default('get.error@key-manager-autogen.flowcrypt.com - handles error during KM key GET', testWithBrowser(undefined, async (t, browser) => { const acct = 'get.error@key-manager-autogen.flowcrypt.com'; const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); - await SetupPageRecipe.autoKeygen(settingsPage, { expectErr: { title: 'Server responded with an unexpected error.', text: 'dunno' } }); + await SetupPageRecipe.autoKeygen(settingsPage, { + expectErr: { + title: 'Server responded with an unexpected error.', + text: '500 when GET-ing http://localhost:8001/flowcrypt-email-key-manager/keys/private (no body): -> Intentional error for get.error to test client behavior', + } + }); })); - ava.default.skip('put.error@key-manager-autogen.flowcrypt.com - automatic setup with key not found on key manager, then generated', testWithBrowser(undefined, async (t, browser) => { + ava.default('put.error@key-manager-autogen.flowcrypt.com - handles error during KM key PUT', testWithBrowser(undefined, async (t, browser) => { const acct = 'put.error@key-manager-autogen.flowcrypt.com'; const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); - await SetupPageRecipe.autoKeygen(settingsPage); - await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); + await SetupPageRecipe.autoKeygen(settingsPage, { + expectErr: { + title: 'Server responded with an unexpected error.', + text: '500 when PUT-ing http://localhost:8001/flowcrypt-email-key-manager/keys/private string: decryptedKey,publicKey,longid -> Intentional error for put.error user to test client behavior', + } + }); + })); } From c31239e273c5f0184eae4bc6588d0045cf91b234 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 19:07:06 +0000 Subject: [PATCH 25/35] test friendly err when KM down --- .../setup/setup-key-manager-autogen.ts | 51 +++++++++++-------- extension/js/common/api/api.ts | 13 +++++ extension/js/common/api/error/api-error.ts | 2 +- test/source/mock.ts | 1 + test/source/mock/backend/backend-data.ts | 25 +++++---- .../tests/page-recipe/setup-page-recipe.ts | 1 - test/source/tests/tests/setup.ts | 10 ++++ test/source/util/index.ts | 1 + 8 files changed, 72 insertions(+), 32 deletions(-) diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index 24c728b693e..7b099a7c7a6 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -10,6 +10,8 @@ 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 { PgpPwd } from '../../../js/common/core/pgp-password.js'; +import { ApiErr } from '../../../js/common/api/error/api-error.js'; +import { Api } from '../../../js/common/api/api.js'; export class SetupKeyManagerAutogenModule { @@ -32,30 +34,37 @@ export class SetupKeyManagerAutogenModule { } const passphrase = PgpPwd.random(); // mustAutogenPassPhraseQuietly const opts: SetupOptions = { passphrase_save: true, submit_main: true, submit_all: true, passphrase }; - const { keys } = await this.view.keyManager!.getPrivateKeys(); - if (keys.length) { // keys already exist on keyserver, auto-import - const { keys: prvs } = await PgpKey.readMany(Buf.fromUtfStr(keys.join('\n'))); - for (const prv of prvs) { - if (!prv.isPrivate()) { - throw new Error(`Key ${await PgpKey.longid(prv)} for user ${this.view.acctEmail} is not a private key`); + try { + const { keys } = await this.view.keyManager!.getPrivateKeys(); + if (keys.length) { // keys already exist on keyserver, auto-import + const { keys: prvs } = await PgpKey.readMany(Buf.fromUtfStr(keys.join('\n'))); + for (const prv of prvs) { + if (!prv.isPrivate()) { + throw new Error(`Key ${await PgpKey.longid(prv)} for user ${this.view.acctEmail} is not a private key`); + } + if (!prv.isFullyDecrypted()) { + throw new Error(`Key ${await PgpKey.longid(prv)} for user ${this.view.acctEmail} from FlowCrypt Email Key Manager is not fully decrypted`); + } + await PgpKey.encrypt(prv, passphrase); } - if (!prv.isFullyDecrypted()) { - throw new Error(`Key ${await PgpKey.longid(prv)} for user ${this.view.acctEmail} from FlowCrypt Email Key Manager is not fully decrypted`); - } - await PgpKey.encrypt(prv, passphrase); + await this.view.saveKeys(prvs, { passphrase_save: true, submit_all: true, submit_main: true, passphrase }); + } else { // generate keys and store them on key manager + const { full_name } = await AcctStore.get(this.view.acctEmail, ['full_name']); + const generated = await PgpKey.create([{ name: full_name || '', email: this.view.acctEmail }], keygenAlgo, passphrase); + const decryptablePrv = await PgpKey.read(generated.private); + const generatedKeyLongid = await PgpKey.longid(decryptablePrv); + await PgpKey.decrypt(decryptablePrv, passphrase); + await this.view.keyManager!.storePrivateKey(decryptablePrv.armor(), decryptablePrv.toPublic().armor(), generatedKeyLongid!); // store decrypted key on KM + await this.view.saveKeys([await PgpKey.read(generated.private)], opts); // store encrypted key + pass phrase locally + } + await this.view.finalizeSetup(opts); + await this.view.setupRender.renderSetupDone(); + } catch (e) { + if (ApiErr.isNetErr(e) && await Api.isInternetAccessible()) { // frendly message when key manager is down, helpful during initial infrastructure setup + e.message = `FlowCrypt Email Key Manager at ${this.view.rules.getPrivateKeyManagerUrl()} is down, please inform your network administrator.`; } - await this.view.saveKeys(prvs, { passphrase_save: true, submit_all: true, submit_main: true, passphrase }); - } else { // generate keys and store them on key manager - const { full_name } = await AcctStore.get(this.view.acctEmail, ['full_name']); - const generated = await PgpKey.create([{ name: full_name || '', email: this.view.acctEmail }], keygenAlgo, passphrase); - const decryptablePrv = await PgpKey.read(generated.private); - const generatedKeyLongid = await PgpKey.longid(decryptablePrv); - await PgpKey.decrypt(decryptablePrv, passphrase); - await this.view.keyManager!.storePrivateKey(decryptablePrv.armor(), decryptablePrv.toPublic().armor(), generatedKeyLongid!); // store decrypted key on KM - await this.view.saveKeys([await PgpKey.read(generated.private)], opts); // store encrypted key + pass phrase locally + throw e; } - await this.view.finalizeSetup(opts); - await this.view.setupRender.renderSetupDone(); } } diff --git a/extension/js/common/api/api.ts b/extension/js/common/api/api.ts index d7caa53b617..8cf73a4f695 100644 --- a/extension/js/common/api/api.ts +++ b/extension/js/common/api/api.ts @@ -14,6 +14,7 @@ import { Contact } from '../core/pgp-key.js'; import { Dict } from '../core/common.js'; import { Env } from '../browser/env.js'; import { secureRandomBytes } from '../platform/util.js'; +import { ApiErr } from './error/api-error.js'; export type ReqFmt = 'JSON' | 'FORM' | 'TEXT'; export type RecipientType = 'to' | 'cc' | 'bcc'; @@ -85,6 +86,18 @@ export class Api { } } + public static isInternetAccessible = async () => { + try { + await Api.download('https://google.com'); + return true; + } catch (e) { + if (ApiErr.isNetErr(e)) { + return false; + } + throw e; + } + } + public static getAjaxProgressXhrFactory = (progressCbs?: ProgressCbs): (() => XMLHttpRequest) | undefined => { if (Env.isContentScript() || Env.isBackgroundPage() || !progressCbs || !Object.keys(progressCbs).length) { // xhr object would cause 'The object could not be cloned.' lastError during BrowserMsg passing diff --git a/extension/js/common/api/error/api-error.ts b/extension/js/common/api/error/api-error.ts index ea139567d18..24d07016317 100644 --- a/extension/js/common/api/error/api-error.ts +++ b/extension/js/common/api/error/api-error.ts @@ -116,7 +116,7 @@ export class ApiErr { return false; } - public static isNetErr = (e: any): boolean => { + public static isNetErr = (e: any): e is Error => { if (e instanceof TypeError && (e.message === 'Failed to fetch' || e.message === 'NetworkError when attempting to fetch resource.')) { return true; // openpgp.js uses fetch()... which produces these errors } diff --git a/test/source/mock.ts b/test/source/mock.ts index 40e7b74ee47..bbda8f74930 100644 --- a/test/source/mock.ts +++ b/test/source/mock.ts @@ -17,6 +17,7 @@ export const acctsWithoutMockData = [ "put.key@key-manager-autogen.flowcrypt.com", "get.error@key-manager-autogen.flowcrypt.com", "put.error@key-manager-autogen.flowcrypt.com", + "fail@key-manager-server-offline.flowcrypt.com", ]; export const mock = async (logger: (line: string) => void) => { diff --git a/test/source/mock/backend/backend-data.ts b/test/source/mock/backend/backend-data.ts index c99e6882c12..9e4b2da772d 100644 --- a/test/source/mock/backend/backend-data.ts +++ b/test/source/mock/backend/backend-data.ts @@ -73,17 +73,24 @@ export class BackendData { "disallow_attester_search_for_domains": ["flowcrypt.com"] }; } + const keyManagerAutogenRules = { + "flags": [ + "NO_PRV_BACKUP", + "ENFORCE_ATTESTER_SUBMIT", + "PRV_AUTOIMPORT_OR_AUTOGEN", + "PASS_PHRASE_QUIET_AUTOGEN", + "DEFAULT_REMEMBER_PASS_PHRASE" + ], + "private_key_manager_url": "http://localhost:8001/flowcrypt-email-key-manager", + "enforce_keygen_algo": "rsa2048", + }; if (domain === 'key-manager-autogen.flowcrypt.com') { + return keyManagerAutogenRules; + } + if (domain === 'key-manager-server-offline.flowcrypt.com') { return { - "flags": [ - "NO_PRV_BACKUP", - "ENFORCE_ATTESTER_SUBMIT", - "PRV_AUTOIMPORT_OR_AUTOGEN", - "PASS_PHRASE_QUIET_AUTOGEN", - "DEFAULT_REMEMBER_PASS_PHRASE" - ], - "private_key_manager_url": "http://localhost:8001/flowcrypt-email-key-manager", - "enforce_keygen_algo": "rsa2048", + ...keyManagerAutogenRules, + "private_key_manager_url": "https://localhost:1230/intentionally-wrong", }; } return { diff --git a/test/source/tests/page-recipe/setup-page-recipe.ts b/test/source/tests/page-recipe/setup-page-recipe.ts index 2fe94fe81f7..5353271add1 100644 --- a/test/source/tests/page-recipe/setup-page-recipe.ts +++ b/test/source/tests/page-recipe/setup-page-recipe.ts @@ -196,7 +196,6 @@ export class SetupPageRecipe extends PageRecipe { if (expectErr) { await settingsPage.waitAll(['@container-err-title', '@container-err-text', '@action-retry-by-reloading']); expect(await settingsPage.read('@container-err-title')).to.contain(expectErr.title); - await Util.sleep(20); expect(await settingsPage.read('@container-err-text')).to.contain(expectErr.text); } else { await settingsPage.waitAndClick('@action-step4done-account-settings'); diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index c96abf9da9f..3d6710f1eee 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -240,7 +240,17 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test text: '500 when PUT-ing http://localhost:8001/flowcrypt-email-key-manager/keys/private string: decryptedKey,publicKey,longid -> Intentional error for put.error user to test client behavior', } }); + })); + ava.default('fail@key-manager-server-offline.flowcrypt.com - shows friendly KM not reachable error', testWithBrowser(undefined, async (t, browser) => { + const acct = 'fail@key-manager-server-offline.flowcrypt.com'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); + await SetupPageRecipe.autoKeygen(settingsPage, { + expectErr: { + title: 'Network connection issue.', + text: 'FlowCrypt Email Key Manager at https://localhost:1230/intentionally-wrong is down, please inform your network admin.', + } + }); })); } diff --git a/test/source/util/index.ts b/test/source/util/index.ts index fae09a10f2f..4ae418d1cb2 100644 --- a/test/source/util/index.ts +++ b/test/source/util/index.ts @@ -73,6 +73,7 @@ Config.secrets.auth.google.push( // these don't contain any secrets, so not wort { "email": "put.key@key-manager-autogen.flowcrypt.com" }, { "email": "get.error@key-manager-autogen.flowcrypt.com" }, { "email": "put.error@key-manager-autogen.flowcrypt.com" }, + { "email": "fail@key-manager-server-offline.flowcrypt.com" }, ); export class Util { From 7bb573c0c7cca29fa0323c90c999be9b1d32700d Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 19:12:19 +0000 Subject: [PATCH 26/35] code style --- test/source/mock/key-manager/key-manager-endpoints.ts | 2 +- test/source/tests/tests/compose.ts | 2 +- test/source/tests/tests/setup.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index e260c51db10..3e3c77ce18a 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -10,7 +10,7 @@ import { expect } from 'chai'; // tslint:disable:max-line-length /* eslint-disable max-len */ -// tslint:disable:no-unused-expressions +// tslint:disable:no-unused-expression /* eslint-disable no-unused-expressions */ 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'; diff --git a/test/source/tests/tests/compose.ts b/test/source/tests/tests/compose.ts index dcf4d05a092..804f87604a9 100644 --- a/test/source/tests/tests/compose.ts +++ b/test/source/tests/tests/compose.ts @@ -19,7 +19,7 @@ import { TestWithBrowser } from '../../test'; import { expect } from "chai"; // tslint:disable:no-blank-lines-func -// tslint:disable:no-unused-expressions +// tslint:disable:no-unused-expression export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: TestWithBrowser) => { diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index 3d6710f1eee..edad5be3d35 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -14,7 +14,7 @@ import { Str } from '../../core/common'; import { MOCK_KM_LAST_INSERTED_KEY } from '../../mock/key-manager/key-manager-endpoints'; // tslint:disable:no-blank-lines-func -// tslint:disable:no-unused-expressions +// tslint:disable:no-unused-expression /* eslint-disable no-unused-expressions */ /* eslint-disable max-len */ From d673a2ac46b14a2e952b1cb79433156073a8f71e Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 19:19:03 +0000 Subject: [PATCH 27/35] only submit main email to attester --- extension/chrome/settings/setup/setup-key-manager-autogen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index 7b099a7c7a6..b011b37e80a 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -47,7 +47,7 @@ export class SetupKeyManagerAutogenModule { } await PgpKey.encrypt(prv, passphrase); } - await this.view.saveKeys(prvs, { passphrase_save: true, submit_all: true, submit_main: true, passphrase }); + await this.view.saveKeys(prvs, { passphrase_save: true, submit_main: true, submit_all: false, passphrase }); } else { // generate keys and store them on key manager const { full_name } = await AcctStore.get(this.view.acctEmail, ['full_name']); const generated = await PgpKey.create([{ name: full_name || '', email: this.view.acctEmail }], keygenAlgo, passphrase); From b5fce9af259b2ed156ba7aa82c6112291790c4b1 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 19:20:15 +0000 Subject: [PATCH 28/35] dry --- extension/chrome/settings/setup/setup-key-manager-autogen.ts | 4 ++-- 1 file changed, 2 insertions(+), 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 b011b37e80a..247a4478e59 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -33,7 +33,7 @@ export class SetupKeyManagerAutogenModule { return; } const passphrase = PgpPwd.random(); // mustAutogenPassPhraseQuietly - const opts: SetupOptions = { passphrase_save: true, submit_main: true, submit_all: true, passphrase }; + const opts: SetupOptions = { passphrase_save: true, submit_main: true, submit_all: false, passphrase }; try { const { keys } = await this.view.keyManager!.getPrivateKeys(); if (keys.length) { // keys already exist on keyserver, auto-import @@ -47,7 +47,7 @@ export class SetupKeyManagerAutogenModule { } await PgpKey.encrypt(prv, passphrase); } - await this.view.saveKeys(prvs, { passphrase_save: true, submit_main: true, submit_all: false, passphrase }); + await this.view.saveKeys(prvs, opts); } else { // generate keys and store them on key manager const { full_name } = await AcctStore.get(this.view.acctEmail, ['full_name']); const generated = await PgpKey.create([{ name: full_name || '', email: this.view.acctEmail }], keygenAlgo, passphrase); From cdd7b078f1b752a4c6808f73495a40dc47e3e893 Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 19:21:49 +0000 Subject: [PATCH 29/35] throw on key parse err --- extension/chrome/settings/setup/setup-key-manager-autogen.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index 247a4478e59..def4dbad978 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -38,6 +38,9 @@ export class SetupKeyManagerAutogenModule { const { keys } = await this.view.keyManager!.getPrivateKeys(); if (keys.length) { // keys already exist on keyserver, auto-import const { keys: prvs } = await PgpKey.readMany(Buf.fromUtfStr(keys.join('\n'))); + if (!prvs.length) { + throw new Error(`Could not parse any valid keys from Key Manager response for user ${this.view.acctEmail}`); + } for (const prv of prvs) { if (!prv.isPrivate()) { throw new Error(`Key ${await PgpKey.longid(prv)} for user ${this.view.acctEmail} is not a private key`); From fb7d159277c21dc842544cdf9a8b35921c26b07c Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 19:26:55 +0000 Subject: [PATCH 30/35] throw if cannot decrypt generated key --- extension/chrome/settings/setup/setup-key-manager-autogen.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index def4dbad978..4db5e1703f9 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -56,7 +56,9 @@ export class SetupKeyManagerAutogenModule { const generated = await PgpKey.create([{ name: full_name || '', email: this.view.acctEmail }], keygenAlgo, passphrase); const decryptablePrv = await PgpKey.read(generated.private); const generatedKeyLongid = await PgpKey.longid(decryptablePrv); - await PgpKey.decrypt(decryptablePrv, passphrase); + if (! await PgpKey.decrypt(decryptablePrv, passphrase)) { + throw new Error('Unexpectedly cannot decrypt newly generated key'); + } await this.view.keyManager!.storePrivateKey(decryptablePrv.armor(), decryptablePrv.toPublic().armor(), generatedKeyLongid!); // store decrypted key on KM await this.view.saveKeys([await PgpKey.read(generated.private)], opts); // store encrypted key + pass phrase locally } From 3bcdde0436dc3b8c37411a19b2ac0fb3bd93ac2d Mon Sep 17 00:00:00 2001 From: Tom J Date: Thu, 27 Feb 2020 19:42:45 +0000 Subject: [PATCH 31/35] safe retry for storing key --- .../settings/modules/backup-automatic-module.ts | 2 +- extension/chrome/settings/setup.ts | 2 +- .../settings/setup/setup-key-manager-autogen.ts | 4 +++- extension/chrome/settings/setup/setup-render.ts | 6 +++--- extension/js/common/settings.ts | 13 ++++++++++++- test/source/tests/tests/setup.ts | 13 +++++++------ 6 files changed, 27 insertions(+), 13 deletions(-) diff --git a/extension/chrome/settings/modules/backup-automatic-module.ts b/extension/chrome/settings/modules/backup-automatic-module.ts index f4ec6f2b770..11e8f0d263e 100644 --- a/extension/chrome/settings/modules/backup-automatic-module.ts +++ b/extension/chrome/settings/modules/backup-automatic-module.ts @@ -20,7 +20,7 @@ export class BackupAutomaticModule extends ViewModule { try { await this.setupCreateSimpleAutomaticInboxBackup(); } catch (e) { - return await Settings.promptToRetry('REQUIRED', e, Lang.setup.failedToBackUpKey, this.setupCreateSimpleAutomaticInboxBackup); + return await Settings.promptToRetry(e, Lang.setup.failedToBackUpKey, this.setupCreateSimpleAutomaticInboxBackup); } } diff --git a/extension/chrome/settings/setup.ts b/extension/chrome/settings/setup.ts index 97b222f1ab5..a88e4823b57 100644 --- a/extension/chrome/settings/setup.ts +++ b/extension/chrome/settings/setup.ts @@ -188,7 +188,7 @@ export class SetupView extends View { try { await this.submitPublicKeyIfNeeded(primaryKi.public, { submit_main, submit_all }); } catch (e) { - return await Settings.promptToRetry('REQUIRED', e, Lang.setup.failedToSubmitToAttester, () => this.finalizeSetup({ submit_main, submit_all })); + return await Settings.promptToRetry(e, Lang.setup.failedToSubmitToAttester, () => this.finalizeSetup({ submit_main, submit_all })); } await AcctStore.set(this.acctEmail, { setup_date: Date.now(), setup_done: true, cryptup_enabled: true }); await AcctStore.remove(this.acctEmail, ['tmp_submit_main', 'tmp_submit_all']); diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index 4db5e1703f9..a56d487f1d7 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -12,6 +12,7 @@ import { Buf } from '../../../js/common/core/buf.js'; import { PgpPwd } from '../../../js/common/core/pgp-password.js'; import { ApiErr } from '../../../js/common/api/error/api-error.js'; import { Api } from '../../../js/common/api/api.js'; +import { Settings } from '../../../js/common/settings.js'; export class SetupKeyManagerAutogenModule { @@ -59,7 +60,8 @@ export class SetupKeyManagerAutogenModule { if (! await PgpKey.decrypt(decryptablePrv, passphrase)) { throw new Error('Unexpectedly cannot decrypt newly generated key'); } - await this.view.keyManager!.storePrivateKey(decryptablePrv.armor(), decryptablePrv.toPublic().armor(), generatedKeyLongid!); // store decrypted key on KM + const storePrvOnKm = () => this.view.keyManager!.storePrivateKey(decryptablePrv.armor(), decryptablePrv.toPublic().armor(), generatedKeyLongid!); + await Settings.retryUntilSuccessful(storePrvOnKm, 'Failed to store newly generated key on FlowCrypt Email Key Manager'); await this.view.saveKeys([await PgpKey.read(generated.private)], opts); // store encrypted key + pass phrase locally } await this.view.finalizeSetup(opts); diff --git a/extension/chrome/settings/setup/setup-render.ts b/extension/chrome/settings/setup/setup-render.ts index 122f7db2e40..02d8fadef1b 100644 --- a/extension/chrome/settings/setup/setup-render.ts +++ b/extension/chrome/settings/setup/setup-render.ts @@ -28,7 +28,7 @@ export class SetupRenderModule { const { sendAs } = await AcctStore.get(this.view.acctEmail, ['sendAs']); this.saveAndFillSubmitPubkeysOption(Object.keys(sendAs!)); } catch (e) { - return await Settings.promptToRetry('REQUIRED', e, Lang.setup.failedToLoadEmailAliases, () => this.renderInitial()); + return await Settings.promptToRetry(e, Lang.setup.failedToLoadEmailAliases, () => this.renderInitial()); } $('.auth_denied_warning').toggleClass('hidden', this.view.scopes!.read || this.view.scopes!.modify); } @@ -96,7 +96,7 @@ export class SetupRenderModule { try { keyserverRes = await this.view.keyserver.lookupEmail(this.view.acctEmail); } catch (e) { - return await Settings.promptToRetry('REQUIRED', e, Lang.setup.failedToCheckIfAcctUsesEncryption, () => this.renderSetupDialog()); + return await Settings.promptToRetry(e, Lang.setup.failedToCheckIfAcctUsesEncryption, () => this.renderSetupDialog()); } if (keyserverRes.pubkey) { this.view.acctEmailAttesterLongid = await PgpKey.longid(keyserverRes.pubkey); @@ -109,7 +109,7 @@ export class SetupRenderModule { this.view.fetchedKeyBackups = backups.keyinfos.backups; this.view.fetchedKeyBackupsUniqueLongids = backups.longids.backups; } catch (e) { - return await Settings.promptToRetry('REQUIRED', e, Lang.setup.failedToCheckAccountBackups, () => this.renderSetupDialog()); + return await Settings.promptToRetry(e, Lang.setup.failedToCheckAccountBackups, () => this.renderSetupDialog()); } if (this.view.fetchedKeyBackupsUniqueLongids.length) { this.displayBlock('step_2_recovery'); diff --git a/extension/js/common/settings.ts b/extension/js/common/settings.ts index 94098e2c4d4..c9fcaeb8ad6 100644 --- a/extension/js/common/settings.ts +++ b/extension/js/common/settings.ts @@ -226,7 +226,18 @@ export class Settings { }); } - public static promptToRetry = async (type: 'REQUIRED', lastErr: any, userMsg: string, retryCb: () => Promise): Promise => { + public static retryUntilSuccessful = async (action: () => Promise, errTitle: string) => { + try { + await action(); + } catch (e) { + return await Settings.promptToRetry(e, errTitle, action); + } + } + + /** + * todo - could probably replace most usages of this method with retryPromptUntilSuccessful which is more intuitive + */ + public static promptToRetry = async (lastErr: any, userMsg: string, retryCb: () => Promise): Promise => { let userErrMsg = `${userMsg} ${ApiErr.eli5(lastErr)}`; if (lastErr instanceof ApiErrResponse && lastErr.res.error.code === 400) { userErrMsg = `${userMsg}, ${lastErr.res.error.message}`; // this will make reason for err 400 obvious to user, very important for our main customer diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index edad5be3d35..9211930ac5a 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -234,12 +234,13 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test ava.default('put.error@key-manager-autogen.flowcrypt.com - handles error during KM key PUT', testWithBrowser(undefined, async (t, browser) => { const acct = 'put.error@key-manager-autogen.flowcrypt.com'; const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); - await SetupPageRecipe.autoKeygen(settingsPage, { - expectErr: { - title: 'Server responded with an unexpected error.', - text: '500 when PUT-ing http://localhost:8001/flowcrypt-email-key-manager/keys/private string: decryptedKey,publicKey,longid -> Intentional error for put.error user to test client behavior', - } - }); + await settingsPage.waitAll(['@action-overlay-retry', '@container-overlay-prompt-text', '@action-show-overlay-details']); + await Util.sleep(0.5); + expect(await settingsPage.read('@container-overlay-prompt-text')).to.contain('Server responded with an unexpected error.'); + await settingsPage.click('@action-show-overlay-details'); + await settingsPage.waitAll('@container-overlay-details'); + await Util.sleep(0.5); + expect(await settingsPage.read('@container-overlay-details')).to.contain('500 when PUT-ing http://localhost:8001/flowcrypt-email-key-manager/keys/private string: decryptedKey,publicKey,longid -> Intentional error for put.error user to test client behavior'); })); ava.default('fail@key-manager-server-offline.flowcrypt.com - shows friendly KM not reachable error', testWithBrowser(undefined, async (t, browser) => { From 7de3bb1689cd32cf089f18c75aefb71ecd492b28 Mon Sep 17 00:00:00 2001 From: Tom J Date: Fri, 28 Feb 2020 08:12:43 +0000 Subject: [PATCH 32/35] fixed test --- extension/chrome/settings/setup/setup-key-manager-autogen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index a56d487f1d7..ffe29dcd3e3 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -68,7 +68,7 @@ export class SetupKeyManagerAutogenModule { await this.view.setupRender.renderSetupDone(); } catch (e) { if (ApiErr.isNetErr(e) && await Api.isInternetAccessible()) { // frendly message when key manager is down, helpful during initial infrastructure setup - e.message = `FlowCrypt Email Key Manager at ${this.view.rules.getPrivateKeyManagerUrl()} is down, please inform your network administrator.`; + e.message = `FlowCrypt Email Key Manager at ${this.view.rules.getPrivateKeyManagerUrl()} is down, please inform your network admin.`; } throw e; } From 77bc0d6ddb07fb3c78b2d9a7b0870c3e5bd4e826 Mon Sep 17 00:00:00 2001 From: Tom J Date: Fri, 28 Feb 2020 08:14:17 +0000 Subject: [PATCH 33/35] throw on unsupported functionality --- extension/chrome/settings/setup/setup-render.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/chrome/settings/setup/setup-render.ts b/extension/chrome/settings/setup/setup-render.ts index 02d8fadef1b..be87e70f4a3 100644 --- a/extension/chrome/settings/setup/setup-render.ts +++ b/extension/chrome/settings/setup/setup-render.ts @@ -36,7 +36,7 @@ export class SetupRenderModule { if (this.view.action !== 'add_key') { await this.renderSetupDone(); } else if (this.view.rules.mustAutoImportOrAutogenPrvWithKeyManager()) { - // todo - handle autogen case + throw new Error('Manual add_key is not supported when PRV_AUTOIMPORT_OR_AUTOGEN org rule is in use'); } else { await this.view.setupRecoverKey.renderAddKeyFromBackup(); } From fccd33c541ab034cda6c653e4ff84439826edfa7 Mon Sep 17 00:00:00 2001 From: Tom J Date: Fri, 28 Feb 2020 08:24:45 +0000 Subject: [PATCH 34/35] change km api --- .../settings/setup/setup-key-manager-autogen.ts | 12 ++++++------ extension/js/common/api/key-manager.ts | 7 ++++--- .../mock/key-manager/key-manager-endpoints.ts | 14 +++++++------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/extension/chrome/settings/setup/setup-key-manager-autogen.ts b/extension/chrome/settings/setup/setup-key-manager-autogen.ts index ffe29dcd3e3..b75233cbf1b 100644 --- a/extension/chrome/settings/setup/setup-key-manager-autogen.ts +++ b/extension/chrome/settings/setup/setup-key-manager-autogen.ts @@ -36,13 +36,13 @@ export class SetupKeyManagerAutogenModule { const passphrase = PgpPwd.random(); // mustAutogenPassPhraseQuietly const opts: SetupOptions = { passphrase_save: true, submit_main: true, submit_all: false, passphrase }; try { - const { keys } = await this.view.keyManager!.getPrivateKeys(); - if (keys.length) { // keys already exist on keyserver, auto-import - const { keys: prvs } = await PgpKey.readMany(Buf.fromUtfStr(keys.join('\n'))); - if (!prvs.length) { + const { privateKeys } = await this.view.keyManager!.getPrivateKeys(); + if (privateKeys.length) { // keys already exist on keyserver, auto-import + const { keys } = await PgpKey.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 prvs) { + for (const prv of keys) { if (!prv.isPrivate()) { throw new Error(`Key ${await PgpKey.longid(prv)} for user ${this.view.acctEmail} is not a private key`); } @@ -51,7 +51,7 @@ export class SetupKeyManagerAutogenModule { } await PgpKey.encrypt(prv, passphrase); } - await this.view.saveKeys(prvs, opts); + await this.view.saveKeys(keys, opts); } else { // generate keys and store them on key manager const { full_name } = await AcctStore.get(this.view.acctEmail, ['full_name']); const generated = await PgpKey.create([{ name: full_name || '', email: this.view.acctEmail }], keygenAlgo, passphrase); diff --git a/extension/js/common/api/key-manager.ts b/extension/js/common/api/key-manager.ts index 54bf3d918dc..141738d8d7f 100644 --- a/extension/js/common/api/key-manager.ts +++ b/extension/js/common/api/key-manager.ts @@ -8,7 +8,8 @@ import { Api, ReqMethod } from './api.js'; import { Dict } from '../core/common.js'; -type LoadPrvRes = { keys: string[] }; +type LoadPrvRes = { privateKeys: { decryptedPrivateKey: string }[] }; +// type LoadPubRes = { publicKeys: { publicKey: string }[] }; export class KeyManager extends Api { @@ -24,8 +25,8 @@ export class KeyManager extends Api { return await this.request('GET', '/keys/private') as LoadPrvRes; } - public storePrivateKey = async (decryptedKey: string, publicKey: string, longid: string): Promise => { - return await this.request('PUT', '/keys/private', { decryptedKey, publicKey, longid }); + public storePrivateKey = async (decryptedPrivateKey: string, publicKey: string, longid: string): Promise => { + return await this.request('PUT', '/keys/private', { decryptedPrivateKey, publicKey, longid }); } private request = async (method: ReqMethod, path: string, vals?: Dict): Promise => { diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index 3e3c77ce18a..cc110bdeff3 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -15,20 +15,20 @@ import { expect } from 'chai'; 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'; -export const MOCK_KM_LAST_INSERTED_KEY: { [acct: string]: { decryptedKey: string, publicKey: string, longid: string } } = {}; // accessed from test runners +export const MOCK_KM_LAST_INSERTED_KEY: { [acct: string]: { decryptedPrivateKey: string, publicKey: string, longid: string } } = {}; // accessed from test runners export const mockKeyManagerEndpoints: HandlersDefinition = { '/flowcrypt-email-key-manager/keys/private': async ({ body }, req) => { const acctEmail = oauth.checkAuthorizationHeaderWithIdToken(req.headers.authorization); if (isGet(req)) { if (acctEmail === 'get.key@key-manager-autogen.flowcrypt.com') { - return { keys: [existingPrv] }; + return { privateKeys: [{ decryptedPrivateKey: existingPrv }] }; } if (acctEmail === 'put.key@key-manager-autogen.flowcrypt.com') { - return { keys: [] }; + return { privateKeys: [] }; } if (acctEmail === 'put.error@key-manager-autogen.flowcrypt.com') { - return { keys: [] }; + return { privateKeys: [] }; } if (acctEmail === 'get.error@key-manager-autogen.flowcrypt.com') { throw new Error('Intentional error for get.error to test client behavior'); @@ -36,9 +36,9 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { throw new HttpClientErr(`Unexpectedly calling mockKeyManagerEndpoints:/keys/private GET with acct ${acctEmail}`); } if (isPut(req)) { - const { decryptedKey, publicKey, longid } = body as Dict; + const { decryptedPrivateKey, publicKey, longid } = body as Dict; if (acctEmail === 'put.key@key-manager-autogen.flowcrypt.com') { - const prvDetails = await PgpKey.parseDetails(decryptedKey); + const prvDetails = await PgpKey.parseDetails(decryptedPrivateKey); expect(prvDetails.keys).to.have.length(1); expect(prvDetails.keys[0].algo.bits).to.equal(2048); expect(prvDetails.keys[0].ids[0].longid).to.equal(longid); @@ -53,7 +53,7 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { expect(pubDetails.keys[0].users).to.have.length(1); expect(pubDetails.keys[0].users[0]).to.equal('First Last '); expect(pubDetails.keys[0].private).to.not.exist; - MOCK_KM_LAST_INSERTED_KEY[acctEmail] = { decryptedKey, publicKey, longid }; + MOCK_KM_LAST_INSERTED_KEY[acctEmail] = { decryptedPrivateKey, publicKey, longid }; return {}; } if (acctEmail === 'put.error@key-manager-autogen.flowcrypt.com') { From 33c766d5b37a2a685680a05955e4c046e1e33ad9 Mon Sep 17 00:00:00 2001 From: Tom J Date: Fri, 28 Feb 2020 08:49:37 +0000 Subject: [PATCH 35/35] fix test after km api change --- test/source/tests/tests/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/source/tests/tests/setup.ts b/test/source/tests/tests/setup.ts index 9211930ac5a..f88cb694139 100644 --- a/test/source/tests/tests/setup.ts +++ b/test/source/tests/tests/setup.ts @@ -240,7 +240,7 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test await settingsPage.click('@action-show-overlay-details'); await settingsPage.waitAll('@container-overlay-details'); await Util.sleep(0.5); - expect(await settingsPage.read('@container-overlay-details')).to.contain('500 when PUT-ing http://localhost:8001/flowcrypt-email-key-manager/keys/private string: decryptedKey,publicKey,longid -> Intentional error for put.error user to test client behavior'); + expect(await settingsPage.read('@container-overlay-details')).to.contain('500 when PUT-ing http://localhost:8001/flowcrypt-email-key-manager/keys/private string: decryptedPrivateKey,publicKey,longid -> Intentional error for put.error user to test client behavior'); })); ava.default('fail@key-manager-server-offline.flowcrypt.com - shows friendly KM not reachable error', testWithBrowser(undefined, async (t, browser) => {