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
8 changes: 4 additions & 4 deletions apps/web/public/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1813,8 +1813,8 @@ function recordVaultCard(status, files, manifestError, userId) {
if (!status.configured) {
content = `<p class="dim small">Create a separate vault passphrase and recovery key. Your account sign-in does not unlock this vault. Files are encrypted in this browser before upload.</p>
<form id="record-vault-setup" class="stack" style="max-width:440px">
<label class="f">Vault passphrase<input name="passphrase" type="password" minlength="16" maxlength="1024" autocomplete="new-password" required></label>
<label class="f">Repeat passphrase<input name="confirm" type="password" minlength="16" maxlength="1024" autocomplete="new-password" required></label>
<label class="f">Vault passphrase<input name="passphrase" type="password" maxlength="1024" autocomplete="new-password" required></label>
<label class="f">Repeat passphrase<input name="confirm" type="password" maxlength="1024" autocomplete="new-password" required></label>
<p class="err" data-vault-error></p><button class="btn primary" type="submit">Create encrypted vault</button>
</form>`;
} else if (!unlocked) {
Expand Down Expand Up @@ -1892,8 +1892,8 @@ function showRecordVaultRecoveryKey({ title, recoveryKey, handle, userId, envelo
function openRecordVaultRecovery(status, userId) {
modal.innerHTML = `<div class="m-h"><h2>Recover encrypted vault</h2><button type="button" class="sm" data-close aria-label="Close">Close</button></div><div class="m-b">
<form id="record-vault-recovery-form" class="stack"><label class="f">Recovery key<textarea name="recoveryKey" rows="3" autocomplete="off" required></textarea></label>
<label class="f">New vault passphrase<input name="passphrase" type="password" minlength="16" maxlength="1024" autocomplete="new-password" required></label>
<label class="f">Repeat new passphrase<input name="confirm" type="password" minlength="16" maxlength="1024" autocomplete="new-password" required></label>
<label class="f">New vault passphrase<input name="passphrase" type="password" maxlength="1024" autocomplete="new-password" required></label>
<label class="f">Repeat new passphrase<input name="confirm" type="password" maxlength="1024" autocomplete="new-password" required></label>
<p class="err" data-vault-error></p><button class="btn primary" type="submit">Recover and rotate keys</button></form>
</div>`;
modal.showModal();
Expand Down
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 6 characters. The tracker encrypts every reading in your browser before saving it.</p>
<p>Choose a passphrase. 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="6" required></label>
<label>Confirm passphrase<input name="confirm" type="password" autocomplete="new-password" minlength="6" required></label>
<label>Passphrase<input name="passphrase" type="password" autocomplete="new-password" required></label>
<label>Confirm passphrase<input name="confirm" type="password" autocomplete="new-password" 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, { minLength: 6 });
const created = await vaultCrypto.createVault(userId, passphrase, { minLength: 1 });
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, { minLength: 6 });
const handle = await vaultCrypto.unlockWithPassphrase(config.userId, passphrase, config.envelope, { minLength: 1 });
unlockForm.reset();
activateVault(handle, config.userId);
} catch (err) {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/tleehealth",
"version": "0.5.3",
"version": "0.5.4",
"description": "tleehealth from the terminal: the CLI, the front-desk TUI, and the API client the MCP server uses.",
"type": "module",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,14 +302,14 @@ async function establishVault(auth, userId, { recover = false } = {}) {
if (recover) {
if (!status.configured) throw new Error('No encrypted vault exists to recover.');
const enteredRecoveryKey = await promptSecret('Recovery key');
const passphrase = await promptSecret('New vault passphrase (at least 16 characters)');
const passphrase = await promptSecret('New vault passphrase');
const confirm = await promptSecret('Repeat new passphrase');
if (passphrase !== confirm) throw new Error('Passphrases do not match.');
handle = await unlockWithRecoveryKey(userId, enteredRecoveryKey, status.envelope);
({ envelope, recoveryKey } = await rewrapVaultKey(userId, handle, passphrase));
} else {
if (status.configured) throw new Error('A vault already exists. Use `tleehealth vault recover` to rotate its passphrase.');
const passphrase = await promptSecret('New vault passphrase (at least 16 characters)');
const passphrase = await promptSecret('New vault passphrase');
const confirm = await promptSecret('Repeat passphrase');
if (passphrase !== confirm) throw new Error('Passphrases do not match.');
const created = await createVault(userId, passphrase);
Expand Down
10 changes: 5 additions & 5 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, minLength = 16) {
function assertPassphrase(passphrase, minLength = 1) {
if (typeof passphrase !== 'string' || passphrase.normalize('NFC').length < minLength) {
throw new Error(`Use a passphrase with at least ${minLength} characters.`);
throw new Error(minLength > 1 ? `Use a passphrase with at least ${minLength} characters.` : 'Enter a passphrase.');
}
if (passphrase.length > 1024) throw new Error('Passphrase is too long.');
}
Expand All @@ -64,7 +64,7 @@ function aadForObject(userId, objectId) {
return encoder.encode(`tleehealth:record-vault:object:v${FORMAT_VERSION}:${userId}:${objectId}`);
}

async function derivePassphraseKey(passphrase, salt, minLength = 16) {
async function derivePassphraseKey(passphrase, salt, minLength = 1) {
assertPassphrase(passphrase, minLength);
const crypto = webcrypto();
const passphraseBytes = encoder.encode(passphrase.normalize('NFC'));
Expand Down Expand Up @@ -133,7 +133,7 @@ export function lockVault(handle) {
handles.delete(handle);
}

export async function createVault(userId, passphrase, { minLength = 16 } = {}) {
export async function createVault(userId, passphrase, { minLength = 1 } = {}) {
assertUserId(userId);
assertPassphrase(passphrase, minLength);
const rawKey = randomBytes(32);
Expand Down Expand Up @@ -182,7 +182,7 @@ export function validateVaultEnvelope(envelope) {
return envelope;
}

export async function unlockWithPassphrase(userId, passphrase, untrustedEnvelope, { minLength = 16 } = {}) {
export async function unlockWithPassphrase(userId, passphrase, untrustedEnvelope, { minLength = 1 } = {}) {
const envelope = validateVaultEnvelope(untrustedEnvelope);
let rawKey;
try {
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/tleehealth-mcp",
"version": "0.5.3",
"version": "0.5.4",
"description": "MCP server for tleehealth.com: read the practice schedule, the navigation caseload and superbill, and log navigation time from any MCP client.",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion test/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe.skipIf(!configured())('api', () => {
expect(await page.text()).toContain('Sign in to record measurements');
const trackerScript = await app.request('/health/health.js', { headers: { cookie } });
expect(trackerScript.status).toBe(200);
expect(await trackerScript.text()).toContain('minlength="6"');
expect(await trackerScript.text()).not.toContain('minlength=');
});

test('CoinPay links to the signed-in account by provider subject and signs into that account', async () => {
Expand Down
6 changes: 3 additions & 3 deletions test/vault-crypto.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ 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, '')).rejects.toThrow('Enter a passphrase');
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 });
const shortPassphrase = await createVault(userId, 'abc');
const unlocked = await unlockWithPassphrase(userId, 'abc', shortPassphrase.envelope);
lockVault(shortPassphrase.handle);
lockVault(unlocked);
expect(() => validateVaultEnvelope({ version: 1, cipher: 'AES-ECB' })).toThrow('invalid vault key envelope');
Expand Down
Loading