From 0655b61d3e13376f0c3148395fdf1434cfb4cbff Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 2 Nov 2026 04:36:30 +0000 Subject: [PATCH] fix: allow six-character health tracker passphrases --- apps/web/public/health/health.js | 10 +++++----- packages/cli/src/vault-crypto.js | 20 ++++++++++---------- test/api.test.js | 4 +++- test/vault-crypto.test.js | 5 +++++ 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/apps/web/public/health/health.js b/apps/web/public/health/health.js index 4d50c0e..d50e0fa 100644 --- a/apps/web/public/health/health.js +++ b/apps/web/public/health/health.js @@ -194,10 +194,10 @@ async function showGate(message = '') { if (!config) { vaultScreen.innerHTML = `

First visit

Create your encrypted tracker

-

Choose a passphrase with at least 16 characters. The tracker encrypts every reading in your browser before saving it.

+

Choose a passphrase with at least 6 characters. The tracker encrypts every reading in your browser before saving it.

- - + +

${message}

`; @@ -255,7 +255,7 @@ async function createLocalVault(event) { if (passphrase !== confirm) { message.textContent = 'The passphrases do not match.'; return; } try { const userId = makeId(); - const created = await vaultCrypto.createVault(userId, passphrase); + const created = await vaultCrypto.createVault(userId, passphrase, { minLength: 6 }); await putVault({ userId, envelope: created.envelope }); vaultHandle = created.handle; vaultUserId = userId; @@ -272,7 +272,7 @@ async function unlockWithPassphrase(event) { const message = document.getElementById('gate-message'); try { const config = await getVault(); - const handle = await vaultCrypto.unlockWithPassphrase(config.userId, passphrase, config.envelope); + const handle = await vaultCrypto.unlockWithPassphrase(config.userId, passphrase, config.envelope, { minLength: 6 }); unlockForm.reset(); activateVault(handle, config.userId); } catch (err) { diff --git a/packages/cli/src/vault-crypto.js b/packages/cli/src/vault-crypto.js index d11f9a0..0a8a5ef 100644 --- a/packages/cli/src/vault-crypto.js +++ b/packages/cli/src/vault-crypto.js @@ -46,9 +46,9 @@ function assertUserId(userId) { } } -function assertPassphrase(passphrase) { - if (typeof passphrase !== 'string' || passphrase.normalize('NFC').length < 16) { - throw new Error('Use a passphrase with at least 16 characters.'); +function assertPassphrase(passphrase, minLength = 16) { + if (typeof passphrase !== 'string' || passphrase.normalize('NFC').length < minLength) { + throw new Error(`Use a passphrase with at least ${minLength} characters.`); } if (passphrase.length > 1024) throw new Error('Passphrase is too long.'); } @@ -64,8 +64,8 @@ function aadForObject(userId, objectId) { return encoder.encode(`tleehealth:record-vault:object:v${FORMAT_VERSION}:${userId}:${objectId}`); } -async function derivePassphraseKey(passphrase, salt) { - assertPassphrase(passphrase); +async function derivePassphraseKey(passphrase, salt, minLength = 16) { + assertPassphrase(passphrase, minLength); const crypto = webcrypto(); const passphraseBytes = encoder.encode(passphrase.normalize('NFC')); try { @@ -133,16 +133,16 @@ export function lockVault(handle) { handles.delete(handle); } -export async function createVault(userId, passphrase) { +export async function createVault(userId, passphrase, { minLength = 16 } = {}) { assertUserId(userId); - assertPassphrase(passphrase); + assertPassphrase(passphrase, minLength); const rawKey = randomBytes(32); const salt = randomBytes(16); const recoveryBytes = randomBytes(32); try { const keyCheck = await makeKeyCheck(rawKey, userId); const [passphraseWrap, recoveryWrap] = await Promise.all([ - wrap(rawKey, await derivePassphraseKey(passphrase, salt), aadForKey(userId, 'passphrase')), + wrap(rawKey, await derivePassphraseKey(passphrase, salt, minLength), aadForKey(userId, 'passphrase')), wrap(rawKey, await importAesKey(recoveryBytes), aadForKey(userId, 'recovery')), ]); return { @@ -182,13 +182,13 @@ export function validateVaultEnvelope(envelope) { return envelope; } -export async function unlockWithPassphrase(userId, passphrase, untrustedEnvelope) { +export async function unlockWithPassphrase(userId, passphrase, untrustedEnvelope, { minLength = 16 } = {}) { const envelope = validateVaultEnvelope(untrustedEnvelope); let rawKey; try { rawKey = await unwrap( envelope.passphrase, - await derivePassphraseKey(passphrase, fromBase64Url(envelope.passphrase.salt)), + await derivePassphraseKey(passphrase, fromBase64Url(envelope.passphrase.salt), minLength), aadForKey(userId, 'passphrase'), ); return makeHandle(rawKey); diff --git a/test/api.test.js b/test/api.test.js index cc6d604..359ac7a 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -64,7 +64,9 @@ describe.skipIf(!configured())('api', () => { const page = await app.request('/health/', { headers: { cookie } }); expect(page.status).toBe(200); expect(await page.text()).toContain('Sign in to record measurements'); - expect((await app.request('/health/health.js', { headers: { cookie } })).status).toBe(200); + const trackerScript = await app.request('/health/health.js', { headers: { cookie } }); + expect(trackerScript.status).toBe(200); + expect(await trackerScript.text()).toContain('minlength="6"'); }); test('CoinPay links to the signed-in account by provider subject and signs into that account', async () => { diff --git a/test/vault-crypto.test.js b/test/vault-crypto.test.js index 0acac14..465eba5 100644 --- a/test/vault-crypto.test.js +++ b/test/vault-crypto.test.js @@ -57,6 +57,11 @@ describe('personal record vault cryptography', () => { test('rejects weak passphrases and malformed key envelopes', async () => { await expect(createVault(userId, 'too short')).rejects.toThrow('at least 16 characters'); + await expect(createVault(userId, '12345', { minLength: 6 })).rejects.toThrow('at least 6 characters'); + const shortPassphrase = await createVault(userId, '123456', { minLength: 6 }); + const unlocked = await unlockWithPassphrase(userId, '123456', shortPassphrase.envelope, { minLength: 6 }); + lockVault(shortPassphrase.handle); + lockVault(unlocked); expect(() => validateVaultEnvelope({ version: 1, cipher: 'AES-ECB' })).toThrow('invalid vault key envelope'); }); });