diff --git a/extension/chrome/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index 82563dc0e26..b9a4d4d3c19 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -22,6 +22,8 @@ import { ComposeView } from '../compose.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; import { ContactPreview, ContactStore } from '../../../js/common/platform/store/contact-store.js'; import { FLOWCRYPT_REPLY_EMAIL_ADDRESSES } from '../../../js/common/api/email-provider/gmail/gmail-parser.js'; +import { opgp } from '../../../js/common/core/crypto/pgp/openpgpjs-custom.js'; +import { KeyWithPrivateFields } from '../../../js/common/core/crypto/pgp/openpgp-key.js'; /** * todo - this class is getting too big @@ -181,18 +183,18 @@ export class ComposeRecipientsModule extends ViewModule { const container = input.parent(); if (validationResult.valid.length) { this.view.errModule.debug(`parseRenderRecipients(force: ${force}) - valid emails(${Str.formatEmailList(validationResult.valid)}`); - recipientsToEvaluate = this.createRecipientsElements( + recipientsToEvaluate = (await this.createRecipientsElements( container, validationResult.valid, sendingType, RecipientStatus.EVALUATING - ) as ValidRecipientElement[]; + )) as ValidRecipientElement[]; } const invalidEmails = validationResult.invalid.filter(em => !!em); // remove empty strings this.view.errModule.debug(`parseRenderRecipients(force: ${force}) - invalid emails(${validationResult.invalid.join(',')})`); if (force && invalidEmails.length) { this.view.errModule.debug(`parseRenderRecipients(force: ${force}) - force add invalid recipients`); - this.createRecipientsElements( + await this.createRecipientsElements( container, invalidEmails.map(invalid => { return { invalid }; @@ -229,15 +231,15 @@ export class ComposeRecipientsModule extends ViewModule { const parsed = Str.parseEmail(email); if (parsed.email) { newRecipients.push( - ...(this.createRecipientsElements( + ...((await this.createRecipientsElements( recipientsContainer, [{ email: parsed.email, name: parsed.name }], sendingType, RecipientStatus.EVALUATING - ) as ValidRecipientElement[]) + )) as ValidRecipientElement[]) ); } else { - this.createRecipientsElements(recipientsContainer, [{ invalid: email }], sendingType, RecipientStatus.WRONG); + await this.createRecipientsElements(recipientsContainer, [{ invalid: email }], sendingType, RecipientStatus.WRONG); } } this.view.S.cached('input_addresses_container_outer').find(`#input-container-${sendingType}`).css('display', ''); @@ -304,7 +306,7 @@ export class ComposeRecipientsModule extends ViewModule { this.view.errModule.debug(`evaluateRecipients.evaluating.recipient.status(${recipientEl.status})`); this.view.errModule.debug(`evaluateRecipients.evaluating: calling getUpToDatePubkeys`); const info = await this.view.storageModule.getUpToDatePubkeys(recipientEl.email); - this.renderPubkeyResult(recipientEl, info); + await this.renderPubkeyResult(recipientEl, info); // Clear promise when after finished // todo - it would be better if we could avoid doing this, eg // recipient.evaluating would be a bool @@ -457,7 +459,7 @@ export class ComposeRecipientsModule extends ViewModule { const emailAndPubkeys = await ContactStore.getOneWithAllPubkeys(undefined, email); for (const recipient of validRecipients) { this.view.errModule.debug(`re-rendering recipient: ${email}`); - this.renderPubkeyResult(recipient, emailAndPubkeys); + await this.renderPubkeyResult(recipient, emailAndPubkeys); } this.showHideCcAndBccInputsIfNeeded(); this.setEmailsPreview(); @@ -913,12 +915,12 @@ export class ComposeRecipientsModule extends ViewModule { this.hideContacts(); }; - private createRecipientsElements = ( + private createRecipientsElements = async ( container: JQuery, emails: { email?: string; name?: string; invalid?: string }[], sendingType: RecipientType, status: RecipientStatus - ): RecipientElement[] => { + ): Promise => { // Do not add padding-bottom for reply box // https://github.com/FlowCrypt/flowcrypt-browser/issues/5935 if (!container.hasClass('input-container')) { @@ -965,7 +967,7 @@ export class ComposeRecipientsModule extends ViewModule { }; this.addedRecipients.push(recipient); if (recipient.status === RecipientStatus.WRONG) { - this.renderPubkeyResult(recipient, undefined); + await this.renderPubkeyResult(recipient, undefined); } result.push(recipient); } @@ -1026,12 +1028,12 @@ export class ComposeRecipientsModule extends ViewModule { const dbContacts = await ContactStore.getOneWithAllPubkeys(undefined, email); if (dbContacts?.sortedPubkeys?.length) { recipientEl.element.classList.remove('no_pgp'); - this.renderPubkeyResult(recipientEl, dbContacts); + await this.renderPubkeyResult(recipientEl, dbContacts); } } }; - private renderPubkeyResult = (recipient: RecipientElement, info: ContactInfoWithSortedPubkeys | undefined | 'fail') => { + private renderPubkeyResult = async (recipient: RecipientElement, info: ContactInfoWithSortedPubkeys | undefined | 'fail') => { // console.log(`>>>> renderPubkeyResult: ${JSON.stringify(info)}`); const el = recipient.element; const emailId = recipient.email?.replace(/[^a-z0-9]+/g, '') ?? ''; @@ -1100,7 +1102,24 @@ export class ComposeRecipientsModule extends ViewModule { // - else EXPIRED. // 3. Otherwise NO_PGP. const firstKeyInfo = info.sortedPubkeys[0]; - if (firstKeyInfo.pubkey.usableForEncryption && !firstKeyInfo.revoked && !KeyUtil.expired(firstKeyInfo.pubkey)) { + let rejectedHashAlgoDetected = false; + if (firstKeyInfo.pubkey.family === 'openpgp') { + const pubkey = await opgp.readKey({ armoredKey: (firstKeyInfo.pubkey as unknown as KeyWithPrivateFields).rawArmored }); + for (const user of pubkey.users) { + for (const sig of user.selfCertifications) { + if (sig.preferredHashAlgorithms) { + const preferredHashAlgorithms = sig.preferredHashAlgorithms; + rejectedHashAlgoDetected = preferredHashAlgorithms.some(v => opgp.config.rejectHashAlgorithms.has(v)); + } + } + } + } + if (rejectedHashAlgoDetected && !firstKeyInfo.revoked && !KeyUtil.expired(firstKeyInfo.pubkey)) { + recipient.status = RecipientStatus.UNUSABLE; + $(el).addClass('unusable'); + Xss.sanitizePrepend(el, ''); + $(el).attr('title', 'Does use encryption but their public key is unusable for encryption.\n\n' + this.formatPubkeysHintText(info.sortedPubkeys)); + } else if (firstKeyInfo.pubkey.usableForEncryption && !firstKeyInfo.revoked && !KeyUtil.expired(firstKeyInfo.pubkey)) { recipient.status = RecipientStatus.HAS_PGP; $(el).addClass('has_pgp'); Xss.sanitizePrepend(el, ''); diff --git a/extension/chrome/elements/compose.ts b/extension/chrome/elements/compose.ts index 8e807f26c77..df70f93051d 100644 --- a/extension/chrome/elements/compose.ts +++ b/extension/chrome/elements/compose.ts @@ -10,7 +10,7 @@ import { Ui } from '../../js/common/browser/ui.js'; import { PromiseCancellation, Url } from '../../js/common/core/common.js'; import { View } from '../../js/common/view.js'; import { XssSafeFactory } from '../../js/common/xss-safe-factory.js'; -import { opgp } from '../../js/common/core/crypto/pgp/openpgpjs-custom.js'; +import { defaultRejectedHashAlgo, opgp } from '../../js/common/core/crypto/pgp/openpgpjs-custom.js'; import { ComposeAttachmentsModule } from './compose-modules/compose-attachments-module.js'; import { ComposeDraftModule } from './compose-modules/compose-draft-module.js'; import { ComposeErrModule } from './compose-modules/compose-err-module.js'; @@ -178,6 +178,11 @@ export class ComposeView extends View { opgp.config.showComment = false; opgp.config.showVersion = false; } + if (this.clientConfiguration.shouldAllowInsecureSha1Hash()) { + opgp.config.rejectHashAlgorithms = new Set([...defaultRejectedHashAlgo]); + } else { + opgp.config.rejectHashAlgorithms = new Set([...defaultRejectedHashAlgo, opgp.enums.hash.sha1]); + } this.pubLookup = new PubLookup(this.clientConfiguration); this.factory = new XssSafeFactory(this.acctEmail, this.tabId); this.draftModule = new ComposeDraftModule(this); diff --git a/extension/chrome/settings/setup.ts b/extension/chrome/settings/setup.ts index 11822dd735e..b3974868696 100644 --- a/extension/chrome/settings/setup.ts +++ b/extension/chrome/settings/setup.ts @@ -14,7 +14,7 @@ import { ClientConfiguration } from '../../js/common/client-configuration.js'; import { Url } from '../../js/common/core/common.js'; import { KeyStoreUtil } from '../../js/common/core/crypto/key-store-util.js'; import { KeyInfoWithIdentity } from '../../js/common/core/crypto/key.js'; -import { opgp } from '../../js/common/core/crypto/pgp/openpgpjs-custom.js'; +import { defaultRejectedHashAlgo, opgp } from '../../js/common/core/crypto/pgp/openpgpjs-custom.js'; import { Lang } from '../../js/common/lang.js'; import { Catch } from '../../js/common/platform/catch.js'; import { CompanyLdapKeyMismatchError } from '../../js/common/platform/error-report.js'; @@ -123,6 +123,11 @@ export class SetupView extends View { opgp.config.showComment = false; opgp.config.showVersion = false; } + if (this.clientConfiguration.shouldAllowInsecureSha1Hash() && typeof opgp !== 'undefined') { + opgp.config.rejectHashAlgorithms = new Set([...defaultRejectedHashAlgo]); + } else { + opgp.config.rejectHashAlgorithms = new Set([...defaultRejectedHashAlgo, opgp.enums.hash.sha1]); + } this.pubLookup = new PubLookup(this.clientConfiguration); if (this.clientConfiguration.usesKeyManager() && this.idToken) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/extension/js/common/client-configuration.ts b/extension/js/common/client-configuration.ts index 76f8d84fb29..f5436930b3b 100644 --- a/extension/js/common/client-configuration.ts +++ b/extension/js/common/client-configuration.ts @@ -17,6 +17,7 @@ type ClientConfiguration$flag = | 'SETUP_ENSURE_IMPORTED_PRV_MATCH_LDAP_PUB' | 'DEFAULT_REMEMBER_PASS_PHRASE' | 'HIDE_ARMOR_META' + | 'ALLOW_INSECURE_SHA1_HASH' | 'FORBID_STORING_PASS_PHRASE' | 'DISABLE_FLOWCRYPT_HOSTED_PASSWORD_MESSAGES'; @@ -270,6 +271,16 @@ export class ClientConfiguration { return (this.clientConfigurationJson.flags || []).includes('HIDE_ARMOR_META'); }; + /** + * With this option enabled, SHA-1 hashes will be allowed in OpenPGP operations + * despite being considered insecure. This should only be used for legacy + * compatibility when communicating with systems that do not support stronger + * algorithms. + */ + public shouldAllowInsecureSha1Hash = (): boolean => { + return (this.clientConfigurationJson.flags || []).includes('ALLOW_INSECURE_SHA1_HASH'); + }; + /** * with this option and recipients are missing a public key, and the user is using flowcrypt.com/shared-tenant-fes (not FES) * it will not give the user option to enter a message password, as if that functionality didn't exist. diff --git a/extension/js/common/core/crypto/pgp/openpgpjs-custom.ts b/extension/js/common/core/crypto/pgp/openpgpjs-custom.ts index d0f2b99747d..13d097c9fef 100644 --- a/extension/js/common/core/crypto/pgp/openpgpjs-custom.ts +++ b/extension/js/common/core/crypto/pgp/openpgpjs-custom.ts @@ -9,13 +9,14 @@ export const opgp = requireOpenpgp(); export type OpenPGPDataType = string | Uint8Array; +export const defaultRejectedHashAlgo = opgp.config.rejectHashAlgorithms; + if (typeof opgp !== 'undefined') { // in certain environments, eg pgp_block.htm or web content script, openpgp is not included opgp.config.versionString = `FlowCrypt Email Encryption ${VERSION}`; opgp.config.showVersion = true; opgp.config.commentString = 'Seamlessly send and receive encrypted email'; opgp.config.showComment = true; - opgp.config.rejectHashAlgorithms = new Set([...opgp.config.rejectHashAlgorithms, opgp.enums.hash.sha1]); opgp.config.allowUnauthenticatedMessages = true; // we manually check for missing MDC and show loud warning to user (no auto-decrypt) opgp.config.allowInsecureDecryptionWithSigningKeys = false; // may get later over-written using ClientConfiguration for some clients // openpgp.config.require_uid_self_cert = false; diff --git a/test/source/mock/fes/shared-tenant-fes-endpoints.ts b/test/source/mock/fes/shared-tenant-fes-endpoints.ts index 0d41016adf6..7f1605791a0 100644 --- a/test/source/mock/fes/shared-tenant-fes-endpoints.ts +++ b/test/source/mock/fes/shared-tenant-fes-endpoints.ts @@ -32,6 +32,7 @@ type FesClientConfigurationFlag = | 'USE_LEGACY_ATTESTER_SUBMIT' | 'DEFAULT_REMEMBER_PASS_PHRASE' | 'HIDE_ARMOR_META' + | 'ALLOW_INSECURE_SHA1_HASH' | 'FORBID_STORING_PASS_PHRASE' | 'DISABLE_FES_ACCESS_TOKEN' | 'SETUP_ENSURE_IMPORTED_PRV_MATCH_LDAP_PUB';