diff --git a/dist/index.html b/dist/index.html
index f53912e..bbbe821 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -5007,12 +5007,12 @@ function isTrashGroup(groupId) {
//#endregion
//#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.
* 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 SALT_LENGTH = 16;
@@ -5146,10 +5146,11 @@ function generatePassword({ length = 16, uppercase = true, lowercase = true, dig
const excludeSet = new Set(exclude.split(""));
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 maxValid = 256 - 256 % charsetLength;
const randomBytes = new Uint8Array(length * 2);
+ crypto.getRandomValues(randomBytes);
let password = "";
let byteIdx = 0;
while (password.length < length) {
diff --git a/src/lib/crypto/crypto.js b/src/lib/crypto/crypto.js
index 242e355..60564cc 100644
--- a/src/lib/crypto/crypto.js
+++ b/src/lib/crypto/crypto.js
@@ -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.
* 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'
@@ -48,7 +48,7 @@ export async function deriveKey(masterPassword, salt) {
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
- false, // not extractable — stays in memory
+ false, // not extractable - stays in memory
['encrypt', 'decrypt']
)
}
@@ -186,12 +186,13 @@ export function generatePassword({
}
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 maxValid = 256 - (256 % charsetLength)
const randomBytes = new Uint8Array(length * 2)
+ crypto.getRandomValues(randomBytes)
let password = ''
let byteIdx = 0
diff --git a/tests/lib/crypto/crypto.test.js b/tests/lib/crypto/crypto.test.js
new file mode 100644
index 0000000..147cf21
--- /dev/null
+++ b/tests/lib/crypto/crypto.test.js
@@ -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()
+ })
+})