Fix passwords not being random
This commit is contained in:
parent
46c49655a7
commit
9d9e599c09
7
dist/index.html
vendored
7
dist/index.html
vendored
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
43
tests/lib/crypto/crypto.test.js
Normal file
43
tests/lib/crypto/crypto.test.js
Normal 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()
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user