5631d85238
- V.1–V.3: saveTestDraft, fork при попытках; миграция 003 staff_id - V.4–V.6: REST /api/tests, activate, PATCH, start attempt - A: HR_DATABASE_URL + Werkzeug/bcrypt, JWT staffId, HR_AUTH - D.1: multipart /api/tests/import/document - Frontend: login, список тестов, экран версий/черновика/попытки - ТЗ: V.10 назначения vs активная версия; журнал приёма Made-with: Cursor
75 lines
2.0 KiB
JavaScript
75 lines
2.0 KiB
JavaScript
/**
|
|
* Проверка хеша в формате Werkzeug 3 (scrypt:… / pbkdf2:…).
|
|
* @see https://github.com/pallets/werkzeug/blob/main/src/werkzeug/security.py
|
|
*/
|
|
import crypto from 'crypto';
|
|
|
|
/**
|
|
* @param {string} pwhash
|
|
* @param {string} password
|
|
* @returns {boolean}
|
|
*/
|
|
function hashInternal(method, salt, password) {
|
|
const methodParts = method.split(':');
|
|
const kind = methodParts[0];
|
|
const saltBytes = Buffer.from(salt, 'utf8');
|
|
const passwordBytes = Buffer.from(password, 'utf8');
|
|
|
|
if (kind === 'scrypt') {
|
|
const n = methodParts[1] ? parseInt(methodParts[1], 10) : 2 ** 15;
|
|
const r = methodParts[2] ? parseInt(methodParts[2], 10) : 8;
|
|
const p = methodParts[3] ? parseInt(methodParts[3], 10) : 1;
|
|
const maxmem = 132 * n * r * p;
|
|
return crypto
|
|
.scryptSync(passwordBytes, saltBytes, 64, { N: n, r, p, maxmem })
|
|
.toString('hex');
|
|
}
|
|
|
|
if (kind === 'pbkdf2') {
|
|
const hashName = methodParts[1] || 'sha256';
|
|
const iterStr = methodParts[2];
|
|
if (!iterStr) {
|
|
throw new Error('pbkdf2: missing iterations');
|
|
}
|
|
const iterations = parseInt(iterStr, 10);
|
|
return crypto
|
|
.pbkdf2Sync(passwordBytes, saltBytes, iterations, 32, hashName)
|
|
.toString('hex');
|
|
}
|
|
|
|
throw new Error(`Invalid hash method: ${kind}`);
|
|
}
|
|
|
|
/**
|
|
* @param {string} pwhash
|
|
* @param {string} password
|
|
* @returns {boolean}
|
|
*/
|
|
export function checkWerkzeugPassword(pwhash, password) {
|
|
if (!pwhash || pwhash.length < 3) {
|
|
return false;
|
|
}
|
|
const parts = pwhash.split('$');
|
|
if (parts.length < 3) {
|
|
return false;
|
|
}
|
|
const hashval = parts.pop();
|
|
const salt = parts.pop();
|
|
const method = parts.join('$');
|
|
if (!method || !salt || !hashval) {
|
|
return false;
|
|
}
|
|
let computed;
|
|
try {
|
|
computed = hashInternal(method, salt, password);
|
|
} catch {
|
|
return false;
|
|
}
|
|
const a = Buffer.from(computed, 'hex');
|
|
const b = Buffer.from(hashval, 'hex');
|
|
if (a.length !== b.length) {
|
|
return false;
|
|
}
|
|
return crypto.timingSafeEqual(a, b);
|
|
}
|