diff --git a/extension/chrome/elements/passphrase.htm b/extension/chrome/elements/passphrase.htm index ddb2fa66bc9..d1f8c159d7c 100644 --- a/extension/chrome/elements/passphrase.htm +++ b/extension/chrome/elements/passphrase.htm @@ -21,7 +21,7 @@

Enter your pass phrase to read encrypted email

- +
diff --git a/extension/chrome/settings/modules/backup-automatic-module.ts b/extension/chrome/settings/modules/backup-automatic-module.ts index 41763b04fcb..e94b09e164b 100644 --- a/extension/chrome/settings/modules/backup-automatic-module.ts +++ b/extension/chrome/settings/modules/backup-automatic-module.ts @@ -31,7 +31,7 @@ export class BackupAutomaticModule extends ViewModule { } try { await this.view.manualModule.doBackupOnEmailProvider(primaryKi.private); - await this.view.renderBackupDone(); + await this.view.renderBackupDone(1); } catch (e) { if (ApiErr.isAuthErr(e)) { await Ui.modal.info("Authorization Error. FlowCrypt needs to reconnect your Gmail account"); diff --git a/extension/chrome/settings/modules/backup-manual-module.ts b/extension/chrome/settings/modules/backup-manual-module.ts index 46c414522a6..98f294ec5dd 100644 --- a/extension/chrome/settings/modules/backup-manual-module.ts +++ b/extension/chrome/settings/modules/backup-manual-module.ts @@ -8,20 +8,20 @@ import { BackupView } from './backup.js'; import { Attachment } from '../../../js/common/core/attachment.js'; import { SendableMsg } from '../../../js/common/api/email-provider/sendable-msg.js'; import { GMAIL_RECOVERY_EMAIL_SUBJECTS } from '../../../js/common/core/const.js'; -import { KeyInfo, KeyUtil } from '../../../js/common/core/crypto/key.js'; +import { KeyIdentity, KeyInfo, KeyUtil, TypedKeyInfo } from '../../../js/common/core/crypto/key.js'; import { Ui } from '../../../js/common/browser/ui.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { BrowserMsg, Bm } from '../../../js/common/browser/browser-msg.js'; import { Catch } from '../../../js/common/platform/catch.js'; import { Browser } from '../../../js/common/browser/browser.js'; -import { Url, PromiseCancellation } from '../../../js/common/core/common.js'; +import { PromiseCancellation, Value } from '../../../js/common/core/common.js'; import { Settings } from '../../../js/common/settings.js'; import { Buf } from '../../../js/common/core/buf.js'; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; +const differentPassphrasesError = `Your keys are protected with different pass phrases.\n\nBacking them up together isn't supported yet.`; export class BackupManualActionModule extends ViewModule { - private ppChangedPromiseCancellation: PromiseCancellation = { cancel: false }; private readonly proceedBtn = $('#module_manual .action_manual_backup'); @@ -55,15 +55,36 @@ export class BackupManualActionModule extends ViewModule { private actionManualBackupHandler = async () => { const selected = $('input[type=radio][name=input_backup_choice]:checked').val(); - const primaryKi = await KeyStore.getFirstRequired(this.view.acctEmail); - if (! await this.isPrivateKeyEncrypted(primaryKi)) { - await Ui.modal.error('Sorry, cannot back up private key because it\'s not protected with a pass phrase.'); + if (this.view.prvKeysToManuallyBackup.length <= 0) { + await Ui.modal.error('No keys are selected to back up! Please select a key to continue.'); + return; + } + const allKis = await KeyStore.getTypedKeyInfos(this.view.acctEmail); + const kinfos = KeyUtil.filterKeys(allKis, this.view.prvKeysToManuallyBackup); + if (kinfos.length <= 0) { + await Ui.modal.error('Sorry, could not extract these keys from storage. Please restart your browser and try again.'); return; } - if (selected === 'inbox') { - await this.backupOnEmailProviderAndUpdateUi(primaryKi); - } else if (selected === 'file') { - await this.backupAsFile(primaryKi); + // todo: this check can also be moved to encryptForBackup method when we solve the same passphrase issue (#4060) + for (const ki of kinfos) { + if (! await this.isPrivateKeyEncrypted(ki)) { + await Ui.modal.error('Sorry, cannot back up private key because it\'s not protected with a pass phrase.'); + return; + } + } + if (selected === 'inbox' || selected === 'file') { + // in setup_manual we don't have passphrase-related message handlers, so limit the checks + const encrypted = await this.encryptForBackup(kinfos, { strength: selected === 'inbox' && this.view.action !== 'setup_manual' }, allKis[0]); + if (encrypted) { + if (selected === 'inbox') { + if (!await this.backupOnEmailProviderAndUpdateUi(encrypted)) { + return; // some error occured, message displayed, can retry, no reload needed + } + } else { + await this.backupAsFile(encrypted); + } + await this.view.renderBackupDone(kinfos.length); + } } else if (selected === 'print') { await this.backupByBrint(); } else { @@ -75,50 +96,86 @@ export class BackupManualActionModule extends ViewModule { return new Attachment({ name: `flowcrypt-backup-${this.view.acctEmail.replace(/[^A-Za-z0-9]+/g, '')}.asc`, type: 'application/pgp-keys', data: Buf.fromUtfStr(armoredKey) }); } - private backupOnEmailProviderAndUpdateUi = async (primaryKi: KeyInfo) => { - const pp = await PassphraseStore.get(this.view.acctEmail, primaryKi.fingerprints[0]); - if (!this.view.parentTabId) { - await Ui.modal.error(`Missing parentTabId. Please restart your browser and try again.`); - return; + private encryptForBackup = async (kinfos: TypedKeyInfo[], checks: { strength: boolean }, primaryKeyIdentity: KeyIdentity): Promise => { + const kisWithPp = await Promise.all(kinfos.map(async (ki) => { + const passphrase = await PassphraseStore.getByKeyIdentity(this.view.acctEmail, ki); + // test that the key can actually be decrypted with the passphrase provided + const mismatch = passphrase && !await KeyUtil.decrypt(await KeyUtil.parse(ki.private), passphrase); + return { ...ki, mismatch, passphrase: mismatch ? undefined : passphrase }; + })); + const distinctPassphrases = Value.arr.unique(kisWithPp.filter(ki => ki.passphrase).map(ki => ki.passphrase!)); + if (distinctPassphrases.length > 1) { + await Ui.modal.error(differentPassphrasesError); + return undefined; } - if (!pp) { - BrowserMsg.send.passphraseDialog(this.view.parentTabId, { type: 'backup', longids: [primaryKi.longid] }); - if (! await PassphraseStore.waitUntilPassphraseChanged(this.view.acctEmail, [primaryKi.longid], 1000, this.ppChangedPromiseCancellation)) { - return; + if (checks.strength && distinctPassphrases[0] && !(Settings.evalPasswordStrength(distinctPassphrases[0]).word.pass)) { + await Ui.modal.warning('Please change your pass phrase first.\n\nIt\'s too weak for this backup method.'); + // Actually, until #956 is resolved, we can only modify the pass phrase of the first key + if (this.view.parentTabId && KeyUtil.identityEquals(kisWithPp[0], primaryKeyIdentity) && kisWithPp[0].passphrase === distinctPassphrases[0]) { + Settings.redirectSubPage(this.view.acctEmail, this.view.parentTabId, '/chrome/settings/modules/change_passphrase.htm'); } - await this.backupOnEmailProviderAndUpdateUi(primaryKi); - return; + return undefined; } - if (!this.isPassPhraseStrongEnough(primaryKi, pp)) { - await Ui.modal.warning('Your key is not protected with strong pass phrase.\n\nYou should change your pass phrase.'); - window.location.href = Url.create('/chrome/settings/modules/change_passphrase.htm', { acctEmail: this.view.acctEmail, parentTabId: this.view.parentTabId }); - return; + if (distinctPassphrases.length === 1) { + // trying to apply the known pass phrase + for (const ki of kisWithPp.filter(ki => !ki.passphrase)) { + if (await KeyUtil.decrypt(await KeyUtil.parse(ki.private), distinctPassphrases[0])) { + ki.passphrase = distinctPassphrases[0]; + } + } + } + const kisMissingPp = kisWithPp.filter(ki => !ki.passphrase); + if (kisMissingPp.length) { + if (distinctPassphrases.length >= 1) { + await Ui.modal.error(differentPassphrasesError); + return undefined; + } + // todo: reset invalid pass phrases (mismatch === true)? + const longids = kisMissingPp.map(ki => ki.longid); + if (this.view.parentTabId) { + BrowserMsg.send.passphraseDialog(this.view.parentTabId, { type: 'backup', longids }); + if (! await PassphraseStore.waitUntilPassphraseChanged(this.view.acctEmail, longids, 1000, this.ppChangedPromiseCancellation)) { + return undefined; + } + } else { + await Ui.modal.error(`Sorry, can't back up private key because its pass phrase can't be extracted. Please restart your browser and try again.`); + return undefined; + } + // re-start the function recursively with newly discovered pass phrases + // todo: #4059 however, this code is never actually executed, because our backup frame gets wiped out by the passphrase frame + return await this.encryptForBackup(kinfos, checks, primaryKeyIdentity); } + return kinfos.map(ki => ki.private).join('\n'); // todo: remove extra \n ? + } + + private backupOnEmailProviderAndUpdateUi = async (data: string): Promise => { const origBtnText = this.proceedBtn.text(); Xss.sanitizeRender(this.proceedBtn, Ui.spinner('white')); try { - await this.doBackupOnEmailProvider(primaryKi.private); + await this.doBackupOnEmailProvider(data); + return true; } catch (e) { if (ApiErr.isNetErr(e)) { - return await Ui.modal.warning('Need internet connection to finish. Please click the button again to retry.'); + await Ui.modal.warning('Need internet connection to finish. Please click the button again to retry.'); } else if (ApiErr.isAuthErr(e)) { - BrowserMsg.send.notificationShowAuthPopupNeeded(this.view.parentTabId, { acctEmail: this.view.acctEmail }); - return await Ui.modal.warning('Account needs to be re-connected first. Please try later.'); + if (this.view.parentTabId) { + BrowserMsg.send.notificationShowAuthPopupNeeded(this.view.parentTabId, { acctEmail: this.view.acctEmail }); + } + await Ui.modal.warning('Account needs to be re-connected first. Please try later.'); } else { Catch.reportErr(e); - return await Ui.modal.error(`Error happened: ${String(e)}`); + await Ui.modal.error(`Error happened: ${String(e)}`); } + return false; } finally { this.proceedBtn.text(origBtnText); } - await this.view.renderBackupDone(); } - private backupAsFile = async (primaryKi: KeyInfo) => { // todo - add a non-encrypted download option - const attachment = this.asBackupFile(primaryKi.private); + private backupAsFile = async (data: string) => { // todo - add a non-encrypted download option + const attachment = this.asBackupFile(data); Browser.saveToDownloads(attachment); await Ui.modal.info('Downloading private key backup file..'); - await this.view.renderBackupDone(); } private backupByBrint = async () => { // todo - implement + add a non-encrypted print option @@ -126,30 +183,7 @@ export class BackupManualActionModule extends ViewModule { } private backupRefused = async () => { - await this.view.renderBackupDone(false); - } - - private isPassPhraseStrongEnough = async (ki: KeyInfo, passphrase: string) => { - const prv = await KeyUtil.parse(ki.private); - if (!prv.fullyEncrypted) { - return false; - } - if (!passphrase) { - const pp = prompt('Please enter your pass phrase:'); - if (!pp) { - return false; - } - if (await KeyUtil.decrypt(prv, pp) !== true) { - await Ui.modal.warning('Pass phrase did not match, please try again.'); - return false; - } - passphrase = pp; - } - if (Settings.evalPasswordStrength(passphrase).word.pass === true) { - return true; - } - await Ui.modal.warning('Please change your pass phrase first.\n\nIt\'s too weak for this backup method.'); - return false; + await this.view.renderBackupDone(0); } private isPrivateKeyEncrypted = async (ki: KeyInfo) => { diff --git a/extension/chrome/settings/modules/backup-status-module.ts b/extension/chrome/settings/modules/backup-status-module.ts index 24442b3f82b..cf12979f4d7 100644 --- a/extension/chrome/settings/modules/backup-status-module.ts +++ b/extension/chrome/settings/modules/backup-status-module.ts @@ -41,20 +41,23 @@ export class BackupStatusModule extends ViewModule { } } + private renderGoManualButton = (htmlEscapedText: string) => { + Xss.sanitizeRender('#module_status .container', ``); + } + private renderBackupSummaryAndActionButtons = (backups: Backups) => { if (!backups.longids.backups.length) { $('.status_summary').text('No backups found on this account. If you lose your device, or it stops working, you will not be able to read your encrypted email.'); - Xss.sanitizeRender('#module_status .container', ''); + this.renderGoManualButton('BACK UP MY KEYS'); } else if (backups.longids.importedNotBackedUp.length) { $('.status_summary').text('Some of your keys have not been backed up.'); - // todo - this would not yet work because currently only backing up first key - // Xss.sanitizeRender('#module_status .container', ''); + this.renderGoManualButton('BACK UP MY KEYS'); } else if (backups.longids.backupsNotImported.length) { $('.status_summary').text('Some of your backups have not been loaded. This may cause incoming encrypted email to not be readable.'); Xss.sanitizeRender('#module_status .container', ''); } else { $('.status_summary').text('Your account keys are backed up and loaded correctly.'); - Xss.sanitizeRender('#module_status .container', ''); + this.renderGoManualButton('SEE BACKUP OPTIONS'); } } diff --git a/extension/chrome/settings/modules/backup.htm b/extension/chrome/settings/modules/backup.htm index 10ad93534ca..2cd24787387 100644 --- a/extension/chrome/settings/modules/backup.htm +++ b/extension/chrome/settings/modules/backup.htm @@ -45,6 +45,10 @@
Your key is stored in the browser. Backups are useful if you ever lose your device.
+
+
Kindly select or deselect the keys you want to backup.
+
+
@@ -107,6 +111,7 @@ + diff --git a/extension/chrome/settings/modules/backup.ts b/extension/chrome/settings/modules/backup.ts index b24e76ca1bb..439caae2393 100644 --- a/extension/chrome/settings/modules/backup.ts +++ b/extension/chrome/settings/modules/backup.ts @@ -3,7 +3,7 @@ 'use strict'; import { BrowserMsg } from '../../../js/common/browser/browser-msg.js'; -import { Url } from '../../../js/common/core/common.js'; +import { Str, Url } from '../../../js/common/core/common.js'; import { Assert } from '../../../js/common/assert.js'; import { Gmail } from '../../../js/common/api/email-provider/gmail/gmail.js'; import { OrgRules } from '../../../js/common/org-rules.js'; @@ -15,6 +15,9 @@ import { BackupManualActionModule as BackupManualModule } from './backup-manual- import { BackupAutomaticModule } from './backup-automatic-module.js'; import { Lang } from '../../../js/common/lang.js'; import { AcctStore, EmailProvider } from '../../../js/common/platform/store/acct-store.js'; +import { KeyStore } from '../../../js/common/platform/store/key-store.js'; +import { KeyIdentity, KeyUtil, TypedKeyInfo } from '../../../js/common/core/crypto/key.js'; +import { Settings } from '../../../js/common/settings.js'; export class BackupView extends View { @@ -22,7 +25,8 @@ export class BackupView extends View { public readonly idToken: string | undefined; public readonly action: 'setup_automatic' | 'setup_manual' | 'backup_manual' | undefined; public readonly gmail: Gmail; - public readonly parentTabId: string | undefined; + public readonly parentTabId: string | undefined; // the master page to interact (settings/index.htm) + public readonly keyIdentity: KeyIdentity | undefined; // the key identity supplied with URL params public readonly statusModule: BackupStatusModule; public readonly manualModule: BackupManualModule; @@ -31,19 +35,29 @@ export class BackupView extends View { public emailProvider: EmailProvider = 'gmail'; public orgRules!: OrgRules; public tabId!: string; + public prvKeysToManuallyBackup: KeyIdentity[] = []; private readonly blocks = ['loading', 'module_status', 'module_manual']; constructor() { super(); - const uncheckedUrlParams = Url.parse(['acctEmail', 'parentTabId', 'action', 'idToken']); + const uncheckedUrlParams = Url.parse(['acctEmail', 'parentTabId', 'action', 'idToken', 'id', 'type']); this.acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail'); this.action = Assert.urlParamRequire.oneof(uncheckedUrlParams, 'action', ['setup_automatic', 'setup_manual', 'backup_manual', undefined]); if (this.action !== 'setup_automatic' && this.action !== 'setup_manual') { this.parentTabId = Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId'); } else { + // when sourced from setup.htm page, passphrase-related message handlers are not wired, + // so we have limited functionality further down the road this.idToken = Assert.urlParamRequire.string(uncheckedUrlParams, 'idToken'); } + { + const id = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'id'); + const type = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'type'); + if (id && type === 'openpgp') { + this.keyIdentity = { id, type }; + } + } this.gmail = new Gmail(this.acctEmail); this.statusModule = new BackupStatusModule(this); this.manualModule = new BackupManualModule(this); @@ -62,29 +76,35 @@ export class BackupView extends View { if (this.action === 'setup_automatic') { $('#button-go-back').css('display', 'none'); await this.automaticModule.simpleSetupAutoBackupRetryUntilSuccessful(); - } else if (this.action === 'setup_manual') { - $('#button-go-back').css('display', 'none'); - this.displayBlock('module_manual'); - $('h1').text('Back up your private key'); - } else if (this.action === 'backup_manual') { - this.displayBlock('module_manual'); - $('h1').text('Back up your private key'); - } else { // action = view status - $('.hide_if_backup_done').css('display', 'none'); - $('h1').text('Key Backups'); - this.displayBlock('loading'); - await this.statusModule.checkAndRenderBackupStatus(); + } else { + await this.preparePrvKeysBackupSelection(); + if (this.action === 'setup_manual') { + $('#button-go-back').css('display', 'none'); + this.displayBlock('module_manual'); + $('h1').text('Back up your private key'); + } else if (this.action === 'backup_manual') { + this.displayBlock('module_manual'); + $('h1').text('Back up your private key'); + } else { // action = view status + $('.hide_if_backup_done').css('display', 'none'); + $('h1').text('Key Backups'); + this.displayBlock('loading'); + await this.statusModule.checkAndRenderBackupStatus(); + } } } - public renderBackupDone = async (backedUp = true) => { + public renderBackupDone = async (backedUpCount: number) => { if (this.action === 'setup_automatic' || this.action === 'setup_manual') { window.location.href = Url.create('/chrome/settings/setup.htm', { acctEmail: this.acctEmail, action: 'finalize', idToken: this.idToken }); - } else if (backedUp) { - await Ui.modal.info('Your private key has been successfully backed up'); - BrowserMsg.send.closePage(this.parentTabId as string); - } else { - window.location.href = Url.create('/chrome/settings/modules/backup.htm', { acctEmail: this.acctEmail, parentTabId: this.parentTabId as string }); + } else if (backedUpCount > 0) { + const pluralOrSingle = backedUpCount > 1 ? "keys have" : "key has"; + await Ui.modal.info(`Your private ${pluralOrSingle} been successfully backed up`); + if (this.parentTabId) { + BrowserMsg.send.closePage(this.parentTabId); + } + } else if (this.parentTabId) { // should be always true as setup_manual is excluded by this point + Settings.redirectSubPage(this.acctEmail, this.parentTabId, '/chrome/settings/modules/backup.htm'); } } @@ -100,6 +120,62 @@ export class BackupView extends View { this.manualModule.setHandlers(); } + private addKeyToBackup = (keyIdentity: KeyIdentity) => { + if (!this.prvKeysToManuallyBackup.some(prvIdentity => KeyUtil.identityEquals(prvIdentity, keyIdentity))) { + this.prvKeysToManuallyBackup.push(keyIdentity); + } + } + + private removeKeyToBackup = (keyIdentity: KeyIdentity) => { + this.prvKeysToManuallyBackup.splice(this.prvKeysToManuallyBackup.findIndex(prvIdentity => KeyUtil.identityEquals(prvIdentity, keyIdentity)), 1); + } + + private preparePrvKeysBackupSelection = async () => { + const kinfos = await KeyStore.getTypedKeyInfos(this.acctEmail); + if (this.keyIdentity && this.keyIdentity.type === 'openpgp' && kinfos.some(ki => KeyUtil.identityEquals(ki, this.keyIdentity!))) { + // todo: error if not found ? + this.addKeyToBackup({ id: this.keyIdentity.id, type: this.keyIdentity.type }); + } else if (kinfos.length > 1) { + await this.renderPrvKeysBackupSelection(kinfos); + } else if (kinfos.length === 1 && kinfos[0].type === 'openpgp') { + this.addKeyToBackup({ id: kinfos[0].id, type: kinfos[0].type }); + } + } + + private renderPrvKeysBackupSelection = async (kinfos: TypedKeyInfo[]) => { + for (const ki of kinfos) { + const email = Xss.escape(String(ki.emails![0])); + const dom = ` +
+
+ +
+
+ `.trim(); + $('.key_backup_selection').append(dom); // xss-escaped + if (ki.type === 'openpgp') { + this.addKeyToBackup({ type: ki.type, id: ki.id }); + } + } + $('.input_prvkey_backup_checkbox').click(Ui.event.handle((target) => { + const type = $(target).data('type') as string; + if (type === 'openpgp') { + const id = $(target).data('id') as string; + if ($(target).prop('checked')) { + this.addKeyToBackup({ type, id }); + } else { + this.removeKeyToBackup({ type, id }); + } + } + })); + $('#key_backup_selection_container').show(); + } } View.run(BackupView); diff --git a/extension/chrome/settings/setup/setup-create-key.ts b/extension/chrome/settings/setup/setup-create-key.ts index 7e78c158667..bd685320357 100644 --- a/extension/chrome/settings/setup/setup-create-key.ts +++ b/extension/chrome/settings/setup/setup-create-key.ts @@ -5,7 +5,7 @@ import { SetupOptions, SetupView } from '../setup.js'; import { Catch } from '../../../js/common/platform/catch.js'; import { Lang } from '../../../js/common/lang.js'; -import { KeyAlgo, KeyUtil } from '../../../js/common/core/crypto/key.js'; +import { KeyAlgo, KeyIdentity, KeyUtil } from '../../../js/common/core/crypto/key.js'; import { Settings } from '../../../js/common/settings.js'; import { Ui } from '../../../js/common/browser/ui.js'; import { Url } from '../../../js/common/core/common.js'; @@ -34,15 +34,17 @@ export class SetupCreateKeyModule { recovered: false, }; const keyAlgo = this.view.orgRules.getEnforcedKeygenAlgo() || $('#step_2a_manual_create .key_type').val() as KeyAlgo; - await this.createSaveKeyPair(opts, keyAlgo); - if (this.view.orgRules.canBackupKeys()) { - await this.view.preFinalizeSetup(opts); - const action = $('#step_2a_manual_create .input_backup_inbox').prop('checked') ? 'setup_automatic' : 'setup_manual'; - // only finalize after backup is done. backup.htm will redirect back to this page with ?action=finalize - window.location.href = Url.create('modules/backup.htm', { action, acctEmail: this.view.acctEmail, idToken: this.view.idToken }); - } else { - await this.view.submitPublicKeysAndFinalizeSetup(opts); - await this.view.setupRender.renderSetupDone(); + const keyIdentity = await this.createSaveKeyPair(opts, keyAlgo); + if (keyIdentity) { + if (this.view.orgRules.canBackupKeys()) { + await this.view.preFinalizeSetup(opts); + const action = $('#step_2a_manual_create .input_backup_inbox').prop('checked') ? 'setup_automatic' : 'setup_manual'; + // only finalize after backup is done. backup.htm will redirect back to this page with ?action=finalize + window.location.href = Url.create('modules/backup.htm', { action, acctEmail: this.view.acctEmail, idToken: this.view.idToken, id: keyIdentity.id, type: keyIdentity.type }); + } else { + await this.view.submitPublicKeysAndFinalizeSetup(opts); + await this.view.setupRender.renderSetupDone(); + } } } catch (e) { Catch.reportErr(e); @@ -65,18 +67,22 @@ export class SetupCreateKeyModule { } } - public createSaveKeyPair = async (options: SetupOptions, keyAlgo: KeyAlgo) => { + public createSaveKeyPair = async (options: SetupOptions, keyAlgo: KeyAlgo): Promise => { await Settings.forbidAndRefreshPageIfCannot('CREATE_KEYS', this.view.orgRules); const { full_name } = await AcctStore.get(this.view.acctEmail, ['full_name']); const pgpUids = [{ name: full_name || '', email: this.view.acctEmail }]; // todo - add all addresses? const expireMonths = this.view.orgRules.getEnforcedKeygenExpirationMonths(); + let keyIdentity: KeyIdentity | undefined; try { const key = await OpenPGPKey.create(pgpUids, keyAlgo, options.passphrase, expireMonths); const prv = await KeyUtil.parse(key.private); + keyIdentity = { id: prv.id, type: prv.type }; await this.view.saveKeysAndPassPhrase([prv], options); + return keyIdentity; } catch (e) { Catch.reportErr(e); Xss.sanitizeRender('#step_2_easy_generating, #step_2a_manual_create', Lang.setup.fcDidntSetUpProperly); + return keyIdentity; // todo: return undefined ? } } } diff --git a/extension/css/cryptup.css b/extension/css/cryptup.css index 1a0d11c2785..a6c3fd12ccb 100644 --- a/extension/css/cryptup.css +++ b/extension/css/cryptup.css @@ -44,6 +44,7 @@ button.swal2-styled:focus, text-decoration: none; } +.prv_fingerprint span, a { color: #337ab7; } .line.red, @@ -835,6 +836,11 @@ body#settings > div#content.backup div#module_manual table.backup_options label } body#settings .password_feedback { display: none; } +div#module_manual .key_backup_selection { + max-height: 300px; + overflow-y: scroll; +} + body#settings > div#content.setup div.manual .password_feedback { display: none; position: absolute; @@ -1086,6 +1092,7 @@ body#settings > div#content.storage .pre { body#settings > div#content .list_table { display: none; } body#settings > div#content .list_table table img { width: 15px; } +.prv_fingerprint span, body#settings > div#content .list_table table { width: 100%; font-size: 14px; @@ -2811,11 +2818,16 @@ body#select_account .action_add_account::before { padding-top: 0; margin-top: 0; } + +.m-0 { margin: 0 !important; } .mt-20 { margin-top: 20px; } .mt-40 { margin-top: 40px; } +.mb-20 { margin-bottom: 20px; } .display_none { display: none; } +.display_inline_block { display: inline-block; } + code { padding: 0.2em 0.4em; margin: 0; diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index d19d0f24f51..a861aef7244 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -19,9 +19,7 @@ import { PubkeyInfo } from '../../platform/store/contact-store.js'; * all dates are expressed as number of milliseconds since Unix Epoch. * This is what `Date.now()` returns and `new Date(x)` accepts. */ -export interface Key { - type: 'openpgp' | 'x509'; - id: string; // a fingerprint of the primary key in OpenPGP, and similarly a fingerprint of the actual cryptographic key (eg RSA fingerprint) in S/MIME +export interface Key extends KeyIdentity { allIds: string[]; // a list of fingerprints, including those for subkeys created: number; revoked: boolean; @@ -70,11 +68,18 @@ export interface KeyInfo { emails?: string[]; // todo - used to be missing - but migration was supposed to add it? setting back to optional for now } -export interface ExtendedKeyInfo extends KeyInfo { - passphrase?: string; +export interface KeyIdentity { + id: string, // a fingerprint of the primary key in OpenPGP, and similarly a fingerprint of the actual cryptographic key (eg RSA fingerprint) in S/MIME type: 'openpgp' | 'x509' } +export interface TypedKeyInfo extends KeyInfo, KeyIdentity { +} + +export interface ExtendedKeyInfo extends TypedKeyInfo { + passphrase?: string; +} + export type KeyAlgo = 'curve25519' | 'rsa2048' | 'rsa3072' | 'rsa4096'; export type PrvPacket = (OpenPGP.packet.SecretKey | OpenPGP.packet.SecretSubkey); @@ -83,6 +88,14 @@ export class UnexpectedKeyTypeError extends Error { } export class KeyUtil { + public static identityEquals = (keyIdentity1: KeyIdentity, keyIdentity2: KeyIdentity) => { + return keyIdentity1.id === keyIdentity2.id && keyIdentity1.type === keyIdentity2.type; + } + + public static filterKeys(kis: T[], ids: KeyIdentity[]): T[] { + return kis.filter(ki => ids.some(i => KeyUtil.identityEquals(i, ki))); + } + public static isWithoutSelfCertifications = async (key: Key) => { // all non-OpenPGP keys are automatically considered to be not // "without self certifications" @@ -361,6 +374,10 @@ export class KeyUtil { }; } + public static typedKeyInfoObj = async (prv: Key): Promise => { + return { ...await KeyUtil.keyInfoObj(prv), id: prv.id, type: prv.type }; + } + public static getPubkeyLongids = (pubkey: Key): string[] => { if (pubkey.type !== 'x509') { return pubkey.allIds.map(id => OpenPGPKey.fingerprintToLongid(id)); diff --git a/extension/js/common/platform/store/key-store.ts b/extension/js/common/platform/store/key-store.ts index d190067b3e6..da15a8805c0 100644 --- a/extension/js/common/platform/store/key-store.ts +++ b/extension/js/common/platform/store/key-store.ts @@ -1,6 +1,6 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ -import { KeyInfo, ExtendedKeyInfo, KeyUtil, Key } from '../../core/crypto/key.js'; +import { KeyInfo, TypedKeyInfo, ExtendedKeyInfo, KeyUtil, Key } from '../../core/crypto/key.js'; import { AcctStore } from './acct-store.js'; import { PassphraseStore } from './passphrase-store.js'; import { AbstractStore } from './abstract-store.js'; @@ -23,8 +23,7 @@ export class KeyStore extends AbstractStore { } public static getFirstOptional = async (acctEmail: string): Promise => { - const stored = await AcctStore.get(acctEmail, ['keys']); - const keys: KeyInfo[] = stored.keys || []; + const keys = await KeyStore.get(acctEmail); return keys[0]; } @@ -34,16 +33,23 @@ export class KeyStore extends AbstractStore { return key as KeyInfo; } - public static getAllWithOptionalPassPhrase = async (acctEmail: string): Promise => { + public static getTypedKeyInfos = async (acctEmail: string): Promise => { const keys = await KeyStore.get(acctEmail); - const withPp: ExtendedKeyInfo[] = []; + const kis: TypedKeyInfo[] = []; for (const ki of keys) { const type = KeyUtil.getKeyType(ki.private); - if (type === 'openpgp' || type === 'x509') { - withPp.push({ ...ki, type, passphrase: await PassphraseStore.get(acctEmail, ki.fingerprints[0]) }); + const id = ki.fingerprints[0]; + if (type !== 'openpgp' && type !== 'x509') { + continue; } + kis.push({ ...ki, type, id }); } - return withPp; + return kis; + } + + public static getAllWithOptionalPassPhrase = async (acctEmail: string): Promise => { + const keys = await KeyStore.getTypedKeyInfos(acctEmail); + return await Promise.all(keys.map(async (ki) => { return { ...ki, passphrase: await PassphraseStore.getByKeyIdentity(acctEmail, ki) }; })); } public static add = async (acctEmail: string, newKey: string | Key) => { diff --git a/extension/js/common/platform/store/passphrase-store.ts b/extension/js/common/platform/store/passphrase-store.ts index 509b282d04e..09db2b03de0 100644 --- a/extension/js/common/platform/store/passphrase-store.ts +++ b/extension/js/common/platform/store/passphrase-store.ts @@ -6,6 +6,7 @@ import { PromiseCancellation, Dict } from '../../core/common.js'; import { Ui } from '../../browser/ui.js'; import { OpenPGPKey } from '../../core/crypto/pgp/openpgp-key.js'; import { InMemoryStore } from './in-memory-store.js'; +import { KeyIdentity } from '../../core/crypto/key.js'; /** * Local or session store of pass phrases @@ -13,7 +14,7 @@ import { InMemoryStore } from './in-memory-store.js'; export class PassphraseStore extends AbstractStore { /** - * todo - make it only accept fingerprints + * todo - phase out in favour of KeyIdentity */ public static set = async (storageType: StorageType, acctEmail: string, fingerprintOrLongid: string, passphrase: string | undefined) => { const longid = fingerprintOrLongid.length === 40 ? OpenPGPKey.fingerprintToLongid(fingerprintOrLongid) : fingerprintOrLongid; @@ -32,7 +33,7 @@ export class PassphraseStore extends AbstractStore { } /** - * todo - make it only accept fingerprints + * todo - phase out in favour of KeyIdentity */ public static get = async (acctEmail: string, fingerprintOrLongid: string, ignoreSession: boolean = false): Promise => { const longid = fingerprintOrLongid.length === 40 ? OpenPGPKey.fingerprintToLongid(fingerprintOrLongid) : fingerprintOrLongid; @@ -48,8 +49,13 @@ export class PassphraseStore extends AbstractStore { return await InMemoryStore.get(acctEmail, storageIndex) ?? undefined; } + public static getByKeyIdentity = async (acctEmail: string, keyIdentity: KeyIdentity, ignoreSession: boolean = false): Promise => { + // todo -- consider x509/openpgp ambiguity + return await PassphraseStore.get(acctEmail, keyIdentity.id, ignoreSession); + } + /** - * todo - make it only accept fingerprints + * todo - phase out in favour of KeyIdentity */ public static waitUntilPassphraseChanged = async ( acctEmail: string, missingOrWrongPpKeyLongids: string[], interval = 1000, cancellation: PromiseCancellation = { cancel: false } diff --git a/extension/js/common/view.ts b/extension/js/common/view.ts index daefc9aa263..92a0bec3935 100644 --- a/extension/js/common/view.ts +++ b/extension/js/common/view.ts @@ -14,7 +14,7 @@ export abstract class View { const view = new viewClass(); (async () => { await view.render(); - view.setHandlers(); + await Promise.resolve(view.setHandlers()); // allow both sync and async View.setTestViewStateLoaded(); })().catch(View.reportAndRenderErr); } catch (e) { @@ -42,7 +42,7 @@ export abstract class View { public abstract render(): Promise; - public abstract setHandlers(): void; + public abstract setHandlers(): void | Promise; public setHandler = (cb: (e: HTMLElement, event: JQuery.Event) => void | Promise, errHandlers?: BrowserEventErrHandler) => { return Ui.event.handle(cb, errHandlers, this); diff --git a/test/source/mock/google/google-endpoints.ts b/test/source/mock/google/google-endpoints.ts index a7ed8fa1263..c61fab9df64 100644 --- a/test/source/mock/google/google-endpoints.ts +++ b/test/source/mock/google/google-endpoints.ts @@ -15,7 +15,8 @@ type DraftSaveModel = { message: { raw: string, threadId: string } }; const allowedRecipients: Array = ['flowcrypt.compatibility@gmail.com', 'human+manualcopypgp@flowcrypt.com', 'censored@email.com', 'test@email.com', 'human@flowcrypt.com', 'human+nopgp@flowcrypt.com', 'expired.on.attester@domain.com', 'ci.tests.gmail@flowcrypt.test', 'smime1@recipient.com', 'smime2@recipient.com', 'smime@recipient.com', - 'smime.attachment@recipient.com', 'auto.refresh.expired.key@recipient.com', 'to@example.com', 'cc@example.com', 'bcc@example.com']; + 'smime.attachment@recipient.com', 'auto.refresh.expired.key@recipient.com', 'to@example.com', 'cc@example.com', 'bcc@example.com', + 'flowcrypt.test.key.multiple.inbox1@gmail.com', 'flowcrypt.test.key.multiple.inbox2@gmail.com']; export const mockGoogleEndpoints: HandlersDefinition = { '/o/oauth2/auth': async ({ query: { client_id, response_type, access_type, state, redirect_uri, scope, login_hint, proceed } }, req) => { diff --git a/test/source/mock/google/strategies/send-message-strategy.ts b/test/source/mock/google/strategies/send-message-strategy.ts index bd8386727f3..2eda9549aac 100644 --- a/test/source/mock/google/strategies/send-message-strategy.ts +++ b/test/source/mock/google/strategies/send-message-strategy.ts @@ -11,6 +11,7 @@ import { HttpClientErr } from '../../lib/api'; import { MsgUtil } from '../../../core/crypto/pgp/msg-util'; import { parsedMailAddressObjectAsArray } from '../google-endpoints.js'; import { Str } from '../../../core/common.js'; +import { GMAIL_RECOVERY_EMAIL_SUBJECTS } from '../../../core/const.js'; // TODO: Make a better structure of ITestMsgStrategy. Because this class doesn't test anything, it only saves message in the Mock class SaveMessageInStorageStrategy implements ITestMsgStrategy { @@ -232,6 +233,8 @@ export class TestBySubjectStrategyContext { this.strategy = new SmimeEncryptedMessageStrategy(); } else if (subject.includes('send signed S/MIME without attachment')) { this.strategy = new SmimeSignedMessageStrategy(); + } else if (GMAIL_RECOVERY_EMAIL_SUBJECTS.includes(subject)) { + this.strategy = new SaveMessageInStorageStrategy(); } else { throw new UnsuportableStrategyError(`There isn't any strategy for this subject: ${subject}`); } diff --git a/test/source/tests/settings.ts b/test/source/tests/settings.ts index 31a7eca21f4..559854735b6 100644 --- a/test/source/tests/settings.ts +++ b/test/source/tests/settings.ts @@ -18,7 +18,10 @@ import { testConstants } from './tooling/consts'; import { PageRecipe } from './page-recipe/abstract-page-recipe'; import { OauthPageRecipe } from './page-recipe/oauth-page-recipe'; import { Pubkey } from '../platform/store/contact-store'; -import { KeyInfo } from '../core/crypto/key'; +import { KeyInfo, KeyUtil } from '../core/crypto/key'; +import { Buf } from '../core/buf'; +import { GoogleData } from '../mock/google/google-data'; +import Parse from './../util/parse'; // tslint:disable:no-blank-lines-func @@ -519,6 +522,293 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T await settingsPage.close(); })); + ava.default('settings - manual backup several keys to file with the same pass phrase', testWithBrowser(undefined, async (t, browser) => { + const acctEmail = 'flowcrypt.test.key.multiple@gmail.com'; + const settingsPage1 = await BrowserRecipe.openSettingsLoginApprove(t, browser, acctEmail); + await SetupPageRecipe.manualEnter(settingsPage1, 'unused', { + submitPubkey: false, + usedPgpBefore: false, + key: { + title: '?', + armored: testConstants.testKeyMultiple1b383d0334e38b28, + passphrase: '1234', + longid: '1b383d0334e38b28', + } + }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); + await settingsPage1.close(); + await SettingsPageRecipe.addKeyTest(t, browser, acctEmail, testConstants.testKeyMultiple98acfa1eadab5b92, '1234', + { isSavePassphraseChecked: true, isSavePassphraseHidden: false }); + await SettingsPageRecipe.addKeyTest(t, browser, acctEmail, testConstants.testKeyMultipleSmimeCEA2D53BB9D24871, '1234', + { isSavePassphraseChecked: true, isSavePassphraseHidden: false }); + await SettingsPageRecipe.addKeyTest(t, browser, acctEmail, testConstants.testKeyMultipleSmimeA35068FD4E037879, '1234', + { isSavePassphraseChecked: true, isSavePassphraseHidden: false }); + // opening backup.htm independently of settings/index.htm page limits functionality but sufficient for this test + const backupPage = await browser.newPage(t, TestUrls.extension(`/chrome/settings/modules/backup.htm?acctEmail=${acctEmail}&action=backup_manual&parentTabId=1%3A0`)); + // OpenPGP keys are checked, x509 keys are unchecked + expect(await backupPage.isChecked('[data-id="47FB03183E03A8ED44E3BBFCCEA2D53BB9D24871"]')).to.equal(false); + expect(await backupPage.isChecked('[data-id="5A08466253C956E9C76C2E95A35068FD4E037879"]')).to.equal(false); + expect(await backupPage.isChecked('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(true); + expect(await backupPage.isChecked('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(true); + // OpenPGP keys are enabled, x509 keys are disabled + expect(await backupPage.isDisabled('[data-id="47FB03183E03A8ED44E3BBFCCEA2D53BB9D24871"]')).to.equal(true); + expect(await backupPage.isDisabled('[data-id="5A08466253C956E9C76C2E95A35068FD4E037879"]')).to.equal(true); + expect(await backupPage.isDisabled('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(false); + expect(await backupPage.isDisabled('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(false); + await backupPage.waitAndClick('@input-backup-step3manual-file'); + await backupPage.waitAndClick('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]'); // uncheck + // backing up to file when only one key is checked + const backupFileRawData1 = await backupPage.awaitDownloadTriggeredByClicking('@action-backup-step3manual-continue'); + const { keys: keys1 } = await KeyUtil.readMany(Buf.fromUtfStr(backupFileRawData1.toString())); + expect(keys1.length).to.equal(1); + expect(keys1[0].id).to.equal("515431151DDD3EA232B37A4C98ACFA1EADAB5B92"); + await backupPage.waitAndRespondToModal('info', 'confirm', 'Downloading private key backup file'); + await backupPage.waitAndRespondToModal('info', 'confirm', 'Your private key has been successfully backed up'); + await backupPage.waitAndClick('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]'); // check + // backing up to file when two keys are checked + const backupFileRawData2 = await backupPage.awaitDownloadTriggeredByClicking('@action-backup-step3manual-continue'); + const { keys: keys2 } = await KeyUtil.readMany(Buf.fromUtfStr(backupFileRawData2.toString())); + expect(keys2.length).to.equal(2); + await backupPage.close(); + })); + + ava.default('settings - manual backup several keys to inbox with the same strong pass phrase', testWithBrowser(undefined, async (t, browser) => { + const acctEmail = 'flowcrypt.test.key.multiple.inbox2@gmail.com'; + const passphrase = 'strong enough passphrase for all keys'; + const settingsPage1 = await BrowserRecipe.openSettingsLoginApprove(t, browser, acctEmail); + const key1b383d0334e38b28 = await KeyUtil.parse(testConstants.testKeyMultiple1b383d0334e38b28); + expect(await KeyUtil.decrypt(key1b383d0334e38b28, '1234')).to.equal(true); + await KeyUtil.encrypt(key1b383d0334e38b28, passphrase); + const key98acfa1eadab5b92 = await KeyUtil.parse(testConstants.testKeyMultiple98acfa1eadab5b92); + expect(await KeyUtil.decrypt(key98acfa1eadab5b92, '1234')).to.equal(true); + await SetupPageRecipe.manualEnter(settingsPage1, 'unused', { + submitPubkey: false, + usedPgpBefore: false, + key: { title: '?', armored: KeyUtil.armor(key1b383d0334e38b28), passphrase, longid: '1b383d0334e38b28' } + }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); + await settingsPage1.close(); + await SettingsPageRecipe.addKeyTest(t, browser, acctEmail, KeyUtil.armor(key98acfa1eadab5b92), passphrase, + { isSavePassphraseChecked: true, isSavePassphraseHidden: false }); + // opening backup.htm independently of settings/index.htm page limits functionality but sufficient for this test + const backupPage = await browser.newPage(t, TestUrls.extension(`/chrome/settings/modules/backup.htm?acctEmail=${acctEmail}&action=backup_manual&parentTabId=1%3A0`)); + expect(await backupPage.isChecked('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(true); + expect(await backupPage.isChecked('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(true); + expect(await backupPage.isDisabled('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(false); + expect(await backupPage.isDisabled('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(false); + await backupPage.waitAndClick('@action-backup-step3manual-continue'); + await backupPage.waitAndRespondToModal('info', 'confirm', 'Your private keys have been successfully backed up'); + const sentMsg = (await GoogleData.withInitializedData(acctEmail)).getMessageBySubject('Your FlowCrypt Backup')!; + const mimeMsg = await Parse.convertBase64ToMimeMsg(sentMsg.raw!); + const { keys } = await KeyUtil.readMany(new Buf(mimeMsg.attachments[0]!.content!)); + expect(keys.length).to.equal(2); + await backupPage.close(); + })); + + ava.default('settings - manual backup several keys to file with a missing but guessed pass phrase', testWithBrowser(undefined, async (t, browser) => { + const acctEmail = 'flowcrypt.test.key.multiple@gmail.com'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acctEmail); + await SetupPageRecipe.manualEnter(settingsPage, 'unused', { + submitPubkey: false, + usedPgpBefore: false, + key: { + title: '?', + armored: testConstants.testKeyMultiple1b383d0334e38b28, + passphrase: '1234', + longid: '1b383d0334e38b28', + } + }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); + await settingsPage.close(); + const inboxPage = await browser.newPage(t, TestUrls.extension(`chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}`)); + await InboxPageRecipe.finishSessionOnInboxPage(inboxPage); + await SettingsPageRecipe.addKeyTest(t, browser, acctEmail, testConstants.testKeyMultiple98acfa1eadab5b92, '1234', + { isSavePassphraseChecked: true, isSavePassphraseHidden: false }); + // opening backup.htm independently of settings/index.htm page limits functionality but sufficient for this test + const backupPage = await browser.newPage(t, TestUrls.extension(`/chrome/settings/modules/backup.htm?acctEmail=${acctEmail}` + + `&action=backup_manual&parentTabId=1%3A0`)); + expect(await backupPage.isChecked('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(true); + expect(await backupPage.isChecked('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(true); + expect(await backupPage.isDisabled('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(false); + expect(await backupPage.isDisabled('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(false); + await backupPage.waitAndClick('@input-backup-step3manual-file'); + // one passphrase is not known but successfully guessed + const backupFileRawData = await backupPage.awaitDownloadTriggeredByClicking('@action-backup-step3manual-continue'); + const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(backupFileRawData.toString())); + expect(keys.length).to.equal(2); + await backupPage.close(); + })); + + ava.default('settings - manual backup several keys to inbox with a missing pass phrase', testWithBrowser(undefined, async (t, browser) => { + const acctEmail = 'flowcrypt.test.key.multiple@gmail.com'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acctEmail); + await SetupPageRecipe.manualEnter(settingsPage, 'unused', { + submitPubkey: false, + usedPgpBefore: false, + key: { + title: '?', + armored: testConstants.testKeyMultiple1b383d0334e38b28, + passphrase: '1234', + longid: '1b383d0334e38b28', + } + }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); + const inboxPage = await browser.newPage(t, TestUrls.extension(`chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}`)); + await InboxPageRecipe.finishSessionOnInboxPage(inboxPage); + await inboxPage.close(); + const key98acfa1eadab5b92 = await KeyUtil.parse(testConstants.testKeyMultiple98acfa1eadab5b92); + expect(await KeyUtil.decrypt(key98acfa1eadab5b92, '1234')).to.equal(true); + await SettingsPageRecipe.addKeyTest(t, browser, acctEmail, KeyUtil.armor(key98acfa1eadab5b92), 'new passphrase strong enough', + { isSavePassphraseChecked: true, isSavePassphraseHidden: false }); + await settingsPage.waitAndClick('@action-open-backup-page'); + const backupFrame = await settingsPage.getFrame(['backup.htm']); + await backupFrame.waitAndClick('@action-go-manual'); + expect(await backupFrame.isChecked('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(true); + expect(await backupFrame.isChecked('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(true); + expect(await backupFrame.isDisabled('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(false); + expect(await backupFrame.isDisabled('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(false); + await backupFrame.waitAndClick('@action-backup-step3manual-continue'); + await backupFrame.waitAndRespondToModal('error', 'confirm', 'Your keys are protected with different pass phrases'); + await settingsPage.close(); + })); + + ava.default('settings - manual backup a key with a missing pass phrase', testWithBrowser(undefined, async (t, browser) => { + const acctEmail = 'flowcrypt.test.key.multiple@gmail.com'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acctEmail); + const key = { title: '?', armored: testConstants.testKeyMultiple1b383d0334e38b28, passphrase: '1234', longid: '1b383d0334e38b28' }; + await SetupPageRecipe.manualEnter(settingsPage, 'unused', { submitPubkey: false, usedPgpBefore: false, key }, + { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); + const inboxPage = await browser.newPage(t, TestUrls.extension(`chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}`)); + await InboxPageRecipe.finishSessionOnInboxPage(inboxPage); + await inboxPage.close(); + await settingsPage.waitAndClick('@action-open-backup-page'); + const backupFrame = await settingsPage.getFrame(['backup.htm']); + await backupFrame.waitAndClick('@action-go-manual'); + await backupFrame.waitAndClick('@action-backup-step3manual-continue'); + const ppFrame = await settingsPage.getFrame(['passphrase.htm']); + await ppFrame.waitAndType('@input-pass-phrase', key.passphrase); + await ppFrame.waitAndClick('@action-confirm-pass-phrase-entry'); + await Util.sleep(2); + expect(ppFrame.frame.isDetached()).to.equal(true); + // todo: #4059 we would expect further iteraction with backupFrame here but it is actually wiped out + await settingsPage.close(); + })); + + ava.default('settings - manual backup several keys to file with different pass phrases', testWithBrowser(undefined, async (t, browser) => { + const acctEmail = 'flowcrypt.test.key.multiple@gmail.com'; + const settingsPage1 = await BrowserRecipe.openSettingsLoginApprove(t, browser, acctEmail); + await SetupPageRecipe.manualEnter(settingsPage1, 'unused', { + submitPubkey: false, + usedPgpBefore: false, + key: { + title: '?', + armored: testConstants.testKeyMultiple1b383d0334e38b28, + passphrase: '1234', + longid: '1b383d0334e38b28', + } + }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); + await settingsPage1.close(); + const key98acfa1eadab5b92 = await KeyUtil.parse(testConstants.testKeyMultiple98acfa1eadab5b92); + expect(await KeyUtil.decrypt(key98acfa1eadab5b92, '1234')).to.equal(true); + await SettingsPageRecipe.addKeyTest(t, browser, acctEmail, KeyUtil.armor(key98acfa1eadab5b92), 'new passphrase strong enough', + { isSavePassphraseChecked: true, isSavePassphraseHidden: false }); + // opening backup.htm independently of settings/index.htm page limits functionality but sufficient for this test + const backupPage = await browser.newPage(t, TestUrls.extension(`/chrome/settings/modules/backup.htm?acctEmail=${acctEmail}&action=backup_manual&parentTabId=1%3A0`)); + expect(await backupPage.isChecked('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(true); + expect(await backupPage.isChecked('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(true); + expect(await backupPage.isDisabled('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(false); + expect(await backupPage.isDisabled('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(false); + await backupPage.waitAndClick('@input-backup-step3manual-file'); + await backupPage.waitAndClick('@action-backup-step3manual-continue'); + await backupPage.waitAndRespondToModal('error', 'confirm', 'Your keys are protected with different pass phrases'); + await backupPage.close(); + })); + + ava.default('settings - setup_manual only backs up supplied key to file', testWithBrowser(undefined, async (t, browser) => { + const acctEmail = 'flowcrypt.test.key.multiple@gmail.com'; + const settingsPage1 = await BrowserRecipe.openSettingsLoginApprove(t, browser, acctEmail); + await SetupPageRecipe.manualEnter(settingsPage1, 'unused', { + submitPubkey: false, + usedPgpBefore: false, + key: { + title: '?', + armored: testConstants.testKeyMultiple1b383d0334e38b28, + passphrase: '1234', + longid: '1b383d0334e38b28', + } + }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); + await settingsPage1.close(); + await SettingsPageRecipe.addKeyTest(t, browser, acctEmail, testConstants.testKeyMultiple98acfa1eadab5b92, '1234', + { isSavePassphraseChecked: true, isSavePassphraseHidden: false }); + const backupPage = await browser.newPage(t, TestUrls.extension(`/chrome/settings/modules/backup.htm?acctEmail=${acctEmail}&action=setup_manual` + + '&type=openpgp&id=515431151DDD3EA232B37A4C98ACFA1EADAB5B92&idToken=fakeheader.01')); + await backupPage.waitAndClick('@input-backup-step3manual-file'); + const backupFileRawData = await backupPage.awaitDownloadTriggeredByClicking('@action-backup-step3manual-continue'); + const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(backupFileRawData.toString())); + expect(keys.length).to.equal(1); + expect(keys[0].id).to.equal("515431151DDD3EA232B37A4C98ACFA1EADAB5B92"); + await backupPage.waitAndRespondToModal('info', 'confirm', 'Downloading private key backup file'); + await backupPage.waitAll('@action-step4done-account-settings'); + await backupPage.close(); + })); + + ava.default('settings - setup_manual only backs up supplied key to inbox', testWithBrowser(undefined, async (t, browser) => { + const acctEmail = 'flowcrypt.test.key.multiple.inbox1@gmail.com'; + const settingsPage1 = await BrowserRecipe.openSettingsLoginApprove(t, browser, acctEmail); + await SetupPageRecipe.manualEnter(settingsPage1, 'unused', { + submitPubkey: false, + usedPgpBefore: false, + key: { + title: '?', + armored: testConstants.testKeyMultiple1b383d0334e38b28, + passphrase: '1234', + longid: '1b383d0334e38b28', + } + }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); + await settingsPage1.close(); + const passphrase = 'strong enough passphrase for inbox backup'; + const key98acfa1eadab5b92 = await KeyUtil.parse(testConstants.testKeyMultiple98acfa1eadab5b92); + expect(await KeyUtil.decrypt(key98acfa1eadab5b92, '1234')).to.equal(true); + await KeyUtil.encrypt(key98acfa1eadab5b92, passphrase); + await SettingsPageRecipe.addKeyTest(t, browser, acctEmail, KeyUtil.armor(key98acfa1eadab5b92), passphrase, + { isSavePassphraseChecked: true, isSavePassphraseHidden: false }); + const backupPage = await browser.newPage(t, TestUrls.extension(`/chrome/settings/modules/backup.htm?acctEmail=${acctEmail}&action=setup_manual` + + '&type=openpgp&id=515431151DDD3EA232B37A4C98ACFA1EADAB5B92&idToken=fakeheader.01')); + await backupPage.waitAndClick('@action-backup-step3manual-continue'); + await backupPage.waitAll('@action-step4done-account-settings'); + const sentMsg = (await GoogleData.withInitializedData(acctEmail)).getMessageBySubject('Your FlowCrypt Backup')!; + const mimeMsg = await Parse.convertBase64ToMimeMsg(sentMsg.raw!); + const { keys } = await KeyUtil.readMany(new Buf(mimeMsg.attachments[0]!.content!)); + expect(keys.length).to.equal(1); + expect(KeyUtil.identityEquals(keys[0], { id: '515431151DDD3EA232B37A4C98ACFA1EADAB5B92', type: 'openpgp' })).to.equal(true); + await backupPage.close(); + })); + + ava.default('settings - manual backup to inbox keys with weak pass phrases results in error', testWithBrowser(undefined, async (t, browser) => { + const acctEmail = 'flowcrypt.test.key.multiple@gmail.com'; + const settingsPage1 = await BrowserRecipe.openSettingsLoginApprove(t, browser, acctEmail); + await SetupPageRecipe.manualEnter(settingsPage1, 'unused', { + submitPubkey: false, + usedPgpBefore: false, + key: { + title: '?', + armored: testConstants.testKeyMultiple1b383d0334e38b28, + passphrase: '1234', + longid: '1b383d0334e38b28', + } + }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); + await settingsPage1.close(); + await SettingsPageRecipe.addKeyTest(t, browser, acctEmail, testConstants.testKeyMultiple98acfa1eadab5b92, '1234', + { isSavePassphraseChecked: true, isSavePassphraseHidden: false }); + // opening backup.htm independently of settings/index.htm page limits functionality but sufficient for this test + const backupPage = await browser.newPage(t, TestUrls.extension(`/chrome/settings/modules/backup.htm?acctEmail=${acctEmail}&action=backup_manual&parentTabId=1%3A0`)); + expect(await backupPage.isChecked('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(true); + expect(await backupPage.isChecked('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(true); + expect(await backupPage.isDisabled('[data-id="CB0485FE44FC22FF09AF0DB31B383D0334E38B28"]')).to.equal(false); + expect(await backupPage.isDisabled('[data-id="515431151DDD3EA232B37A4C98ACFA1EADAB5B92"]')).to.equal(false); + await backupPage.waitAndClick('@action-backup-step3manual-continue'); + await backupPage.waitAndRespondToModal('warning', 'confirm', `It's too weak for this backup method`); + await Util.sleep(2); + await backupPage.waitAny('@action-show-confirm-new-pp'); + await backupPage.close(); + })); + ava.default('settings - manual enter and key update honor FORBID_STORING_PASS_PHRASE OrgRule', testWithBrowser(undefined, async (t, browser) => { const { settingsPage, passphrase } = await BrowserRecipe.setUpFcForbidPpStoringAcct(t, browser); const { cryptup_userforbidstoringpassphraseorgruleflowcrypttest_passphrase_B8F687BCDE14435A: savedPassphrase1, diff --git a/test/source/tests/tooling/consts.ts b/test/source/tests/tooling/consts.ts index ed093edc7dd..1d941df8278 100644 --- a/test/source/tests/tooling/consts.ts +++ b/test/source/tests/tooling/consts.ts @@ -279,6 +279,112 @@ Qhb2hMOaD7jK2qcP3mTFIBuYt0bPEZLhyMssa9ssK0dcY+JWs6LOeMPzCC1+bz7jCHiBCG5DBG8j h6VN43eiceJMKCGcndDbIkmHT/JiAtUj2VdKXAldLPIuECUzXtTGL8LdqgSYvxo0b0RiIv6pBNI+ =Iqrh -----END PGP PRIVATE KEY BLOCK-----`, + testKeyMultipleSmimeCEA2D53BB9D24871: `-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA2mG28OIpKeGji8ZrFL1adAQ+one3cSv2W3Qu658stV/55EQB +xH98a4z2ZOCjz8vBRkoKf4yC9SPrxIJXBD2wZutWSnWBclbcvZJlmtG+mJH/Lqyk +dIQH0gmYXhObmtDkOnH5nTiGSiVUSy4wEKpdzARpeGlqp+vOrIKImsyMW4CfBz4u +iPRj0pN0Ugx6pLF6kFGKDS+VIItlhmIQcn/gQebEurwNE4A/hZEZxRguemPdFpJ3 +t0KH2T+f1TwXmFe258ZCQJeCOcgzTjCq5wSC+qSgVKqJY1+gEVBSwlhfshtazFAY +BoE9Rg8dktfn4KUsUQaWJnsX7HrEoQNJyct9/QIDAQABAoIBAEWBklOK+CUPXxlu +1sgsGG0SqMjXJ7jKkEe9a+2spPM5j/S6PIHJPdWRcR7cbOcHaAWyLTM5irjxbOoH +viBobgvj3XwneELnKKzhupsTJQG0fi7h3hoDp+WPDkLrJLyavfloK1WCFyPkS7b+ +BWhYgTAdO2CK1NqLcCVyCn6Smh3I9LsA0z9VsMdPW4pKtQxNEuLMy3zb5Avy9RWH ++/K3ysm/HHZi7yT84VKKzJEdCBqyEqIDEPibulbEZkQuBrvfrOWzz1lqI63ZDFkm +by8XtyDh3Tox228Es4gdxmk4QCwEOU/51CuSCMvWAaUWd0ob+VS4mISCijLkbBCf +nT41nQECgYEA9K+ln0pMBTgHD4hUhA3tIr1X8YtSl08FbZYg/dCPhq4/S0CnJJ0G +h9G4V7yyNyYu0yZj+FK9ZC46j8oTKTFt6vB+gQLHG/HowE0df4MbIGCA/vpbsZdO +/+2WUtfJ/ecKx9/89wHQkQIX95LWLu4P9+xncq3kp+YbVpBkXq2yPL0CgYEA5Hqz +qtHokU2PGHecKTbm6dkwYNqg2wO4JuN3CwPIDUUJDbonTHBKJOOfoFhGbza5vc75 +k8HiMWUz29cFZv8d0wSpTI94yESAS5GqEp38+E/HI/jozGv0UUnd7Z4QP7IlXDIn +2l7n0p6tPDmdQITfUK+PjI/6ow//1kX8uL54ekECgYA5um8S486HtK5Fxd5awYZf +GdjzzfEQbb5UGoVyHJCgL+AS+w/0HW/6/0nEuWo6hLGrc3VGkw7H4fhDEGPw5g6O +zWqrOWKMf0hwhkEdYms1+k602ZBkl9Q1oXJD3VF0q9vpSDlAdZclfx70dBpAPD5c +OdQ2RDycODfe9nYXuFdC6QKBgGlebR2KUMcyDZwrDUm0mCmgkD8alLRssrC8llnu +FISKIfLCtvz5jDgXAbbg/xqrEl4G5fLOp5JAKfpo34TwgsCelbVJNVqmvMQdGWIc +ml2p7R0Z3cLoxnP2rPK4wqWdcr1iJB8BRchMhXgQ2gS3QoXjcUBR5jTtW/9lp3dL +4CQBAoGAJPZ8pb1wmMohs4ZvcnbH0Y+uoMdoxQ+FLD4PzaPlRvZUQ8/PIBYyJ/f+ +lpiLc4Tbuv6u4a6thk6X8i/hFQQs9Uo6cil5Onj7llOh9YrmAqPAeiZTZLwHhd1L +6VL6r+WR5hdFzNldGFQdZXJwUXArSmUHebw8QESneljBahWgKC0= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIEUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADB0MRMwEQYDVQQIDApTb21l +LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFzAVBgNV +BAMMDlNvbWUgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJhdXRob3JpdHlAdGVz +dC5jb20wIBcNMjEwOTI2MTAzOTUwWhgPMjEyMTA5MjYxMDM5NTBaMDAxLjAsBgNV +BAMTJWZsb3djcnlwdC50ZXN0LmtleS5tdWx0aXBsZUBnbWFpbC5jb20wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaYbbw4ikp4aOLxmsUvVp0BD6id7dx +K/ZbdC7rnyy1X/nkRAHEf3xrjPZk4KPPy8FGSgp/jIL1I+vEglcEPbBm61ZKdYFy +Vty9kmWa0b6Ykf8urKR0hAfSCZheE5ua0OQ6cfmdOIZKJVRLLjAQql3MBGl4aWqn +686sgoiazIxbgJ8HPi6I9GPSk3RSDHqksXqQUYoNL5Ugi2WGYhByf+BB5sS6vA0T +gD+FkRnFGC56Y90Wkne3QofZP5/VPBeYV7bnxkJAl4I5yDNOMKrnBIL6pKBUqolj +X6ARUFLCWF+yG1rMUBgGgT1GDx2S1+fgpSxRBpYmexfsesShA0nJy339AgMBAAGj +MjAwMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgL0MBMGA1UdJQQMMAoGCCsGAQUF +BwMEMA0GCSqGSIb3DQEBBQUAA4ICAQDbBf3xb7esrnxEjwL/2dvnHAqgUTVaf6O4 +hW9mUp4c727fbDqat9OlilI/fLcwBN/NgEf+9jSWq9uHvjklCIXNTaK8zTkuQdtX +FXZjT7N9EXWO93wUFtBY/fUIhJAEaL7Z6rmWUkK6UW/mtV7I6gxiZ2GAAE2wFKyS +atb/Q+sgyeSOZjxux9NRMh8ZXmHTBQ/RUkzyLFM4Kgn5qfbM2vpkVuwjc4aH6r6F +qeRKf1HULybJn/aHQ6zxVqNYodekUHe55DIAA71X2PVEaltN7szE4K+obdcE4y2I +OW0w2H/SvBgngkn1aIvHIOltSBczvnSqKyQnLae4FW8PJJyk/X1HkAZPlMIRXXl9 +eUsv81oSiSK7YmRGuObgldBeDYmmECXnknsOPekK+mBaBbNGZNFMS6iJZV4anl4n +UGgG49YEW97yFhJ4r7CxLhP2Eo7Fbfax4wUDPfaNLv9OK3zn27sjA/QSI3q7eBvi +vn9dCnAk/hIBj/FsiyZstcczmvnHE9w9NDdCc0bbYcfwHAAs7XNejjDcenA+9ngq +4hQ6ofK/4WIVekDJLRvoxNY94qjErT6iXL5aduYu8BVri2Q3Jqrp9YILLVH83rkl +W8ZiWSb2P6bWZG3sxVNxS5iM6v39NbubnqK5UxE6+/GCeo+lHRNzZhhR3VRpKVw2 +xSncWuKSTg== +-----END CERTIFICATE-----`, + testKeyMultipleSmimeA35068FD4E037879: `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuhfYg465kFwSBiy2l8HPsLIhqub64Hnf4cQW0j2tK5vmnP1k +QR3QbiGj3e9fPItetecNmDRvqLp3VibqPEzsaDJEJK2S+FGKb6asde2pTg4zfkrf +VoMIr8gnMgnrmbY8p98T1ob+CzFMvGLxAjdDC0Jz4FTHC/WRmb++kE6KgE2ekFXe +8hjS4I2RYq2glOY8SGIGhVQq+/Y2GIU6GT2OS+pK8jbkYbzYd6SMT6J0qW7H5M1e +5y8bg98dbBmhUGWb4HXA1HhwgfPfEh5JfM0rANS1wMMJaelayZqmHXU2/6gBYlv9 +yyM32Jfpf/eGLlfats59BQVHqcSZaxxeme2f5QIDAQABAoIBAQCLj+KjF2D3cPVb +iCEQQxv9yjoRr9SqkPxkluj+l57Nu8gCKM8NPszYK4Z3pVPpViSbi0LYyUqMYfup +stmsi4wowLn2P+6fKJgIEBn6/1gf3+qiZoiEmxTir9slXedBijiHNXftRaQ0xK5m +AlX5cXRod1fr/+ifgXXGttS16RFaEpQlWl5XHZzMiWfPFcAfuLomoV6bl97f45Jy +I9LcZIv/0/USwzP56ZLodxZOV0D5pOtJmRfJZ3Zz4Hkh5Y7X1H5gDqNzZCvl+DAo +UMLHCdbjfucIDesLAwizAYtrz/ozer1sKSyTYu0CF9t1jZrBqTeZAtfgQPPlzb1Y +8orstFvpAoGBAOknVdklo3ju4qqOPhP2wiSmCcTzggfA6yC62SQVjEGpIzIbZHyF +aE5VZKKl37JSP7Sv5ADhYqKlmQDpKh2JupmrBJag+OxacZZiAHaU/FO2+XNQGSeO +61ZPbwZ3yc6Vsr80EwkVn8c//vvTjm2WFtXIPKd0Vj7fRwVx3BTz9RILAoGBAMxT +/x4yu9AgCHICkJ4EXAW4WR/thR8jUj2pJINEisO2L1tj44G4chlZBdKySMSmArQr +O20H9X7R4hTKXKvtPEtyzcknx+XbAV8tAzvWgoE8g2YnwoX7dkDIFBwqfZfrLVmP +/oLzlMBAEYTcGtdd4xzxLptZSATZcOJOe/0bgrvPAoGAagJOkEsCxuum9/Fw3Fbn +8w3jhz9IsgnPXrRWIogBm6ExtYiq6csmn+dHuIQ+769h89+9hYD0/grZRIa/dWq4 +A0K09V//jkeZOc0ZdNFM4StixEF5Dnl3G2lGi+RPR7Tc7v7YYNsKKOg6IXoRWlZR +z6YZnfHRfQi8HAenD4fUt/0CgYBmtt7rnlNdl4zHS1TJqqVb1sUu0Kb39TiVeJ67 +QkyDf+Ukiv1GK6Nn7KGJvkfoWw/G5Gi2MacOfpGpI+UcJGEICIk8UrwhZ0u0PZt/ +jCEEuJfRVbFqyvMyM6IwWofSQ84DVOh7idlAH7Tu1frEQxU1amSkgWg7z9b4XL3l +idvZbwKBgQCj4FU18598duZgT3tSeHYBJ3Anctus6JfqrPBMEpKjuuI8nDW1KWPZ +P6fMYW/HwX2zpr9nhbZPZrZohDJjAx2S4NhTpXzLFpSdQK5xsTrcSBQNsKMalSFU ++75KlSfsvSpauGW8+S4yT+rBe/ZCQfXioAGnI3dsV9+kopkhf/32ng== +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIEUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADB0MRMwEQYDVQQIDApTb21l +LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFzAVBgNV +BAMMDlNvbWUgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJhdXRob3JpdHlAdGVz +dC5jb20wIBcNMjEwOTI2MTA0NTM5WhgPMjEyMTA5MjYxMDQ1MzlaMDAxLjAsBgNV +BAMTJWZsb3djcnlwdC50ZXN0LmtleS5tdWx0aXBsZUBnbWFpbC5jb20wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6F9iDjrmQXBIGLLaXwc+wsiGq5vrg +ed/hxBbSPa0rm+ac/WRBHdBuIaPd7188i1615w2YNG+oundWJuo8TOxoMkQkrZL4 +UYpvpqx17alODjN+St9WgwivyCcyCeuZtjyn3xPWhv4LMUy8YvECN0MLQnPgVMcL +9ZGZv76QToqATZ6QVd7yGNLgjZFiraCU5jxIYgaFVCr79jYYhToZPY5L6kryNuRh +vNh3pIxPonSpbsfkzV7nLxuD3x1sGaFQZZvgdcDUeHCB898SHkl8zSsA1LXAwwlp +6VrJmqYddTb/qAFiW/3LIzfYl+l/94YuV9q2zn0FBUepxJlrHF6Z7Z/lAgMBAAGj +MjAwMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgL0MBMGA1UdJQQMMAoGCCsGAQUF +BwMEMA0GCSqGSIb3DQEBBQUAA4ICAQDgqD8lwAQ7vUPUdgPsX8ZJrCbLAAebPUMO +JllYuSJxWnHoNhiIzQV81Cb+6JfxXXcM3XPc9VCtU3Jz3b7tS+/uxtzZnQ4vU9SC +aEApX4kiwPfeAaHDXe21hHjEEiIEY65ethodILqtXZ9JVnzFbGyZl8ZzOyn8BGQ3 +wVygQmj7vagGc41mFKTBPQvaY5mkx9YyfUfemZ2hsJ9LSXj9nZurISleLY6hgBYG +cyH7hq+bQZzfC0F2MDq4Hv4n/KUQzNNd0FS45/98g9jIuXP/I2TonYjqUf0EckCd +ayQs6QK9vZ+Mj/aC6e2K4FBhJEp0+Jm7b8r1FWuQoigGuXKGbY9atR0JaL5r77d/ +fFjOk8dPQgY8qJBntcGVFsjA63MbAuM0rKOFeWiXc1tR2qyE89Ovrcrj84d+AOg0 +zyIy1KhxeIunpI+H/9MJTv/1bInUU9TYypGtmFYW//3g2oyRQu08T9+R81sSQCFp +5v0mtEb1Xd+8+FRUN7NXeT+9wZ29Zr5rTuUWt+RIqEc/TN30qdOj7rZiFq1GiFHW +BAOvkzvTSeZKYEZdNEC0yicS5O3dO84HaZ4wBB42XvtFMYEPU6EHPn/RjsUjDGpj +RiNuW9jHvl7KOpFRmx3OQR/B+AdzjTRqsN5PzLMygb9nx2BG7xV0sg14mkwWhsXk +Z0Mdtqg+Vg== +-----END CERTIFICATE-----`, rsa1024subkeyOnly: `-----BEGIN PGP PRIVATE KEY BLOCK----- xcLYBGANjIkBCADQU/PgpIj3pe0/A0HZS6r4DbwIb1Rr6sEGVE/juigYZJr8UYKGAA47rHWLiDW7 diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 83575eb551d..1f5d304345b 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -849,8 +849,8 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const parsed1 = await KeyUtil.parse(key1.private); const parsed2 = await KeyUtil.parse(key2.private); const kisWithPp: ExtendedKeyInfo[] = [ // supply both key1 and key2 for decrypt - { ... await KeyUtil.keyInfoObj(parsed1), type: parsed1.type, passphrase }, - { ... await KeyUtil.keyInfoObj(parsed2), type: parsed2.type, passphrase }, + { ... await KeyUtil.typedKeyInfoObj(parsed1), passphrase }, + { ... await KeyUtil.typedKeyInfoObj(parsed2), passphrase }, ]; // we are testing a private method here because the outcome of this method is not directly testable from the // public method that uses it. It only makes the public method faster, which is hard to test. @@ -936,7 +936,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const pubkeys = [await KeyUtil.parse(justPrimaryPub)]; const encrypted = await MsgUtil.encryptMessage({ pubkeys, data, armor: true }) as PgpMsgMethod.EncryptPgpArmorResult; const parsed = await KeyUtil.parse(prvEncryptForSubkeyOnlyProtected); - const kisWithPp: ExtendedKeyInfo[] = [{ ... await KeyUtil.keyInfoObj(parsed), type: parsed.type, passphrase }]; + const kisWithPp: ExtendedKeyInfo[] = [{ ... await KeyUtil.typedKeyInfoObj(parsed), type: parsed.type, passphrase }]; const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData: encrypted.data }); // todo - later we'll have an org rule for ignoring this, and then it will be expected to pass as follows: // expect(decrypted.success).to.equal(true); diff --git a/test/source/util/index.ts b/test/source/util/index.ts index a1419fa4163..3b8eb1b2633 100644 --- a/test/source/util/index.ts +++ b/test/source/util/index.ts @@ -80,7 +80,7 @@ export class Config { return await Promise.all(Config._secrets.keys .filter(key => key.armored && titles.includes(key.title)).map(async key => { const parsed = await KeyUtil.parse(key.armored!); - return { ...await KeyUtil.keyInfoObj(parsed), type: parsed.type, passphrase: key.passphrase }; + return { ...await KeyUtil.typedKeyInfoObj(parsed), passphrase: key.passphrase }; })); }