Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -181,18 +183,18 @@ export class ComposeRecipientsModule extends ViewModule<ComposeView> {
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 };
Expand Down Expand Up @@ -229,15 +231,15 @@ export class ComposeRecipientsModule extends ViewModule<ComposeView> {
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', '');
Expand Down Expand Up @@ -304,7 +306,7 @@ export class ComposeRecipientsModule extends ViewModule<ComposeView> {
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
Expand Down Expand Up @@ -457,7 +459,7 @@ export class ComposeRecipientsModule extends ViewModule<ComposeView> {
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();
Expand Down Expand Up @@ -913,12 +915,12 @@ export class ComposeRecipientsModule extends ViewModule<ComposeView> {
this.hideContacts();
};

private createRecipientsElements = (
private createRecipientsElements = async (
container: JQuery,
emails: { email?: string; name?: string; invalid?: string }[],
sendingType: RecipientType,
status: RecipientStatus
): RecipientElement[] => {
): Promise<RecipientElement[]> => {
// Do not add padding-bottom for reply box
// https://github.com/FlowCrypt/flowcrypt-browser/issues/5935
if (!container.hasClass('input-container')) {
Expand Down Expand Up @@ -965,7 +967,7 @@ export class ComposeRecipientsModule extends ViewModule<ComposeView> {
};
this.addedRecipients.push(recipient);
if (recipient.status === RecipientStatus.WRONG) {
this.renderPubkeyResult(recipient, undefined);
await this.renderPubkeyResult(recipient, undefined);
}
result.push(recipient);
}
Expand Down Expand Up @@ -1026,12 +1028,12 @@ export class ComposeRecipientsModule extends ViewModule<ComposeView> {
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, '') ?? '';
Expand Down Expand Up @@ -1100,7 +1102,24 @@ export class ComposeRecipientsModule extends ViewModule<ComposeView> {
// - 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, '<img src="/img/svgs/revoked.svg" class="revoked-or-expired">');
$(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, '<img class="lock-icon" src="/img/svgs/locked-icon.svg" />');
Expand Down
7 changes: 6 additions & 1 deletion extension/chrome/elements/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion extension/chrome/settings/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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]);
}
Comment on lines +126 to +130
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @martgil, I think we shouldn't completely disable SHA1 algorithm with rejectHashAlgorithms property, as we already show error error verifying signature: Insecure hash algorithm: SHA1. Sender is using old, insecure OpenPGP software. when user verifies message signed with sha1 key.
Disabling sha1 entirely would also make it impossible for users to decrypt older messages encrypted with sha1 keys.
So let's just remove opgp.config.rejectHashAlgorithms property altogether.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand completely, Roma, sorry. I'll be closing this PR and proceed with removing the rejectHashAlgorithms property in openpgp configuration.

this.pubLookup = new PubLookup(this.clientConfiguration);
if (this.clientConfiguration.usesKeyManager() && this.idToken) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand Down
11 changes: 11 additions & 0 deletions extension/js/common/client-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion extension/js/common/core/crypto/pgp/openpgpjs-custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions test/source/mock/fes/shared-tenant-fes-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading