Some checks failed
test / test (push) Failing after 24s
Реализация целевого дизайна (docs/architecture/target-design.md): - src/: модульная структура (config, logger, errors, http, core: auth/history/jobs/engines/comfy/codex/images) - Очередь заданий: FIFO, кониурентность codex=1 / comfy-upscale=2, MAX_QUEUED_JOBS=5, JSONL-журнал и восстановление после рестарта (running → interrupted, upscale requeue) - Персистентные сессии (sha256-хеши токенов), scrypt-хеш пароля, rate-limit входа, Origin-проверка, magic-byte валидация аплоадов - Атомарная запись манифеста истории, каскадное удаление, URL с фактическим расширением + 302-алиас /image.jpg, миграция history/ → data/history - Даунскейл sharp до 1024px перед Codex + масштабирование области референса - SSE /api/events с фолбэком на polling, фронтенд переведён на ES-модули (public/js/) - Тесты node:test + supertest (27), CI workflow test.yml, Docker/деплой: том kadr-data - Документация: README, docs/architecture/
115 lines
3.0 KiB
JavaScript
115 lines
3.0 KiB
JavaScript
"use strict";
|
||
|
||
const crypto = require("crypto");
|
||
const { promisify } = require("util");
|
||
|
||
const scryptAsync = promisify(crypto.scrypt);
|
||
|
||
const KEY_LEN = 64;
|
||
const SALT_LEN = 16;
|
||
const SCRYPT_OPTIONS = {
|
||
N: 16384,
|
||
r: 8,
|
||
p: 1,
|
||
maxmem: 32 * 1024 * 1024,
|
||
};
|
||
|
||
/**
|
||
* Хеширует пароль с помощью scrypt и случайной соли.
|
||
* @param {string} password
|
||
* @returns {Promise<string>} Формат: "saltHex:derivedKeyHex"
|
||
*/
|
||
async function hashPassword(password) {
|
||
if (typeof password !== "string") {
|
||
throw new TypeError("Пароль должен быть строкой");
|
||
}
|
||
const salt = crypto.randomBytes(SALT_LEN);
|
||
const derivedKey = await scryptAsync(password, salt, KEY_LEN, SCRYPT_OPTIONS);
|
||
return `${salt.toString("hex")}:${derivedKey.toString("hex")}`;
|
||
}
|
||
|
||
/**
|
||
* Синхронное хеширование (для инициализации при старте).
|
||
* @param {string} password
|
||
* @returns {string}
|
||
*/
|
||
function hashPasswordSync(password) {
|
||
if (typeof password !== "string") {
|
||
throw new TypeError("Пароль должен быть строкой");
|
||
}
|
||
const salt = crypto.randomBytes(SALT_LEN);
|
||
const derivedKey = crypto.scryptSync(password, salt, KEY_LEN, SCRYPT_OPTIONS);
|
||
return `${salt.toString("hex")}:${derivedKey.toString("hex")}`;
|
||
}
|
||
|
||
/**
|
||
* Проверяет пароль против сохранённого scrypt-хеша с защитой от тайминг-атак.
|
||
* @param {string} password
|
||
* @param {string} storedHash
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function verifyPassword(password, storedHash) {
|
||
if (typeof password !== "string" || typeof storedHash !== "string") {
|
||
return false;
|
||
}
|
||
|
||
const parts = storedHash.split(":");
|
||
if (parts.length !== 2) {
|
||
return false;
|
||
}
|
||
|
||
const [saltHex, keyHex] = parts;
|
||
try {
|
||
const salt = Buffer.from(saltHex, "hex");
|
||
const originalKey = Buffer.from(keyHex, "hex");
|
||
|
||
if (salt.length !== SALT_LEN || originalKey.length !== KEY_LEN) {
|
||
return false;
|
||
}
|
||
|
||
const derivedKey = await scryptAsync(password, salt, KEY_LEN, SCRYPT_OPTIONS);
|
||
return crypto.timingSafeEqual(originalKey, derivedKey);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Синхронная проверка пароля.
|
||
* @param {string} password
|
||
* @param {string} storedHash
|
||
* @returns {boolean}
|
||
*/
|
||
function verifyPasswordSync(password, storedHash) {
|
||
if (typeof password !== "string" || typeof storedHash !== "string") {
|
||
return false;
|
||
}
|
||
|
||
const parts = storedHash.split(":");
|
||
if (parts.length !== 2) {
|
||
return false;
|
||
}
|
||
|
||
const [saltHex, keyHex] = parts;
|
||
try {
|
||
const salt = Buffer.from(saltHex, "hex");
|
||
const originalKey = Buffer.from(keyHex, "hex");
|
||
|
||
if (salt.length !== SALT_LEN || originalKey.length !== KEY_LEN) {
|
||
return false;
|
||
}
|
||
|
||
const derivedKey = crypto.scryptSync(password, salt, KEY_LEN, SCRYPT_OPTIONS);
|
||
return crypto.timingSafeEqual(originalKey, derivedKey);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
hashPassword,
|
||
hashPasswordSync,
|
||
verifyPassword,
|
||
verifyPasswordSync,
|
||
};
|