Skip to content
Merged
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
6 changes: 3 additions & 3 deletions src/components/ChangePasswordCard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,9 @@ describe('ChangePasswordCard', () => {
fireEvent.submit(screen.getByTestId('change-password-card'));
});

expect(screen.getByTestId('change-password-local-error')).toHaveTextContent(
/at least 16/i
);
const error = screen.getByTestId('change-password-local-error');
expect(error).toHaveTextContent(/at least 8/i);
expect(error).toHaveTextContent(/3 of/i);
expect(cardAuthService.deriveChangePasswordKeys).not.toHaveBeenCalled();
expect(cardAuthService.changePassword).not.toHaveBeenCalled();
});
Expand Down
10 changes: 5 additions & 5 deletions src/components/VaultImportModal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,9 @@ describe('VaultImportModal — save flow (EMPTY, first write)', () => {
);

await userEvent.click(screen.getByTestId('vault-import-save'));
expect(await screen.findByTestId('vault-import-error')).toHaveTextContent(
/at least 16/i
);
const error = await screen.findByTestId('vault-import-error');
expect(error).toHaveTextContent(/at least 8/i);
expect(error).toHaveTextContent(/3 of/i);
expect(vault.save).not.toHaveBeenCalled();
});

Expand All @@ -359,15 +359,15 @@ describe('VaultImportModal — save flow (EMPTY, first write)', () => {
pasteInto(screen.getByTestId('vault-import-paste'), `${VALID_WIF_1},MN 1`);
await userEvent.type(
screen.getByTestId('vault-import-password'),
'my-secret-passphrase'
'My-secret-passphrase1'
);
await waitFor(() =>
expect(screen.getByTestId('vault-import-save')).not.toBeDisabled()
);
await userEvent.click(screen.getByTestId('vault-import-save'));
await waitFor(() => expect(vault.save).toHaveBeenCalledTimes(1));
expect(vault.save.mock.calls[0][1]).toEqual({
password: 'my-secret-passphrase',
password: 'My-secret-passphrase1',
email: 'user@example.com',
});
await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1));
Expand Down
8 changes: 4 additions & 4 deletions src/context/VaultContext.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,7 @@ describe('VaultProvider — load() single-flight + cache (Codex round 1)', () =>
// ---------------------------------------------------------------------------
describe('VaultProvider — save() first write (EMPTY → UNLOCKED)', () => {
test('encrypts under a fresh DK, PUTs with *, installs keys, transitions UNLOCKED', async () => {
const password = 'correct horse battery';
const password = 'Correct horse battery 1';
const email = 'new@example.com';
const saltV = SALT_A;
const data = { keys: [{ label: 'mn1', wif: 'KxFoo...' }] };
Expand Down Expand Up @@ -1092,7 +1092,7 @@ describe('VaultProvider — save() first write (EMPTY → UNLOCKED)', () => {
act(async () => {
await last.save(
{ keys: [] },
{ password: 'correct horse battery', email: 'x@y.com' }
{ password: 'Correct horse battery 1', email: 'x@y.com' }
);
})
).rejects.toMatchObject({ code: 'missing_salt_v' });
Expand Down Expand Up @@ -1122,7 +1122,7 @@ describe('VaultProvider — save() first write (EMPTY → UNLOCKED)', () => {
try {
await last.save(
{ k: 1 },
{ password: 'correct horse battery', email: 'user@example.com' }
{ password: 'Correct horse battery 1', email: 'user@example.com' }
);
} catch (e) {
caught = e;
Expand Down Expand Up @@ -1326,7 +1326,7 @@ describe('VaultProvider — save() update (UNLOCKED → UNLOCKED)', () => {
try {
await last.save(
{ version: 1, keys: [] },
{ password: 'correct horse battery' }
{ password: 'Correct horse battery 1' }
);
} catch (err) {
caught = err;
Expand Down
17 changes: 14 additions & 3 deletions src/lib/passwordPolicy.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
export const MIN_VAULT_PASSWORD_LENGTH = 16;
export const MIN_VAULT_PASSWORD_LENGTH = 8;
export const MIN_VAULT_PASSWORD_CLASSES = 3;

export const VAULT_PASSWORD_HINT =
'Use at least 16 characters. A long passphrase is best; this protects your encrypted voting-key vault if server blobs ever leak.';
'Use at least 8 characters with at least 3 of: lowercase, uppercase, number, symbol.';

function countCharacterClasses(password) {
let count = 0;
if (/[a-z]/.test(password)) count += 1;
if (/[A-Z]/.test(password)) count += 1;
if (/[0-9]/.test(password)) count += 1;
if (/[^A-Za-z0-9]/.test(password)) count += 1;
return count;
}

export function validateVaultPassword(password) {
if (
typeof password !== 'string' ||
password.length < MIN_VAULT_PASSWORD_LENGTH
password.length < MIN_VAULT_PASSWORD_LENGTH ||
countCharacterClasses(password) < MIN_VAULT_PASSWORD_CLASSES
) {
return {
code: 'password_too_short',
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Register.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const ERROR_COPY = {
server_misconfigured:
'The sysnode server is temporarily unavailable. Please try again in a moment.',
invalid_body:
'Please enter a valid email and a password of at least 16 characters.',
'Please enter a valid email and a password that meets the requirements.',
// Thrown client-side from kdf.js:subtleCrypto when window.crypto.subtle
// is missing — which in practice means the SPA is being served over
// plain HTTP from a non-localhost origin. Give the user the actionable
Expand Down
26 changes: 14 additions & 12 deletions src/pages/Register.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,17 @@ test('validates password length and mismatch client-side', async () => {
await userEvent.type(screen.getByLabelText(/^password/i), 'short');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'short');
await userEvent.click(screen.getByRole('button', { name: /create account/i }));
expect(await screen.findByRole('alert')).toHaveTextContent(/at least 16/i);
const weakAlert = await screen.findByRole('alert');
expect(weakAlert).toHaveTextContent(/at least 8/i);
expect(weakAlert).toHaveTextContent(/3 of/i);
expect(service.register).not.toHaveBeenCalled();

const pw = screen.getByLabelText(/^password/i);
const cf = screen.getByLabelText(/confirm password/i);
await userEvent.clear(pw);
await userEvent.clear(cf);
await userEvent.type(pw, 'correct horse battery staple');
await userEvent.type(cf, 'correct horse battery mismatch');
await userEvent.type(pw, 'Correct horse battery 1');
await userEvent.type(cf, 'Correct horse battery 2');
await userEvent.click(screen.getByRole('button', { name: /create account/i }));
expect(await screen.findByRole('alert')).toHaveTextContent(/don'?t match/i);
expect(service.register).not.toHaveBeenCalled();
Expand All @@ -69,8 +71,8 @@ test('flags the offending fields aria-invalid on client-side validation errors',
// Password mismatch should flag BOTH password and confirm — we don't
// know which one the user mistyped.
await userEvent.type(email, 'a@b.com');
await userEvent.type(pw, 'correct horse battery staple');
await userEvent.type(cf, 'correct horse battery mismatch');
await userEvent.type(pw, 'Correct horse battery 1');
await userEvent.type(cf, 'Correct horse battery 2');
await userEvent.click(screen.getByRole('button', { name: /create account/i }));
await screen.findByRole('alert');

Expand Down Expand Up @@ -98,11 +100,11 @@ test('renders the WebCrypto-unavailable copy with an actionable fix', async () =
await userEvent.type(screen.getByLabelText(/^email/i), 'a@b.com');
await userEvent.type(
screen.getByLabelText(/^password/i),
'correct horse battery staple'
'Correct horse battery 1'
);
await userEvent.type(
screen.getByLabelText(/confirm password/i),
'correct horse battery staple'
'Correct horse battery 1'
);
await userEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -128,11 +130,11 @@ test('surfaces an unknown error\'s own message rather than the generic fallback'
await userEvent.type(screen.getByLabelText(/^email/i), 'a@b.com');
await userEvent.type(
screen.getByLabelText(/^password/i),
'correct horse battery staple'
'Correct horse battery 1'
);
await userEvent.type(
screen.getByLabelText(/confirm password/i),
'correct horse battery staple'
'Correct horse battery 1'
);
await userEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -148,11 +150,11 @@ test('shows the "check your inbox" screen on success', async () => {
await userEvent.type(screen.getByLabelText(/^email/i), 'a@b.com');
await userEvent.type(
screen.getByLabelText(/^password/i),
'correct horse battery staple'
'Correct horse battery 1'
);
await userEvent.type(
screen.getByLabelText(/confirm password/i),
'correct horse battery staple'
'Correct horse battery 1'
);
await userEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -161,6 +163,6 @@ test('shows the "check your inbox" screen on success', async () => {
);
expect(service.register).toHaveBeenCalledWith(
'a@b.com',
'correct horse battery staple'
'Correct horse battery 1'
);
});
Loading