diff --git a/extension/chrome/elements/backup.ts b/extension/chrome/elements/backup.ts index c3fe5e28665..4c68d4e4027 100644 --- a/extension/chrome/elements/backup.ts +++ b/extension/chrome/elements/backup.ts @@ -5,7 +5,7 @@ import { Assert } from '../../js/common/assert.js'; import { Browser } from '../../js/common/browser/browser.js'; import { BrowserMsg } from '../../js/common/browser/browser-msg.js'; -import { KeyInfo, KeyUtil } from '../../js/common/core/crypto/key.js'; +import { KeyUtil, KeyInfoWithIdentity } from '../../js/common/core/crypto/key.js'; import { Ui } from '../../js/common/browser/ui.js'; import { Url, Str } from '../../js/common/core/common.js'; import { View } from '../../js/common/view.js'; @@ -18,7 +18,7 @@ View.run(class BackupView extends View { private readonly parentTabId: string; private readonly frameId: string; private readonly armoredPrvBackup: string; - private storedPrvWithMatchingLongid: KeyInfo | undefined; + private storedPrvWithMatchingLongid: KeyInfoWithIdentity | undefined; constructor() { super(); diff --git a/extension/chrome/elements/compose-modules/compose-draft-module.ts b/extension/chrome/elements/compose-modules/compose-draft-module.ts index 5cf2b6023cd..408dd75bd13 100644 --- a/extension/chrome/elements/compose-modules/compose-draft-module.ts +++ b/extension/chrome/elements/compose-modules/compose-draft-module.ts @@ -7,7 +7,7 @@ import { AjaxErr } from '../../../js/common/api/shared/api-error.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { BrowserMsg } from '../../../js/common/browser/browser-msg.js'; import { Buf } from '../../../js/common/core/buf.js'; -import { Catch } from '../../../js/common/platform/catch.js'; +import { Catch, UnreportableError } from '../../../js/common/platform/catch.js'; import { EncryptedMsgMailFormatter } from './formatters/encrypted-mail-msg-formatter.js'; import { Env } from '../../../js/common/browser/env.js'; import { GlobalStore } from '../../../js/common/platform/store/global-store.js'; @@ -20,7 +20,6 @@ import { Xss } from '../../../js/common/platform/xss.js'; import { ViewModule } from '../../../js/common/view-module.js'; import { ComposeView } from '../compose.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; -import { KeyUtil } from '../../../js/common/core/crypto/key.js'; import { SendableMsg, InvalidRecipientError } from '../../../js/common/api/email-provider/sendable-msg.js'; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; @@ -29,6 +28,7 @@ export class ComposeDraftModule extends ViewModule { public wasMsgLoadedFromDraft = false; private currentlySavingDraft = false; + private disableSendingDrafts = false; private saveDraftInterval?: number; private lastDraftBody?: string; private lastDraftSubject = ''; @@ -113,12 +113,25 @@ export class ComposeDraftModule extends ViewModule { }; public draftSave = async (forceSave: boolean = false): Promise => { + if (this.disableSendingDrafts) { + return; + } if (this.hasBodyChanged(this.view.inputModule.squire.getHTML()) || this.hasSubjectChanged(String(this.view.S.cached('input_subject').val())) || forceSave) { this.currentlySavingDraft = true; try { const msgData = this.view.inputModule.extractAll(); - const primaryKi = await this.view.storageModule.getKey(msgData.from); - const pubkeys = [{ isMine: true, email: msgData.from, pubkey: await KeyUtil.parse(primaryKi.public) }]; + const { pubkeys } = await this.view.storageModule.collectSingleFamilyKeys([], msgData.from, true); + // collectSingleFamilyKeys filters out bad keys, but only if there are any good keys available + // if no good keys available, it leaves bad keys so we can explain the issue here + if (pubkeys.some(pub => pub.pubkey.expiration && pub.pubkey.expiration < Date.now())) { + throw new UnreportableError('Your account keys are expired'); + } + if (pubkeys.some(pub => pub.pubkey.revoked)) { + throw new UnreportableError('Your account keys are revoked'); + } + if (pubkeys.some(pub => !pub.pubkey.usableForEncryption)) { + throw new UnreportableError('Your account keys are not usable for encryption'); + } msgData.pwd = undefined; // not needed for drafts const sendable = await new EncryptedMsgMailFormatter(this.view, true).sendableMsg(msgData, pubkeys); if (this.view.replyParams?.inReplyTo) { @@ -167,6 +180,13 @@ export class ComposeDraftModule extends ViewModule { Catch.reportErr(e); this.view.S.cached('send_btn_note').text('Not saved (error)'); Ui.toast(`Draft not saved: ${e}`, false, 5); + if (!ApiErr.isNetErr(e)) { + // no point trying again on fatal error + // eg maybe keys could be broken or missing + this.disableSendingDrafts = true; + // todo - could in the future add "once" click handler on send_btn_note + // which would set this back to false & re-run draftSave + } } } this.currentlySavingDraft = false; diff --git a/extension/chrome/elements/compose-modules/compose-err-module.ts b/extension/chrome/elements/compose-modules/compose-err-module.ts index 64e75854586..2973a6ea15f 100644 --- a/extension/chrome/elements/compose-modules/compose-err-module.ts +++ b/extension/chrome/elements/compose-modules/compose-err-module.ts @@ -62,6 +62,7 @@ export class ComposeErrModule extends ViewModule { }; public handleSendErr = async (e: any) => { + this.view.errModule.debug(`handleSendErr: ${String(e)}`); if (ApiErr.isNetErr(e)) { let netErrMsg = 'Could not send message due to network error. Please check your internet connection and try again.\n'; netErrMsg += '(This may also be caused by missing extension permissions).)'; @@ -85,8 +86,12 @@ export class ComposeErrModule extends ViewModule { await Ui.modal.error(e.message, true); } else { if (!(e instanceof ComposerResetBtnTrigger || e instanceof ComposerNotReadyError)) { - Catch.reportErr(e); - await Ui.modal.error(`Failed to send message due to: ${String(e)}`); + if (String(e).includes('revoked') || String(e).includes('expired')) { + await Ui.modal.warning(`Failed to send message due to: ${String(e)}`); + } else { + await Ui.modal.error(`Failed to send message due to: ${String(e)}`); + Catch.reportErr(e); + } } } if (!(e instanceof ComposerNotReadyError)) { diff --git a/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts b/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts index fe9f78cc00b..304918b03b0 100644 --- a/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts +++ b/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts @@ -3,17 +3,17 @@ 'use strict'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; -import { KeyUtil } from '../../../js/common/core/crypto/key.js'; import { Lang } from '../../../js/common/lang.js'; import { Ui } from '../../../js/common/browser/ui.js'; import { ViewModule } from '../../../js/common/view-module.js'; import { ComposeView } from '../compose.js'; import { Str } from '../../../js/common/core/common.js'; +import { KeyStoreUtil } from "../../../js/common/core/crypto/key-store-util.js"; export class ComposeMyPubkeyModule extends ViewModule { private toggledManually = false; - private wkdFingerprints: { [acctEmail: string]: string[] } = {}; + private wkdFingerprints: { [acctEmail: string]: string[] | undefined } = {}; public setHandlers = () => { this.view.S.cached('icon_pubkey').attr('title', Lang.compose.includePubkeyIconTitle); @@ -38,13 +38,14 @@ export class ComposeMyPubkeyModule extends ViewModule { (async () => { const senderEmail = this.view.senderModule.getSender(); // todo: disable attaching S/MIME certificate #4075 - const senderKi = await this.view.storageModule.getKey(senderEmail); - const primaryFingerprint = (await KeyUtil.parse(senderKi.private)).id; + const parsedPrvs = await KeyStoreUtil.parse(await this.view.storageModule.getAccountKeys(senderEmail)); // if we have cashed this fingerprint, setAttachPreference(false) rightaway and return const cached = this.wkdFingerprints[senderEmail]; - if (Array.isArray(cached) && cached.includes(primaryFingerprint)) { - this.setAttachPreference(false); - return; + for (const parsedPrv of parsedPrvs.filter(prv => prv.key.usableForEncryption || prv.key.usableForSigning)) { + if (cached && cached.includes(parsedPrv.key.id)) { + this.setAttachPreference(false); // at least one of our valid keys is on WKD: no need to attach + return; + } } const myDomain = Str.getDomainFromEmailAddress(senderEmail); const foreignRecipients = this.view.recipientsModule.getValidRecipients().map(r => r.email) @@ -55,9 +56,11 @@ export class ComposeMyPubkeyModule extends ViewModule { const { keys } = await this.view.pubLookup.wkd.rawLookupEmail(senderEmail); const fingerprints = keys.map(key => key.id); this.wkdFingerprints[senderEmail] = fingerprints; - if (fingerprints.includes(primaryFingerprint)) { - this.setAttachPreference(false); - return; + for (const parsedPrv of parsedPrvs) { + if (fingerprints.includes(parsedPrv.key.id)) { + this.setAttachPreference(false); + return; + } } } for (const recipient of foreignRecipients) { diff --git a/extension/chrome/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index d6594516187..990915b8e43 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -956,7 +956,7 @@ export class ComposeRecipientsModule extends ViewModule { }; private formatPubkeyId = (pubkeyInfo: PubkeyInfo): string => { - return `${Str.spaced(pubkeyInfo.pubkey.id)} (${pubkeyInfo.pubkey.type})`; + return `${Str.spaced(pubkeyInfo.pubkey.id)} (${pubkeyInfo.pubkey.family})`; }; private generateRecipientId = (): string => { diff --git a/extension/chrome/elements/compose-modules/compose-render-module.ts b/extension/chrome/elements/compose-modules/compose-render-module.ts index 284468a1452..e2683e1b49c 100644 --- a/extension/chrome/elements/compose-modules/compose-render-module.ts +++ b/extension/chrome/elements/compose-modules/compose-render-module.ts @@ -18,6 +18,7 @@ import { ComposeView } from '../compose.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { GmailParser } from '../../../js/common/api/email-provider/gmail/gmail-parser.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; +import { KeyStoreUtil } from "../../../js/common/core/crypto/key-store-util.js"; import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; import { KeyUtil } from '../../../js/common/core/crypto/key.js'; @@ -221,8 +222,16 @@ export class ComposeRenderModule extends ViewModule {

I was not able to read your encrypted message because it was encrypted for a wrong key.

My current public key is attached below. Please update your records and send me a new encrypted message.

Thank you`); - const primaryKi = await KeyStore.getFirstRequired(this.view.acctEmail); - const attachment = Attachment.keyinfoAsPubkeyAttachment(primaryKi); + const prvs = await KeyStoreUtil.parse(await KeyStore.getRequired(this.view.acctEmail)); + // todo - send all valid? + const mostUsefulPrv = KeyStoreUtil.chooseMostUseful(prvs, 'ONLY-FULLY-USABLE'); + if (!mostUsefulPrv) { + await Ui.modal.warning('None of your private keys are usable.\n\n' + + 'If you are part of an enterprise deployment, ask your Help Desk\n\n.' + + 'Other users, please check Settings -> Additional settings -> My keys.'); + return; + } + const attachment = Attachment.keyinfoAsPubkeyAttachment(mostUsefulPrv.keyInfo); this.view.attachmentsModule.attachment.addFile(new File([attachment.getData()], attachment.name)); this.view.sendBtnModule.popover.toggleItemTick($('.action-toggle-encrypt-sending-option'), 'encrypt', false); // don't encrypt this.view.sendBtnModule.popover.toggleItemTick($('.action-toggle-sign-sending-option'), 'sign', false); // don't sign diff --git a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts index 1c418ed1bb9..bcddecd5a47 100644 --- a/extension/chrome/elements/compose-modules/compose-send-btn-module.ts +++ b/extension/chrome/elements/compose-modules/compose-send-btn-module.ts @@ -13,7 +13,7 @@ import { ComposerUserError } from './compose-err-module.js'; import { ComposeSendBtnPopoverModule } from './compose-send-btn-popover-module.js'; import { GeneralMailFormatter } from './formatters/general-mail-formatter.js'; import { GmailParser, GmailRes } from '../../../js/common/api/email-provider/gmail/gmail-parser.js'; -import { KeyInfo } from '../../../js/common/core/crypto/key.js'; +import { KeyInfoWithIdentity } from '../../../js/common/core/crypto/key.js'; import { getUniqueRecipientEmails, SendBtnTexts } from './compose-types.js'; import { SendableMsg } from '../../../js/common/api/email-provider/sendable-msg.js'; import { Ui } from '../../../js/common/browser/ui.js'; @@ -115,10 +115,8 @@ export class ComposeSendBtnModule extends ViewModule { const emails = getUniqueRecipientEmails(newMsgData.recipients); await ContactStore.update(undefined, emails, { lastUse: Date.now() }); const msgObj = await GeneralMailFormatter.processNewMsg(this.view, newMsgData); - if (msgObj) { - await this.finalizeSendableMsg(msgObj); - await this.doSendMsg(msgObj.msg); - } + await this.finalizeSendableMsg(msgObj); + await this.doSendMsg(msgObj.msg); } catch (e) { await this.view.errModule.handleSendErr(e); } finally { @@ -127,7 +125,7 @@ export class ComposeSendBtnModule extends ViewModule { } }; - private finalizeSendableMsg = async ({ msg, senderKi }: { msg: SendableMsg, senderKi: KeyInfo | undefined }) => { + private finalizeSendableMsg = async ({ msg, senderKi }: { msg: SendableMsg, senderKi: KeyInfoWithIdentity | undefined }) => { const choices = this.view.sendBtnModule.popover.choices; for (const k of Object.keys(this.additionalMsgHeaders)) { msg.headers[k] = this.additionalMsgHeaders[k]; diff --git a/extension/chrome/elements/compose-modules/compose-storage-module.ts b/extension/chrome/elements/compose-modules/compose-storage-module.ts index 639553346b0..40a6f71858b 100644 --- a/extension/chrome/elements/compose-modules/compose-storage-module.ts +++ b/extension/chrome/elements/compose-modules/compose-storage-module.ts @@ -3,111 +3,101 @@ 'use strict'; import { BrowserMsg } from '../../../js/common/browser/browser-msg.js'; -import { KeyInfo, KeyUtil, Key, PubkeyInfo, PubkeyResult, ContactInfoWithSortedPubkeys } from '../../../js/common/core/crypto/key.js'; +import { KeyUtil, Key, PubkeyInfo, PubkeyResult, ContactInfoWithSortedPubkeys, KeyInfoWithIdentity } from '../../../js/common/core/crypto/key.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { Assert } from '../../../js/common/assert.js'; import { Catch, UnreportableError } from '../../../js/common/platform/catch.js'; import { CollectKeysResult } from './compose-types.js'; -import { PUBKEY_LOOKUP_RESULT_FAIL } from './compose-err-module.js'; +import { ComposerResetBtnTrigger, PUBKEY_LOOKUP_RESULT_FAIL } from './compose-err-module.js'; import { ViewModule } from '../../../js/common/view-module.js'; import { ComposeView } from '../compose.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; import { compareAndSavePubkeysToStorage } from '../../../js/common/shared.js'; +import { KeyFamily } from '../../../js/common/core/crypto/key.js'; +import { ParsedKeyInfo } from '../../../js/common/core/crypto/key-store-util.js'; export class ComposeStorageModule extends ViewModule { - // if `type` is supplied, returns undefined if no keys of this type are found - public getKeyOptional = async (senderEmail: string | undefined, type?: 'openpgp' | 'x509' | undefined) => { - const keys = await KeyStore.getTypedKeyInfos(this.view.acctEmail); - let result: KeyInfo | undefined; - if (senderEmail !== undefined) { - const filteredKeys = KeyUtil.filterKeysByTypeAndSenderEmail(keys, senderEmail, type); - if (type === undefined) { - // prioritize openpgp - result = filteredKeys.find(key => key.type === 'openpgp'); - } - if (result === undefined) { - result = filteredKeys[0]; - } + + public getAccountKeys = async (senderEmail: string | undefined, family?: 'openpgp' | 'x509' | undefined): Promise => { + const unfilteredKeys = await KeyStore.get(this.view.acctEmail); + Assert.abortAndRenderErrorIfKeyinfoEmpty(unfilteredKeys); + const matchingFamily = unfilteredKeys.filter(ki => !family || ki.family === family); + if (!senderEmail) { + return matchingFamily; } - if (result === undefined) { - this.view.errModule.debug(`ComposerStorage.getKeyOptional: could not find key based on senderEmail: ${senderEmail}, using primary instead`); - result = keys.find(k => type === undefined || type === k.type); - } else { - this.view.errModule.debug(`ComposerStorage.getKeyOptional: found key based on senderEmail: ${senderEmail}`); + const matchingFamilyAndSenderEmail = matchingFamily.filter(ki => ki.emails?.includes(senderEmail)); + if (!matchingFamilyAndSenderEmail.length) { + // if couldn't find any key that matches email, use all from this family + // x509 keys may not have email on them, and sometimes OpenPGP users use keys with other email + return matchingFamily; } - return result; - }; - - public getKey = async (senderEmail: string | undefined, type?: 'openpgp' | 'x509' | undefined): Promise => { - const result = await this.getKeyOptional(senderEmail, type); - Assert.abortAndRenderErrorIfKeyinfoEmpty(result); - this.view.errModule.debug(`ComposerStorage.getKey: returning key longid: ${result!.longid}`); - return result!; + return matchingFamilyAndSenderEmail; }; // used when encryption is needed // returns a set of keys of a single family ('openpgp' or 'x509') - public collectSingleFamilyKeys = async (recipients: string[], senderEmail: string, needSigning: boolean): Promise => { - const contacts = await ContactStore.getEncryptionKeys(undefined, recipients); - const resultsPerType: { [type: string]: CollectKeysResult } = {}; + public collectSingleFamilyKeys = async ( + recipients: string[], + senderEmail: string, + needSigning: boolean + ): Promise => { + const resultsPerFamily: { [type: string]: CollectKeysResult } = {}; const OPENPGP = 'openpgp'; const X509 = 'x509'; - for (const i of [OPENPGP, X509]) { - const type = i as ('openpgp' | 'x509'); - // senderKi for draft encryption! - const senderKi = await this.getKeyOptional(senderEmail, type); - const { pubkeys, emailsWithoutPubkeys } = this.collectPubkeysByType(type, contacts); - if (senderKi !== undefined) { - // add own key for encryption - pubkeys.push({ pubkey: await KeyUtil.parse(senderKi.public), email: senderEmail, isMine: true }); - } - const result = { senderKi, pubkeys, emailsWithoutPubkeys }; - if (!emailsWithoutPubkeys.length && (senderKi !== undefined || !needSigning)) { - return result; // return right away + const contacts = recipients.length + ? await ContactStore.getEncryptionKeys(undefined, recipients) + : []; // in case collecting only our own keys for draft + for (const family of [OPENPGP, X509]) { + const collected = await this.collectSingleFamilyKeysInternal( + family as KeyFamily, + senderEmail, + contacts + ); + if (!collected.emailsWithoutPubkeys.length && (collected.senderKis.length || !needSigning)) { + return collected; // return right away - we have all we needed in single family } - resultsPerType[type] = result; + resultsPerFamily[family] = collected; } // per discussion https://github.com/FlowCrypt/flowcrypt-browser/issues/4069#issuecomment-957313631 // if one emailsWithoutPubkeys isn't subset of the other, throw an error - if (!resultsPerType[OPENPGP].emailsWithoutPubkeys.every(email => resultsPerType[X509].emailsWithoutPubkeys.includes(email)) && - !resultsPerType[X509].emailsWithoutPubkeys.every(email => resultsPerType[OPENPGP].emailsWithoutPubkeys.includes(email))) { - let err = `Cannot use mixed OpenPGP (${resultsPerType[OPENPGP].pubkeys.filter(p => !p.isMine).map(p => p.email).join(', ')}) and ` - + `S/MIME (${resultsPerType[X509].pubkeys.filter(p => !p.isMine).map(p => p.email).join(', ')}) public keys yet.`; + if (!resultsPerFamily[OPENPGP].emailsWithoutPubkeys.every(email => resultsPerFamily[X509].emailsWithoutPubkeys.includes(email)) && + !resultsPerFamily[X509].emailsWithoutPubkeys.every(email => resultsPerFamily[OPENPGP].emailsWithoutPubkeys.includes(email))) { + let err = `Cannot use mixed OpenPGP (${resultsPerFamily[OPENPGP].pubkeys.filter(p => !p.isMine).map(p => p.email).join(', ')}) and ` + + `S/MIME (${resultsPerFamily[X509].pubkeys.filter(p => !p.isMine).map(p => p.email).join(', ')}) public keys yet.`; err += 'If you need to email S/MIME recipient, do not add any OpenPGP recipient at the same time.'; throw new UnreportableError(err); } const rank = (x: [string, CollectKeysResult]) => { - return x[1].emailsWithoutPubkeys.length * 100 + (x[1].senderKi ? 0 : 10) + (x[0] === 'openpgp' ? 0 : 1); + return x[1].emailsWithoutPubkeys.length * 100 + (x[1].senderKis.length ? 0 : 10) + (x[0] === 'openpgp' ? 0 : 1); }; - return Object.entries(resultsPerType).sort((a, b) => rank(a) - rank(b))[0][1]; + return Object.entries(resultsPerFamily).sort((a, b) => rank(a) - rank(b))[0][1]; }; public passphraseGet = async (senderKi: { longid: string }) => { - if (!senderKi) { - senderKi = await KeyStore.getFirstRequired(this.view.acctEmail); - } return await PassphraseStore.get(this.view.acctEmail, senderKi); }; - public decryptSenderKey = async (senderKi: KeyInfo): Promise => { - const prv = await KeyUtil.parse(senderKi.private); - const passphrase = await this.passphraseGet(senderKi); - if (typeof passphrase === 'undefined' && !prv.fullyDecrypted) { - const longids = [senderKi.longid]; + /** + * returns decrypted key (potentially using user-entered pass phrase) + * otherwise throws ComposerResetBtnTrigger when pass phrase not entered + */ + public decryptSenderKey = async (parsedKey: ParsedKeyInfo): Promise => { + const passphrase = await this.passphraseGet(parsedKey.keyInfo); + if (typeof passphrase === 'undefined' && !parsedKey.key.fullyDecrypted) { + const longids = [parsedKey.keyInfo.longid]; BrowserMsg.send.passphraseDialog(this.view.parentTabId, { type: 'sign', longids }); if (await PassphraseStore.waitUntilPassphraseChanged(this.view.acctEmail, longids, 1000, this.view.ppChangedPromiseCancellation)) { - return await this.decryptSenderKey(senderKi); - } else { // reset - no passphrase entered - this.view.sendBtnModule.resetSendBtn(); - return undefined; + return await this.decryptSenderKey(parsedKey); + } else {// reset - no passphrase entered + throw new ComposerResetBtnTrigger('no pass phrase entered'); } } else { - if (!prv.fullyDecrypted) { - await KeyUtil.decrypt(prv, passphrase!); // checked !== undefined above + if (!parsedKey.key.fullyDecrypted) { + await KeyUtil.decrypt(parsedKey.key, passphrase!); // checked !== undefined above } - return prv; + return parsedKey; } }; @@ -186,11 +176,48 @@ export class ComposeStorageModule extends ViewModule { } }; - private collectPubkeysByType = (type: 'openpgp' | 'x509', contacts: { email: string, keys: Key[] }[]): { pubkeys: PubkeyResult[], emailsWithoutPubkeys: string[] } => { + private collectSingleFamilyKeysInternal = async ( + family: KeyFamily, + senderEmail: string, + contacts: { email: string, keys: Key[] }[] + ): Promise => { + const senderKisUnfiltered = await this.getAccountKeys(senderEmail, family); // for draft encryption! + const senderPubsUnfiltered = await Promise.all(senderKisUnfiltered.map(ki => KeyUtil.parse(ki.public))); + const senderPubs = senderPubsUnfiltered.some(k => k.usableForEncryption) + // if non-expired present, return non-expired only + // that way, there will be no error if some keys are valid + // but if all are invalid, downstream code can inform the user what happened + ? senderPubsUnfiltered.filter(k => k.usableForEncryption) + : senderPubsUnfiltered; + const { pubkeys, emailsWithoutPubkeys } = this.collectPubkeysByFamily(family, contacts); + for (const senderPub of senderPubs) { // add own key for encryption + pubkeys.push({ pubkey: senderPub, email: senderEmail, isMine: true }); + } + const senderKis = []; + const isAnySenderKeyUsableForSigning = senderPubsUnfiltered.some(k => k.usableForSigning); + for (const senderKi of senderKisUnfiltered) { + if (!isAnySenderKeyUsableForSigning) { + // if none is usable, add all + // then downstream code can diagnose and show the issue to user + senderKis.push(senderKi); + } else { + const relatedPub = senderPubsUnfiltered.find(pub => pub.id === senderKi.fingerprints[0]); + // want to avoid parsing the prvs when pubs were already parsed + // therefore checking parameters of already parsed related pub, which are equal + // but actually pushing prv since it's meant for signing + if (relatedPub?.usableForSigning) { + senderKis.push(senderKi); + } + } + } + return { senderKis, pubkeys, emailsWithoutPubkeys, family }; + }; + + private collectPubkeysByFamily = (family: KeyFamily, contacts: { email: string, keys: Key[] }[]): { pubkeys: PubkeyResult[], emailsWithoutPubkeys: string[] } => { const pubkeys: PubkeyResult[] = []; const emailsWithoutPubkeys: string[] = []; for (const contact of contacts) { - let keysPerEmail = contact.keys.filter(k => k.type === type); + let keysPerEmail = contact.keys.filter(k => k.family === family); // if non-expired present, return non-expired only if (keysPerEmail.some(k => k.usableForEncryption)) { keysPerEmail = keysPerEmail.filter(k => k.usableForEncryption); diff --git a/extension/chrome/elements/compose-modules/compose-types.ts b/extension/chrome/elements/compose-modules/compose-types.ts index 92808d00a4e..49a1b57935a 100644 --- a/extension/chrome/elements/compose-modules/compose-types.ts +++ b/extension/chrome/elements/compose-modules/compose-types.ts @@ -4,7 +4,7 @@ import { RecipientType } from '../../../js/common/api/shared/api.js'; import { ParsedRecipients } from '../../../js/common/api/email-provider/email-provider-api.js'; -import { KeyInfo, PubkeyResult } from '../../../js/common/core/crypto/key.js'; +import { KeyFamily, PubkeyResult, KeyInfoWithIdentity } from '../../../js/common/core/crypto/key.js'; import { EmailParts, Value } from '../../../js/common/core/common.js'; export enum RecipientStatus { @@ -46,7 +46,7 @@ export type MessageToReplyOrForward = { decryptedFiles: File[] }; -export type CollectKeysResult = { pubkeys: PubkeyResult[], emailsWithoutPubkeys: string[], senderKi: KeyInfo | undefined }; +export type CollectKeysResult = { pubkeys: PubkeyResult[], emailsWithoutPubkeys: string[], senderKis: KeyInfoWithIdentity[], family: KeyFamily }; export type PopoverOpt = 'encrypt' | 'sign' | 'richtext'; export type PopoverChoices = { [key in PopoverOpt]: boolean }; diff --git a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts index cfb60249de6..7b788343aea 100644 --- a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts @@ -6,7 +6,7 @@ import { BaseMailFormatter } from './base-mail-formatter.js'; import { ComposerResetBtnTrigger } from '../compose-err-module.js'; import { Mime, SendableMsgBody } from '../../../../js/common/core/mime.js'; import { getUniqueRecipientEmails, NewMsgData } from '../compose-types.js'; -import { Str, Url, Value } from '../../../../js/common/core/common.js'; +import { Str, Value } from '../../../../js/common/core/common.js'; import { ApiErr } from '../../../../js/common/api/shared/api-error.js'; import { Attachment } from '../../../../js/common/core/attachment.js'; import { Buf } from '../../../../js/common/core/buf.js'; @@ -105,7 +105,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { const { data: encrypted } = await this.encryptDataArmor(Buf.fromUtfStr(newMsg.plaintext), undefined, pubs, signingPrv); return await SendableMsg.createInlineArmored(this.acctEmail, this.headers(newMsg), Buf.fromUint8(encrypted).toUtfStr(), [], { isDraft: this.isDraft }); } - const x509certs = pubsForEncryption.filter(pub => pub.type === 'x509'); + const x509certs = pubsForEncryption.filter(pub => pub.family === 'x509'); if (x509certs.length) { // s/mime const attachments: Attachment[] = this.isDraft ? [] : await this.view.attachmentsModule.attachment.collectAttachments(); // collects attachments const msgBody = { 'text/plain': newMsg.plaintext }; @@ -148,7 +148,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { }; private encryptDataArmor = async (data: Buf, pwd: string | undefined, pubs: PubkeyResult[], signingPrv?: Key): Promise => { - const pgpPubs = pubs.filter(pub => pub.pubkey.type === 'openpgp'); + const pgpPubs = pubs.filter(pub => pub.pubkey.family === 'openpgp'); const encryptAsOfDate = await this.encryptMsgAsOfDateIfSomeAreExpiredAndUserConfirmedModal(pgpPubs); const pubsForEncryption = pubs.map(entry => entry.pubkey); return await MsgUtil.encryptMessage({ pubkeys: pubsForEncryption, signingPrv, pwd, data, armor: true, date: encryptAsOfDate }) as PgpMsgMethod.EncryptAnyArmorResult; @@ -205,23 +205,6 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { if (Math.max(...usableUntil) > Date.now()) { // all keys either don't expire or expire in the future return undefined; } - for (const myKey of pubs.filter(ap => ap.isMine)) { - if (myKey.pubkey.usableForEncryptionButExpired) { - const path = Url.create(chrome.runtime.getURL('chrome/settings/index.htm'), { - acctEmail: myKey.email, - page: '/chrome/settings/modules/my_key_update.htm', - pageUrlParams: JSON.stringify({ fingerprint: myKey.pubkey.id }), - }); - const errModalLines = [ - 'This message could not be encrypted because your own Private Key is expired.', - '', - 'You can extend the expiration of this key in other OpenPGP software (such as GnuPG), then re-import the updated key ' + - `here.` - ]; - await Ui.modal.error(errModalLines.join('\n'), true); - throw new ComposerResetBtnTrigger(); - } - } const usableTimeFrom = Math.max(...usableFrom); const usableTimeUntil = Math.min(...usableUntil); if (usableTimeFrom > usableTimeUntil) { // used public keys have no intersection of usable dates diff --git a/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts b/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts index 14fb1eeee2e..a3e3ab1a848 100644 --- a/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/general-mail-formatter.ts @@ -3,45 +3,65 @@ 'use strict'; import { EncryptedMsgMailFormatter } from './encrypted-mail-msg-formatter.js'; -import { Key, KeyInfo } from "../../../../js/common/core/crypto/key.js"; +import { KeyInfoWithIdentity } from "../../../../js/common/core/crypto/key.js"; import { getUniqueRecipientEmails, NewMsgData } from "../compose-types.js"; import { PlainMsgMailFormatter } from './plain-mail-msg-formatter.js'; import { SendableMsg } from '../../../../js/common/api/email-provider/sendable-msg.js'; import { SignedMsgMailFormatter } from './signed-msg-mail-formatter.js'; import { ComposeView } from '../../compose.js'; +import { KeyStoreUtil, ParsedKeyInfo } from "../../../../js/common/core/crypto/key-store-util.js"; +import { UnreportableError } from '../../../../js/common/platform/catch.js'; export class GeneralMailFormatter { // returns undefined in case user cancelled decryption of the signing key - public static processNewMsg = async (view: ComposeView, newMsgData: NewMsgData): Promise<{ msg: SendableMsg, senderKi: KeyInfo | undefined } | undefined> => { + public static processNewMsg = async (view: ComposeView, newMsgData: NewMsgData): Promise<{ msg: SendableMsg, senderKi: KeyInfoWithIdentity | undefined }> => { const choices = view.sendBtnModule.popover.choices; const recipientsEmails = getUniqueRecipientEmails(newMsgData.recipients); if (!choices.encrypt && !choices.sign) { // plain + view.S.now('send_btn_text').text('Formatting...'); return { senderKi: undefined, msg: await new PlainMsgMailFormatter(view).sendableMsg(newMsgData) }; } - let signingPrv: Key | undefined; if (!choices.encrypt && choices.sign) { // sign only view.S.now('send_btn_text').text('Signing...'); - const senderKi = await view.storageModule.getKey(newMsgData.from); - signingPrv = await view.storageModule.decryptSenderKey(senderKi); - if (!signingPrv) { - return undefined; + const senderKis = await view.storageModule.getAccountKeys(newMsgData.from); + const signingKey = await GeneralMailFormatter.chooseSigningKeyAndDecryptIt(view, senderKis); + if (!signingKey) { + throw new UnreportableError('Could not find account key usable for signing this plain text message'); } - return { senderKi, msg: await new SignedMsgMailFormatter(view).sendableMsg(newMsgData, signingPrv) }; + return { senderKi: signingKey!.keyInfo, msg: await new SignedMsgMailFormatter(view).sendableMsg(newMsgData, signingKey!.key) }; } // encrypt (optionally sign) - const result = await view.storageModule.collectSingleFamilyKeys(recipientsEmails, newMsgData.from, choices.sign); - if (choices.sign && result.senderKi !== undefined) { - signingPrv = await view.storageModule.decryptSenderKey(result.senderKi); - if (!signingPrv) { - return undefined; - } - } - if (result.emailsWithoutPubkeys.length) { + const singleFamilyKeys = await view.storageModule.collectSingleFamilyKeys(recipientsEmails, newMsgData.from, choices.sign); + if (singleFamilyKeys.emailsWithoutPubkeys.length) { await view.errModule.throwIfEncryptionPasswordInvalid(newMsgData); } + let signingKey: ParsedKeyInfo | undefined; + if (choices.sign) { + signingKey = await GeneralMailFormatter.chooseSigningKeyAndDecryptIt(view, singleFamilyKeys.senderKis); + if (!signingKey && singleFamilyKeys.family === 'openpgp') { + // we are ignoring missing signing keys for x509 family for now. We skip signing when missing + // see https://github.com/FlowCrypt/flowcrypt-browser/pull/4372/files#r845012403 + throw new UnreportableError(`Could not find account ${singleFamilyKeys.family} key usable for signing this encrypted message`); + } + } view.S.now('send_btn_text').text('Encrypting...'); - return { senderKi: result.senderKi, msg: await new EncryptedMsgMailFormatter(view).sendableMsg(newMsgData, result.pubkeys, signingPrv) }; + return { senderKi: signingKey?.keyInfo, msg: await new EncryptedMsgMailFormatter(view).sendableMsg(newMsgData, singleFamilyKeys.pubkeys, signingKey?.key) }; + }; + + private static chooseSigningKeyAndDecryptIt = async ( + view: ComposeView, + senderKis: KeyInfoWithIdentity[] + ): Promise => { + const parsedSenderPrvs = await KeyStoreUtil.parse(senderKis); + // to consider - currently we choose first valid key for signing. Should we sign with all? + // alternatively we could use most recenlty modified valid key + const parsedSenderPrv = parsedSenderPrvs.find(k => k.key.usableForSigning); + if (!parsedSenderPrv) { + return undefined; + } + // throws ComposerResetBtnTrigger when user closes pass phrase dialog without entering + return await view.storageModule.decryptSenderKey(parsedSenderPrv); }; } diff --git a/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts b/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts index d6acee95626..a446d97ac00 100644 --- a/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/signed-msg-mail-formatter.ts @@ -17,7 +17,7 @@ export class SignedMsgMailFormatter extends BaseMailFormatter { public sendableMsg = async (newMsg: NewMsgData, signingPrv: Key): Promise => { this.view.errModule.debug(`SignedMsgMailFormatter.sendableMsg signing with key: ${signingPrv.id}`); const attachments = this.isDraft ? [] : await this.view.attachmentsModule.attachment.collectAttachments(); - if (signingPrv.type === 'x509') { + if (signingPrv.family === 'x509') { // todo: attachments, richtext #4046, #4047 if (this.isDraft) { throw new Error('signed-only PKCS#7 drafts are not supported'); diff --git a/extension/chrome/elements/passphrase.ts b/extension/chrome/elements/passphrase.ts index 8119bcddd9d..656b7dfdaeb 100644 --- a/extension/chrome/elements/passphrase.ts +++ b/extension/chrome/elements/passphrase.ts @@ -2,7 +2,7 @@ 'use strict'; -import { KeyInfo, KeyUtil } from '../../js/common/core/crypto/key.js'; +import { KeyUtil, KeyInfoWithIdentity } from '../../js/common/core/crypto/key.js'; import { StorageType } from '../../js/common/platform/store/abstract-store.js'; import { Assert } from '../../js/common/assert.js'; import { BrowserMsg } from '../../js/common/browser/browser-msg.js'; @@ -26,7 +26,7 @@ View.run(class PassphraseView extends View { private readonly longids: string[]; private readonly type: string; private readonly initiatorFrameId?: string; - private keysWeNeedPassPhraseFor: KeyInfo[] | undefined; + private keysWeNeedPassPhraseFor: KeyInfoWithIdentity[] | undefined; private orgRules!: OrgRules; constructor() { diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts index 24f0e301d5f..5fd0ea3c029 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-decrypt-module.ts @@ -105,8 +105,7 @@ export class PgpBlockViewDecryptModule { this.view.renderModule.renderText('Decrypting...'); await this.decryptAndRender(encryptedData, verificationPubs); } else { - const primaryKi = await KeyStore.getFirstOptional(this.view.acctEmail); - if (!result.longids.chosen && !primaryKi) { + if (!result.longids.chosen && !(await KeyStore.get(this.view.acctEmail)).length) { await this.view.errorModule.renderErr(Lang.pgpBlock.notProperlySetUp + this.view.errorModule.btnHtml('FlowCrypt settings', 'green settings'), undefined); } else if (result.error.type === DecryptErrTypes.keyMismatch) { await this.view.errorModule.handlePrivateKeyMismatch(kisWithPp.map(ki => ki.public), encryptedData, this.isPwdMsgBasedOnMsgSnippet === true); diff --git a/extension/chrome/elements/pgp_pubkey.ts b/extension/chrome/elements/pgp_pubkey.ts index 6987b1efcc3..55858d4909d 100644 --- a/extension/chrome/elements/pgp_pubkey.ts +++ b/extension/chrome/elements/pgp_pubkey.ts @@ -22,8 +22,8 @@ View.run(class PgpPubkeyView extends View { private readonly frameId: string; private readonly compact: boolean; // means the details take up very little space. private readonly minimized: boolean; // means I have to click to see details. - private publicKeys: Key[] | undefined; - private primaryPubKey: Key | undefined; + private parsedPublicKeys: Key[] | undefined; + private firstParsedPublicKey: Key | undefined; private isExpired: boolean | undefined; constructor() { @@ -45,12 +45,12 @@ View.run(class PgpPubkeyView extends View { if (pubKey.revoked) { await ContactStore.saveRevocation(undefined, pubKey); } - this.publicKeys = [pubKey]; + this.parsedPublicKeys = [pubKey]; } catch (e) { console.error('Unusable key: ' + e); - this.publicKeys = []; + this.parsedPublicKeys = []; } - this.primaryPubKey = this.publicKeys ? this.publicKeys[0] : undefined; + this.firstParsedPublicKey = this.parsedPublicKeys ? this.parsedPublicKeys[0] : undefined; $('.pubkey').text(this.armoredPubkey); if (this.compact) { $('.hide_if_compact').remove(); @@ -58,22 +58,22 @@ View.run(class PgpPubkeyView extends View { $('.line').removeClass('line'); } $('.line.fingerprints, .line.add_contact').css('display', this.minimized ? 'none' : 'block'); - if (this.publicKeys.length === 1) { - $('.line.fingerprints .fingerprint').text(Str.spaced(this.primaryPubKey?.id || 'err')); + if (this.parsedPublicKeys.length === 1) { + $('.line.fingerprints .fingerprint').text(Str.spaced(this.firstParsedPublicKey?.id || 'err')); } else { $('.line.fingerprints').css({ display: 'none' }); } - if (this.primaryPubKey) { - if (!this.primaryPubKey.usableForEncryptionButExpired && !this.primaryPubKey.usableForSigningButExpired - && !this.primaryPubKey.usableForEncryption && !this.primaryPubKey.usableForSigning) { + if (this.firstParsedPublicKey) { + if (!this.firstParsedPublicKey.usableForEncryptionButExpired && !this.firstParsedPublicKey.usableForSigningButExpired + && !this.firstParsedPublicKey.usableForEncryption && !this.firstParsedPublicKey.usableForSigning) { this.showKeyNotUsableError(); } else { if (this.compact) { $('.hide_if_compact_and_not_error').remove(); } let emailText = ''; - if (this.publicKeys.length === 1) { - const email = this.primaryPubKey.emails[0]; + if (this.parsedPublicKeys.length === 1) { + const email = this.firstParsedPublicKey.emails[0]; if (email) { emailText = email; $('.input_email').val(email); // checked above @@ -81,7 +81,7 @@ View.run(class PgpPubkeyView extends View { } else { emailText = 'more than one person'; $('.input_email').css({ display: 'none' }); - Xss.sanitizeAppend('.add_contact', Xss.escape(' for ' + this.publicKeys.map(pub => pub.emails[0]).filter(e => !!e).join(', '))); + Xss.sanitizeAppend('.add_contact', Xss.escape(' for ' + this.parsedPublicKeys.map(pub => pub.emails[0]).filter(e => !!e).join(', '))); } Xss.sanitizePrepend('#pgp_block.pgp_pubkey .result', `This message includes a Public Key for .`); $('.pubkey').addClass('good'); @@ -122,8 +122,8 @@ View.run(class PgpPubkeyView extends View { }; private setBtnText = async () => { - if (this.publicKeys!.length > 1) { - $('.action_add_contact').text('import ' + this.publicKeys!.length + ' public keys'); + if (this.parsedPublicKeys!.length > 1) { + $('.action_add_contact').text('import ' + this.parsedPublicKeys!.length + ' public keys'); } else { const contactWithPubKeys = await ContactStore.getOneWithAllPubkeys( undefined, String($('.input_email').val())); @@ -142,9 +142,9 @@ View.run(class PgpPubkeyView extends View { }; private addContactHandler = async (addContactBtn: HTMLElement) => { - if (this.publicKeys!.length > 1) { + if (this.parsedPublicKeys!.length > 1) { const emails = new Set(); - for (const pubkey of this.publicKeys!) { + for (const pubkey of this.parsedPublicKeys!) { const email = pubkey.emails[0]; if (email) { await ContactStore.update(undefined, email, { pubkey: KeyUtil.armor(pubkey) }); @@ -157,10 +157,10 @@ View.run(class PgpPubkeyView extends View { BrowserMsg.send.reRenderRecipient('broadcast', { email }); } $('.input_email').remove(); - } else if (this.publicKeys!.length) { + } else if (this.parsedPublicKeys!.length) { if (Str.isEmailValid(String($('.input_email').val()))) { const email = String($('.input_email').val()); - await ContactStore.update(undefined, email, { pubkey: KeyUtil.armor(this.publicKeys![0]) }); + await ContactStore.update(undefined, email, { pubkey: KeyUtil.armor(this.parsedPublicKeys![0]) }); BrowserMsg.send.addToContacts(this.parentTabId); Xss.sanitizeReplace(addContactBtn, `${Xss.escape(String($('.input_email').val()))} added`); $('.input_email').remove(); diff --git a/extension/chrome/settings/index.ts b/extension/chrome/settings/index.ts index af38b05add9..eb907398d7b 100644 --- a/extension/chrome/settings/index.ts +++ b/extension/chrome/settings/index.ts @@ -4,7 +4,7 @@ import { Bm, BrowserMsg } from '../../js/common/browser/browser-msg.js'; import { Ui } from '../../js/common/browser/ui.js'; -import { KeyUtil, TypedKeyInfo } from '../../js/common/core/crypto/key.js'; +import { KeyUtil, KeyInfoWithIdentity } from '../../js/common/core/crypto/key.js'; import { Str, Url, UrlParams } from '../../js/common/core/common.js'; import { ApiErr } from '../../js/common/api/shared/api-error.js'; import { Assert } from '../../js/common/assert.js'; @@ -22,6 +22,7 @@ import { Xss } from '../../js/common/platform/xss.js'; import { XssSafeFactory } from '../../js/common/xss-safe-factory.js'; import { AcctStore, EmailProvider } from '../../js/common/platform/store/acct-store.js'; import { KeyStore } from '../../js/common/platform/store/key-store.js'; +import { KeyStoreUtil } from "../../js/common/core/crypto/key-store-util.js"; import { GlobalStore } from '../../js/common/platform/store/global-store.js'; import { PassphraseStore } from '../../js/common/platform/store/passphrase-store.js'; import Swal from 'sweetalert2'; @@ -148,8 +149,9 @@ View.run(class SettingsView extends View { } })); $('.action_open_public_key_page').click(this.setHandler(async () => { - const ki = await KeyStore.getFirstRequired(this.acctEmail!); - const escapedFp = Xss.escape(ki.fingerprints[0]); + const prvs = await KeyStoreUtil.parse(await KeyStore.getRequired(this.acctEmail!)); + const mostUsefulPrv = KeyStoreUtil.chooseMostUseful(prvs, 'EVEN-IF-UNUSABLE'); + const escapedFp = Xss.escape(mostUsefulPrv!.key.id); await Settings.renderSubPage(this.acctEmail!, this.tabId, 'modules/my_key.htm', `&fingerprint=${escapedFp}`); })); $('.action_show_encrypted_inbox').click(this.setHandler(() => { @@ -254,7 +256,7 @@ View.run(class SettingsView extends View { if (this.advanced) { $("#settings").toggleClass("advanced"); } - const privateKeys = await KeyStore.getTypedKeyInfos(this.acctEmail); + const privateKeys = await KeyStore.get(this.acctEmail); if (privateKeys.length > 4) { $('.key_list').css('overflow-y', 'scroll'); } @@ -362,12 +364,12 @@ View.run(class SettingsView extends View { private checkGoogleAcct = async () => { try { const { sendAs } = await this.gmail!.fetchAcctAliases(); - const primary = sendAs.find(addr => addr.isPrimary === true); - if (!primary) { + const primarySendAs = sendAs.find(addr => addr.isPrimary === true); + if (!primarySendAs) { await Ui.modal.warning(`Your account sendAs does not have any primary sendAsEmail`); return; } - const googleAcctEmailAddr = primary.sendAsEmail; + const googleAcctEmailAddr = primarySendAs.sendAsEmail; $('#status-row #status_google').text(`g:${googleAcctEmailAddr}:ok`); if (googleAcctEmailAddr !== this.acctEmail) { $('#status-row #status_google').text(`g:${googleAcctEmailAddr}:changed`).addClass('bad').attr('title', 'Account email address has changed'); @@ -395,7 +397,7 @@ View.run(class SettingsView extends View { } }; - private addKeyRowsHtml = async (privateKeys: TypedKeyInfo[]) => { + private addKeyRowsHtml = async (privateKeys: KeyInfoWithIdentity[]) => { let html = ''; const canRemoveKey = !this.orgRules || !this.orgRules.usesKeyManager(); for (let i = 0; i < privateKeys.length; i++) { @@ -405,7 +407,7 @@ View.run(class SettingsView extends View { const date = Str.monthName(created.getMonth()) + ' ' + created.getDate() + ', ' + created.getFullYear(); let removeKeyBtn = ''; if (canRemoveKey && privateKeys.length > 1) { - removeKeyBtn = `(remove)`; + removeKeyBtn = `(remove)`; } const escapedEmail = Xss.escape(prv.emails[0] || ''); const escapedLink = `${escapedEmail}`; @@ -423,16 +425,16 @@ View.run(class SettingsView extends View { if (canRemoveKey) { $('.action_remove_key').click(this.setHandler(async target => { // the UI below only gets rendered when account_email is available - const type = $(target).data('type') as string; + const family = $(target).data('type') as string; const id = $(target).data('id') as string; const longid = $(target).data('longid') as string; - if (type === 'openpgp' || type === 'x509') { - await KeyStore.remove(this.acctEmail!, { type, id }); + if (family === 'openpgp' || family === 'x509') { + await KeyStore.remove(this.acctEmail!, { family, id }); await PassphraseStore.set('local', this.acctEmail!, { longid }, undefined); await PassphraseStore.set('session', this.acctEmail!, { longid }, undefined); this.reload(true); } else { - Catch.report(`unexpected key type: ${type}`); + Catch.report(`unexpected key type: ${family}`); } })); } diff --git a/extension/chrome/settings/modules/add_key.ts b/extension/chrome/settings/modules/add_key.ts index 22c55eacedb..6849ad094f8 100644 --- a/extension/chrome/settings/modules/add_key.ts +++ b/extension/chrome/settings/modules/add_key.ts @@ -109,7 +109,7 @@ View.run(class AddKeyView extends View { if (e instanceof UserAlert) { return await Ui.modal.warning(e.message, Ui.testCompatibilityLink); } else if (e instanceof KeyCanBeFixed) { - return await Ui.modal.error(`This type of key cannot be set as non-primary yet. ${Lang.general.contactForSupportSentence(!!this.fesUrl)}`, + return await Ui.modal.error(`This type of key cannot be set as additional keys yet. ${Lang.general.contactForSupportSentence(!!this.fesUrl)}`, false, Ui.testCompatibilityLink); } else if (e instanceof UnexpectedKeyTypeError) { return await Ui.modal.warning(`This does not appear to be a validly formatted key.\n\n${e.message}`); diff --git a/extension/chrome/settings/modules/backup-automatic-module.ts b/extension/chrome/settings/modules/backup-automatic-module.ts index 0bbbcf54fb2..171a1ac4035 100644 --- a/extension/chrome/settings/modules/backup-automatic-module.ts +++ b/extension/chrome/settings/modules/backup-automatic-module.ts @@ -11,7 +11,7 @@ import { Ui } from '../../../js/common/browser/ui.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { GoogleAuth } from '../../../js/common/api/email-provider/gmail/google-auth.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; -import { KeyUtil } from '../../../js/common/core/crypto/key.js'; +import { KeyStoreUtil } from "../../../js/common/core/crypto/key-store-util.js"; export class BackupAutomaticModule extends ViewModule { @@ -25,13 +25,13 @@ export class BackupAutomaticModule extends ViewModule { }; private setupCreateSimpleAutomaticInboxBackup = async () => { - const primaryKi = await KeyStore.getFirstRequired(this.view.acctEmail); - if (!(await KeyUtil.parse(primaryKi.private)).fullyEncrypted) { + const prvs = await KeyStoreUtil.parse(await KeyStore.getRequired(this.view.acctEmail)); + if (prvs.find(prv => !prv.key.fullyEncrypted)) { await Ui.modal.warning('Key not protected with a pass phrase, skipping'); throw new UnreportableError('Key not protected with a pass phrase, skipping'); } try { - await this.view.manualModule.doBackupOnEmailProvider(primaryKi.private); + await this.view.manualModule.doBackupOnEmailProvider(prvs.map(prv => prv.keyInfo)); await this.view.renderBackupDone(1); } catch (e) { if (ApiErr.isAuthErr(e)) { diff --git a/extension/chrome/settings/modules/backup-manual-module.ts b/extension/chrome/settings/modules/backup-manual-module.ts index a9327e8d483..0922c91e5b1 100644 --- a/extension/chrome/settings/modules/backup-manual-module.ts +++ b/extension/chrome/settings/modules/backup-manual-module.ts @@ -8,7 +8,7 @@ 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 { KeyIdentity, KeyInfo, KeyUtil, TypedKeyInfo } from '../../../js/common/core/crypto/key.js'; +import { KeyUtil, KeyInfoWithIdentity } 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'; @@ -41,9 +41,9 @@ export class BackupManualActionModule extends ViewModule { this.proceedBtn.click(this.view.setHandlerPrevent('double', () => this.actionManualBackupHandler())); }; - public doBackupOnEmailProvider = async (armoredKey: string) => { + public doBackupOnEmailProvider = async (encryptedPrvs: KeyInfoWithIdentity[]) => { const emailMsg = String(await $.get({ url: '/chrome/emails/email_intro.template.htm', dataType: 'html' })); - const emailAttachments = [this.asBackupFile(armoredKey)]; + const emailAttachments = encryptedPrvs.map(prv => this.asBackupFile(prv)); const headers = { from: this.view.acctEmail, recipients: { to: [{ email: this.view.acctEmail }] }, subject: GMAIL_RECOVERY_EMAIL_SUBJECTS[0] }; const msg = await SendableMsg.createPlain(this.view.acctEmail, headers, { 'text/html': emailMsg }, emailAttachments); if (this.view.emailProvider === 'gmail') { @@ -55,36 +55,40 @@ export class BackupManualActionModule extends ViewModule { private actionManualBackupHandler = async () => { const selected = $('input[type=radio][name=input_backup_choice]:checked').val(); - if (this.view.prvKeysToManuallyBackup.length <= 0) { + if (!this.view.identityOfKeysToManuallyBackup.length) { 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) { + const keyInfosToBackup = KeyUtil.filterKeysByIdentity( + await KeyStore.get(this.view.acctEmail), + this.view.identityOfKeysToManuallyBackup + ); + if (!keyInfosToBackup.length) { await Ui.modal.error('Sorry, could not extract these keys from storage. Please restart your browser and try again.'); return; } - // 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); + for (const ki of keyInfosToBackup) { + if (! await this.isPrivateKeyEncrypted(ki)) { + // todo: this check can also be moved to encryptForBackup method when we solve the same passphrase issue (#4060) + await Ui.modal.error('Sorry, cannot back up private key because it\'s not protected with a pass phrase.'); + return; + } + } + const checkStrength = selected === 'inbox' && this.view.action !== 'setup_manual'; + const encryptedArmoredPrvs = await this.encryptForBackup(keyInfosToBackup, { checkStrength }); + if (!encryptedArmoredPrvs) { + return; // error modal was already rendered inside encryptForBackup + } + if (selected === 'inbox') { + if (!await this.backupOnEmailProviderAndUpdateUi(encryptedArmoredPrvs)) { + return; // some error occured, message displayed, can retry, no reload needed } - await this.view.renderBackupDone(kinfos.length); + } else { + await this.backupAsFiles(encryptedArmoredPrvs); } + await this.view.renderBackupDone(keyInfosToBackup.length); } else if (selected === 'print') { await this.backupByBrint(); } else { @@ -92,12 +96,16 @@ export class BackupManualActionModule extends ViewModule { } }; - private asBackupFile = (armoredKey: string) => { - 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 asBackupFile = (prv: KeyInfoWithIdentity) => { + return new Attachment({ + name: `flowcrypt-backup-${this.view.acctEmail.replace(/[^A-Za-z0-9]+/g, '')}-${prv.id}.asc`, + type: 'application/pgp-keys', + data: Buf.fromUtfStr(prv.private) + }); }; - private encryptForBackup = async (kinfos: TypedKeyInfo[], checks: { strength: boolean }, primaryKeyIdentity: KeyIdentity): Promise => { - const kisWithPp = await Promise.all(kinfos.map(async (ki) => { + private encryptForBackup = async (keyInfos: KeyInfoWithIdentity[], checks: { checkStrength: boolean }): Promise => { + const kisWithPp = await Promise.all(keyInfos.map(async (ki) => { const passphrase = await PassphraseStore.get(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); @@ -108,10 +116,10 @@ export class BackupManualActionModule extends ViewModule { await Ui.modal.error(differentPassphrasesError); return undefined; } - if (checks.strength && distinctPassphrases[0] && !(Settings.evalPasswordStrength(distinctPassphrases[0]).word.pass)) { + if (checks.checkStrength && 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]) { + if (this.view.parentTabId && kisWithPp[0].passphrase === distinctPassphrases[0]) { Settings.redirectSubPage(this.view.acctEmail, this.view.parentTabId, '/chrome/settings/modules/change_passphrase.htm'); } return undefined; @@ -143,16 +151,16 @@ export class BackupManualActionModule extends ViewModule { } // 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 await this.encryptForBackup(keyInfos, checks); } - return kinfos.map(ki => ki.private).join('\n'); // todo: remove extra \n ? + return keyInfos; }; - private backupOnEmailProviderAndUpdateUi = async (data: string): Promise => { + private backupOnEmailProviderAndUpdateUi = async (encryptedPrvs: KeyInfoWithIdentity[]): Promise => { const origBtnText = this.proceedBtn.text(); Xss.sanitizeRender(this.proceedBtn, Ui.spinner('white')); try { - await this.doBackupOnEmailProvider(data); + await this.doBackupOnEmailProvider(encryptedPrvs); return true; } catch (e) { if (ApiErr.isNetErr(e)) { @@ -172,9 +180,11 @@ export class BackupManualActionModule extends ViewModule { } }; - private backupAsFile = async (data: string) => { // todo - add a non-encrypted download option - const attachment = this.asBackupFile(data); - Browser.saveToDownloads(attachment); + private backupAsFiles = async (encryptedPrvs: KeyInfoWithIdentity[]) => { // todo - add a non-encrypted download option + for (const encryptedArmoredPrv of encryptedPrvs) { + const attachment = this.asBackupFile(encryptedArmoredPrv); + Browser.saveToDownloads(attachment); + } await Ui.modal.info('Downloading private key backup file..'); }; @@ -186,7 +196,7 @@ export class BackupManualActionModule extends ViewModule { await this.view.renderBackupDone(0); }; - private isPrivateKeyEncrypted = async (ki: KeyInfo) => { + private isPrivateKeyEncrypted = async (ki: KeyInfoWithIdentity) => { const prv = await KeyUtil.parse(ki.private); if (await KeyUtil.decrypt(prv, '', undefined, 'OK-IF-ALREADY-DECRYPTED') === true) { return false; diff --git a/extension/chrome/settings/modules/backup-status-module.ts b/extension/chrome/settings/modules/backup-status-module.ts index 06f7cf87001..6fdf5d729cb 100644 --- a/extension/chrome/settings/modules/backup-status-module.ts +++ b/extension/chrome/settings/modules/backup-status-module.ts @@ -10,8 +10,8 @@ import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { Browser } from '../../../js/common/browser/browser.js'; import { BrowserMsg } from '../../../js/common/browser/browser-msg.js'; import { Backups } from '../../../js/common/api/email-provider/email-provider-api.js'; -import { KeyInfo } from '../../../js/common/core/crypto/key.js'; import { Str } from '../../../js/common/core/common.js'; +import { KeyInfoWithIdentity } from '../../../js/common/core/crypto/key.js'; export class BackupStatusModule extends ViewModule { @@ -75,7 +75,7 @@ export class BackupStatusModule extends ViewModule { $('pre.status_details').text(detailLines.join('\n')); }; - private describeBackupCounts = (longids: string[], keyinfos: KeyInfo[]) => { + private describeBackupCounts = (longids: string[], keyinfos: KeyInfoWithIdentity[]) => { let text = `${longids.length}`; if (keyinfos.length !== longids.length) { text += ` keys represented by ${Str.pluralize(keyinfos.length, 'backup')}`; diff --git a/extension/chrome/settings/modules/backup.ts b/extension/chrome/settings/modules/backup.ts index fd54c15952f..29da27201cf 100644 --- a/extension/chrome/settings/modules/backup.ts +++ b/extension/chrome/settings/modules/backup.ts @@ -16,7 +16,7 @@ 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 { KeyIdentity, KeyUtil, KeyInfoWithIdentity } from '../../../js/common/core/crypto/key.js'; import { Settings } from '../../../js/common/settings.js'; export class BackupView extends View { @@ -35,7 +35,7 @@ export class BackupView extends View { public emailProvider: EmailProvider = 'gmail'; public orgRules!: OrgRules; public tabId!: string; - public prvKeysToManuallyBackup: KeyIdentity[] = []; + public identityOfKeysToManuallyBackup: KeyIdentity[] = []; public fesUrl?: string; private readonly blocks = ['loading', 'module_status', 'module_manual']; @@ -54,9 +54,9 @@ export class BackupView extends View { } { const id = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'id'); - const type = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'type'); - if (id && type === 'openpgp') { - this.keyIdentity = { id, type }; + const family = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'type'); + if (id && family === 'openpgp') { + this.keyIdentity = { id, family }; } } this.gmail = new Gmail(this.acctEmail); @@ -123,28 +123,28 @@ export class BackupView extends View { }; private addKeyToBackup = (keyIdentity: KeyIdentity) => { - if (!this.prvKeysToManuallyBackup.some(prvIdentity => KeyUtil.identityEquals(prvIdentity, keyIdentity))) { - this.prvKeysToManuallyBackup.push(keyIdentity); + if (!this.identityOfKeysToManuallyBackup.some(prvIdentity => KeyUtil.identityEquals(prvIdentity, keyIdentity))) { + this.identityOfKeysToManuallyBackup.push(keyIdentity); } }; private removeKeyToBackup = (keyIdentity: KeyIdentity) => { - this.prvKeysToManuallyBackup.splice(this.prvKeysToManuallyBackup.findIndex(prvIdentity => KeyUtil.identityEquals(prvIdentity, keyIdentity)), 1); + this.identityOfKeysToManuallyBackup.splice(this.identityOfKeysToManuallyBackup.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!))) { + const kinfos = await KeyStore.get(this.acctEmail); + if (this.keyIdentity && this.keyIdentity.family === 'openpgp' && kinfos.some(ki => KeyUtil.identityEquals(ki, this.keyIdentity!))) { // todo: error if not found ? - this.addKeyToBackup({ id: this.keyIdentity.id, type: this.keyIdentity.type }); + this.addKeyToBackup({ id: this.keyIdentity.id, family: this.keyIdentity.family }); } 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 }); + } else if (kinfos.length === 1 && kinfos[0].family === 'openpgp') { + this.addKeyToBackup({ id: kinfos[0].id, family: kinfos[0].family }); } }; - private renderPrvKeysBackupSelection = async (kinfos: TypedKeyInfo[]) => { + private renderPrvKeysBackupSelection = async (kinfos: KeyInfoWithIdentity[]) => { for (const ki of kinfos) { const email = Xss.escape(String(ki.emails![0])); const dom = ` @@ -152,27 +152,27 @@ export class BackupView extends View {
`.trim(); $('.key_backup_selection').append(dom); // xss-escaped - if (ki.type === 'openpgp') { - this.addKeyToBackup({ type: ki.type, id: ki.id }); + if (ki.family === 'openpgp') { + this.addKeyToBackup({ family: ki.family, id: ki.id }); } } $('.input_prvkey_backup_checkbox').click(Ui.event.handle((target) => { - const type = $(target).data('type') as string; - if (type === 'openpgp') { + const family = $(target).data('type') as string; + if (family === 'openpgp') { const id = $(target).data('id') as string; if ($(target).prop('checked')) { - this.addKeyToBackup({ type, id }); + this.addKeyToBackup({ family, id }); } else { - this.removeKeyToBackup({ type, id }); + this.removeKeyToBackup({ family, id }); } } })); diff --git a/extension/chrome/settings/modules/change_passphrase.ts b/extension/chrome/settings/modules/change_passphrase.ts index d4cf8771332..be56cfc7f88 100644 --- a/extension/chrome/settings/modules/change_passphrase.ts +++ b/extension/chrome/settings/modules/change_passphrase.ts @@ -5,7 +5,7 @@ import { Assert } from '../../../js/common/assert.js'; import { Catch } from '../../../js/common/platform/catch.js'; import { KeyImportUi } from '../../../js/common/ui/key-import-ui.js'; -import { KeyInfo, Key, KeyUtil } from '../../../js/common/core/crypto/key.js'; +import { 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'; @@ -13,6 +13,7 @@ import { View } from '../../../js/common/view.js'; import { initPassphraseToggle } from '../../../js/common/ui/passphrase-ui.js'; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; +import { KeyStoreUtil, ParsedKeyInfo } from "../../../js/common/core/crypto/key-store-util.js"; import { OrgRules } from '../../../js/common/org-rules.js'; import { BrowserMsg } from '../../../js/common/browser/browser-msg.js'; import { Lang } from '../../../js/common/lang.js'; @@ -25,8 +26,7 @@ View.run(class ChangePassPhraseView extends View { private readonly parentTabId: string; private readonly keyImportUi = new KeyImportUi({}); - private primaryKi: KeyInfo | undefined; - private primaryPrv: Key | undefined; + private mostUsefulPrv: ParsedKeyInfo | undefined; private orgRules!: OrgRules; constructor() { @@ -43,16 +43,18 @@ View.run(class ChangePassPhraseView extends View { await initPassphraseToggle(['current_pass_phrase', 'new_pass_phrase', 'new_pass_phrase_confirm']); const privateKeys = await KeyStore.get(this.acctEmail); if (privateKeys.length > 1) { - $('#step_0_enter_current .sentence').text('Enter the current passphrase for your primary key'); - $('#step_0_enter_current #current_pass_phrase').attr('placeholder', 'Current primary key pass phrase'); - $('#step_1_enter_new #new_pass_phrase').attr('placeholder', 'Enter a new primary key pass phrase'); + $('#step_0_enter_current .sentence').text('Enter the current passphrase for your key'); + $('#step_0_enter_current #current_pass_phrase').attr('placeholder', 'Current key pass phrase'); + $('#step_1_enter_new #new_pass_phrase').attr('placeholder', 'Enter a new key pass phrase'); } - const primaryKi = await KeyStore.getFirstRequired(this.acctEmail); - this.primaryKi = primaryKi; - const storedOrSessionPp = await PassphraseStore.get(this.acctEmail, this.primaryKi); - const key = await KeyUtil.parse(this.primaryKi.private); - this.primaryPrv = key; - if (this.primaryPrv.fullyDecrypted || (storedOrSessionPp && await KeyUtil.decrypt(this.primaryPrv, storedOrSessionPp))) { + // todo - should be working across all keys. Existing keys may be encrypted for various pass phrases, + // which will complicate UI once implemented + this.mostUsefulPrv = KeyStoreUtil.chooseMostUseful( + await KeyStoreUtil.parse(await KeyStore.getRequired(this.acctEmail)), + 'EVEN-IF-UNUSABLE' + ); + const storedOrSessionPp = await PassphraseStore.get(this.acctEmail, this.mostUsefulPrv!.keyInfo); + if (this.mostUsefulPrv?.key.fullyDecrypted || (storedOrSessionPp && await KeyUtil.decrypt(this.mostUsefulPrv!.key, storedOrSessionPp))) { this.displayBlock('step_1_enter_new'); // current pp is already known $('#new_pass_phrase').focus(); } else { @@ -73,9 +75,9 @@ View.run(class ChangePassPhraseView extends View { }; private actionTestCurrentPassPhraseHandler = async () => { - const prv = await KeyUtil.parse(this.primaryKi!.private); + const prv = await KeyUtil.parse(this.mostUsefulPrv!.keyInfo.private); if (await KeyUtil.decrypt(prv, String($('#current_pass_phrase').val())) === true) { - this.primaryPrv = prv; + this.mostUsefulPrv!.key = prv; this.displayBlock('step_1_enter_new'); $('#new_pass_phrase').focus(); } else { @@ -109,17 +111,17 @@ View.run(class ChangePassPhraseView extends View { return; } try { - await KeyUtil.encrypt(this.primaryPrv!, newPp); + await KeyUtil.encrypt(this.mostUsefulPrv!.key, newPp); } catch (e) { Catch.reportErr(e); await Ui.modal.error(`There was an unexpected error. ${Lang.general.contactForSupportSentence(!!this.fesUrl)}\n\n${e instanceof Error ? e.stack : String(e)}`); return; } - await KeyStore.add(this.acctEmail, this.primaryPrv!); + await KeyStore.add(this.acctEmail, this.mostUsefulPrv!.key); const shouldSavePassphraseInStorage = !this.orgRules.forbidStoringPassPhrase() && - !!(await PassphraseStore.get(this.acctEmail, this.primaryKi!, true)); - await PassphraseStore.set('local', this.acctEmail, this.primaryKi!, shouldSavePassphraseInStorage ? newPp : undefined); - await PassphraseStore.set('session', this.acctEmail, this.primaryKi!, shouldSavePassphraseInStorage ? undefined : newPp); + !!(await PassphraseStore.get(this.acctEmail, this.mostUsefulPrv!.keyInfo, true)); + await PassphraseStore.set('local', this.acctEmail, this.mostUsefulPrv!.keyInfo, shouldSavePassphraseInStorage ? newPp : undefined); + await PassphraseStore.set('session', this.acctEmail, this.mostUsefulPrv!.keyInfo, shouldSavePassphraseInStorage ? undefined : newPp); if (this.orgRules.canBackupKeys()) { await Ui.modal.info('Now that you changed your pass phrase, you should back up your key. New backup will be protected with new passphrase.'); Settings.redirectSubPage(this.acctEmail, this.parentTabId, '/chrome/settings/modules/backup.htm', '&action=backup_manual'); diff --git a/extension/chrome/settings/modules/contacts.ts b/extension/chrome/settings/modules/contacts.ts index b8dc732aecc..ef727dc3f81 100644 --- a/extension/chrome/settings/modules/contacts.ts +++ b/extension/chrome/settings/modules/contacts.ts @@ -2,7 +2,7 @@ 'use strict'; -import { KeyUtil } from '../../../js/common/core/crypto/key.js'; +import { KeyFamily, KeyUtil } from '../../../js/common/core/crypto/key.js'; import { Str, Url } from '../../../js/common/core/common.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { Assert } from '../../../js/common/assert.js'; @@ -126,7 +126,7 @@ View.run(class ContactsView extends View { let tableContents = ''; for (const pubkey of contact.sortedPubkeys) { const keyid = Xss.escape(pubkey.pubkey.id); - const type = Xss.escape(pubkey.pubkey.type); + const type = Xss.escape(pubkey.pubkey.family); let status: string; if (pubkey.revoked) { status = 'revoked'; @@ -157,9 +157,9 @@ View.run(class ContactsView extends View { private actionRenderViewPublicKeyHandler = async (viewPubkeyButton: HTMLElement) => { const parentRow = $(viewPubkeyButton).closest('[email]'); const id = parentRow.attr('keyid')!; - const type = parentRow.attr('type')!; + const family = parentRow.attr('type')! as KeyFamily; const email = parentRow.attr('email')!; - const armoredPubkey = await ContactStore.getPubkey(undefined, { id, type }); + const armoredPubkey = await ContactStore.getPubkey(undefined, { id, family }); if (!armoredPubkey) { // todo: show error message like 'key disappeared'? return; @@ -169,7 +169,7 @@ View.run(class ContactsView extends View { Xss.sanitizeRender('h1', `${this.backBtn}${this.space}${email}      `); $('#view_contact .key_dump').text(armoredPubkey); $('#view_contact #container-pubkey-details').text([ - `Type: ${key.type}`, + `Type: ${key.family}`, `Fingerprint: ${Str.spaced(key.id || 'none')}`, `Users: ${key.emails?.join(', ')}`, `Created on: ${key.created ? new Date(key.created) : ''}`, @@ -213,9 +213,9 @@ View.run(class ContactsView extends View { private actionRemovePublicKey = async (rmPubkeyButton: HTMLElement) => { const parentRow = $(rmPubkeyButton).closest('[email]'); const id = parentRow.attr('keyid')!; - const type = parentRow.attr('type')!; + const family = parentRow.attr('type')! as KeyFamily; // todo - rename attr to "family" const email = parentRow.attr('email')!; - await ContactStore.unlinkPubkey(undefined, email, { id, type }); + await ContactStore.unlinkPubkey(undefined, email, { id, family }); await this.loadAndRenderContactList(); }; diff --git a/extension/chrome/settings/modules/keyserver.ts b/extension/chrome/settings/modules/keyserver.ts index cbef3bf1777..f647a9a1e5a 100644 --- a/extension/chrome/settings/modules/keyserver.ts +++ b/extension/chrome/settings/modules/keyserver.ts @@ -15,6 +15,7 @@ import { Xss } from '../../../js/common/platform/xss.js'; import { PubLookup } from '../../../js/common/api/pub-lookup.js'; import { OrgRules } from '../../../js/common/org-rules.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; +import { KeyStoreUtil } from "../../../js/common/core/crypto/key-store-util.js"; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; import { KeyUtil } from '../../../js/common/core/crypto/key.js'; @@ -80,9 +81,14 @@ View.run(class KeyserverView extends View { return await Ui.modal.error('Disallowed by your organisation rules'); } Xss.sanitizeRender(target, Ui.spinner('white')); - const primaryKi = await KeyStore.getFirstRequired(this.acctEmail); + const mostUsefulPrv = await KeyStoreUtil.chooseMostUseful( + await KeyStoreUtil.parse(await KeyStore.getRequired(this.acctEmail)), 'ONLY-FULLY-USABLE'); + if (!mostUsefulPrv) { + await Ui.modal.warning('This account has no usable key set up (may be expired or revoked). Check Additional Settings -> My Keys'); + return; + } try { - await this.pubLookup.attester.initialLegacySubmit(String($(target).attr('email')), primaryKi.public); + await this.pubLookup.attester.initialLegacySubmit(String($(target).attr('email')), mostUsefulPrv.keyInfo.public); } catch (e) { ApiErr.reportIfSignificant(e); await Ui.modal.error(ApiErr.eli5(e)); @@ -96,9 +102,15 @@ View.run(class KeyserverView extends View { return await Ui.modal.error('Disallowed by your organisation rules'); } Xss.sanitizeRender(target, Ui.spinner('white')); - const primaryKi = await KeyStore.getFirstRequired(this.acctEmail); + const prvs = await KeyStoreUtil.parse(await KeyStore.getRequired(this.acctEmail)); + const openpgpPrvs = prvs.filter(prv => prv.key.family === 'openpgp'); // attester doesn't support x509 + const mostUsefulPrv = KeyStoreUtil.chooseMostUseful(openpgpPrvs, 'ONLY-FULLY-USABLE'); + if (!mostUsefulPrv) { + await Ui.modal.warning('This account has no usable key set up (may be expired or revoked). Check Additional Settings -> My Keys'); + return; + } try { - const responseText = await this.pubLookup.attester.replacePubkey(String($(target).attr('email')), primaryKi.public); + const responseText = await this.pubLookup.attester.replacePubkey(String($(target).attr('email')), mostUsefulPrv.keyInfo.public); await Ui.modal.info(responseText); BrowserMsg.send.closePage(this.parentTabId); } catch (e) { diff --git a/extension/chrome/settings/modules/my_key.ts b/extension/chrome/settings/modules/my_key.ts index 31cfcb2bab3..89a6d91ce16 100644 --- a/extension/chrome/settings/modules/my_key.ts +++ b/extension/chrome/settings/modules/my_key.ts @@ -7,7 +7,7 @@ import { Assert } from '../../../js/common/assert.js'; import { Attachment } from '../../../js/common/core/attachment.js'; import { Browser } from '../../../js/common/browser/browser.js'; import { Buf } from '../../../js/common/core/buf.js'; -import { KeyInfo, Key, KeyUtil } from '../../../js/common/core/crypto/key.js'; +import { KeyInfoWithIdentity, Key, KeyUtil } from '../../../js/common/core/crypto/key.js'; import { Ui } from '../../../js/common/browser/ui.js'; import { Url, Str } from '../../../js/common/core/common.js'; import { View } from '../../../js/common/view.js'; @@ -27,7 +27,7 @@ View.run(class MyKeyView extends View { private readonly fingerprint: string; private readonly myKeyUserIdsUrl: string; private readonly myKeyUpdateUrl: string; - private keyInfo!: KeyInfo; + private keyInfo!: KeyInfoWithIdentity; private pubKey!: Key; private orgRules!: OrgRules; private pubLookup!: PubLookup; @@ -46,7 +46,7 @@ View.run(class MyKeyView extends View { this.orgRules = await OrgRules.newInstance(this.acctEmail); this.pubLookup = new PubLookup(this.orgRules); [this.keyInfo] = await KeyStore.get(this.acctEmail, [this.fingerprint]); - Assert.abortAndRenderErrorIfKeyinfoEmpty(this.keyInfo); + Assert.abortAndRenderErrorIfKeyinfoEmpty(this.keyInfo ? [this.keyInfo] : []); this.pubKey = await KeyUtil.parse(this.keyInfo.public); $('.action_view_user_ids').attr('href', this.myKeyUserIdsUrl); $('.action_view_update').attr('href', this.myKeyUpdateUrl); diff --git a/extension/chrome/settings/modules/my_key_update.ts b/extension/chrome/settings/modules/my_key_update.ts index 83b5d9e8d88..b912b28ddb3 100644 --- a/extension/chrome/settings/modules/my_key_update.ts +++ b/extension/chrome/settings/modules/my_key_update.ts @@ -4,7 +4,7 @@ import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { Assert } from '../../../js/common/assert.js'; -import { KeyInfo, Key, KeyUtil } from '../../../js/common/core/crypto/key.js'; +import { Key, KeyInfoWithIdentity, KeyUtil } from '../../../js/common/core/crypto/key.js'; import { Lang } from '../../../js/common/lang.js'; import { PgpArmor } from '../../../js/common/core/crypto/pgp/pgp-armor.js'; import { Settings } from '../../../js/common/settings.js'; @@ -26,7 +26,7 @@ View.run(class MyKeyUpdateView extends View { private readonly showKeyUrl: string; private readonly inputPrivateKey = $('.input_private_key'); private readonly prvHeaders = PgpArmor.headers('privateKey'); - private primaryKi: KeyInfo | undefined; + private ki: KeyInfoWithIdentity | undefined; private orgRules!: OrgRules; private pubLookup!: PubLookup; @@ -53,12 +53,12 @@ View.run(class MyKeyUpdateView extends View { } else { $('#content').show(); this.pubLookup = new PubLookup(this.orgRules); - [this.primaryKi] = await KeyStore.get(this.acctEmail, [this.fingerprint]); - Assert.abortAndRenderErrorIfKeyinfoEmpty(this.primaryKi); + [this.ki] = await KeyStore.get(this.acctEmail, [this.fingerprint]); + Assert.abortAndRenderErrorIfKeyinfoEmpty(this.ki ? [this.ki] : []); $('.action_show_public_key').attr('href', this.showKeyUrl); $('.email').text(this.acctEmail); - $('.fingerprint').text(Str.spaced(this.primaryKi.fingerprints[0])); - this.inputPrivateKey.attr('placeholder', this.inputPrivateKey.attr('placeholder') + ' (' + this.primaryKi.fingerprints[0] + ')'); + $('.fingerprint').text(Str.spaced(this.ki.fingerprints[0])); + this.inputPrivateKey.attr('placeholder', this.inputPrivateKey.attr('placeholder') + ' (' + this.ki.fingerprints[0] + ')'); } }; @@ -69,14 +69,14 @@ View.run(class MyKeyUpdateView extends View { private storeUpdatedKeyAndPassphrase = async (updatedPrv: Key, updatedPrvPassphrase: string) => { const shouldSavePassphraseInStorage = !this.orgRules.forbidStoringPassPhrase() && - !!(await PassphraseStore.get(this.acctEmail, this.primaryKi!, true)); + !!(await PassphraseStore.get(this.acctEmail, this.ki!, true)); await KeyStore.add(this.acctEmail, updatedPrv); - await PassphraseStore.set('local', this.acctEmail, this.primaryKi!, shouldSavePassphraseInStorage ? updatedPrvPassphrase : undefined); - await PassphraseStore.set('session', this.acctEmail, this.primaryKi!, shouldSavePassphraseInStorage ? undefined : updatedPrvPassphrase); + await PassphraseStore.set('local', this.acctEmail, this.ki!, shouldSavePassphraseInStorage ? updatedPrvPassphrase : undefined); + await PassphraseStore.set('session', this.acctEmail, this.ki!, shouldSavePassphraseInStorage ? undefined : updatedPrvPassphrase); if (this.orgRules.canSubmitPubToAttester() && await Ui.modal.confirm('Public and private key updated locally.\n\nUpdate public records with new Public Key?')) { try { // todo: make sure this is never called for x509 keys - await Ui.modal.info(await this.pubLookup.attester.updatePubkey(this.primaryKi!.longid, KeyUtil.armor(await KeyUtil.asPublicKey(updatedPrv)))); + await Ui.modal.info(await this.pubLookup.attester.updatePubkey(this.ki!.longid, KeyUtil.armor(await KeyUtil.asPublicKey(updatedPrv)))); } catch (e) { ApiErr.reportIfSignificant(e); await Ui.modal.error(`Error updating public records:\n\n${ApiErr.eli5(e)}\n\n(but local update was successful)`); @@ -93,8 +93,8 @@ View.run(class MyKeyUpdateView extends View { await Ui.modal.warning(Lang.setup.keyFormattedWell(this.prvHeaders.begin, String(this.prvHeaders.end)), Ui.testCompatibilityLink); } else if (updatedKey.isPublic) { await Ui.modal.warning('This was a public key. Please insert a private key instead. It\'s a block of text starting with "' + this.prvHeaders.begin + '"'); - } else if (updatedKey.id !== (await KeyUtil.parse(this.primaryKi!.public)).id) { - await Ui.modal.warning(`This key ${Str.spaced(updatedKey.id || 'err')} does not match your current key ${Str.spaced(this.primaryKi!.fingerprints[0])}`); + } else if (updatedKey.id !== (await KeyUtil.parse(this.ki!.public)).id) { + await Ui.modal.warning(`This key ${Str.spaced(updatedKey.id || 'err')} does not match your current key ${Str.spaced(this.ki!.fingerprints[0])}`); } else if (await KeyUtil.decrypt(updatedKey, updatedKeyPassphrase) !== true) { await Ui.modal.error('The pass phrase does not match.\n\nPlease enter pass phrase of the newly updated key.'); } else { diff --git a/extension/chrome/settings/modules/my_key_user_ids.ts b/extension/chrome/settings/modules/my_key_user_ids.ts index 8ac1c7ed2fd..16bfc327c3e 100644 --- a/extension/chrome/settings/modules/my_key_user_ids.ts +++ b/extension/chrome/settings/modules/my_key_user_ids.ts @@ -2,7 +2,7 @@ 'use strict'; -import { KeyInfo, KeyUtil } from '../../../js/common/core/crypto/key.js'; +import { KeyInfoWithIdentity, KeyUtil } from '../../../js/common/core/crypto/key.js'; import { Assert } from '../../../js/common/assert.js'; import { Url, Str } from '../../../js/common/core/common.js'; @@ -15,24 +15,24 @@ View.run(class MyKeyUserIdsView extends View { private readonly acctEmail: string; private readonly fingerprint: string; private readonly myKeyUrl: string; - private primaryKi: KeyInfo | undefined; + private ki: KeyInfoWithIdentity | undefined; constructor() { super(); const uncheckedUrlParams = Url.parse(['acctEmail', 'fingerprint', 'parentTabId']); this.acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail'); - this.fingerprint = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'fingerprint') || 'primary'; + this.fingerprint = Assert.urlParamRequire.string(uncheckedUrlParams, 'fingerprint'); this.myKeyUrl = Url.create('my_key.htm', uncheckedUrlParams); } public render = async () => { - [this.primaryKi] = await KeyStore.get(this.acctEmail, [this.fingerprint]); - Assert.abortAndRenderErrorIfKeyinfoEmpty(this.primaryKi); + [this.ki] = await KeyStore.get(this.acctEmail, [this.fingerprint]); + Assert.abortAndRenderErrorIfKeyinfoEmpty(this.ki ? [this.ki] : []); $('.action_show_public_key').attr('href', this.myKeyUrl); - const prv = await KeyUtil.parse(this.primaryKi.private); + const prv = await KeyUtil.parse(this.ki.private); Xss.sanitizeRender('.user_ids', prv.identities.map((uid: string) => `
${Xss.escape(uid)}
`).join('')); $('.email').text(this.acctEmail); - $('.fingerprint').text(Str.spaced(this.primaryKi.fingerprints[0])); + $('.fingerprint').text(Str.spaced(this.ki.fingerprints[0])); }; public setHandlers = () => { diff --git a/extension/chrome/settings/modules/security.htm b/extension/chrome/settings/modules/security.htm index ff0227802dc..05b87f608c9 100644 --- a/extension/chrome/settings/modules/security.htm +++ b/extension/chrome/settings/modules/security.htm @@ -94,6 +94,7 @@

Password protected messages

+ diff --git a/extension/chrome/settings/modules/security.ts b/extension/chrome/settings/modules/security.ts index 0869d052368..ad4c1fcd18f 100644 --- a/extension/chrome/settings/modules/security.ts +++ b/extension/chrome/settings/modules/security.ts @@ -6,7 +6,6 @@ import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { Assert } from '../../../js/common/assert.js'; import { Catch } from '../../../js/common/platform/catch.js'; -import { KeyInfo } 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'; @@ -15,6 +14,7 @@ import { Xss } from '../../../js/common/platform/xss.js'; 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 { KeyStoreUtil, ParsedKeyInfo } from "../../../js/common/core/crypto/key-store-util.js"; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; import { OrgRules } from '../../../js/common/org-rules.js'; import { AccountServer } from '../../../js/common/api/account-server.js'; @@ -24,7 +24,7 @@ View.run(class SecurityView extends View { private readonly acctEmail: string; private readonly parentTabId: string; - private primaryKi: KeyInfo | undefined; + private prvs!: ParsedKeyInfo[]; private authInfo: FcUuidAuth | undefined; private orgRules!: OrgRules; private acctServer: AccountServer; @@ -39,7 +39,7 @@ View.run(class SecurityView extends View { public render = async () => { await initPassphraseToggle(['passphrase_entry']); - this.primaryKi = await KeyStore.getFirstRequired(this.acctEmail); + this.prvs = await KeyStoreUtil.parse(await KeyStore.getRequired(this.acctEmail)); this.authInfo = await AcctStore.authInfo(this.acctEmail); const storage = await AcctStore.get(this.acctEmail, ['hide_message_password', 'outgoing_language']); this.orgRules = await OrgRules.newInstance(this.acctEmail); @@ -60,19 +60,19 @@ View.run(class SecurityView extends View { }; private renderPassPhraseOptionsIfStoredPermanently = async () => { - const keys = await KeyStore.get(this.acctEmail); - if (await this.isAnyPassPhraseStoredPermanently(keys)) { + if (await this.isAnyPassPhraseStoredPermanently(this.prvs)) { $('.forget_passphrase').css('display', ''); $('.action_forget_pp').click(this.setHandler(() => { $('.forget_passphrase').css('display', 'none'); $('.passphrase_entry_container').css('display', ''); })); $('.confirm_passphrase_requirement_change').click(this.setHandler(async () => { - const primaryKiPP = await PassphraseStore.get(this.acctEmail, this.primaryKi!); - if ($('input#passphrase_entry').val() === primaryKiPP) { - for (const key of keys) { - await PassphraseStore.set('local', this.acctEmail, key, undefined); - await PassphraseStore.set('session', this.acctEmail, key, undefined); + const allPassPhrases = (await Promise.all(this.prvs.map(prv => PassphraseStore.get(this.acctEmail, prv.keyInfo)))) + .filter(pp => !!pp); + if (allPassPhrases.includes(String($('input#passphrase_entry').val()))) { + for (const key of this.prvs) { + await PassphraseStore.set('local', this.acctEmail, key.keyInfo, undefined); + await PassphraseStore.set('session', this.acctEmail, key.keyInfo, undefined); } window.location.reload(); } else { @@ -124,9 +124,9 @@ View.run(class SecurityView extends View { window.location.reload(); }; - private isAnyPassPhraseStoredPermanently = async (keys: KeyInfo[]) => { + private isAnyPassPhraseStoredPermanently = async (keys: ParsedKeyInfo[]) => { for (const key of keys) { - if (await PassphraseStore.get(this.acctEmail, key, true)) { + if (await PassphraseStore.get(this.acctEmail, key.keyInfo, true)) { return true; } } diff --git a/extension/chrome/settings/modules/test_passphrase.ts b/extension/chrome/settings/modules/test_passphrase.ts index 635f7bcf198..12fadaf0413 100644 --- a/extension/chrome/settings/modules/test_passphrase.ts +++ b/extension/chrome/settings/modules/test_passphrase.ts @@ -5,7 +5,7 @@ import { Assert } from '../../../js/common/assert.js'; import { BrowserMsg } from '../../../js/common/browser/browser-msg.js'; import { Lang } from '../../../js/common/lang.js'; -import { Key, KeyUtil } from '../../../js/common/core/crypto/key.js'; +import { 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'; @@ -13,11 +13,12 @@ import { View } from '../../../js/common/view.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { initPassphraseToggle } from '../../../js/common/ui/passphrase-ui.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; +import { KeyStoreUtil, ParsedKeyInfo } from "../../../js/common/core/crypto/key-store-util.js"; View.run(class TestPassphrase extends View { private readonly acctEmail: string; private readonly parentTabId: string; - private primaryKey: Key | undefined; + private mostUsefulPrv: ParsedKeyInfo | undefined; constructor() { super(); @@ -27,10 +28,14 @@ View.run(class TestPassphrase extends View { } public render = async () => { - const keyInfo = await KeyStore.getFirstRequired(this.acctEmail); + // todo - should test all somehow. But each key may have different pass phrase, + // therefore UI will get more complicated + this.mostUsefulPrv = KeyStoreUtil.chooseMostUseful( + await KeyStoreUtil.parse(await KeyStore.getRequired(this.acctEmail)), + 'EVEN-IF-UNUSABLE' + ); await initPassphraseToggle(['password']); - this.primaryKey = await KeyUtil.parse(keyInfo.private); - if (!this.primaryKey.fullyEncrypted) { + if (!this.mostUsefulPrv?.key.fullyEncrypted) { const setUpPpUrl = Url.create('change_passphrase.htm', { acctEmail: this.acctEmail, parentTabId: this.parentTabId }); Xss.sanitizeRender('#content', `
No pass phrase set up yet: set up pass phrase
`); return; @@ -44,7 +49,7 @@ View.run(class TestPassphrase extends View { }; private verifyHandler = async () => { - if (await KeyUtil.decrypt(this.primaryKey!, String($('#password').val())) === true) { + if (await KeyUtil.decrypt(this.mostUsefulPrv!.key, String($('#password').val())) === true) { Xss.sanitizeRender('#content', `
${Lang.setup.ppMatchAllSet}
diff --git a/extension/chrome/settings/setup.ts b/extension/chrome/settings/setup.ts index 8ebf5473f8f..5c1e9f80837 100644 --- a/extension/chrome/settings/setup.ts +++ b/extension/chrome/settings/setup.ts @@ -7,7 +7,7 @@ import { Url } from '../../js/common/core/common.js'; import { ApiErr } from '../../js/common/api/shared/api-error.js'; import { Assert } from '../../js/common/assert.js'; import { Catch } from '../../js/common/platform/catch.js'; -import { KeyInfo, Key, KeyUtil } from '../../js/common/core/crypto/key.js'; +import { Key, KeyInfoWithIdentity, KeyUtil } from '../../js/common/core/crypto/key.js'; import { Gmail } from '../../js/common/api/email-provider/gmail/gmail.js'; import { Google } from '../../js/common/api/email-provider/gmail/google.js'; import { KeyImportUi } from '../../js/common/ui/key-import-ui.js'; @@ -26,6 +26,7 @@ import { initPassphraseToggle } from '../../js/common/ui/passphrase-ui.js'; import { PubLookup } from '../../js/common/api/pub-lookup.js'; import { Scopes, AcctStoreDict, AcctStore } from '../../js/common/platform/store/acct-store.js'; import { KeyStore } from '../../js/common/platform/store/key-store.js'; +import { KeyStoreUtil } from "../../js/common/core/crypto/key-store-util.js"; import { PassphraseStore } from '../../js/common/platform/store/passphrase-store.js'; import { ContactStore } from '../../js/common/platform/store/contact-store.js'; import { KeyManager } from '../../js/common/api/key-server/key-manager.js'; @@ -63,7 +64,7 @@ export class SetupView extends View { public pubLookup!: PubLookup; public keyManager: KeyManager | undefined; // not set if no url in org rules - public fetchedKeyBackups: KeyInfo[] = []; + public fetchedKeyBackups: KeyInfoWithIdentity[] = []; public fetchedKeyBackupsUniqueLongids: string[] = []; public importedKeysUniqueLongids: string[] = []; public mathingPassphrases: string[] = []; @@ -222,9 +223,12 @@ export class SetupView extends View { public submitPublicKeys = async ( { submit_main, submit_all }: { submit_main: boolean, submit_all: boolean } ): Promise => { - const primaryKi = await KeyStore.getFirstRequired(this.acctEmail); + const mostUsefulPrv = KeyStoreUtil.chooseMostUseful( + await KeyStoreUtil.parse(await KeyStore.getRequired(this.acctEmail)), + 'ONLY-FULLY-USABLE' + ); try { - await this.submitPublicKeyIfNeeded(primaryKi.public, { submit_main, submit_all }); + await this.submitPublicKeyIfNeeded(mostUsefulPrv?.keyInfo.public, { submit_main, submit_all }); } catch (e) { return await Settings.promptToRetry( e, @@ -301,12 +305,25 @@ export class SetupView extends View { return true; }; - private submitPublicKeyIfNeeded = async (armoredPubkey: string, options: { submit_main: boolean, submit_all: boolean }) => { + /** + * empty pubkey means key not usable + */ + private submitPublicKeyIfNeeded = async ( + armoredPubkey: string | undefined, + options: { submit_main: boolean, submit_all: boolean } + ) => { if (!options.submit_main) { return; } if (!this.orgRules.canSubmitPubToAttester()) { - await Ui.modal.error('Not submitting public key to Attester - disabled for your org'); + if (!this.orgRules.usesKeyManager) { // users who use EKM get their setup automated - no need to inform them of this + // other users chose this manually - let them know it's not allowed + await Ui.modal.error('Not submitting public key to Attester - disabled for your org'); + } + return; + } + if (!armoredPubkey) { + await Ui.modal.warning('Public key not usable - not sumbitting to Attester'); return; } const pub = await KeyUtil.parse(armoredPubkey); diff --git a/extension/chrome/settings/setup/setup-create-key.ts b/extension/chrome/settings/setup/setup-create-key.ts index ad64bd556ad..129415a5a36 100644 --- a/extension/chrome/settings/setup/setup-create-key.ts +++ b/extension/chrome/settings/setup/setup-create-key.ts @@ -38,7 +38,7 @@ export class SetupCreateKeyModule { await this.view.submitPublicKeys(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 }); + window.location.href = Url.create('modules/backup.htm', { action, acctEmail: this.view.acctEmail, idToken: this.view.idToken, id: keyIdentity.id, type: keyIdentity.family }); } else { await this.view.submitPublicKeys(opts); await this.view.finalizeSetup(); @@ -73,6 +73,6 @@ export class SetupCreateKeyModule { const key = await OpenPGPKey.create(pgpUids, keyAlgo, options.passphrase, expireMonths); const prv = await KeyUtil.parse(key.private); await this.view.saveKeysAndPassPhrase([prv], options); - return { id: prv.id, type: prv.type }; + return { id: prv.id, family: prv.family }; }; } diff --git a/extension/chrome/settings/setup/setup-import-key.ts b/extension/chrome/settings/setup/setup-import-key.ts index a2053b903f3..b94bc87b99c 100644 --- a/extension/chrome/settings/setup/setup-import-key.ts +++ b/extension/chrome/settings/setup/setup-import-key.ts @@ -31,7 +31,7 @@ export class SetupImportKeyModule { }; try { const checked = await this.view.keyImportUi.checkPrv(this.view.acctEmail, String($('#step_2b_manual_enter .input_private_key').val()), options.passphrase); - if (checked.decrypted.type === 'x509') { + if (checked.decrypted.family === 'x509') { if (!await Ui.modal.confirm('Using S/MIME as the only key on account is experimental. ' + 'You should instead import an OpenPGP key here, and then add S/MIME keys as additional keys in FlowCrypt Settings.' + '\n\nContinue anyway? (not recommented).')) { diff --git a/extension/js/background_page/migrations.ts b/extension/js/background_page/migrations.ts index a59b87f0d32..d2accf0fc5c 100644 --- a/extension/js/background_page/migrations.ts +++ b/extension/js/background_page/migrations.ts @@ -3,7 +3,7 @@ 'use strict'; import { storageLocalGetAll, storageLocalRemove } from '../common/browser/chrome.js'; -import { KeyInfo, KeyUtil } from '../common/core/crypto/key.js'; +import { KeyInfoWithIdentity, KeyUtil } from '../common/core/crypto/key.js'; import { SmimeKey } from '../common/core/crypto/smime/smime-key.js'; import { Str } from '../common/core/common.js'; import { ContactStore, Email, Pubkey } from '../common/platform/store/contact-store.js'; @@ -31,7 +31,7 @@ type PubkeyMigrationData = { const addKeyInfoFingerprints = async () => { for (const acctEmail of await GlobalStore.acctEmailsGet()) { const originalKis = await KeyStore.get(acctEmail); - const updated: KeyInfo[] = []; + const updated: KeyInfoWithIdentity[] = []; for (const originalKi of originalKis) { updated.push(await KeyUtil.keyInfoObj(await KeyUtil.parse(originalKi.private))); } @@ -78,7 +78,7 @@ export const migrateGlobal = async () => { }; const processSmimeKey = (pubkey: Pubkey, tx: IDBTransaction, data: PubkeyMigrationData, next: () => void) => { - if (KeyUtil.getKeyType(pubkey.armoredKey) !== 'x509') { + if (KeyUtil.getKeyFamily(pubkey.armoredKey) !== 'x509') { next(); return; } @@ -176,7 +176,7 @@ export const updateOpgpRevocations = async (db: IDBDatabase): Promise => { const search = tx.objectStore('pubkeys').getAll(); ContactStore.setReqPipe(search, resolve, reject); }); - const revokedKeys = (await Promise.all(pubkeys.filter(entity => KeyUtil.getKeyType(entity.armoredKey) === 'openpgp'). + const revokedKeys = (await Promise.all(pubkeys.filter(entity => KeyUtil.getKeyFamily(entity.armoredKey) === 'openpgp'). map(async (entity) => await KeyUtil.parse(entity.armoredKey)))). filter(k => k.revoked); const txUpdate = db.transaction(['revocations'], 'readwrite'); diff --git a/extension/js/common/api/email-provider/email-provider-api.ts b/extension/js/common/api/email-provider/email-provider-api.ts index dc152a02995..777fd5d729f 100644 --- a/extension/js/common/api/email-provider/email-provider-api.ts +++ b/extension/js/common/api/email-provider/email-provider-api.ts @@ -6,7 +6,7 @@ import { Api, ChunkedCb, ProgressCb } from '../shared/api.js'; -import { KeyInfo } from '../../core/crypto/key.js'; +import { KeyInfoWithIdentity } from '../../core/crypto/key.js'; import { GmailRes } from './gmail/gmail-parser.js'; import { GmailResponseFormat } from './gmail/gmail.js'; import { SendableMsg } from './sendable-msg.js'; @@ -27,7 +27,7 @@ export type ReplyParams = { }; export type Backups = { - keyinfos: { backups: KeyInfo[], backupsImported: KeyInfo[], backupsNotImported: KeyInfo[], importedNotBackedUp: KeyInfo[] }, + keyinfos: { backups: KeyInfoWithIdentity[], backupsImported: KeyInfoWithIdentity[], backupsNotImported: KeyInfoWithIdentity[], importedNotBackedUp: KeyInfoWithIdentity[] }, longids: { backups: string[], backupsImported: string[], backupsNotImported: string[], importedNotBackedUp: string[] }, }; diff --git a/extension/js/common/api/email-provider/gmail/google.ts b/extension/js/common/api/email-provider/gmail/google.ts index 9fae27267b7..87b8ab39a42 100644 --- a/extension/js/common/api/email-provider/gmail/google.ts +++ b/extension/js/common/api/email-provider/gmail/google.ts @@ -56,9 +56,9 @@ export class Google { GoogleAuth.apiGoogleCallRetryAuthErrorOneTime(acctEmail, { xhr, url: searchOtherContactsUrl, method, data, headers, contentType, crossDomain: true, async: true }) as Promise ]); - const primaryContacts = contacts[0].results || []; + const userContacts = contacts[0].results || []; const otherContacts = contacts[1].results || []; - const contactsMerged = [...primaryContacts, ...otherContacts]; + const contactsMerged = [...userContacts, ...otherContacts]; return contactsMerged .filter(entry => !!(entry.person?.emailAddresses || []).find(email => email.metadata.primary === true)) // find all entries that have primary email .map(entry => { diff --git a/extension/js/common/api/email-provider/sendable-msg.ts b/extension/js/common/api/email-provider/sendable-msg.ts index 78a4dce775d..3db6e0ce1ec 100644 --- a/extension/js/common/api/email-provider/sendable-msg.ts +++ b/extension/js/common/api/email-provider/sendable-msg.ts @@ -7,7 +7,7 @@ import { Mime, MimeEncodeType, SendableMsgBody } from '../../core/mime.js'; import { Attachment } from '../../core/attachment.js'; import { Buf } from '../../core/buf.js'; import { KeyStore } from '../../platform/store/key-store.js'; -import { KeyUtil } from '../../core/crypto/key.js'; +import { KeyStoreUtil } from "../../core/crypto/key-store-util.js"; import { ParsedRecipients } from './email-provider-api.js'; type SendableMsgHeaders = { @@ -94,10 +94,13 @@ export class SendableMsg { }; private static create = async (acctEmail: string, { from, recipients, subject, thread, body, attachments, type, isDraft, externalId }: SendableMsgDefinition): Promise => { - const primaryKi = await KeyStore.getFirstRequired(acctEmail); + const mostUsefulPrv = KeyStoreUtil.chooseMostUseful( + await KeyStoreUtil.parse(await KeyStore.getRequired(acctEmail)), + 'EVEN-IF-UNUSABLE' + ); const headers: Dict = {}; - if (primaryKi && KeyUtil.getKeyType(primaryKi.private) === 'openpgp') { - headers.Openpgp = `id=${primaryKi.longid}`; // todo - use autocrypt format + if (mostUsefulPrv && mostUsefulPrv.key.family === 'openpgp') { + headers.Openpgp = `id=${mostUsefulPrv.key.id}`; // todo - use autocrypt format } return new SendableMsg( acctEmail, diff --git a/extension/js/common/assert.ts b/extension/js/common/assert.ts index 73b6e7d23bd..0e036a60857 100644 --- a/extension/js/common/assert.ts +++ b/extension/js/common/assert.ts @@ -5,7 +5,7 @@ import { Catch, UnreportableError } from './platform/catch.js'; import { Dict, UrlParam, UrlParams } from './core/common.js'; import { Browser } from './browser/browser.js'; -import { KeyInfo, KeyUtil } from './core/crypto/key.js'; +import { KeyInfoWithIdentity, KeyUtil } from './core/crypto/key.js'; import { Settings } from './settings.js'; import { Ui } from './browser/ui.js'; import { Xss } from './platform/xss.js'; @@ -37,26 +37,31 @@ export class Assert { public static abortAndRenderErrOnUnprotectedKey = async (acctEmail?: string, tabId?: string) => { if (acctEmail) { - const primaryKi = await KeyStore.getFirstOptional(acctEmail); + const kis = await KeyStore.get(acctEmail); + const parsedKeys = await Promise.all(kis.map(ki => KeyUtil.parse(ki.private))); const { setup_done } = await AcctStore.get(acctEmail, ['setup_done']); - if (setup_done && primaryKi && !(await KeyUtil.parse(primaryKi.private)).fullyEncrypted) { - if (window.location.pathname === '/chrome/settings/index.htm') { - await Settings.renderSubPage(acctEmail, tabId!, '/chrome/settings/modules/change_passphrase.htm'); - } else { - const msg = `Protect your key with a pass phrase to finish setup.`; - const r = await Ui.renderOverlayPromptAwaitUserChoice({ finishSetup: {}, later: { color: 'gray' } }, msg, undefined, - Lang.general.contactIfNeedAssistance(await isFesUsed(acctEmail))); - if (r === 'finish_setup') { - await Browser.openSettingsPage('index.htm', acctEmail); + if (setup_done && kis.length) { + const key = parsedKeys.find(k => !k.fullyEncrypted); + if (key) { + // can fix one key at a time. When they reload, it will complain about another key + if (window.location.pathname === '/chrome/settings/index.htm') { + await Settings.renderSubPage(acctEmail, tabId!, '/chrome/settings/modules/change_passphrase.htm'); + } else { + const msg = `Protect your key with a pass phrase to finish setup.`; + const r = await Ui.renderOverlayPromptAwaitUserChoice({ finishSetup: {}, later: { color: 'gray' } }, msg, undefined, + Lang.general.contactIfNeedAssistance(await isFesUsed(acctEmail))); + if (r === 'finish_setup') { + await Browser.openSettingsPage('index.htm', acctEmail); + } } } } } }; - public static abortAndRenderErrorIfKeyinfoEmpty = (ki: KeyInfo | undefined, doThrow: boolean = true) => { - if (!ki) { - const msg = `Cannot find primary key. Is FlowCrypt not set up yet? ${Ui.retryLink()}`; + public static abortAndRenderErrorIfKeyinfoEmpty = (kis: KeyInfoWithIdentity[], doThrow: boolean = true) => { + if (!kis.length) { + const msg = `Cannot find any account key. Is FlowCrypt not set up yet? ${Ui.retryLink()}`; const target = $($('#content').length ? '#content' : 'body'); target.addClass('error-occured'); Xss.sanitizeRender(target, msg); diff --git a/extension/js/common/core/crypto/key-store-util.ts b/extension/js/common/core/crypto/key-store-util.ts new file mode 100644 index 00000000000..f60ca8d0ce7 --- /dev/null +++ b/extension/js/common/core/crypto/key-store-util.ts @@ -0,0 +1,40 @@ +/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ + +import { Key, KeyInfoWithIdentity, KeyUtil } from './key.js'; + +export type ParsedKeyInfo = { keyInfo: KeyInfoWithIdentity, key: Key }; + +export class KeyStoreUtil { + + public static parse = async (keyInfos: KeyInfoWithIdentity[]): Promise => { + const parsed: ParsedKeyInfo[] = []; + for (const keyInfo of keyInfos) { + const key = await KeyUtil.parse(keyInfo.private); + parsed.push({ keyInfo, key }); + } + return parsed; + }; + + public static chooseMostUseful = ( + prvs: ParsedKeyInfo[], criteria: 'ONLY-FULLY-USABLE' | 'AT-LEAST-USABLE-BUT-EXPIRED' | 'EVEN-IF-UNUSABLE' + ): ParsedKeyInfo | undefined => { + const usablePrv = prvs.find(prv => prv.key.usableForEncryption && prv.key.usableForSigning) + || prvs.find(prv => prv.key.usableForEncryption) + || prvs.find(prv => prv.key.usableForSigning); + if (usablePrv || criteria === 'ONLY-FULLY-USABLE') { + return usablePrv; + } + const usableExpiredPrv = prvs.find(prv => (prv.key.usableForEncryption || prv.key.usableForEncryptionButExpired) + && (prv.key.usableForSigning || prv.key.usableForSigningButExpired) + ) + || prvs.find(prv => prv.key.usableForEncryption || prv.key.usableForEncryptionButExpired) + || prvs.find(prv => prv.key.usableForSigning || prv.key.usableForSigningButExpired); + if (usableExpiredPrv || criteria === 'AT-LEAST-USABLE-BUT-EXPIRED') { + return usableExpiredPrv; + } + // criteria === EVEN-IF-UNUSABLE + return prvs.find(prv => !prv.key.revoked) + || prvs[0]; + }; + +} diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index 9a50421022c..14578c5feac 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -48,7 +48,7 @@ export interface Key extends KeyIdentity { export type PubkeyResult = { pubkey: Key, email: string, isMine: boolean }; -export interface KeyInfo { +export interface StoredKeyInfo { private: string; public: string; // this cannot be Pubkey has it's being passed to localstorage longid: string; @@ -80,15 +80,17 @@ export interface PubkeyInfoWithLastCheck extends PubkeyInfo { lastCheck?: number | undefined; } +export type KeyFamily = 'openpgp' | 'x509'; + 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' + family: KeyFamily; } -export interface TypedKeyInfo extends KeyInfo, KeyIdentity { +export interface KeyInfoWithIdentity extends StoredKeyInfo, KeyIdentity { } -export interface ExtendedKeyInfo extends TypedKeyInfo { +export interface KeyInfoWithIdentityAndOptionalPp extends KeyInfoWithIdentity { passphrase?: string; } @@ -101,19 +103,19 @@ export class UnexpectedKeyTypeError extends Error { } export class KeyUtil { public static identityEquals = (keyIdentity1: KeyIdentity, keyIdentity2: KeyIdentity) => { - return keyIdentity1.id === keyIdentity2.id && keyIdentity1.type === keyIdentity2.type; + return keyIdentity1.id === keyIdentity2.id && keyIdentity1.family === keyIdentity2.family; }; - public static filterKeys(kis: T[], ids: KeyIdentity[]): T[] { + public static filterKeysByIdentity(kis: T[], ids: KeyIdentity[]): T[] { return kis.filter(ki => ids.some(i => KeyUtil.identityEquals(i, ki))); } - public static filterKeysByTypeAndSenderEmail = (keys: TypedKeyInfo[], email: string, type: 'openpgp' | 'x509' | undefined): TypedKeyInfo[] => { - let foundKeys: TypedKeyInfo[] = []; + public static filterKeysByTypeAndSenderEmail = (keys: KeyInfoWithIdentity[], email: string, type: 'openpgp' | 'x509' | undefined): KeyInfoWithIdentity[] => { + let foundKeys: KeyInfoWithIdentity[] = []; if (type) { - foundKeys = keys.filter(key => key.emails?.includes(email.toLowerCase()) && key.type === type); + foundKeys = keys.filter(key => key.emails?.includes(email.toLowerCase()) && key.family === type); if (!foundKeys.length) { - foundKeys = keys.filter(key => key.type === type); + foundKeys = keys.filter(key => key.family === type); } } else { foundKeys = keys.filter(key => key.emails?.includes(email.toLowerCase())); @@ -134,7 +136,7 @@ export class KeyUtil { public static isWithoutSelfCertifications = async (key: Key) => { // all non-OpenPGP keys are automatically considered to be not // "without self certifications" - if (key.type !== 'openpgp') { + if (key.family !== 'openpgp') { return false; } return await OpenPGPKey.isWithoutSelfCertifications(key); @@ -183,7 +185,7 @@ export class KeyUtil { }; public static parseMany = async (text: string): Promise => { - const keyType = KeyUtil.getKeyType(text); + const keyType = KeyUtil.getKeyFamily(text); if (keyType === 'openpgp') { return await OpenPGPKey.parseMany(text); } else if (keyType === 'x509') { @@ -250,8 +252,8 @@ export class KeyUtil { public static diagnose = async (key: Key, passphrase: string): Promise> => { let result = new Map(); - result.set(`Key type`, key.type); - if (key.type === 'openpgp') { + result.set(`Key type`, key.family); + if (key.family === 'openpgp') { const opgpresult = await OpenPGPKey.diagnose(key, passphrase); result = new Map([...result, ...opgpresult]); } @@ -275,12 +277,12 @@ export class KeyUtil { }; public static asPublicKey = async (key: Key): Promise => { - if (key.type === 'openpgp') { + if (key.family === 'openpgp') { return await OpenPGPKey.asPublicKey(key); - } else if (key.type === 'x509') { + } else if (key.family === 'x509') { return SmimeKey.asPublicKey(key); } - throw new UnexpectedKeyTypeError(`Key type is ${key.type}, expecting OpenPGP or x509 S/MIME`); + throw new UnexpectedKeyTypeError(`Key type is ${key.family}, expecting OpenPGP or x509 S/MIME`); }; public static expired = (key: Key): boolean => { @@ -327,7 +329,7 @@ export class KeyUtil { return await KeyUtil.decrypt(key, passphrase); }; - public static getKeyType = (pubkey: string): 'openpgp' | 'x509' | 'unknown' => { + public static getKeyFamily = (pubkey: string): KeyFamily | 'unknown' => { if (pubkey.includes(PgpArmor.headers('certificate').begin)) { return 'x509'; } else if (pubkey.startsWith(PgpArmor.headers('pkcs12').begin)) { @@ -342,42 +344,42 @@ export class KeyUtil { }; public static decrypt = async (key: Key, passphrase: string, optionalKeyid?: OpenPGP.Keyid, optionalBehaviorFlag?: 'OK-IF-ALREADY-DECRYPTED'): Promise => { - if (key.type === 'openpgp') { + if (key.family === 'openpgp') { return await OpenPGPKey.decryptKey(key, passphrase, optionalKeyid, optionalBehaviorFlag); - } else if (key.type === 'x509') { + } else if (key.family === 'x509') { return await SmimeKey.decryptKey(key, passphrase, optionalBehaviorFlag); } else { - throw new Error(`KeyUtil.decrypt does not support key type ${key.type}`); + throw new Error(`KeyUtil.decrypt does not support key type ${key.family}`); } }; public static encrypt = async (key: Key, passphrase: string) => { - if (key.type === 'openpgp') { + if (key.family === 'openpgp') { return await OpenPGPKey.encryptKey(key, passphrase); - } else if (key.type === 'x509') { + } else if (key.family === 'x509') { return await SmimeKey.encryptKey(key, passphrase); } else { - throw new Error(`KeyUtil.encrypt does not support key type ${key.type}`); + throw new Error(`KeyUtil.encrypt does not support key type ${key.family}`); } }; public static reformatKey = async (privateKey: Key, passphrase: string, userIds: { email: string | undefined; name: string }[], expireSeconds: number) => { - if (privateKey.type === 'openpgp') { + if (privateKey.family === 'openpgp') { return await OpenPGPKey.reformatKey(privateKey, passphrase, userIds, expireSeconds); } else { - throw new Error(`KeyUtil.reformatKey does not support key type ${privateKey.type}`); + throw new Error(`KeyUtil.reformatKey does not support key type ${privateKey.family}`); } }; public static revoke = async (key: Key): Promise => { - if (key.type === 'openpgp') { + if (key.family === 'openpgp') { return await OpenPGPKey.revoke(key); } else { - throw new Error(`KeyUtil.revoke does not support key type ${key.type}`); + throw new Error(`KeyUtil.revoke does not support key type ${key.family}`); } }; - public static keyInfoObj = async (prv: Key): Promise => { + public static keyInfoObj = async (prv: Key): Promise => { if (!prv.isPrivate) { throw new Error('Key passed into KeyUtil.keyInfoObj must be a Private Key'); } @@ -388,29 +390,27 @@ export class KeyUtil { longid: KeyUtil.getPrimaryLongid(pubkey), emails: prv.emails, fingerprints: prv.allIds, + id: prv.id, + family: prv.family }; }; - 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') { + if (pubkey.family !== 'x509') { return pubkey.allIds.map(id => OpenPGPKey.fingerprintToLongid(id)); } return [KeyUtil.getPrimaryLongid(pubkey)]; }; public static getPrimaryLongid = (pubkey: Key): string => { - if (pubkey.type !== 'x509') { + if (pubkey.family !== 'x509') { return OpenPGPKey.fingerprintToLongid(pubkey.id); } return SmimeKey.getKeyLongid(pubkey); }; - public static getKeyInfoLongids = (ki: ExtendedKeyInfo): string[] => { - if (ki.type !== 'x509') { + public static getKeyInfoLongids = (ki: KeyInfoWithIdentityAndOptionalPp): string[] => { + if (ki.family !== 'x509') { return ki.fingerprints.map(fp => OpenPGPKey.fingerprintToLongid(fp)); } return [ki.longid]; diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index 03b51481582..bf6d98b5add 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -1,7 +1,7 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ 'use strict'; -import { Key, KeyInfo, ExtendedKeyInfo, KeyUtil } from '../key.js'; +import { Key, KeyInfoWithIdentity, KeyInfoWithIdentityAndOptionalPp, KeyUtil } from '../key.js'; import { MsgBlockType, ReplaceableMsgBlockType } from '../../msg-block.js'; import { Buf } from '../../buf.js'; import { PgpArmor, PreparedForDecrypt } from './pgp-armor.js'; @@ -24,7 +24,7 @@ export namespace PgpMsgMethod { export namespace Arg { export type Encrypt = { pubkeys: Key[], signingPrv?: Key, pwd?: string, data: Uint8Array, filename?: string, armor: boolean, date?: Date }; export type Type = { data: Uint8Array | string }; - export type Decrypt = { kisWithPp: ExtendedKeyInfo[], encryptedData: Uint8Array, msgPwd?: string, verificationPubs: string[] }; + export type Decrypt = { kisWithPp: KeyInfoWithIdentityAndOptionalPp[], encryptedData: Uint8Array, msgPwd?: string, verificationPubs: string[] }; export type DiagnosePubkeys = { armoredPubs: string[], message: Uint8Array }; export type VerifyDetached = { plaintext: Uint8Array, sigText: Uint8Array, verificationPubs: string[] }; } @@ -49,10 +49,10 @@ export namespace PgpMsgMethod { type SortedKeysForDecrypt = { encryptedFor: string[]; signedBy: string[]; - prvMatching: ExtendedKeyInfo[]; - prvForDecrypt: ExtendedKeyInfo[]; - prvForDecryptDecrypted: { ki: ExtendedKeyInfo, decrypted: Key }[]; - prvForDecryptWithoutPassphrases: KeyInfo[]; + prvMatching: KeyInfoWithIdentityAndOptionalPp[]; + prvForDecrypt: KeyInfoWithIdentityAndOptionalPp[]; + prvForDecryptDecrypted: { ki: KeyInfoWithIdentityAndOptionalPp, decrypted: Key }[]; + prvForDecryptWithoutPassphrases: KeyInfoWithIdentity[]; }; export type DecryptSuccess = { success: true; signature?: VerifyRes; isEncrypted?: boolean, filename?: string, content: Buf }; @@ -210,7 +210,7 @@ export class MsgUtil { }; public static encryptMessage: PgpMsgMethod.Encrypt = async ({ pubkeys, signingPrv, pwd, data, filename, armor, date }) => { - const keyTypes = new Set(pubkeys.map(k => k.type)); + const keyTypes = new Set(pubkeys.map(k => k.family)); if (keyTypes.has('openpgp') && keyTypes.has('x509')) { throw new Error('Mixed key types are not allowed: ' + [...keyTypes]); } @@ -240,7 +240,7 @@ export class MsgUtil { return diagnosis; }; - private static getSortedKeys = async (kiWithPp: ExtendedKeyInfo[], msg: OpenPGP.message.Message): Promise => { + private static getSortedKeys = async (kiWithPp: KeyInfoWithIdentityAndOptionalPp[], msg: OpenPGP.message.Message): Promise => { const keys: SortedKeysForDecrypt = { encryptedFor: [], signedBy: [], @@ -279,7 +279,7 @@ export class MsgUtil { return keys; }; - private static getSmimeKeys = async (kiWithPp: ExtendedKeyInfo[], msg: SmimeMsg): Promise => { + private static getSmimeKeys = async (kiWithPp: KeyInfoWithIdentityAndOptionalPp[], msg: SmimeMsg): Promise => { const keys: SortedKeysForDecrypt = { encryptedFor: [], signedBy: [], diff --git a/extension/js/common/core/crypto/pgp/openpgp-key.ts b/extension/js/common/core/crypto/pgp/openpgp-key.ts index ed74dcc0a01..08e2d4396c0 100644 --- a/extension/js/common/core/crypto/pgp/openpgp-key.ts +++ b/extension/js/common/core/crypto/pgp/openpgp-key.ts @@ -49,8 +49,8 @@ export class OpenPGPKey { }; public static asPublicKey = async (pubkey: Key): Promise => { - if (pubkey.type !== 'openpgp') { - throw new UnexpectedKeyTypeError(`Key type is ${pubkey.type}, expecting OpenPGP`); + if (pubkey.family !== 'openpgp') { + throw new UnexpectedKeyTypeError(`Key type is ${pubkey.family}, expecting OpenPGP`); } if (pubkey.isPrivate) { return await OpenPGPKey.convertExternalLibraryObjToKey(OpenPGPKey.extractStrengthUncheckedExternalLibraryObjFromKey(pubkey).toPublic()); @@ -215,7 +215,7 @@ export class OpenPGPKey { const missingPrivateKeyForSigning = signingKeyIgnoringExpiration?.keyPacket ? OpenPGPKey.arePrivateParamsMissing(signingKeyIgnoringExpiration.keyPacket) : false; const missingPrivateKeyForDecryption = encryptionKeyIgnoringExpiration?.keyPacket ? OpenPGPKey.arePrivateParamsMissing(encryptionKeyIgnoringExpiration.keyPacket) : false; Object.assign(key, { - type: 'openpgp', + family: 'openpgp', id: fingerprint.toUpperCase(), allIds: keyWithoutWeakPackets.getKeys().map(k => k.getFingerprint().toUpperCase()), usableForEncryption: encryptionKey ? true : false, @@ -445,7 +445,7 @@ export class OpenPGPKey { public static verify = async (msg: OpenpgpMsgOrCleartext, pubs: PubkeyInfo[]): Promise => { // todo: double-check if S/MIME ever gets here - const validKeys = pubs.filter(x => !x.revoked && x.pubkey.type === 'openpgp').map(x => x.pubkey); + const validKeys = pubs.filter(x => !x.revoked && x.pubkey.family === 'openpgp').map(x => x.pubkey); // todo: #4172 revoked longid may result in incorrect "Missing pubkey..." output const verifyRes: VerifyRes = { match: null, // tslint:disable-line:no-null-keyword @@ -631,8 +631,8 @@ export class OpenPGPKey { }; private static extractExternalLibraryObjFromKey = (pubkey: Key) => { - if (pubkey.type !== 'openpgp') { - throw new UnexpectedKeyTypeError(`Key type is ${pubkey.type}, expecting OpenPGP`); + if (pubkey.family !== 'openpgp') { + throw new UnexpectedKeyTypeError(`Key type is ${pubkey.family}, expecting OpenPGP`); } const opgpKey = (pubkey as unknown as { [internal]: OpenPGP.key.Key })[internal]; if (!opgpKey) { @@ -642,8 +642,8 @@ export class OpenPGPKey { }; private static extractStrengthUncheckedExternalLibraryObjFromKey = (pubkey: Key) => { - if (pubkey.type !== 'openpgp') { - throw new UnexpectedKeyTypeError(`Key type is ${pubkey.type}, expecting OpenPGP`); + if (pubkey.family !== 'openpgp') { + throw new UnexpectedKeyTypeError(`Key type is ${pubkey.family}, expecting OpenPGP`); } const raw = (pubkey as unknown as { rawKey: OpenPGP.key.Key }); return raw?.rawKey; diff --git a/extension/js/common/core/crypto/smime/smime-key.ts b/extension/js/common/core/crypto/smime/smime-key.ts index eb010310e39..442a4b032a1 100644 --- a/extension/js/common/core/crypto/smime/smime-key.ts +++ b/extension/js/common/core/crypto/smime/smime-key.ts @@ -184,8 +184,8 @@ export class SmimeKey { }; public static asPublicKey = (key: Key): Key => { - if (key.type !== 'x509') { - throw new UnexpectedKeyTypeError(`Key type is ${key.type}, expecting x509 S/MIME`); + if (key.family !== 'x509') { + throw new UnexpectedKeyTypeError(`Key type is ${key.family}, expecting x509 S/MIME`); } if (key.isPrivate) { return SmimeKey.getKeyFromCertificate(SmimeKey.getArmoredCertificate(key), undefined); @@ -273,7 +273,7 @@ export class SmimeKey { const expired = expiration < Date.now(); const usableIgnoringExpiration = SmimeKey.isEmailCertificate(certificate) && !SmimeKey.isKeyWeak(certificate); const key = { - type: 'x509', + family: 'x509', id: fingerprint, allIds: [fingerprint], usableForEncryption: usableIgnoringExpiration && !expired, diff --git a/extension/js/common/platform/store/abstract-store.ts b/extension/js/common/platform/store/abstract-store.ts index 005e29020d7..ba69a2515b5 100644 --- a/extension/js/common/platform/store/abstract-store.ts +++ b/extension/js/common/platform/store/abstract-store.ts @@ -2,7 +2,7 @@ 'use strict'; -import { KeyInfo } from '../../core/crypto/key.js'; +import { KeyInfoWithIdentity, StoredKeyInfo } from '../../core/crypto/key.js'; import { Dict, emailKeyIndex } from '../../core/common.js'; import { DomainRulesJson } from '../../org-rules.js'; import { GmailRes } from '../../api/email-provider/gmail/gmail-parser.js'; @@ -13,7 +13,7 @@ import { StoredAdminCode } from './global-store.js'; type SerializableTypes = FlatTypes | string[] | number[] | boolean[] | DomainRulesJson; export type StorageType = 'session' | 'local'; export type FlatTypes = null | undefined | number | string | boolean; -type Storable = FlatTypes | string[] | KeyInfo[] | Dict +type Storable = FlatTypes | string[] | StoredKeyInfo[] | KeyInfoWithIdentity[] | Dict | GmailRes.OpenId | DomainRulesJson; export type Serializable = SerializableTypes | SerializableTypes[] | Dict | Dict[]; diff --git a/extension/js/common/platform/store/acct-store.ts b/extension/js/common/platform/store/acct-store.ts index 55adb04565c..99fc8bc2090 100644 --- a/extension/js/common/platform/store/acct-store.ts +++ b/extension/js/common/platform/store/acct-store.ts @@ -2,7 +2,7 @@ import { Env } from '../../browser/env.js'; import { GoogleAuth } from '../../api/email-provider/gmail/google-auth.js'; -import { KeyInfo } from '../../core/crypto/key.js'; +import { KeyInfoWithIdentity, StoredKeyInfo } from '../../core/crypto/key.js'; import { Dict } from '../../core/common.js'; import { DomainRulesJson } from '../../org-rules.js'; import { BrowserMsg, BgNotReadyErr } from '../../browser/browser-msg.js'; @@ -40,7 +40,7 @@ export type SendAsAlias = { }; export type AcctStoreDict = { - keys?: KeyInfo[]; + keys?: (StoredKeyInfo | KeyInfoWithIdentity)[]; // todo - migrate to KeyInfoWithIdentity only notification_setup_needed_dismissed?: boolean; email_provider?: EmailProvider; google_token_scopes?: string[]; // these are actuall scope urls the way the provider expects them diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 663d59845d7..3e396481c55 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -4,7 +4,7 @@ import { AbstractStore } from './abstract-store.js'; import { Catch } from '../catch.js'; import { BrowserMsg } from '../../browser/browser-msg.js'; import { DateUtility, EmailParts, Str, Value } from '../../core/common.js'; -import { Key, KeyUtil, PubkeyInfo, ContactInfoWithSortedPubkeys } from '../../core/crypto/key.js'; +import { Key, KeyUtil, PubkeyInfo, ContactInfoWithSortedPubkeys, KeyIdentity } from '../../core/crypto/key.js'; // tslint:disable:no-null-keyword @@ -586,12 +586,12 @@ export class ContactStore extends AbstractStore { }; // todo: return parsed and with applied revocation - public static getPubkey = async (db: IDBDatabase | undefined, { id, type }: { id: string, type: string }): + public static getPubkey = async (db: IDBDatabase | undefined, { id, family }: KeyIdentity): Promise => { if (!db) { // relay op through background process - return (await BrowserMsg.send.bg.await.db({ f: 'getPubkey', args: [{ id, type }] })) as string | undefined; + return (await BrowserMsg.send.bg.await.db({ f: 'getPubkey', args: [{ id, family }] })) as string | undefined; } - const internalFingerprint = ContactStore.getPubkeyId({ id, type }); + const internalFingerprint = ContactStore.getPubkeyId({ id, family }); const tx = db.transaction(['pubkeys'], 'readonly'); const pubkeyEntity: Pubkey = await new Promise((resolve, reject) => { const req = tx.objectStore('pubkeys').get(internalFingerprint); @@ -600,13 +600,13 @@ export class ContactStore extends AbstractStore { return pubkeyEntity?.armoredKey; }; - public static unlinkPubkey = async (db: IDBDatabase | undefined, email: string, { id, type }: { id: string, type: string }): + public static unlinkPubkey = async (db: IDBDatabase | undefined, email: string, { id, family }: KeyIdentity): Promise => { if (!db) { // relay op through background process - await BrowserMsg.send.bg.await.db({ f: 'unlinkPubkey', args: [email, { id, type }] }); + await BrowserMsg.send.bg.await.db({ f: 'unlinkPubkey', args: [email, { id, family }] }); return; } - const internalFingerprint = ContactStore.getPubkeyId({ id, type }); + const internalFingerprint = ContactStore.getPubkeyId({ id, family }); const tx = db.transaction(['emails', 'pubkeys'], 'readwrite'); await new Promise((resolve, reject) => { ContactStore.setTxHandlers(tx, resolve, reject); @@ -747,8 +747,8 @@ export class ContactStore extends AbstractStore { return KeyUtil.sortPubkeyInfos(pubkeyInfos); }; - private static getPubkeyId = ({ id, type }: { id: string, type: string }): string => { - return (type === 'x509') ? (id + x509postfix) : id; + private static getPubkeyId = (keyIdentity: KeyIdentity): string => { + return (keyIdentity.family === 'x509') ? (keyIdentity.id + x509postfix) : keyIdentity.id; }; private static stripFingerprint = (fp: string): string => { @@ -782,7 +782,7 @@ export class ContactStore extends AbstractStore { let pubkeyEntity: Pubkey | undefined; if (update.pubkey) { const internalFingerprint = ContactStore.getPubkeyId(update.pubkey!); - if (update.pubkey.type === 'openpgp' && !update.pubkey.revoked && revocations.some(r => r.fingerprint === internalFingerprint)) { + if (update.pubkey.family === 'openpgp' && !update.pubkey.revoked && revocations.some(r => r.fingerprint === internalFingerprint)) { // we have this fingerprint revoked but the supplied key isn't // so let's not save it // pubkeyEntity = undefined diff --git a/extension/js/common/platform/store/key-store.ts b/extension/js/common/platform/store/key-store.ts index eae8a085b06..0ab66a954aa 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, TypedKeyInfo, ExtendedKeyInfo, KeyUtil, Key, KeyIdentity } from '../../core/crypto/key.js'; +import { StoredKeyInfo, KeyInfoWithIdentity, KeyInfoWithIdentityAndOptionalPp, KeyUtil, Key, KeyIdentity } from '../../core/crypto/key.js'; import { AcctStore } from './acct-store.js'; import { PassphraseStore } from './passphrase-store.js'; import { AbstractStore } from './abstract-store.js'; @@ -11,9 +11,9 @@ import { Assert } from '../../assert.js'; */ export class KeyStore extends AbstractStore { - public static get = async (acctEmail: string, fingerprints?: string[]): Promise => { + public static get = async (acctEmail: string, fingerprints?: string[]): Promise => { const stored = await AcctStore.get(acctEmail, ['keys']); - const keys: KeyInfo[] = stored.keys || []; + const keys: KeyInfoWithIdentity[] = KeyStore.addIdentityToKeyInfos(stored.keys || []); if (!fingerprints) { return keys; } @@ -22,33 +22,14 @@ export class KeyStore extends AbstractStore { return keys.filter(ki => fingerprints.includes(ki.fingerprints[0])); }; - public static getFirstOptional = async (acctEmail: string): Promise => { + public static getRequired = async (acctEmail: string): Promise => { const keys = await KeyStore.get(acctEmail); - return keys[0]; + Assert.abortAndRenderErrorIfKeyinfoEmpty(keys); + return keys; }; - public static getFirstRequired = async (acctEmail: string): Promise => { - const key = await KeyStore.getFirstOptional(acctEmail); - Assert.abortAndRenderErrorIfKeyinfoEmpty(key); - return key as KeyInfo; - }; - - public static getTypedKeyInfos = async (acctEmail: string): Promise => { + public static getAllWithOptionalPassPhrase = async (acctEmail: string): Promise => { const keys = await KeyStore.get(acctEmail); - const kis: TypedKeyInfo[] = []; - for (const ki of keys) { - const type = KeyUtil.getKeyType(ki.private); - const id = ki.fingerprints[0]; - if (type !== 'openpgp' && type !== 'x509') { - continue; - } - kis.push({ ...ki, type, id }); - } - 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.get(acctEmail, ki) }; })); }; @@ -71,19 +52,19 @@ export class KeyStore extends AbstractStore { await KeyStore.set(acctEmail, keyinfos); }; - public static set = async (acctEmail: string, keyinfos: KeyInfo[]) => { + public static set = async (acctEmail: string, keyinfos: KeyInfoWithIdentity[]) => { await AcctStore.set(acctEmail, { keys: keyinfos }); }; public static remove = async (acctEmail: string, keyIdentity: KeyIdentity): Promise => { - const privateKeys = await KeyStore.getTypedKeyInfos(acctEmail); + const privateKeys = await KeyStore.get(acctEmail); const filteredPrivateKeys = privateKeys.filter(ki => !KeyUtil.identityEquals(ki, keyIdentity)); await KeyStore.set(acctEmail, filteredPrivateKeys); }; - public static getKeyInfosThatCurrentlyHavePassPhraseInSession = async (acctEmail: string): Promise => { - const keys = await KeyStore.getTypedKeyInfos(acctEmail); - const result: TypedKeyInfo[] = []; + public static getKeyInfosThatCurrentlyHavePassPhraseInSession = async (acctEmail: string): Promise => { + const keys = await KeyStore.get(acctEmail); + const result: KeyInfoWithIdentity[] = []; for (const ki of keys) { if (! await PassphraseStore.get(acctEmail, ki, true) && await PassphraseStore.get(acctEmail, ki, false)) { result.push(ki); @@ -91,4 +72,17 @@ export class KeyStore extends AbstractStore { } return result; }; + + private static addIdentityToKeyInfos = (keyInfos: StoredKeyInfo[]): KeyInfoWithIdentity[] => { + const kis: KeyInfoWithIdentity[] = []; + for (const ki of keyInfos) { + const family = KeyUtil.getKeyFamily(ki.private); + const id = ki.fingerprints[0]; + if (family !== 'openpgp' && family !== 'x509') { + continue; + } + kis.push({ ...ki, family, id }); + } + return kis; + }; } diff --git a/extension/js/common/settings.ts b/extension/js/common/settings.ts index f109ee045c3..9c94115e348 100644 --- a/extension/js/common/settings.ts +++ b/extension/js/common/settings.ts @@ -14,7 +14,7 @@ import { Env } from './browser/env.js'; import { Gmail } from './api/email-provider/gmail/gmail.js'; import { GoogleAuth } from './api/email-provider/gmail/google-auth.js'; import { Lang } from './lang.js'; -import { ExtendedKeyInfo, Key, KeyUtil } from './core/crypto/key.js'; +import { KeyInfoWithIdentityAndOptionalPp, Key, KeyUtil } from './core/crypto/key.js'; import { PgpPwd } from './core/crypto/pgp/pgp-password.js'; import { OrgRules } from './org-rules.js'; import { Xss } from './platform/xss.js'; @@ -114,10 +114,10 @@ export class Settings { throw new Error(`Filter is empty for account_email "${oldAcctEmail}"`); } // in case the destination email address was already set up with an account, recover keys and pass phrases before it's overwritten - const oldAccountPrivateKeys = await KeyStore.getTypedKeyInfos(oldAcctEmail); - const newAccountPrivateKeys = await KeyStore.getTypedKeyInfos(newAcctEmail); - const oldAcctPassPhrases: ExtendedKeyInfo[] = []; - const newAcctPassPhrases: ExtendedKeyInfo[] = []; + const oldAccountPrivateKeys = await KeyStore.get(oldAcctEmail); + const newAccountPrivateKeys = await KeyStore.get(newAcctEmail); + const oldAcctPassPhrases: KeyInfoWithIdentityAndOptionalPp[] = []; + const newAcctPassPhrases: KeyInfoWithIdentityAndOptionalPp[] = []; for (const ki of oldAccountPrivateKeys) { const passphrase = await PassphraseStore.get(oldAcctEmail, ki, true); if (passphrase) { diff --git a/extension/js/common/ui/attachment-ui.ts b/extension/js/common/ui/attachment-ui.ts index 6a2de755de7..74f9043faf7 100644 --- a/extension/js/common/ui/attachment-ui.ts +++ b/extension/js/common/ui/attachment-ui.ts @@ -87,7 +87,7 @@ export class AttachmentUI { const file = this.attachedFiles[uploadFileId]; const data = await this.readAttachmentDataAsUint8(uploadFileId); const pubsForEncryption = pubs.map(entry => entry.pubkey); - if (pubs.find(pub => pub.pubkey.type === 'x509')) { + if (pubs.find(pub => pub.pubkey.family === 'x509')) { throw new UnreportableError('Attachments are not yet supported when sending to recipients using S/MIME x509 certificates.'); } const encrypted = await MsgUtil.encryptMessage({ pubkeys: pubsForEncryption, data, filename: file.name, armor: false }) as OpenPGP.EncryptBinaryResult; diff --git a/extension/js/common/ui/key-import-ui.ts b/extension/js/common/ui/key-import-ui.ts index c5f916eac48..ef8d6b9f3a1 100644 --- a/extension/js/common/ui/key-import-ui.ts +++ b/extension/js/common/ui/key-import-ui.ts @@ -243,7 +243,7 @@ export class KeyImportUi { // non-OpenPGP keys are considered to be always normalized // TODO: PgpKey.normalize depends on OpenPGP.key.Key objects, when this is resolved // this check for key type should be moved to PgpKey.normalize function. - if (KeyUtil.getKeyType(armored) !== 'openpgp') { + if (KeyUtil.getKeyFamily(armored) !== 'openpgp') { return { normalized: armored }; } const normalized = await KeyUtil.normalize(armored); diff --git a/test/source/browser/controllable.ts b/test/source/browser/controllable.ts index 44e7acfbfc4..ac498ca5cc0 100644 --- a/test/source/browser/controllable.ts +++ b/test/source/browser/controllable.ts @@ -437,8 +437,15 @@ abstract class ControllableBase { throw Error(`Frame not found within ${timeout}s: ${urlMatchables.join(',')}`); }; - public awaitDownloadTriggeredByClicking = async (selector: string | (() => Promise)): Promise => { - const resolvePromise: Promise = (async () => { + /** + * when downloading several files, only notices files with unique names + */ + public awaitDownloadTriggeredByClicking = async ( + selector: string | (() => Promise), + expectFileCount = 1 + ): Promise> => { + const files: Dict = {}; + const resolvePromise: Promise = (async () => { const downloadPath = path.resolve(__dirname, 'download', Util.lousyRandom()); mkdirp.sync(downloadPath); await (this.target as any)._client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath }); @@ -447,11 +454,16 @@ abstract class ControllableBase { } else { await selector(); } - const filename = await this.waitForFileToDownload(downloadPath); - return fs.readFileSync(path.resolve(downloadPath, filename)); + while (Object.keys(files).length < expectFileCount) { + const newFilenames = await this.waitForFilesToDownload(downloadPath); + for (const filename of newFilenames) { + files[filename] = fs.readFileSync(path.resolve(downloadPath, filename)); + } + } })(); const timeoutPromise = newTimeoutPromise(`awaitDownloadTriggeredByClicking timeout for ${selector}`, 20); - return await Promise.race([resolvePromise, timeoutPromise]); + await Promise.race([resolvePromise, timeoutPromise]); + return files; }; protected log = (msg: string) => { @@ -510,13 +522,14 @@ abstract class ControllableBase { return matchingLinks; }; - private waitForFileToDownload = async (downloadPath: string) => { - let filename; - while (!filename || filename.endsWith('.crdownload')) { - filename = fs.readdirSync(downloadPath)[0]; - await Util.sleep(1); + private waitForFilesToDownload = async (downloadPath: string): Promise => { + while (true) { + const filenames = fs.readdirSync(downloadPath); + if (!filenames.some(fn => fn.endsWith('.crdownload'))) { + return filenames; + } + await Util.sleep(0.2); } - return filename; }; } diff --git a/test/source/mock/backend/backend-data.ts b/test/source/mock/backend/backend-data.ts index 3d3f73c897f..a8a8af0680f 100644 --- a/test/source/mock/backend/backend-data.ts +++ b/test/source/mock/backend/backend-data.ts @@ -112,6 +112,9 @@ export class BackendData { if (domain === 'key-manager-autoimport-no-prv-create.flowcrypt.test') { return { ...keyManagerAutogenRules, flags: [...keyManagerAutogenRules.flags, 'NO_PRV_CREATE'] }; } + if (domain === 'key-manager-autoimport-no-prv-create-no-attester-submit.flowcrypt.test') { + return { ...keyManagerAutogenRules, flags: [...keyManagerAutogenRules.flags, 'NO_PRV_CREATE', 'NO_ATTESTER_SUBMIT'] }; + } if (domain === 'key-manager-choose-passphrase.flowcrypt.test') { return { ...keyManagerAutogenRules, flags: [ diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index f35b6aaa55f..5419fbf83b9 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -189,6 +189,24 @@ oEdXpz065GJRpAccNRQ1iZTLln2yNKVFp1PuyBs2zqUdo0O/cy0XgYV4z6Vt -----END PGP PRIVATE KEY BLOCK----- `; +const revokedPrv = ` +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: BCPG v1.69 + +lFgEYW8BThYJKwYBBAHaRw8BAQdAYtEoS4d+3cwQWXcs3lvMQueypexTYai7uXQm +xqyOoKoAAP92ki9qlV4AX2m+WPUq//vL03GGge+Y4rcyN3f5/Y/2AhMdiHUEIBYK +AB0FAmFvAiwWIQQ5MbdBPbsvpgzvhc5fFZeJEKF4CgAKCRBfFZeJEKF4CqTkAQCm +fxfc57sQWB1+jmWSCHq5umaDJFSl9geRATb9Lor5PQEA8azanLnXlpRUCCJHLtsm +6hgMops48vU2l3RuB6cqAwa0FXJldm9la2RAZmxvd2NyeXB0LmNvbYheBBMWCgAG +BQJhbwFrAAoJEF8Vl4kQoXgKEP8A/2B2biuLIDPIaEPg/xkZbca1ESTnqxZEEHcD +/5FRf6psAQDcVYKzZtSXqmZqxc/xACLT/Oiu5mJKFHZjaZUAzdPUBLQVcmV2b2tl +ZEBmbG93Y3J5cHQuY29tiF4EExYKAAYFAmFvAWsACgkQXxWXiRCheAp5ygEAt2sP +yeSm0uVPwODhwX7ezB9jW6uVt0R8S8iM3rQdEMsA/jDep5LNn47K6o8VrDt0zYo6 +7j75aKC1vFGkOGlD1TwF +=1tta +-----END PGP PRIVATE KEY BLOCK----- +`; + export const MOCK_KM_LAST_INSERTED_KEY: { [acct: string]: { decryptedPrivateKey: string, publicKey: string } } = {}; // accessed from test runners export const mockKeyManagerEndpoints: HandlersDefinition = { @@ -231,6 +249,15 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { if (acctEmail === 'expire@key-manager-keygen-expiration.flowcrypt.test') { return { privateKeys: [] }; } + if (acctEmail === 'first.key.revoked@key-manager-autoimport-no-prv-create.flowcrypt.test') { + return { privateKeys: [{ decryptedPrivateKey: revokedPrv }, { decryptedPrivateKey: twoKeys2 }] }; + } + if (acctEmail === 'revoked@key-manager-autoimport-no-prv-create.flowcrypt.test') { + return { privateKeys: [{ decryptedPrivateKey: revokedPrv }] }; + } + if (acctEmail === 'revoked@key-manager-autoimport-no-prv-create-no-attester-submit.flowcrypt.test') { + return { privateKeys: [{ decryptedPrivateKey: revokedPrv }] }; + } if (acctEmail === 'get.error@key-manager-autogen.flowcrypt.test') { throw new Error('Intentional error for get.error to test client behavior'); } diff --git a/test/source/test.ts b/test/source/test.ts index ff1991edeb9..815bf11b106 100644 --- a/test/source/test.ts +++ b/test/source/test.ts @@ -33,8 +33,8 @@ process.setMaxListeners(60); const consts = { // higher concurrency can cause 429 google errs when composing TIMEOUT_SHORT: minutes(1), TIMEOUT_EACH_RETRY: minutes(3), - TIMEOUT_ALL_RETRIES: minutes(13), // this has to suffer waiting for semaphore between retries, thus almost the same as below - TIMEOUT_OVERALL: minutes(14), + TIMEOUT_ALL_RETRIES: minutes(18), // this has to suffer waiting for semaphore between retries, thus almost the same as below + TIMEOUT_OVERALL: minutes(19), ATTEMPTS: testGroup === 'STANDARD-GROUP' ? oneIfNotPooled(3) : process.argv.includes('--retry=false') ? 1 : 3, POOL_SIZE: oneIfNotPooled(isMock ? 20 : 3), PROMISE_TIMEOUT_OVERALL: undefined as any as Promise, // will be set right below diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index cab2bbbf911..312494b105e 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -413,6 +413,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te })); for (const inputMethod of ['mouse', 'keyboard']) { + ava.default(`compose - reply - pass phrase dialog - dialog ok (${inputMethod})`, testWithBrowser('compatibility', async (t, browser) => { const pp = Config.key('flowcrypt.compatibility.1pp1').passphrase; const { inboxPage, replyFrame } = await setRequirePassPhraseAndOpenRepliedMessage(t, browser, pp); @@ -491,13 +492,11 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te await composeFrame.waitAndClick('@action-send', { delay: 2 }); const passphraseDialog = await inboxPage.getFrame(['passphrase.htm']); expect(passphraseDialog.frame.isDetached()).to.equal(false); - await Util.sleep(0.5); - expect(await composeFrame.read('@action-send')).to.eq('Loading...'); + await composeFrame.waitForContent('@action-send', 'Loading...'); await passphraseDialog.waitForContent('@passphrase-text', 'Enter FlowCrypt pass phrase to sign email'); await passphraseDialog.waitForContent('@which-key', '47FB 0318 3E03 A8ED 44E3 BBFC CEA2 D53B B9D2 4871'); await ComposePageRecipe.cancelPassphraseDialog(inboxPage, inputMethod); - await Util.sleep(0.5); - expect(await composeFrame.read('@action-send')).to.eq('Encrypt, Sign and Send'); + await composeFrame.waitForContent('@action-send', 'Encrypt, Sign and Send'); })); } // end of tests per inputMethod @@ -821,7 +820,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const fileText = await composePage.awaitDownloadTriggeredByClicking(async () => { await attachment.click('#download'); }); - expect(fileText.toString()).to.equal(`small text file\nnot much here\nthis worked\n`); + expect(Object.values(fileText).pop()!.toString()).to.equal(`small text file\nnot much here\nthis worked\n`); await composePage.close(); })); @@ -1245,7 +1244,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const fileText = await inboxPage.awaitDownloadTriggeredByClicking(async () => { await composeFrame.click('.qq-file-id-0'); }); - expect(fileText.toString()).to.equal(`small text file\nnot much here\nthis worked\n`); + expect(Object.values(fileText).pop()!.toString()).to.equal(`small text file\nnot much here\nthis worked\n`); await composeFrame.waitAndClick('@action-send', { delay: 2 }); await inboxPage.waitTillGone('@container-new-message'); })); @@ -1265,7 +1264,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const fileText = await inboxPage.awaitDownloadTriggeredByClicking(async () => { await composeFrame.click('.qq-file-id-0'); }); - expect(fileText.toString()).to.equal(`small text file\nnot much here\nthis worked\n`); + expect(Object.values(fileText).pop()!.toString()).to.equal(`small text file\nnot much here\nthis worked\n`); await composeFrame.waitAndClick('@action-send', { delay: 2 }); await inboxPage.waitTillGone('@container-new-message'); })); @@ -1597,6 +1596,36 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te // also see '/api/v1/message' in fes-endpoints.ts mock })); + ava.default('first.key.revoked@key-manager-autoimport-no-prv-create.flowcrypt.test - selects valid own key when saving draft or sending', testWithBrowser(undefined, async (t, browser) => { + const acct = 'first.key.revoked@key-manager-autoimport-no-prv-create.flowcrypt.test'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); + await SetupPageRecipe.autoSetupWithEKM(settingsPage); + const composePage = await ComposePageRecipe.openStandalone(t, browser, acct); + await ComposePageRecipe.fillMsg(composePage, { to: 'mock.only.pubkey@flowcrypt.com' }, 'choose valid key'); + await ComposePageRecipe.noToastAppears(composePage); // no error saving draft + await ComposePageRecipe.sendAndClose(composePage); // no error sending msg + })); + + ava.default('revoked@key-manager-autoimport-no-prv-create.flowcrypt.test - shows modal not submitting to attester', testWithBrowser(undefined, async (t, browser) => { + const acct = 'revoked@key-manager-autoimport-no-prv-create.flowcrypt.test'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); + await SetupPageRecipe.autoSetupWithEKM(settingsPage, { expectWarnModal: 'Public key not usable - not sumbitting to Attester' }); + })); + + ava.default('revoked@key-manager-autoimport-no-prv-create-no-attester-submit.flowcrypt.test - cannot draft or send msg', testWithBrowser(undefined, async (t, browser) => { + const acct = 'revoked@key-manager-autoimport-no-prv-create-no-attester-submit.flowcrypt.test'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); + await SetupPageRecipe.autoSetupWithEKM(settingsPage); + const composePage = await ComposePageRecipe.openStandalone(t, browser, acct); + await ComposePageRecipe.fillMsg(composePage, { to: 'mock.only.pubkey@flowcrypt.com' }, 'no valid key'); + await ComposePageRecipe.waitForToastToAppearAndDisappear(composePage, 'Draft not saved: Error: Your account keys are revoked'); + await composePage.waitAndClick('@action-send', { delay: 1 }); + await PageRecipe.waitForModalAndRespond(composePage, 'error', { + contentToCheck: 'Failed to send message due to: Error: Could not find account openpgp key usable for signing this encrypted message', + clickOn: 'confirm' + }); + })); + } }; diff --git a/test/source/tests/flaky.ts b/test/source/tests/flaky.ts index 3048ab016f7..a93c5f21fa3 100644 --- a/test/source/tests/flaky.ts +++ b/test/source/tests/flaky.ts @@ -27,7 +27,7 @@ export const defineFlakyTests = (testVariant: TestVariant, testWithBrowser: Test if (testVariant !== 'CONSUMER-LIVE-GMAIL') { - ava.default('compose - own key expired', testWithBrowser(undefined, async (t, browser) => { + ava.default('compose - own key expired - update and retry', testWithBrowser(undefined, async (t, browser) => { const expiredKey = "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: FlowCrypt 7.0.1 Gmail Encryption\nComment: Seamlessly send and receive encrypted email\n\n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\n+gib+vSxHvLhC6oDBA==\n=RIPF\n-----END PGP PRIVATE KEY BLOCK-----"; // eslint-disable-line max-len const validKey = "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: FlowCrypt 7.0.1 Gmail Encryption\nComment: Seamlessly send and receive encrypted email\n\n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\nGw==\n=18AI\n-----END PGP PRIVATE KEY BLOCK-----"; // eslint-disable-line max-len // Setup Expired key @@ -37,19 +37,32 @@ export const defineFlakyTests = (testVariant: TestVariant, testWithBrowser: Test await settingsPage.type('@input-step2bmanualenter-ascii-key', expiredKey); await settingsPage.type('@input-step2bmanualenter-passphrase', "qweasd"); await settingsPage.waitAndClick('@input-step2bmanualenter-save'); - await SettingsPageRecipe.waitForModalAndRespond(settingsPage, 'confirm', { contentToCheck: 'You are importing a key that is expired.', clickOn: 'confirm' }); - await settingsPage.close(); + await SettingsPageRecipe.waitForModalAndRespond(settingsPage, 'confirm', { + contentToCheck: 'You are importing a key that is expired.', + clickOn: 'confirm' + }); + await SettingsPageRecipe.waitForModalAndRespond(settingsPage, 'warning', { + contentToCheck: 'Public key not usable - not sumbitting to Attester', + clickOn: 'confirm', + }); + await settingsPage.waitAndClick('@action-step4done-account-settings'); // Try To send message with expired key let composePage = await ComposePageRecipe.openStandalone(t, browser, 'flowcrypt.test.key.new.manual@gmail.com'); await ComposePageRecipe.fillMsg(composePage, { to: 'human@flowcrypt.com' }, 'Own Key Expired'); await composePage.waitAndClick('@action-send'); - await ComposePageRecipe.waitForModalAndRespond(composePage, 'error', { contentToCheck: 'your own Private Key is expired', timeout: 45 }); - const settingsWithUpdatePrvForm = await browser.newPageTriggeredBy(t, () => composePage.waitAndClick('#action_update_prv')); - const urls = await settingsWithUpdatePrvForm.getFramesUrls(['my_key_update.htm']); + await ComposePageRecipe.waitForModalAndRespond(composePage, 'error', { + contentToCheck: 'Failed to send message due to: Error: Could not find account openpgp key usable for signing this encrypted message', + timeout: 45, + clickOn: 'confirm' + }); await composePage.close(); - await settingsWithUpdatePrvForm.close(); + await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); + await settingsPage.waitAndClick('@action-show-key-0'); + const urls = await settingsPage.getFramesUrls(['my_key.htm'], { appearIn: 5 }); + await settingsPage.close(); // Updating the key to valid one const updatePrvPage = await browser.newPage(t, urls[0]); + await updatePrvPage.waitAndClick('@action-update-prv'); await updatePrvPage.waitAndType('@input-prv-key', validKey); await updatePrvPage.type('@input-passphrase', 'qweasd'); await updatePrvPage.waitAndClick('@action-update-key'); @@ -57,7 +70,7 @@ export const defineFlakyTests = (testVariant: TestVariant, testWithBrowser: Test await updatePrvPage.close(); // Try send message again composePage = await ComposePageRecipe.openStandalone(t, browser, 'flowcrypt.test.key.new.manual@gmail.com'); - await ComposePageRecipe.fillMsg(composePage, { to: 'human@flowcrypt.com' }, 'Own Key Expired'); + await ComposePageRecipe.fillMsg(composePage, { to: 'human@flowcrypt.com' }, 'Own Key Expired no more'); await ComposePageRecipe.sendAndClose(composePage); })); @@ -94,8 +107,8 @@ export const defineFlakyTests = (testVariant: TestVariant, testWithBrowser: Test await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); const fingerprint = (await settingsPage.read('.good', true)).split(' ').join(''); const myKeyFrame = await browser.newPage(t, `chrome/settings/modules/my_key.htm?placement=settings&parentTabId=60%3A0&acctEmail=${acctEmail}&fingerprint=${fingerprint}`); - const raw = await myKeyFrame.awaitDownloadTriggeredByClicking('@action-download-prv'); - const key = await KeyUtil.parse(raw.toString()); + const files = await myKeyFrame.awaitDownloadTriggeredByClicking('@action-download-prv'); + const key = await KeyUtil.parse(Object.values(files).pop()!.toString()); expect(key.algo.bits).to.equal(3072); expect(key.algo.algorithm).to.equal('rsa_encrypt_sign'); await myKeyFrame.close(); diff --git a/test/source/tests/page-recipe/abstract-page-recipe.ts b/test/source/tests/page-recipe/abstract-page-recipe.ts index ff46c4b60ed..b94849be86f 100644 --- a/test/source/tests/page-recipe/abstract-page-recipe.ts +++ b/test/source/tests/page-recipe/abstract-page-recipe.ts @@ -4,13 +4,13 @@ import { BrowserHandle, Controllable, ControllablePage } from '../../browser'; import { AvaContext } from '../tooling/'; import { ElementHandle, JSHandle } from 'puppeteer'; -import { expect } from 'chai'; import { Util } from '../../util'; type ModalOpts = { contentToCheck?: string, clickOn?: 'confirm' | 'cancel', getTriggeredPage?: boolean, timeout?: number }; type ModalType = 'confirm' | 'error' | 'info' | 'warning'; export abstract class PageRecipe { + public static getElementPropertyJson = async (elem: ElementHandle, property: string) => { return await (await elem.getProperty(property) as JSHandle).jsonValue() as string; }; @@ -23,7 +23,10 @@ export abstract class PageRecipe { const modalContainer = await controllable.waitAny(`.ui-modal-${type}`, { timeout }); if (typeof contentToCheck !== 'undefined') { const contentElement = await modalContainer.$('.swal2-html-container'); - expect(await PageRecipe.getElementPropertyJson(contentElement!, 'textContent')).to.include(contentToCheck); + const actualContent = await PageRecipe.getElementPropertyJson(contentElement!, 'textContent'); + if (!actualContent.includes(contentToCheck)) { + throw new Error(`Expected modal to contain "${contentToCheck}" but contained "${actualContent}"`); + } } if (clickOn) { const button = await modalContainer.$(`button.ui-modal-${type}-${clickOn}`); @@ -31,6 +34,19 @@ export abstract class PageRecipe { } }; + public static waitForToastToAppearAndDisappear = async (controllable: Controllable, containsText: string | RegExp): Promise => { + await controllable.waitForContent('.ui-toast-title', containsText); + await controllable.waitTillGone('.ui-toast-title'); + }; + + public static noToastAppears = async (controllable: Controllable, waitSeconds = 5): Promise => { + await controllable.notPresent('.ui-toast-container'); + for (let i = 0; i < waitSeconds; i++) { + await Util.sleep(1); + await controllable.notPresent('.ui-toast-container'); + } + }; + public static sendMessage = async (controllable: Controllable, msg: any) => { return await controllable.target.evaluate(async (msg) => await new Promise((resolve) => { chrome.runtime.sendMessage(msg, resolve); diff --git a/test/source/tests/page-recipe/setup-page-recipe.ts b/test/source/tests/page-recipe/setup-page-recipe.ts index f346fa0248f..ba2a10f5f38 100644 --- a/test/source/tests/page-recipe/setup-page-recipe.ts +++ b/test/source/tests/page-recipe/setup-page-recipe.ts @@ -251,9 +251,15 @@ export class SetupPageRecipe extends PageRecipe { }; public static autoSetupWithEKM = async (settingsPage: ControllablePage, - { expectErrView, enterPp, expectErrModal }: { + { + expectErrView, + expectErrModal, + expectWarnModal, + enterPp + }: { expectErrView?: { title: string, text: string }, expectErrModal?: string, + expectWarnModal?: string, enterPp?: { passphrase: string, checks?: SavePassphraseChecks } } = {}): Promise => { if (enterPp) { @@ -269,6 +275,9 @@ export class SetupPageRecipe extends PageRecipe { } await settingsPage.waitAndClick('@input-step2ekm-continue'); } + if (expectWarnModal) { + await settingsPage.waitAndRespondToModal('warning', 'confirm', expectWarnModal); + } if (expectErrView) { // this err is rendered in `view.ts` - `View` base class await settingsPage.waitAll(['@container-err-title', '@container-err-text', '@action-retry-by-reloading']); expect(await settingsPage.read('@container-err-title')).to.contain(expectErrView.title); diff --git a/test/source/tests/settings.ts b/test/source/tests/settings.ts index 58904efdfef..ec737940f0b 100644 --- a/test/source/tests/settings.ts +++ b/test/source/tests/settings.ts @@ -17,7 +17,7 @@ import { SetupPageRecipe } from './page-recipe/setup-page-recipe'; import { testConstants } from './tooling/consts'; import { PageRecipe } from './page-recipe/abstract-page-recipe'; import { OauthPageRecipe } from './page-recipe/oauth-page-recipe'; -import { KeyInfo, KeyUtil } from '../core/crypto/key'; +import { KeyInfoWithIdentity, KeyUtil } from '../core/crypto/key'; import { Buf } from '../core/buf'; import { GoogleData } from '../mock/google/google-data'; import Parse from './../util/parse'; @@ -383,7 +383,7 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T cryptup_userforbidstoringpassphraseorgruleflowcrypttest_keys: keys } = await settingsPage.getFromLocalStorage(['cryptup_userforbidstoringpassphraseorgruleflowcrypttest_passphrase_B8F687BCDE14435A', 'cryptup_userforbidstoringpassphraseorgruleflowcrypttest_keys']); - expect((keys as KeyInfo[])[0].longid).to.equal('B8F687BCDE14435A'); + expect((keys as KeyInfoWithIdentity[])[0].longid).to.equal('B8F687BCDE14435A'); expect(savedPassphrase1).to.be.an('undefined'); const newPp = `temp ci test pp: ${Util.lousyRandom()}`; // decrypt msg, enter pp and make sure it's not stored to the local storage @@ -594,15 +594,15 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T 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())); + const { keys: keys1 } = await KeyUtil.readMany(Buf.fromUtfStr(Object.values(backupFileRawData1).pop()!.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())); + const backupFileRawData2 = await backupPage.awaitDownloadTriggeredByClicking('@action-backup-step3manual-continue', 2); + const { keys: keys2 } = await KeyUtil.readMany(Buf.fromUtfStr(Buf.concat(Object.values(backupFileRawData2)).toString())); expect(keys2.length).to.equal(2); await backupPage.close(); })); @@ -634,7 +634,7 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T 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!)); + const { keys } = await KeyUtil.readMany(Buf.concat(mimeMsg.attachments.map(a => a.content))); expect(keys.length).to.equal(2); await backupPage.close(); })); @@ -666,8 +666,8 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T 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())); + const downloadedFiles = await backupPage.awaitDownloadTriggeredByClicking('@action-backup-step3manual-continue', 2); + const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(Buffer.concat(Object.values(downloadedFiles)).toString())); expect(keys.length).to.equal(2); await backupPage.close(); })); @@ -775,8 +775,8 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T 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())); + const downloadedFiles = await backupPage.awaitDownloadTriggeredByClicking('@action-backup-step3manual-continue'); + const { keys } = await KeyUtil.readMany(Buf.fromUtfStr(Object.values(downloadedFiles).pop()!.toString())); expect(keys.length).to.equal(1); expect(keys[0].id).to.equal("515431151DDD3EA232B37A4C98ACFA1EADAB5B92"); await backupPage.waitAndRespondToModal('info', 'confirm', 'Downloading private key backup file'); @@ -812,7 +812,7 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T 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); + expect(KeyUtil.identityEquals(keys[0], { id: '515431151DDD3EA232B37A4C98ACFA1EADAB5B92', family: 'openpgp' })).to.equal(true); await backupPage.close(); })); @@ -852,7 +852,7 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T = await settingsPage.getFromLocalStorage(['cryptup_userforbidstoringpassphraseorgruleflowcrypttest_passphrase_B8F687BCDE14435A', 'cryptup_userforbidstoringpassphraseorgruleflowcrypttest_keys']); expect(savedPassphrase1).to.be.an('undefined'); - expect((keys as KeyInfo[])[0].longid).to.equal('B8F687BCDE14435A'); + expect((keys as KeyInfoWithIdentity[])[0].longid).to.equal('B8F687BCDE14435A'); await SettingsPageRecipe.toggleScreen(settingsPage, 'additional'); // open key at index 0 const myKeyFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, `@action-show-key-0`, ['my_key.htm', 'placement=settings']); @@ -917,7 +917,7 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T 'cryptup_userdefaultrememberpassphraseorgruleflowcrypttest_passphrase_07481C8ACF9D49FE', 'cryptup_userdefaultrememberpassphraseorgruleflowcrypttest_keys']); expect((newRules as { flags: string[] }).flags).to.include('DEFAULT_REMEMBER_PASS_PHRASE'); - expect((keys as KeyInfo[])[0].longid).to.equal('07481C8ACF9D49FE'); + expect((keys as KeyInfoWithIdentity[])[0].longid).to.equal('07481C8ACF9D49FE'); expect(savedPassphrase2).not.to.be.an('undefined'); await newSettingsPage.close(); await settingsPage.close(); @@ -951,7 +951,7 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T 'cryptup_userforbidstoringpassphraseorgruleflowcrypttest_keys', 'cryptup_userforbidstoringpassphraseorgruleflowcrypttest_passphrase_07481C8ACF9D49FE']); expect((newRules as { flags: string[] }).flags).to.include('FORBID_STORING_PASS_PHRASE'); - expect((keys as KeyInfo[])[0].longid).to.equal('07481C8ACF9D49FE'); + expect((keys as KeyInfoWithIdentity[])[0].longid).to.equal('07481C8ACF9D49FE'); expect(savedPassphrase2).to.be.an('undefined'); await newSettingsPage.close(); await settingsPage.close(); @@ -963,7 +963,7 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T cryptup_userforbidstoringpassphraseorgruleflowcrypttest_keys: keys1 } = await settingsPage.getFromLocalStorage(['cryptup_userforbidstoringpassphraseorgruleflowcrypttest_passphrase_B8F687BCDE14435A', 'cryptup_userforbidstoringpassphraseorgruleflowcrypttest_keys']); - expect((keys1 as KeyInfo[])[0].longid).to.equal('B8F687BCDE14435A'); + expect((keys1 as KeyInfoWithIdentity[])[0].longid).to.equal('B8F687BCDE14435A'); expect(savedPassphrase1).to.be.an('undefined'); await SettingsPageRecipe.addKeyTest(t, browser, acctEmail, testConstants.testKeyMultiple98acfa1eadab5b92, '1234', { isSavePassphraseChecked: false, isSavePassphraseHidden: true }); @@ -971,7 +971,7 @@ export const defineSettingsTests = (testVariant: TestVariant, testWithBrowser: T cryptup_userforbidstoringpassphraseorgruleflowcrypttest_keys: keys2 } = await settingsPage.getFromLocalStorage(['cryptup_userforbidstoringpassphraseorgruleflowcrypttest_passphrase_98ACFA1EADAB5B92', 'cryptup_userforbidstoringpassphraseorgruleflowcrypttest_keys']); - expect((keys2 as KeyInfo[]).map(ki => ki.longid)).to.include.members(['B8F687BCDE14435A', '98ACFA1EADAB5B92']); + expect((keys2 as KeyInfoWithIdentity[]).map(ki => ki.longid)).to.include.members(['B8F687BCDE14435A', '98ACFA1EADAB5B92']); expect(savedPassphrase2).to.be.an('undefined'); })); diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index acc427969d4..878062282f1 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -12,7 +12,7 @@ import { Str } from './../core/common'; import { MOCK_KM_LAST_INSERTED_KEY } from './../mock/key-manager/key-manager-endpoints'; import { MOCK_ATTESTER_LAST_INSERTED_PUB } from './../mock/attester/attester-endpoints'; import { BrowserRecipe } from './tooling/browser-recipe'; -import { KeyInfo, KeyUtil } from '../core/crypto/key'; +import { KeyInfoWithIdentity, KeyUtil } from '../core/crypto/key'; import { testConstants } from './tooling/consts'; // tslint:disable:no-blank-lines-func @@ -434,7 +434,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== await Util.sleep(1); await settingsPage.notPresent(['@action-open-backup-page']); const { cryptup_haspuborgrulestestflowcrypttest_keys: keys } = await settingsPage.getFromLocalStorage(['cryptup_haspuborgrulestestflowcrypttest_keys']); - const ki = keys as KeyInfo[]; + const ki = keys as KeyInfoWithIdentity[]; expect(ki.length).to.equal(1); expect(ki[0].private).to.include('PGP PRIVATE KEY'); expect(ki[0].private).to.not.include('Version'); @@ -545,7 +545,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== ]); expect((rules as { flags: string[] }).flags).not.to.include('FORBID_STORING_PASS_PHRASE'); expect((rules as { flags: string[] }).flags).to.include('DEFAULT_REMEMBER_PASS_PHRASE'); - expect((keys as KeyInfo[])[0].longid).to.equal('00B0115807969D75'); + expect((keys as KeyInfoWithIdentity[])[0].longid).to.equal('00B0115807969D75'); expect(savedPassphrase).to.equal(passphrase); })); @@ -563,7 +563,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== ]); expect((rules as { flags: string[] }).flags).to.include('FORBID_STORING_PASS_PHRASE'); expect((rules as { flags: string[] }).flags).not.to.include('DEFAULT_REMEMBER_PASS_PHRASE'); - expect((keys as KeyInfo[])[0].longid).to.equal('00B0115807969D75'); + expect((keys as KeyInfoWithIdentity[])[0].longid).to.equal('00B0115807969D75'); expect(savedPassphrase).to.be.an('undefined'); })); diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 03ae1005226..8f907c33822 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -8,7 +8,7 @@ import { PgpHash } from '../core/crypto/pgp/pgp-hash'; import { TestVariant, Util } from '../util'; import chai = require('chai'); import chaiAsPromised = require('chai-as-promised'); -import { KeyUtil, ExtendedKeyInfo } from '../core/crypto/key'; +import { KeyUtil, KeyInfoWithIdentityAndOptionalPp } from '../core/crypto/key'; import { UnreportableError } from '../platform/catch.js'; import { Buf } from '../core/buf'; import { OpenPGPKey } from '../core/crypto/pgp/openpgp-key'; @@ -184,7 +184,7 @@ export const defineUnitNodeTests = (testVariant: TestVariant) => { writeFileSync("./test.p12", buf); */ const key = await KeyUtil.parse(testConstants.smimeCert); expect(key.id).to.equal('1D695D97A7C8A473E36C6E1D8C150831E4061A74'); - expect(key.type).to.equal('x509'); + expect(key.family).to.equal('x509'); expect(key.usableForEncryption).to.equal(true); expect(key.usableForSigning).to.equal(true); expect(key.usableForEncryptionButExpired).to.equal(false); @@ -314,7 +314,7 @@ sOLAw7KgpiL2+0v777saxSO5vtufJCKk4OOEaVDufeijlejKTM+H7twVer4iGqiW expect(key.allIds.length).to.equal(2); expect(key.allIds[0]).to.equal('3449178FCAAF758E24CB68BE62CB4E6F9ECA6FA1'); expect(key.allIds[1]).to.equal('2D3391762FAC9394F7D5E9EDB30FE36B3AEC2F8F'); - expect(key.type).to.equal('openpgp'); + expect(key.family).to.equal('openpgp'); expect(key.usableForEncryption).equal(false); expect(key.usableForSigning).equal(false); expect(key.usableForEncryptionButExpired).equal(true); @@ -386,7 +386,7 @@ cmKFmmDYm+rrWuAv6Q== expect(key.allIds.length).to.equal(2); expect(key.allIds[0]).to.equal('7C3B38BB2C8A7E693C29DF455C08033166AF91E3'); expect(key.allIds[1]).to.equal('28A4CCBFA1AF056C3B73EA4DECF8F9D42D8DFED8'); - expect(key.type).to.equal('openpgp'); + expect(key.family).to.equal('openpgp'); expect(key.usableForEncryption).equal(true); expect(key.usableForSigning).equal(true); expect(key.usableForEncryptionButExpired).equal(false); @@ -485,7 +485,7 @@ zZFGf6poIjKUC8V2Zww6 expect(errs.length).to.equal(0); expect(keys.some(key => key.id === '5A5F75AEA28751C3EE8CFFC3AC5F0CE1BB2B99DD')).to.equal(true); expect(keys.some(key => key.id === 'BBC75684E46EF0948D31359992C4E7841B3AFF74')).to.equal(true); - expect(keys.every(key => key.type === 'openpgp')).to.equal(true); + expect(keys.every(key => key.family === 'openpgp')).to.equal(true); t.pass(); }); @@ -556,7 +556,7 @@ vpQiyk4ceuTNkUZ/qmgiMpQLxXZnDDo= expect(errs.length).to.equal(0); expect(keys.some(key => key.id === '5A5F75AEA28751C3EE8CFFC3AC5F0CE1BB2B99DD')).to.equal(true); expect(keys.some(key => key.id === 'BBC75684E46EF0948D31359992C4E7841B3AFF74')).to.equal(true); - expect(keys.every(key => key.type === 'openpgp')).to.equal(true); + expect(keys.every(key => key.family === 'openpgp')).to.equal(true); t.pass(); }); @@ -565,7 +565,7 @@ vpQiyk4ceuTNkUZ/qmgiMpQLxXZnDDo= expect(keys.length).to.equal(1); expect(errs.length).to.equal(0); expect(keys[0].id).to.equal('1D695D97A7C8A473E36C6E1D8C150831E4061A74'); - expect(keys[0].type).to.equal('x509'); + expect(keys[0].family).to.equal('x509'); t.pass(); }); @@ -642,7 +642,7 @@ ${testConstants.smimeCert}`), { instanceOf: Error, message: `Invalid PEM formatt expect(keys.length).to.equal(1); expect(errs.length).to.equal(0); expect(keys[0].id).to.equal('1D695D97A7C8A473E36C6E1D8C150831E4061A74'); - expect(keys[0].type).to.equal('x509'); + expect(keys[0].family).to.equal('x509'); t.pass(); }); @@ -762,8 +762,8 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY expect(errs.length).to.equal(0); expect(keys.some(key => key.id === '1D695D97A7C8A473E36C6E1D8C150831E4061A74')).to.equal(true); expect(keys.some(key => key.id === '3449178FCAAF758E24CB68BE62CB4E6F9ECA6FA1')).to.equal(true); - expect(keys.some(key => key.type === 'openpgp')).to.equal(true); - expect(keys.some(key => key.type === 'x509')).to.equal(true); + expect(keys.some(key => key.family === 'openpgp')).to.equal(true); + expect(keys.some(key => key.family === 'x509')).to.equal(true); t.pass(); }); @@ -921,9 +921,9 @@ jSB6A93JmnQGIkAem/kzGkKclmfAdGfc4FS+3Cn+6Q==Xmrz const m = await opgp.message.readArmored(Buf.fromUint8(data).toUtfStr()); 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.typedKeyInfoObj(parsed1), passphrase }, - { ... await KeyUtil.typedKeyInfoObj(parsed2), passphrase }, + const kisWithPp: KeyInfoWithIdentityAndOptionalPp[] = [ // supply both key1 and key2 for decrypt + { ... await KeyUtil.keyInfoObj(parsed1), passphrase }, + { ... await KeyUtil.keyInfoObj(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. @@ -1022,7 +1022,7 @@ jSB6A93JmnQGIkAem/kzGkKclmfAdGfc4FS+3Cn+6Q==Xmrz 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.typedKeyInfoObj(parsed), type: parsed.type, passphrase }]; + const kisWithPp: KeyInfoWithIdentityAndOptionalPp[] = [{ ... await KeyUtil.keyInfoObj(parsed), family: parsed.family, passphrase }]; const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData: encrypted.data, verificationPubs: [] }); // todo - later we'll have an org rule for ignoring this, and then it will be expected to pass as follows: // expect(decrypted.success).to.equal(true); @@ -1884,7 +1884,7 @@ PBcqDCjq5jgMhU1oyVclRK7jJdmu0Azvwo2lleLAFLdCzHEXWXUz const parsed = await KeyUtil.parseBinary(key, 'test'); expect(parsed.length).to.be.equal(1); expect(parsed[0].id).to.be.equal('60EFFE4DF7B2114A77021459C273F0AA864AFF7F'); - expect(parsed[0].type).to.be.equal('x509'); + expect(parsed[0].family).to.be.equal('x509'); expect(parsed[0].emails.length).to.be.equal(1); expect(parsed[0].emails[0]).to.be.equal('test@example.com'); expect(parsed[0].isPrivate).to.be.equal(true); @@ -1896,7 +1896,7 @@ PBcqDCjq5jgMhU1oyVclRK7jJdmu0Azvwo2lleLAFLdCzHEXWXUz const p8 = readFileSync("test/samples/smime/human-pwd-pem.txt", 'utf8'); let parsed = await KeyUtil.parse(p8); expect(parsed.id).to.equal('9B5FCFF576A032495AFE77805354351B39AB3BC6'); - expect(parsed.type).to.equal('x509'); + expect(parsed.family).to.equal('x509'); expect(parsed.emails.length).to.equal(1); expect(parsed.emails[0]).to.equal('human@flowcrypt.com'); expect(parsed.isPrivate).to.equal(true); @@ -1915,7 +1915,7 @@ PBcqDCjq5jgMhU1oyVclRK7jJdmu0Azvwo2lleLAFLdCzHEXWXUz expect(armoredAfterDecryption).to.include('-----BEGIN RSA PRIVATE KEY-----'); parsed = await KeyUtil.parse(armoredAfterDecryption); expect(parsed.id).to.equal('9B5FCFF576A032495AFE77805354351B39AB3BC6'); - expect(parsed.type).to.equal('x509'); + expect(parsed.family).to.equal('x509'); expect(parsed.emails.length).to.equal(1); expect(parsed.emails[0]).to.equal('human@flowcrypt.com'); expect(parsed.isPrivate).to.equal(true); @@ -1928,7 +1928,7 @@ PBcqDCjq5jgMhU1oyVclRK7jJdmu0Azvwo2lleLAFLdCzHEXWXUz const p8 = readFileSync("test/samples/smime/human-pwd-shuffled-pem.txt", 'utf8'); let parsed = await KeyUtil.parse(p8); expect(parsed.id).to.equal('9B5FCFF576A032495AFE77805354351B39AB3BC6'); - expect(parsed.type).to.equal('x509'); + expect(parsed.family).to.equal('x509'); expect(parsed.emails.length).to.equal(1); expect(parsed.emails[0]).to.equal('human@flowcrypt.com'); expect(parsed.isPrivate).to.equal(true); @@ -1947,7 +1947,7 @@ PBcqDCjq5jgMhU1oyVclRK7jJdmu0Azvwo2lleLAFLdCzHEXWXUz expect(armoredAfterDecryption).to.include('-----BEGIN RSA PRIVATE KEY-----'); parsed = await KeyUtil.parse(armoredAfterDecryption); expect(parsed.id).to.equal('9B5FCFF576A032495AFE77805354351B39AB3BC6'); - expect(parsed.type).to.equal('x509'); + expect(parsed.family).to.equal('x509'); expect(parsed.emails.length).to.equal(1); expect(parsed.emails[0]).to.equal('human@flowcrypt.com'); expect(parsed.isPrivate).to.equal(true); @@ -1960,7 +1960,7 @@ PBcqDCjq5jgMhU1oyVclRK7jJdmu0Azvwo2lleLAFLdCzHEXWXUz const p8 = readFileSync("test/samples/smime/human-unprotected-pem.txt", 'utf8'); let parsed = await KeyUtil.parse(p8); expect(parsed.id).to.equal('9B5FCFF576A032495AFE77805354351B39AB3BC6'); - expect(parsed.type).to.equal('x509'); + expect(parsed.family).to.equal('x509'); expect(parsed.emails.length).to.equal(1); expect(parsed.emails[0]).to.equal('human@flowcrypt.com'); expect(parsed.isPrivate).to.equal(true); @@ -1976,7 +1976,7 @@ PBcqDCjq5jgMhU1oyVclRK7jJdmu0Azvwo2lleLAFLdCzHEXWXUz expect(armoredAfterEncryption).to.not.include('-----BEGIN PRIVATE KEY-----'); parsed = await KeyUtil.parse(armoredAfterEncryption); expect(parsed.id).to.equal('9B5FCFF576A032495AFE77805354351B39AB3BC6'); - expect(parsed.type).to.equal('x509'); + expect(parsed.family).to.equal('x509'); expect(parsed.emails.length).to.equal(1); expect(parsed.emails[0]).to.equal('human@flowcrypt.com'); expect(parsed.isPrivate).to.equal(true); diff --git a/test/source/util/index.ts b/test/source/util/index.ts index 022c6c82f6e..1b49cdfad87 100644 --- a/test/source/util/index.ts +++ b/test/source/util/index.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import { Keyboard, KeyInput } from 'puppeteer'; import { testKeyConstants } from '../tests/tooling/consts'; -import { ExtendedKeyInfo, KeyUtil } from '../core/crypto/key.js'; +import { KeyInfoWithIdentityAndOptionalPp, KeyUtil } from '../core/crypto/key.js'; export type TestVariant = 'CONSUMER-MOCK' | 'ENTERPRISE-MOCK' | 'CONSUMER-LIVE-GMAIL' | 'UNIT-TESTS'; @@ -78,11 +78,11 @@ export class Config { return Config.secrets().keys.filter(k => k.title === title)[0]; }; - public static getKeyInfo = async (titles: string[]): Promise => { + public static getKeyInfo = async (titles: string[]): Promise => { 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.typedKeyInfoObj(parsed), passphrase: key.passphrase }; + return { ...await KeyUtil.keyInfoObj(parsed), passphrase: key.passphrase }; })); };