diff --git a/extension/chrome/elements/compose-modules/compose-err-module.ts b/extension/chrome/elements/compose-modules/compose-err-module.ts index 6ee5695046d..87ecc8532f3 100644 --- a/extension/chrome/elements/compose-modules/compose-err-module.ts +++ b/extension/chrome/elements/compose-modules/compose-err-module.ts @@ -15,6 +15,7 @@ import { Xss } from '../../../js/common/platform/xss.js'; import { ViewModule } from '../../../js/common/view-module.js'; import { ComposeView } from '../compose.js'; import { AjaxErrMsgs } from '../../../js/common/api/shared/api-error.js'; +import { Lang } from '../../../js/common/lang.js'; export class ComposerUserError extends Error { } class ComposerNotReadyError extends ComposerUserError { } @@ -147,6 +148,10 @@ export class ComposeErrModule extends ViewModule { `Sharing password over email undermines password based encryption.\n\n` + `You can ask the recipient to also install FlowCrypt, messages between FlowCrypt users don't need a password.`); } + if (!this.view.pwdOrPubkeyContainerModule.isMessagePasswordStrong(pwd)) { + const pwdErrText = this.view.fesUrl ? Lang.compose.enterprisePasswordPolicy : Lang.compose.consumerPasswordPolicy; + throw new ComposerUserError(pwdErrText.split('\n').join('
')); + } } else { this.view.S.cached('input_password').focus(); throw new ComposerUserError('Some recipients don\'t have encryption set up. Please add a password.'); diff --git a/extension/chrome/elements/compose-modules/compose-pwd-or-pubkey-container-module.ts b/extension/chrome/elements/compose-modules/compose-pwd-or-pubkey-container-module.ts index 1774737c512..0eba94dec03 100644 --- a/extension/chrome/elements/compose-modules/compose-pwd-or-pubkey-container-module.ts +++ b/extension/chrome/elements/compose-modules/compose-pwd-or-pubkey-container-module.ts @@ -4,19 +4,18 @@ import { RecipientStatus, SendBtnTexts } from './compose-types.js'; -import { KeyImportUi } from '../../../js/common/ui/key-import-ui.js'; import { Catch } from '../../../js/common/platform/catch.js'; import { Str } from '../../../js/common/core/common.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { ViewModule } from '../../../js/common/view-module.js'; import { ComposeView } from '../compose.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; +import { Lang } from '../../../js/common/lang.js'; +import { Xss } from '../../../js/common/platform/xss.js'; export class ComposePwdOrPubkeyContainerModule extends ViewModule { private MSG_EXPIRE_DAYS_DEFAULT = 3; // todo - update to 7 (needs backend work) - private keyImportUI = new KeyImportUi({}); - private rmPwdStrengthValidationElements: (() => void) | undefined; constructor(view: ComposeView, hideMsgPwd: boolean | undefined) { super(view); @@ -78,16 +77,35 @@ export class ComposePwdOrPubkeyContainerModule extends ViewModule { } } this.view.sizeModule.setInputTextHeightManuallyIfNeeded(); + const pwdOk = this.isMessagePasswordStrong(String(this.view.S.cached('input_password').val())); + this.view.S.cached('input_password').css('color', pwdOk ? '#31a217' : '#d14836'); // green : red }; public isVisible = () => { return !this.view.S.cached('password_or_pubkey').is(':hidden'); }; + public isMessagePasswordStrong = (pwd: string): boolean => { + const isLengthValid = pwd.length >= 8; + if (this.view.fesUrl) { // enterprise FES - use common corporate password rules + const isContentValid = /[0-9]/.test(pwd) && /[A-Z]/.test(pwd) && /[a-z]/.test(pwd) && /[^A-Za-z0-9]/.test(pwd); + if (!isContentValid || !isLengthValid) { + return false; + } + } else { // consumers - just 8 chars requirement + if (!isLengthValid) { + return false; + } + } + return true; + }; + private showMsgPwdUiAndColorBtn = async (anyNopgp: boolean, anyRevoked: boolean) => { if (!this.isVisible()) { const authInfo = await AcctStore.authInfo(this.view.acctEmail); const expirationTextEl = this.view.S.cached('expiration_note').find('#expiration_note_message_expire'); + const pwdPolicy = this.view.fesUrl ? Lang.compose.enterprisePasswordPolicy : Lang.compose.consumerPasswordPolicy; + $('#password-policy-container').html(Xss.htmlSanitize(pwdPolicy.split('\n').join('
'))); // xss-sanitized if (!authInfo) { expirationTextEl.text(Str.pluralize(this.MSG_EXPIRE_DAYS_DEFAULT, 'day')); } else { @@ -116,10 +134,6 @@ export class ComposePwdOrPubkeyContainerModule extends ViewModule { this.view.S.cached('warning_nopgp').css('display', anyNopgp ? 'inline-block' : 'none'); this.view.S.cached('warning_revoked').css('display', anyRevoked ? 'inline-block' : 'none'); this.view.sizeModule.setInputTextHeightManuallyIfNeeded(); - if (!this.rmPwdStrengthValidationElements) { - const { removeValidationElements } = this.keyImportUI.renderPassPhraseStrengthValidationInput($("#input_password"), undefined, 'pwd'); - this.rmPwdStrengthValidationElements = removeValidationElements; - } }; private hideMsgPwdUi = () => { @@ -128,10 +142,6 @@ export class ComposePwdOrPubkeyContainerModule extends ViewModule { this.view.S.cached('add_intro').css('display', 'none'); this.view.S.cached('input_intro').text(''); this.view.S.cached('intro_container').css('display', 'none'); - if (this.rmPwdStrengthValidationElements) { - this.rmPwdStrengthValidationElements(); - this.rmPwdStrengthValidationElements = undefined; - } this.view.sizeModule.setInputTextHeightManuallyIfNeeded(); }; diff --git a/extension/chrome/elements/compose.htm b/extension/chrome/elements/compose.htm index c16864bcda4..c6c043ece48 100644 --- a/extension/chrome/elements/compose.htm +++ b/extension/chrome/elements/compose.htm @@ -122,7 +122,7 @@

New Secure Message

Recipient will have access to this message for (loading). You are responsible for sharing this password with them (use other medium to share the password - not email). -

+



(Messages sent to FlowCrypt users don't expire this way and don't need password exchange)
diff --git a/extension/chrome/elements/compose.ts b/extension/chrome/elements/compose.ts index 412a59c9c5b..b0402fc8b93 100644 --- a/extension/chrome/elements/compose.ts +++ b/extension/chrome/elements/compose.ts @@ -43,6 +43,7 @@ export class ComposeView extends View { public readonly isReplyBox: boolean; public readonly replyMsgId: string; public readonly replyPubkeyMismatch: boolean; + public fesUrl?: string; public skipClickPrompt: boolean; public draftId: string; public threadId: string = ''; @@ -144,7 +145,7 @@ export class ComposeView extends View { } public render = async () => { - const storage = await AcctStore.get(this.acctEmail, ['sendAs', 'hide_message_password']); + const storage = await AcctStore.get(this.acctEmail, ['sendAs', 'hide_message_password', 'fesUrl']); this.orgRules = await OrgRules.newInstance(this.acctEmail); if (this.orgRules.shouldHideArmorMeta()) { opgp.config.show_comment = false; @@ -160,6 +161,7 @@ export class ComposeView extends View { this.recipientsModule = new ComposeRecipientsModule(this); this.sendBtnModule = new ComposeSendBtnModule(this); this.pwdOrPubkeyContainerModule = new ComposePwdOrPubkeyContainerModule(this, storage.hide_message_password); + this.fesUrl = storage.fesUrl; this.sizeModule = new ComposeSizeModule(this); this.senderModule = new ComposeSenderModule(this); this.footerModule = new ComposeFooterModule(this); diff --git a/extension/js/common/lang.ts b/extension/js/common/lang.ts index 5c5b77aa8bb..301206e1bf1 100644 --- a/extension/js/common/lang.ts +++ b/extension/js/common/lang.ts @@ -79,6 +79,8 @@ export const Lang = { // tslint:disable-line:variable-name pubkeyExpiredConfirmCompose: 'The public key of one of your recipients is expired.\n\nThe right thing to do is to ask the recipient to send you an updated Public Key.\n\nAre you sure you want to encrypt this message for an expired public key? (NOT RECOMMENDED)', needReadAccessToReply: 'FlowCrypt has limited functionality. Your browser needs to access this conversation to reply.', addMissingPermission: 'Add missing permission', + enterprisePasswordPolicy: 'Please use password with the following properties:\n - one uppercase\n - one lowercase\n - one number\n - one special character eg &\"#-\'_%-@,;:!*()\n - 8 characters length', + consumerPasswordPolicy: 'Please use a password at least 8 characters long', }, general: { somethingWentWrongTryAgain: 'Something went wrong, please try again. If this happens again, please write us at human@flowcrypt.com to fix it. ', diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index bb328a47f37..797d070b45a 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -1535,13 +1535,19 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); await SetupPageRecipe.manualEnter(settingsPage, 'flowcrypt.test.key.used.pgp', { submitPubkey: false, usedPgpBefore: false }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); - const msgPwd = 'super hard password for the message'; const subject = 'PWD encrypted message with FES - ID TOKEN'; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'user@standardsubdomainfes.test:8001'); await ComposePageRecipe.fillMsg(composePage, { to: 'to@example.com', bcc: 'bcc@example.com' }, subject); const fileInput = await composePage.target.$('input[type=file]'); await fileInput!.uploadFile('test/samples/small.txt'); - await ComposePageRecipe.sendAndClose(composePage, { password: msgPwd }); + // lousy pwd + await composePage.waitAndType('@input-password', 'lousy pwd'); + await composePage.waitAndClick('@action-send', { delay: 1 }); + await composePage.waitAndRespondToModal('error', 'confirm', 'Please use password with the following properties'); + // good pwd + await composePage.waitAndType('@input-password', 'gO0d-pwd'); + await composePage.waitAndClick('@action-send', { delay: 1 }); + await ComposePageRecipe.closed(composePage); // this test is using PwdEncryptedMessageWithFesIdTokenTestStrategy to check sent result based on subject "PWD encrypted message with flowcrypt.com/api" // also see '/api/v1/message' in fes-endpoints.ts mock }));