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
10 changes: 5 additions & 5 deletions apps/web/public/health/health.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,10 @@ async function showGate(message = '') {
if (!config) {
vaultScreen.innerHTML = `
<p class="eyebrow">First visit</p><h2>Create your encrypted tracker</h2>
<p>Choose a passphrase with at least 16 characters. The tracker encrypts every reading in your browser before saving it.</p>
<p>Choose a passphrase with at least 6 characters. The tracker encrypts every reading in your browser before saving it.</p>
<form id="setup-form" class="gate-form">
<label>Passphrase<input name="passphrase" type="password" autocomplete="new-password" minlength="16" required></label>
<label>Confirm passphrase<input name="confirm" type="password" autocomplete="new-password" minlength="16" required></label>
<label>Passphrase<input name="passphrase" type="password" autocomplete="new-password" minlength="6" required></label>
<label>Confirm passphrase<input name="confirm" type="password" autocomplete="new-password" minlength="6" required></label>
<button class="primary" type="submit">Create private tracker</button><p class="hint" id="gate-message" role="status">${message}</p>
</form>
<label class="import">Import encrypted backup<input id="gate-import" type="file" accept="application/json,.json"></label>`;
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
20 changes: 10 additions & 10 deletions packages/cli/src/vault-crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion test/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
5 changes: 5 additions & 0 deletions test/vault-crypto.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Loading