Fix passwords not being random

This commit is contained in:
Timothy Farrell 2026-05-16 23:32:25 +00:00
parent 46c49655a7
commit 9d9e599c09
3 changed files with 52 additions and 7 deletions

7
dist/index.html vendored
View File

@ -5007,12 +5007,12 @@ function isTrashGroup(groupId) {
//#endregion //#endregion
//#region src/lib/crypto/crypto.js //#region src/lib/crypto/crypto.js
/** /**
* Crypto module Web Crypto API wrapper. * Crypto module - Web Crypto API wrapper.
* *
* Uses PBKDF2 for key derivation and AES-GCM for symmetric encryption. * Uses PBKDF2 for key derivation and AES-GCM for symmetric encryption.
* All operations are async and use the browser's native Web Crypto API. * All operations are async and use the browser's native Web Crypto API.
* *
* The derived encryption key is kept in memory only never written to disk. * The derived encryption key is kept in memory only - never written to disk.
*/ */
var PBKDF2_ITERATIONS = 6e5; var PBKDF2_ITERATIONS = 6e5;
var SALT_LENGTH = 16; var SALT_LENGTH = 16;
@ -5146,10 +5146,11 @@ function generatePassword({ length = 16, uppercase = true, lowercase = true, dig
const excludeSet = new Set(exclude.split("")); const excludeSet = new Set(exclude.split(""));
charset = charset.split("").filter((c) => !excludeSet.has(c)).join(""); charset = charset.split("").filter((c) => !excludeSet.has(c)).join("");
} }
if (!charset) throw new Error("Password charset is empty enable at least one character type"); if (!charset) throw new Error("Password charset is empty - enable at least one character type");
const charsetLength = charset.length; const charsetLength = charset.length;
const maxValid = 256 - 256 % charsetLength; const maxValid = 256 - 256 % charsetLength;
const randomBytes = new Uint8Array(length * 2); const randomBytes = new Uint8Array(length * 2);
crypto.getRandomValues(randomBytes);
let password = ""; let password = "";
let byteIdx = 0; let byteIdx = 0;
while (password.length < length) { while (password.length < length) {

View File

@ -1,10 +1,10 @@
/** /**
* Crypto module Web Crypto API wrapper. * Crypto module - Web Crypto API wrapper.
* *
* Uses PBKDF2 for key derivation and AES-GCM for symmetric encryption. * Uses PBKDF2 for key derivation and AES-GCM for symmetric encryption.
* All operations are async and use the browser's native Web Crypto API. * All operations are async and use the browser's native Web Crypto API.
* *
* The derived encryption key is kept in memory only never written to disk. * The derived encryption key is kept in memory only - never written to disk.
*/ */
import { generateId } from '../models/schema.js' import { generateId } from '../models/schema.js'
@ -48,7 +48,7 @@ export async function deriveKey(masterPassword, salt) {
}, },
keyMaterial, keyMaterial,
{ name: 'AES-GCM', length: 256 }, { name: 'AES-GCM', length: 256 },
false, // not extractable stays in memory false, // not extractable - stays in memory
['encrypt', 'decrypt'] ['encrypt', 'decrypt']
) )
} }
@ -186,12 +186,13 @@ export function generatePassword({
} }
if (!charset) { if (!charset) {
throw new Error('Password charset is empty enable at least one character type') throw new Error('Password charset is empty - enable at least one character type')
} }
const charsetLength = charset.length const charsetLength = charset.length
const maxValid = 256 - (256 % charsetLength) const maxValid = 256 - (256 % charsetLength)
const randomBytes = new Uint8Array(length * 2) const randomBytes = new Uint8Array(length * 2)
crypto.getRandomValues(randomBytes)
let password = '' let password = ''
let byteIdx = 0 let byteIdx = 0

View File

@ -0,0 +1,43 @@
import { describe, it, expect } from 'vitest'
import { generatePassword } from '../../../src/lib/crypto/crypto.js'
describe('generatePassword', () => {
it('should produce different passwords on consecutive calls', () => {
const passwords = new Set()
for (let i = 0; i < 50; i++) {
passwords.add(generatePassword({ length: 16 }))
}
// With 50 calls and 94^16 possible values, all should be unique
expect(passwords.size).toBe(50)
})
it('should respect the length option', () => {
for (let len of [4, 8, 16, 32, 64]) {
const pw = generatePassword({ length: len })
expect(pw.length).toBe(len)
}
})
it('should only use allowed character types', () => {
const pw = generatePassword({ uppercase: true, lowercase: false, digits: false, symbols: false })
expect(pw).toMatch(/^[A-Z]+$/)
const pw2 = generatePassword({ uppercase: false, lowercase: true, digits: false, symbols: false })
expect(pw2).toMatch(/^[a-z]+$/)
const pw3 = generatePassword({ uppercase: false, lowercase: false, digits: true, symbols: false })
expect(pw3).toMatch(/^[0-9]+$/)
})
it('should exclude specified characters', () => {
const pw = generatePassword({ length: 32, exclude: '0OIl1' })
for (const ch of '0OIl1') {
expect(pw).not.toContain(ch)
}
})
it('should throw when charset is empty', () => {
expect(() => generatePassword({ uppercase: false, lowercase: false, digits: false, symbols: false }))
.toThrow()
})
})