import { ensureCrypto } from "#Source/environment/feature.ts" const INTERNAL_FORMAT_VERSION = "v1" const INTERNAL_ALGORITHM = "pbkdf2" const INTERNAL_HASH = "sha512" const INTERNAL_ITERATIONS = 210_000 const INTERNAL_KEY_LENGTH_BITS = 512 const INTERNAL_SALT_LENGTH_BYTES = 16 const INTERNAL_HEX_REGEXP = /^[0-9a-f]+$/i // Step 1: 获取可用的 SubtleCrypto。 // // 密码推导依赖 Web Crypto 的 PBKDF2 能力。 // 这里先通过 environment feature 的 ensureCrypto 统一检查运行时是否支持 crypto, // 再补充校验 subtle 是否可用,避免在不支持环境里出现隐式失败。 const internalGetSubtleCrypto = (): SubtleCrypto => { const runtimeCrypto = ensureCrypto() if (runtimeCrypto.subtle === undefined) { throw new Error("Password utilities require crypto.subtle") } return runtimeCrypto.subtle } // Step 2: 生成指定长度的安全随机字节。 // // salt 的安全性来自随机源;此处同时做长度校验,保证调用方不会传入无效长度。 const internalGetRandomValues = (length: number): Uint8Array => { if (Number.isInteger(length) === false || length <= 0) { throw new RangeError(`Expected length to be a positive integer, got: ${length}`) } const runtimeCrypto = ensureCrypto() if (typeof runtimeCrypto.getRandomValues !== "function") { throw new Error("Password utilities require crypto.getRandomValues") } const buffer = new Uint8Array(length) runtimeCrypto.getRandomValues(buffer) return buffer } // Step 3: 把字节序列编码成十六进制文本。 // // 每个字节固定输出两位字符,便于传输和持久化,同时保持长度信息稳定。 const internalBytesToHex = (bytes: Uint8Array): string => { return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("") } // Step 4: 把十六进制文本恢复成字节序列。 // // PBKDF2 需要原始 salt 字节,不能直接使用十六进制字符串。 const internalHexToBytes = (hex: string): Uint8Array => { if (hex.length === 0 || hex.length % 2 !== 0 || INTERNAL_HEX_REGEXP.test(hex) === false) { throw new TypeError(`Expected an even-length hexadecimal string, got: ${hex}`) } const bytes = new Uint8Array(hex.length / 2) for (let index = 0; index < hex.length; index = index + 2) { bytes[index / 2] = Number.parseInt(hex.slice(index, index + 2), 16) } return bytes } // Step 5: 统一使用 UTF-8 编码密码文本。 const internalTextToBytes = (text: string): Uint8Array => { return new TextEncoder().encode(text) } // Step 6: 生成独立的 ArrayBuffer 输入给 Web Crypto。 const internalBytesToArrayBuffer = (bytes: Uint8Array): ArrayBuffer => { return Uint8Array.from(bytes).buffer } // Step 7: 以固定时序比较散列文本,降低可观察的提前返回差异。 const internalSafeEqual = (left: string, right: string): boolean => { if (left.length !== right.length) { return false } let difference = 0 for (let index = 0; index < left.length; index = index + 1) { difference = difference | ((left.codePointAt(index) ?? 0) ^ (right.codePointAt(index) ?? 0)) } return difference === 0 } // Step 8: 执行 PBKDF2-SHA-512 推导。 // // 输入为明文密码与十六进制 salt,输出固定长度的十六进制 hash。 const internalDeriveHash = async (password: string, saltHex: string): Promise => { const subtleCrypto = internalGetSubtleCrypto() const importedKey = await subtleCrypto.importKey( "raw", internalBytesToArrayBuffer(internalTextToBytes(password)), "PBKDF2", false, ["deriveBits"], ) const derivedBits = await subtleCrypto.deriveBits( { name: "PBKDF2", hash: "SHA-512", salt: internalBytesToArrayBuffer(internalHexToBytes(saltHex)), iterations: INTERNAL_ITERATIONS, }, importedKey, INTERNAL_KEY_LENGTH_BITS, ) return internalBytesToHex(new Uint8Array(derivedBits)) } // Step 9: 组装最终的持久化密码格式。 const internalBuildHashedPassword = (saltHex: string, hashHex: string): HashedPassword => { return `${INTERNAL_FORMAT_VERSION}:${INTERNAL_ALGORITHM}:${INTERNAL_HASH}:${INTERNAL_ITERATIONS}:${saltHex}:${hashHex}` } // Step 10: 解析并校验持久化密码格式。 // // 只有当版本、算法、迭代次数、salt/hash 的长度与字符集都合法时, // 才认为该字符串是可用于验证的 HashedPassword。 const internalParseHashedPassword = ( hashedPassword: HashedPassword, ): { saltHex: string; hashHex: string } | null => { const parts = hashedPassword.split(":") if (parts.length !== 6) { return null } const version = parts[0] const algorithm = parts[1] const hashName = parts[2] const iterationsText = parts[3] const saltHex = parts[4]! const hashHex = parts[5]! if (version !== INTERNAL_FORMAT_VERSION) { return null } if (algorithm !== INTERNAL_ALGORITHM || hashName !== INTERNAL_HASH) { return null } if (iterationsText !== String(INTERNAL_ITERATIONS)) { return null } if ( saltHex.length !== INTERNAL_SALT_LENGTH_BYTES * 2 || INTERNAL_HEX_REGEXP.test(saltHex) === false ) { return null } if ( hashHex.length !== INTERNAL_KEY_LENGTH_BITS / 4 || INTERNAL_HEX_REGEXP.test(hashHex) === false ) { return null } return { saltHex, hashHex, } } /** * @description Format: v1:pbkdf2:sha512:210000:salt:hash */ export type HashedPassword = string /** * @description 把明文密码转换为可持久化的带版本散列字符串。 * 输出格式:`v1:pbkdf2:sha512:210000:salt:hash` * 其中 salt 与 hash 都是十六进制文本。 */ export const hashPassword = async (password: string): Promise => { const saltHex = internalBytesToHex(internalGetRandomValues(INTERNAL_SALT_LENGTH_BYTES)) const hashHex = await internalDeriveHash(password, saltHex) return internalBuildHashedPassword(saltHex, hashHex) } /** * @description 校验明文密码是否与持久化散列匹配。 * 该函数会先解析并校验格式,再用同一 salt 重算 hash, * 最后用固定时序比较完整字符串,返回 true/false。 */ export const comparePassword = async ( password: string, hashedPassword: HashedPassword, ): Promise => { const parsedHashedPassword = internalParseHashedPassword(hashedPassword) if (parsedHashedPassword === null) { return false } const recalculatedHashHex = await internalDeriveHash(password, parsedHashedPassword.saltHex) const recalculatedHashedPassword = internalBuildHashedPassword( parsedHashedPassword.saltHex, recalculatedHashHex, ) return internalSafeEqual(recalculatedHashedPassword, hashedPassword) }