/* * Wire * Copyright (C) 2022 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * */ import React, {useCallback, useEffect, useRef, useState} from 'react'; import {amplify} from 'amplify'; import cx from 'classnames'; import {StatusCodes as HTTP_STATUS} from 'http-status-codes'; import {container} from 'tsyringe'; import {ValidationUtil} from '@wireapp/commons'; import {WebAppEvents} from '@wireapp/webapp-events'; import * as Icon from 'Components/Icon'; import {ModalComponent} from 'Components/Modals/ModalComponent'; import {SIGN_OUT_REASON} from 'src/script/auth/SignOutReason'; import {ClientRepository} from 'src/script/client'; import {ClientState} from 'src/script/client/ClientState'; import {Config} from 'src/script/Config'; import {AppLockRepository} from 'src/script/user/AppLockRepository'; import {AppLockState} from 'src/script/user/AppLockState'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; export enum APPLOCK_STATE { FORGOT = 'applock.forgot', LOCKED = 'applock.locked', NONE = 'applock.none', SETUP = 'applock.setup', SETUP_CHANGE = 'applock.setup_change', WIPE_CONFIRM = 'applock.wipe-confirm', WIPE_PASSWORD = 'applock.wipe-password', } const DEFAULT_INACTIVITY_APP_LOCK_TIMEOUT_IN_SEC = 60; const passwordRegex = new RegExp(ValidationUtil.getNewPasswordPattern(Config.getConfig().NEW_PASSWORD_MINIMUM_LENGTH)); const passwordRegexDigit = /(?=.*[0-9])/; const passwordRegexLength = new RegExp(`^.{${Config.getConfig().NEW_PASSWORD_MINIMUM_LENGTH},}$`); const passwordRegexLower = /(?=.*[a-z])/; const passwordRegexSpecial = /(?=.*[!@#$%^&*(),.?":{}|<>])/; const passwordRegexUpper = /(?=.*[A-Z])/; export interface AppLockProps { appLockRepository?: AppLockRepository; appLockState?: AppLockState; clientRepository: ClientRepository; clientState?: ClientState; } const AppLock: React.FC = ({ clientRepository, clientState = container.resolve(ClientState), appLockState = container.resolve(AppLockState), appLockRepository = container.resolve(AppLockRepository), }) => { const [state, setState] = useState(APPLOCK_STATE.NONE); const [wipeError, setWipeError] = useState(''); const [unlockError, setUnlockError] = useState(''); const [isVisible, setIsVisible] = useState(false); const [isLoading, setIsLoading] = useState(false); const [setupPassphrase, setSetupPassphrase] = useState(''); const [inactivityTimeoutId, setInactivityTimeoutId] = useState(); const [scheduledTimeoutId, setScheduledTimeoutId] = useState(); const {isAppLockActivated, isAppLockEnabled, isAppLockEnforced} = useKoSubscribableChildren(appLockState, [ 'isAppLockActivated', 'isAppLockEnabled', 'isAppLockEnforced', ]); const {current: appObserver} = useRef( new MutationObserver(mutationRecords => { const [{attributeName}] = mutationRecords; if (attributeName === 'style') { amplify.publish(WebAppEvents.LIFECYCLE.SIGN_OUT, SIGN_OUT_REASON.USER_REQUESTED); } }), ); const {current: modalObserver} = useRef( new MutationObserver(() => { const modalInDOM = document.querySelector('[data-uie-name="applock-modal"]'); if (!modalInDOM) { amplify.publish(WebAppEvents.LIFECYCLE.SIGN_OUT, SIGN_OUT_REASON.USER_REQUESTED); } }), ); const getInactivityAppLockTimeoutInSeconds = () => { const backendTimeout = appLockState.appLockInactivityTimeoutSecs(); return Number.isFinite(backendTimeout) ? backendTimeout : DEFAULT_INACTIVITY_APP_LOCK_TIMEOUT_IN_SEC; }; const getScheduledAppLockTimeoutInSeconds = () => { const configTimeout = Config.getConfig().FEATURE?.APPLOCK_SCHEDULED_TIMEOUT; return Number.isFinite(configTimeout) ? configTimeout : null; }; const isScheduledAppLockEnabled = () => { return getScheduledAppLockTimeoutInSeconds() !== null; }; const startAppLockTimeout = useCallback(() => { window.clearTimeout(inactivityTimeoutId); const id = window.setTimeout(showAppLock, getInactivityAppLockTimeoutInSeconds() * 1000); setInactivityTimeoutId(id); }, [inactivityTimeoutId]); const clearAppLockTimeout = useCallback(() => { window.clearTimeout(inactivityTimeoutId); }, [inactivityTimeoutId]); useEffect(() => { amplify.subscribe(WebAppEvents.PREFERENCES.CHANGE_APP_LOCK_PASSPHRASE, changePassphrase); }, []); useEffect(() => { if (isAppLockEnabled) { showAppLock(); } else if (appLockState.hasPassphrase()) { appLockRepository.removeCode(); } }, [isAppLockEnabled]); useEffect(() => { if (isAppLockActivated) { window.addEventListener('blur', startAppLockTimeout); window.addEventListener('focus', clearAppLockTimeout); return () => { window.removeEventListener('blur', startAppLockTimeout); window.removeEventListener('focus', clearAppLockTimeout); }; } return clearAppLockTimeout(); }, [isAppLockActivated, clearAppLockTimeout, startAppLockTimeout]); useEffect(() => { const app = window.document.querySelector('#app'); app?.style.setProperty('filter', isVisible ? 'blur(100px)' : '', 'important'); app?.style.setProperty('pointer-events', isVisible ? 'none' : 'auto', 'important'); if (isVisible) { modalObserver.observe(document.querySelector('#wire-main'), { childList: true, subtree: true, }); appObserver.observe(document.querySelector('#app'), {attributes: true}); } return () => { modalObserver.disconnect(); appObserver.disconnect(); }; }, [state, isVisible]); const showAppLock = () => { setState(appLockState.hasPassphrase() ? APPLOCK_STATE.LOCKED : APPLOCK_STATE.SETUP); setIsVisible(true); }; const onUnlock = async (event: React.FormEvent) => { event.preventDefault(); const target = event.target as HTMLFormElement & {password: HTMLInputElement}; const isCorrectCode = await appLockRepository.checkCode(target.password.value); if (isCorrectCode) { setIsVisible(false); startScheduledTimeout(); return; } setUnlockError(t('modalAppLockLockedError')); }; const startScheduledTimeout = () => { if (isScheduledAppLockEnabled()) { window.clearTimeout(scheduledTimeoutId); setScheduledTimeoutId(window.setTimeout(showAppLock, getScheduledAppLockTimeoutInSeconds() * 1000)); } }; const onSetCode = async (event: React.FormEvent) => { event.preventDefault(); await appLockRepository.setCode(setupPassphrase); setIsVisible(false); startScheduledTimeout(); }; const onWipeDatabase = async (event: React.FormEvent) => { const target = event.target as HTMLFormElement & {password: HTMLInputElement}; try { setIsLoading(true); const currentClientId = clientState.currentClient.id; await clientRepository.clientService.deleteClient(currentClientId, target.password.value); appLockRepository.removeCode(); amplify.publish(WebAppEvents.LIFECYCLE.SIGN_OUT, SIGN_OUT_REASON.USER_REQUESTED, true); } catch ({code, message}) { setIsLoading(false); if ([HTTP_STATUS.BAD_REQUEST, HTTP_STATUS.UNAUTHORIZED, HTTP_STATUS.FORBIDDEN].includes(code)) { return setWipeError(t('modalAppLockWipePasswordError')); } setWipeError(message); } }; const changePassphrase = () => { setState(APPLOCK_STATE.SETUP); setIsVisible(true); }; const isSetupPassphraseValid = passwordRegex.test(setupPassphrase); const isSetupPassphraseLower = passwordRegexLower.test(setupPassphrase); const isSetupPassphraseUpper = passwordRegexUpper.test(setupPassphrase); const isSetupPassphraseDigit = passwordRegexDigit.test(setupPassphrase); const isSetupPassphraseLength = passwordRegexLength.test(setupPassphrase); const isSetupPassphraseSpecial = passwordRegexSpecial.test(setupPassphrase); const clearWipeError = () => setWipeError(''); const clearUnlockError = () => setUnlockError(''); const onGoBack = () => setState(APPLOCK_STATE.LOCKED); const onClickForgot = () => setState(APPLOCK_STATE.FORGOT); const onClickWipe = () => setState(APPLOCK_STATE.WIPE_CONFIRM); const onClickWipeConfirm = () => setState(APPLOCK_STATE.WIPE_PASSWORD); const onClosed = () => { setState(APPLOCK_STATE.NONE); setSetupPassphrase(''); }; const onCancelAppLock = () => { appLockRepository.setEnabled(false); setIsVisible(false); }; const headerText = () => { switch (state) { case APPLOCK_STATE.SETUP_CHANGE: return t('modalAppLockSetupChangeTitle', Config.getConfig().BRAND_NAME); case APPLOCK_STATE.SETUP: return t('modalAppLockSetupTitle'); case APPLOCK_STATE.LOCKED: return t('modalAppLockLockedTitle', Config.getConfig().BRAND_NAME); case APPLOCK_STATE.FORGOT: return t('modalAppLockForgotTitle'); case APPLOCK_STATE.WIPE_CONFIRM: return t('modalAppLockWipeConfirmTitle'); case APPLOCK_STATE.WIPE_PASSWORD: return t('modalAppLockWipePasswordTitle', Config.getConfig().BRAND_NAME); default: return ''; } }; return (
{!isAppLockEnforced && !isAppLockActivated && ( )}

{headerText()}

{state === APPLOCK_STATE.SETUP && (


'})}} data-uie-name="label-applock-set-text" /> {/* eslint jsx-a11y/no-autofocus : "off" */} setSetupPassphrase(event.target.value)} data-uie-status={isSetupPassphraseValid ? 'valid' : 'invalid'} data-uie-name="input-applock-set-a" autoComplete="new-password" id="input-applock-set-a" />

{t('modalAppLockSetupLong', { minPasswordLength: Config.getConfig().NEW_PASSWORD_MINIMUM_LENGTH.toString(), })}

{t('modalAppLockSetupLower')}

{t('modalAppLockSetupUppercase')}

{t('modalAppLockSetupDigit')}

{t('modalAppLockSetupSpecial')}

{!isAppLockEnforced && ( )}
)} {state === APPLOCK_STATE.SETUP_CHANGE && (

'}, ), }} data-uie-name="label-applock-set-text" />
{t('modalAppLockPasscode')}
setSetupPassphrase(event.target.value)} data-uie-status={isSetupPassphraseValid ? 'valid' : 'invalid'} data-uie-name="input-applock-set-a" autoComplete="new-password" />

{t('modalAppLockSetupLong', { minPasswordLength: Config.getConfig().NEW_PASSWORD_MINIMUM_LENGTH.toString(), })}

{t('modalAppLockSetupLower')}

{t('modalAppLockSetupUppercase')}

{t('modalAppLockSetupDigit')}

{t('modalAppLockSetupSpecial')}

)} {state === APPLOCK_STATE.LOCKED && (
{t('modalAppLockPasscode')}

{unlockError}

)} {state === APPLOCK_STATE.FORGOT && (
{t('modalAppLockForgotMessage')}
)} {state === APPLOCK_STATE.WIPE_CONFIRM && (
{t('modalAppLockWipeConfirmMessage')}
)} {state === APPLOCK_STATE.WIPE_PASSWORD && (

{wipeError}

)}
); }; export {AppLock};