Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { randomBytes, scrypt as scryptCb, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';
const scrypt = promisify(scryptCb) as (
password: string | Buffer,
salt: string | Buffer,
keylen: number,
options: { N: number; r: number; p: number; maxmem: number },
) => Promise<Buffer>;
/**
* scrypt parameters. N=2^15 costs roughly 100ms on a modern laptop, which is
* the point: it makes an offline attack on a stolen `_wellformed_user` table
* expensive without making login feel slow.
*/
const PARAMS = { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 };
const KEY_LENGTH = 64;
const SALT_LENGTH = 16;
export const MIN_PASSWORD_LENGTH = 10;
export const MAX_PASSWORD_LENGTH = 200;
export async function hashPassword(password: string): Promise<string> {
const salt = randomBytes(SALT_LENGTH);
const key = await scrypt(password.normalize('NFKC'), salt, KEY_LENGTH, PARAMS);
return ['scrypt', PARAMS.N, PARAMS.r, PARAMS.p, salt.toString('base64'), key.toString('base64')].join('$');
}
/** Constant-time comparison; returns false rather than throwing on junk input. */
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
const parts = stored.split('$');
if (parts.length !== 6 || parts[0] !== 'scrypt') return false;
const [, nRaw, rRaw, pRaw, saltRaw, keyRaw] = parts as [string, string, string, string, string, string];
const N = Number(nRaw);
const r = Number(rRaw);
const p = Number(pRaw);
if (!Number.isInteger(N) || !Number.isInteger(r) || !Number.isInteger(p)) return false;
let expected: Buffer;
let actual: Buffer;
try {
expected = Buffer.from(keyRaw, 'base64');
actual = await scrypt(password.normalize('NFKC'), Buffer.from(saltRaw, 'base64'), expected.length, {
N,
r,
p,
maxmem: PARAMS.maxmem,
});
} catch {
return false;
}
return expected.length === actual.length && timingSafeEqual(expected, actual);
}
export interface PasswordProblem {
message: string;
}
export function checkPasswordStrength(password: string): PasswordProblem | null {
if (password.length < MIN_PASSWORD_LENGTH) {
return { message: `Passwords must be at least ${MIN_PASSWORD_LENGTH} characters.` };
}
if (password.length > MAX_PASSWORD_LENGTH) {
return { message: `Passwords must be at most ${MAX_PASSWORD_LENGTH} characters.` };
}
if (/^\s|\s$/.test(password)) {
return { message: 'Passwords cannot start or end with whitespace.' };
}
if (COMMON_PASSWORDS.has(password.toLowerCase())) {
return { message: 'That password is too common. Choose something else.' };
}
return null;
}
const COMMON_PASSWORDS = new Set([
'password',
'password1',
'password123',
'passw0rd123',
'1234567890',
'12345678901',
'qwertyuiop',
'letmein123',
'admin12345',
'administrator',
'welcome123',
'iloveyou123',
'changeme123',
]);