mirror of
https://github.com/ZSCGR/CloudFlare-ImgBed.git
synced 2026-08-16 13:43:43 +08:00
206 lines
6.5 KiB
JavaScript
206 lines
6.5 KiB
JavaScript
/**
|
||
* 密码哈希工具
|
||
* 使用 Web Crypto API 的 PBKDF2 (100,000 iterations) + 盐值进行密码哈希
|
||
* 向后兼容旧版 SHA-256 哈希和明文密码存储
|
||
*/
|
||
|
||
const HASH_PREFIX_SHA256 = '$sha256$';
|
||
const HASH_PREFIX_PBKDF2 = '$pbkdf2$';
|
||
const PBKDF2_ITERATIONS = 100000;
|
||
|
||
/**
|
||
* 生成随机盐值
|
||
* @returns {string} 16字节的十六进制盐值
|
||
*/
|
||
function generateSalt() {
|
||
const array = new Uint8Array(16);
|
||
crypto.getRandomValues(array);
|
||
return Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('');
|
||
}
|
||
|
||
/**
|
||
* 将十六进制字符串转为 Uint8Array
|
||
* @param {string} hex
|
||
* @returns {Uint8Array}
|
||
*/
|
||
function hexToBytes(hex) {
|
||
const bytes = new Uint8Array(hex.length / 2);
|
||
for (let i = 0; i < hex.length; i += 2) {
|
||
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
||
}
|
||
return bytes;
|
||
}
|
||
|
||
/**
|
||
* 将 ArrayBuffer 转为十六进制字符串
|
||
* @param {ArrayBuffer} buffer
|
||
* @returns {string}
|
||
*/
|
||
function bufferToHex(buffer) {
|
||
return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||
}
|
||
|
||
/**
|
||
* 使用 PBKDF2 对密码进行哈希(推荐,新密码默认使用)
|
||
* @param {string} password - 明文密码
|
||
* @param {string} [salt] - 盐值(十六进制),不传则自动生成
|
||
* @returns {Promise<string>} 哈希后的密码字符串(格式:$pbkdf2$salt$hash)
|
||
*/
|
||
export async function hashPassword(password, salt = null) {
|
||
if (!salt) {
|
||
salt = generateSalt();
|
||
}
|
||
const encoder = new TextEncoder();
|
||
const keyMaterial = await crypto.subtle.importKey(
|
||
'raw',
|
||
encoder.encode(password),
|
||
'PBKDF2',
|
||
false,
|
||
['deriveBits']
|
||
);
|
||
const derivedBits = await crypto.subtle.deriveBits(
|
||
{
|
||
name: 'PBKDF2',
|
||
salt: hexToBytes(salt),
|
||
iterations: PBKDF2_ITERATIONS,
|
||
hash: 'SHA-256',
|
||
},
|
||
keyMaterial,
|
||
256 // 32 bytes
|
||
);
|
||
const hashHex = bufferToHex(derivedBits);
|
||
return `${HASH_PREFIX_PBKDF2}${salt}$${hashHex}`;
|
||
}
|
||
|
||
/**
|
||
* 使用旧版 SHA-256 对密码进行哈希(仅用于验证旧哈希)
|
||
* @param {string} password - 明文密码
|
||
* @param {string} salt - 盐值
|
||
* @returns {Promise<string>} 哈希后的密码字符串(格式:$sha256$salt$hash)
|
||
*/
|
||
async function hashPasswordSHA256(password, salt) {
|
||
const encoder = new TextEncoder();
|
||
const data = encoder.encode(salt + password);
|
||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||
const hashHex = bufferToHex(hashBuffer);
|
||
return `${HASH_PREFIX_SHA256}${salt}$${hashHex}`;
|
||
}
|
||
|
||
/**
|
||
* 判断密码是否已经是哈希格式
|
||
* @param {string} password - 密码字符串
|
||
* @returns {boolean}
|
||
*/
|
||
export function isHashed(password) {
|
||
return typeof password === 'string' &&
|
||
(password.startsWith(HASH_PREFIX_PBKDF2) || password.startsWith(HASH_PREFIX_SHA256));
|
||
}
|
||
|
||
/**
|
||
* 判断哈希是否为旧版 SHA-256 格式(需要升级)
|
||
* @param {string} password - 密码字符串
|
||
* @returns {boolean}
|
||
*/
|
||
export function needsRehash(password) {
|
||
return typeof password === 'string' && password.startsWith(HASH_PREFIX_SHA256);
|
||
}
|
||
|
||
/**
|
||
* 验证密码是否匹配
|
||
* 兼容 PBKDF2、SHA-256 哈希和明文密码三种存储格式
|
||
* @param {string} inputPassword - 用户输入的明文密码
|
||
* @param {string} storedPassword - 数据库中存储的密码(可能是明文、SHA-256 哈希或 PBKDF2 哈希)
|
||
* @returns {Promise<boolean>} 是否匹配
|
||
*/
|
||
export async function verifyPassword(inputPassword, storedPassword) {
|
||
if (!storedPassword || !inputPassword) {
|
||
return false;
|
||
}
|
||
|
||
if (storedPassword.startsWith(HASH_PREFIX_PBKDF2)) {
|
||
// PBKDF2 哈希格式:$pbkdf2$salt$hash
|
||
const parts = storedPassword.split('$');
|
||
// ['', 'pbkdf2', salt, hash]
|
||
if (parts.length !== 4) {
|
||
return false;
|
||
}
|
||
const salt = parts[2];
|
||
const expectedHash = await hashPassword(inputPassword, salt);
|
||
return timingSafeEqual(expectedHash, storedPassword);
|
||
} else if (storedPassword.startsWith(HASH_PREFIX_SHA256)) {
|
||
// 旧版 SHA-256 哈希格式:$sha256$salt$hash
|
||
const parts = storedPassword.split('$');
|
||
// ['', 'sha256', salt, hash]
|
||
if (parts.length !== 4) {
|
||
return false;
|
||
}
|
||
const salt = parts[2];
|
||
const expectedHash = await hashPasswordSHA256(inputPassword, salt);
|
||
return timingSafeEqual(expectedHash, storedPassword);
|
||
} else {
|
||
// 存储的是明文密码,直接比对(向后兼容)
|
||
return inputPassword === storedPassword;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 恒定时间字符串比较,防止时序攻击
|
||
* @param {string} a
|
||
* @param {string} b
|
||
* @returns {boolean}
|
||
*/
|
||
function timingSafeEqual(a, b) {
|
||
if (a.length !== b.length) {
|
||
return false;
|
||
}
|
||
const encoder = new TextEncoder();
|
||
const bufA = encoder.encode(a);
|
||
const bufB = encoder.encode(b);
|
||
let result = 0;
|
||
for (let i = 0; i < bufA.length; i++) {
|
||
result |= bufA[i] ^ bufB[i];
|
||
}
|
||
return result === 0;
|
||
}
|
||
|
||
/**
|
||
* 生成安全的会话 Token
|
||
* @returns {string} 随机会话 Token
|
||
*/
|
||
export function generateSessionToken() {
|
||
const array = new Uint8Array(32);
|
||
crypto.getRandomValues(array);
|
||
return Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('');
|
||
}
|
||
|
||
/**
|
||
* 如果密码使用旧版哈希或明文存储,自动升级为 PBKDF2
|
||
* @param {Object} db - 数据库实例
|
||
* @param {string} plainPassword - 用户输入的明文密码
|
||
* @param {string} storedPassword - 当前存储的密码/哈希
|
||
* @param {string} configPath - 配置路径,如 'auth.admin.adminPassword' 或 'auth.user.authCode'
|
||
*/
|
||
export async function rehashIfNeeded(db, plainPassword, storedPassword, configPath) {
|
||
if (!storedPassword || (storedPassword.startsWith(HASH_PREFIX_PBKDF2) && !needsRehash(storedPassword))) {
|
||
return;
|
||
}
|
||
try {
|
||
const settingsStr = await db.get('manage@sysConfig@security');
|
||
if (!settingsStr) return;
|
||
const settings = JSON.parse(settingsStr);
|
||
|
||
// 按路径定位字段,如 'auth.admin.adminPassword'
|
||
const keys = configPath.split('.');
|
||
let target = settings;
|
||
for (let i = 0; i < keys.length - 1; i++) {
|
||
target = target?.[keys[i]];
|
||
}
|
||
if (!target) return;
|
||
|
||
target[keys[keys.length - 1]] = await hashPassword(plainPassword);
|
||
await db.put('manage@sysConfig@security', JSON.stringify(settings));
|
||
} catch (e) {
|
||
console.error(`Failed to rehash password at ${configPath}:`, e);
|
||
}
|
||
}
|