Skip to content

Commit

Permalink
move password verification to service worker (#1014)
Browse files Browse the repository at this point in the history
  • Loading branch information
ost-ptk committed Jun 18, 2024
1 parent 1aeb759 commit 16d3317
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 46 deletions.
109 changes: 85 additions & 24 deletions src/apps/popup/pages/password-protection-page/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import React from 'react';
import * as Yup from 'yup';
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AnyObject } from 'yup/es/types';

import {
ERROR_DISPLAYED_BEFORE_ATTEMPT_IS_DECREMENTED,
LOGIN_RETRY_ATTEMPTS_LIMIT
} from '@src/constants';
import { getErrorMessageForIncorrectPassword } from '@src/utils';

import {
selectPasswordHash,
selectPasswordSaltHash
} from '@background/redux/keys/selectors';
import { loginRetryCountReseted } from '@background/redux/login-retry-count/actions';
import {
loginRetryCountIncremented,
loginRetryCountReseted
} from '@background/redux/login-retry-count/actions';
import { selectLoginRetryCount } from '@background/redux/login-retry-count/selectors';
import { dispatchToMainStore } from '@background/redux/utils';

import {
Expand All @@ -17,24 +30,40 @@ import {
UnlockProtectedPageContent
} from '@libs/layout';
import { Button } from '@libs/ui/components';
import { calculateSubmitButtonDisabled } from '@libs/ui/forms/get-submit-button-state-from-validation';
import { useUnlockWalletForm } from '@libs/ui/forms/unlock-wallet';

interface BackupSecretPhrasePasswordPageType {
setPasswordConfirmed?: () => void;
onClick?: (password: string) => Promise<void>;
isLoading?: boolean;
}

interface VerifyPasswordMessageEvent extends MessageEvent {
data: {
isPasswordCorrect: Yup.StringSchema<
string | undefined,
AnyObject,
string | undefined
>;
};
}

export const PasswordProtectionPage = ({
setPasswordConfirmed,
onClick,
isLoading = false
}: BackupSecretPhrasePasswordPageType) => {
const [isSubmitting, setIsSubmitting] = useState(false);

const { t } = useTranslation();

const passwordHash = useSelector(selectPasswordHash);
const passwordSaltHash = useSelector(selectPasswordSaltHash);
const loginRetryCount = useSelector(selectLoginRetryCount);

const attemptsLeft =
LOGIN_RETRY_ATTEMPTS_LIMIT -
loginRetryCount -
ERROR_DISPLAYED_BEFORE_ATTEMPT_IS_DECREMENTED;

if (passwordHash == null || passwordSaltHash == null) {
throw Error("Password doesn't exist");
Expand All @@ -43,30 +72,62 @@ export const PasswordProtectionPage = ({
const {
register,
handleSubmit,
formState: { errors, isDirty },
getValues
} = useUnlockWalletForm(passwordHash, passwordSaltHash);

const isSubmitButtonDisabled = calculateSubmitButtonDisabled({
isDirty
formState: { errors },
getValues,
setError
} = useForm({
defaultValues: {
password: ''
}
});

const onSubmit = () => {
if (onClick) {
const { password } = getValues();
setIsSubmitting(true);

const { password } = getValues();

const worker = new Worker(
new URL('@background/workers/verify-password-worker.ts', import.meta.url)
);

worker.postMessage({
passwordHash,
passwordSaltHash,
password
});

onClick(password).then(() => {
if (setPasswordConfirmed) {
setPasswordConfirmed();
worker.onmessage = (event: VerifyPasswordMessageEvent) => {
const { isPasswordCorrect } = event.data;

if (!isPasswordCorrect) {
dispatchToMainStore(loginRetryCountIncremented());
const errorMessage = getErrorMessageForIncorrectPassword(attemptsLeft);

setError('password', {
message: t(errorMessage)
});
setIsSubmitting(false);
} else {
if (onClick) {
onClick(password).then(() => {
if (setPasswordConfirmed) {
setPasswordConfirmed();
}
dispatchToMainStore(loginRetryCountReseted());
});
} else {
if (setPasswordConfirmed) {
setPasswordConfirmed();
}
dispatchToMainStore(loginRetryCountReseted());
}
dispatchToMainStore(loginRetryCountReseted());
});
} else {
if (setPasswordConfirmed) {
setPasswordConfirmed();
}
dispatchToMainStore(loginRetryCountReseted());
}
};

worker.onerror = error => {
console.error(error);
setIsSubmitting(false);
};
};

return (
Expand All @@ -88,8 +149,8 @@ export const PasswordProtectionPage = ({
)}
renderFooter={() => (
<FooterButtonsContainer>
<Button disabled={isSubmitButtonDisabled || isLoading}>
{isLoading ? t('Loading') : t('Continue')}
<Button disabled={isSubmitting || isLoading}>
{isSubmitting || isLoading ? t('Loading') : t('Continue')}
</Button>
</FooterButtonsContainer>
)}
Expand Down
22 changes: 22 additions & 0 deletions src/background/workers/verify-password-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { verifyPasswordAgainstHash } from '@libs/crypto/hashing';

interface VerifyPasswordEvent extends MessageEvent {
data: {
passwordHash: string;
passwordSaltHash: string;
password: string;
};
}

onmessage = async (event: VerifyPasswordEvent) => {
const { passwordHash, passwordSaltHash, password } = event.data;
const isPasswordCorrect = await verifyPasswordAgainstHash(
passwordHash,
passwordSaltHash,
password
);

postMessage({
isPasswordCorrect
});
};
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const ERC20_TOKEN_ACTIVITY_REFRESH_RATE = 30 * SECOND;
export const VALIDATORS_REFRESH_RATE = 30 * SECOND;

export const LOGIN_RETRY_ATTEMPTS_LIMIT = 5;
export const ERROR_DISPLAYED_BEFORE_ATTEMPT_IS_DECREMENTED = 1;

export const MOTES_PER_CSPR_RATE = '1000000000'; // 1 000 000 000 MOTES === 1 CSPR
export const TRANSFER_COST_MOTES = '100000000'; // 0.1 CSPR
Expand Down
77 changes: 67 additions & 10 deletions src/libs/layout/unlock-vault/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import * as Yup from 'yup';
import { Player } from '@lottiefiles/react-lottie-player';
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { AnyObject } from 'yup/es/types';

import {
ERROR_DISPLAYED_BEFORE_ATTEMPT_IS_DECREMENTED,
LOGIN_RETRY_ATTEMPTS_LIMIT
} from '@src/constants';
import { getErrorMessageForIncorrectPassword } from '@src/utils';

import {
selectKeyDerivationSaltHash,
selectPasswordHash,
selectPasswordSaltHash
} from '@background/redux/keys/selectors';
import { loginRetryCountIncremented } from '@background/redux/login-retry-count/actions';
import { selectLoginRetryCount } from '@background/redux/login-retry-count/selectors';
import { unlockVault } from '@background/redux/sagas/actions';
import { UnlockVault } from '@background/redux/sagas/types';
import { dispatchToMainStore } from '@background/redux/utils';
Expand All @@ -28,10 +39,7 @@ import {
SpacingSize
} from '@libs/layout';
import { Button, Typography } from '@libs/ui/components';
import {
UnlockWalletFormValues,
useUnlockWalletForm
} from '@libs/ui/forms/unlock-wallet';
import { UnlockWalletFormValues } from '@libs/ui/forms/unlock-wallet';

import { UnlockVaultPageContent } from './content';

Expand All @@ -43,6 +51,16 @@ interface UnlockVaultPageProps {
popupLayout?: boolean;
}

interface VerifyPasswordMessageEvent extends MessageEvent {
data: {
isPasswordCorrect: Yup.StringSchema<
string | undefined,
AnyObject,
string | undefined
>;
};
}

export const UnlockVaultPage = ({ popupLayout }: UnlockVaultPageProps) => {
const [isLoading, setIsLoading] = useState(false);

Expand All @@ -53,6 +71,12 @@ export const UnlockVaultPage = ({ popupLayout }: UnlockVaultPageProps) => {
const passwordSaltHash = useSelector(selectPasswordSaltHash);
const keyDerivationSaltHash = useSelector(selectKeyDerivationSaltHash);
const vaultCipher = useSelector(selectVaultCipher);
const loginRetryCount = useSelector(selectLoginRetryCount);

const attemptsLeft =
LOGIN_RETRY_ATTEMPTS_LIMIT -
loginRetryCount -
ERROR_DISPLAYED_BEFORE_ATTEMPT_IS_DECREMENTED;

if (passwordHash == null || passwordSaltHash == null) {
throw Error("Password doesn't exist");
Expand All @@ -61,14 +85,23 @@ export const UnlockVaultPage = ({ popupLayout }: UnlockVaultPageProps) => {
const {
register,
handleSubmit,
formState: { errors },
resetField,
formState: { errors }
} = useUnlockWalletForm(passwordHash, passwordSaltHash);
setError
} = useForm({
defaultValues: {
password: ''
}
});

async function handleUnlockVault({ password }: UnlockWalletFormValues) {
if (isLoading) return;

setIsLoading(true);

const verifyPasswordWorker = new Worker(
new URL('@background/workers/verify-password-worker.ts', import.meta.url)
);
const unlockVaultWorker = new Worker(
new URL('@background/workers/unlock-vault-worker.ts', import.meta.url)
);
Expand All @@ -77,12 +110,31 @@ export const UnlockVaultPage = ({ popupLayout }: UnlockVaultPageProps) => {
throw Error("Key derivation salt doesn't exist");
}

unlockVaultWorker.postMessage({
password,
keyDerivationSaltHash,
vaultCipher
verifyPasswordWorker.postMessage({
passwordHash,
passwordSaltHash,
password
});

verifyPasswordWorker.onmessage = (event: VerifyPasswordMessageEvent) => {
const { isPasswordCorrect } = event.data;
const errorMessage = getErrorMessageForIncorrectPassword(attemptsLeft);

if (!isPasswordCorrect) {
dispatchToMainStore(loginRetryCountIncremented());
setError('password', {
message: t(errorMessage)
});
setIsLoading(false);
} else {
unlockVaultWorker.postMessage({
password,
keyDerivationSaltHash,
vaultCipher
});
}
};

unlockVaultWorker.onmessage = (event: UnlockMessageEvent) => {
const {
vault,
Expand Down Expand Up @@ -139,6 +191,11 @@ export const UnlockVaultPage = ({ popupLayout }: UnlockVaultPageProps) => {
}
};

verifyPasswordWorker.onerror = error => {
console.error(error);
setIsLoading(false);
};

unlockVaultWorker.onerror = error => {
console.error(error);
setIsLoading(false);
Expand Down
20 changes: 8 additions & 12 deletions src/libs/ui/forms/form-validation-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ import { useSelector } from 'react-redux';
import {
AuctionManagerEntryPoint,
DELEGATION_MIN_AMOUNT_MOTES,
ERROR_DISPLAYED_BEFORE_ATTEMPT_IS_DECREMENTED,
LOGIN_RETRY_ATTEMPTS_LIMIT,
MAX_DELEGATORS,
STAKE_COST_MOTES,
TRANSFER_COST_MOTES,
TRANSFER_MIN_AMOUNT_MOTES
} from '@src/constants';
import { isValidPublicKey, isValidSecretKeyHash, isValidU64 } from '@src/utils';
import {
getErrorMessageForIncorrectPassword,
isValidPublicKey,
isValidSecretKeyHash,
isValidU64
} from '@src/utils';

import { loginRetryCountIncremented } from '@background/redux/login-retry-count/actions';
import { selectLoginRetryCount } from '@background/redux/login-retry-count/selectors';
Expand All @@ -23,8 +29,6 @@ import { CSPRtoMotes, motesToCSPR } from '@libs/ui/utils/formatters';

export const minPasswordLength = 16;

const ERROR_DISPLAYED_BEFORE_ATTEMPT_IS_DECREMENTED = 1;

export function useCreatePasswordRule() {
const { t } = useTranslation();
const passwordAmountCharactersMessage = t(
Expand All @@ -38,22 +42,14 @@ export function useVerifyPasswordAgainstHashRule(
passwordHash: string,
passwordSaltHash: string
) {
const { t } = useTranslation();
const loginRetryCount = useSelector(selectLoginRetryCount);

const attemptsLeft =
LOGIN_RETRY_ATTEMPTS_LIMIT -
loginRetryCount -
ERROR_DISPLAYED_BEFORE_ATTEMPT_IS_DECREMENTED;

const errorMessage =
attemptsLeft === 1
? t(
'Password is incorrect. You’ve got last attempt, after that you’ll have to wait for 5 mins'
)
: t(
`Password is incorrect. You’ve got ${attemptsLeft} attempts, after that you’ll have to wait for 5 mins`
);
const errorMessage = getErrorMessageForIncorrectPassword(attemptsLeft);

return Yup.string().test('authenticate', errorMessage, async password => {
const result = await verifyPasswordAgainstHash(
Expand Down
5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,3 +399,8 @@ export const fetchAndDispatchExtendedDeployInfo = (deployHash: string) => {
// Note: this timeout is needed because the deploy is not immediately visible in the explorer
}, 2000);
};

export const getErrorMessageForIncorrectPassword = (attemptsLeft: number) =>
attemptsLeft === 1
? 'Password is incorrect. You’ve got last attempt, after that you’ll have to wait for 5 mins'
: `Password is incorrect. You’ve got ${attemptsLeft} attempts, after that you’ll have to wait for 5 mins`;

0 comments on commit 16d3317

Please sign in to comment.