Files
OJ2/apps/api/src/auth/password.ts
T
xuyueandClaude Opus 5 ed56a209ea
Deploy / deploy (push) Has been cancelled
chore(格式): Prettier 统一到全仓,后端和契约一次性格式化
原来只有 `apps/web` 在 Prettier 下(配置在 `apps/web/.prettierrc.toml`、脚本在
web 的 package.json),后端和契约从来没格式化过 —— 手写在 100 列上下,`db/schema.ts`
还是 drizzle-kit pull 留下的 tab 缩进。两套口径分叉久了,跨端改一处就得记着「这边
什么风格」。

- 配置搬到根目录 `.prettierrc.toml`,内容不变(`semi=false`,其余全默认,
  printWidth 80 —— 和前端已有的格式一致,不另立一套宽度);
- 脚本统一成根目录 `bun run fmt`,覆盖 `apps/*/src`、`apps/web/tests` 和两个构建
  配置;web 自己那份 `fmt` 和重复的 prettier 依赖删掉;
- `.prettierignore` 挡掉两类不该碰的:drizzle-kit 生成的 `src/db/meta/` 结构快照
  (它是 db:generate 的比对输入,只该由 drizzle-kit 写)、unplugin 每次 dev 都会
  重写的 `auto-imports.d.ts` / `components.d.ts`;
- 全量跑了一遍。纯格式,无行为改动:api typecheck / check:routes / check:ast、
  前端 type-check 全过,起 api 打了接口确认正常。前端这 39 个文件的小改动是
  prettier 版本漂移(类型断言的换行口径变了),不是新配置带来的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 08:27:34 -06:00

93 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { pbkdf2, timingSafeEqual } from "node:crypto"
import { promisify } from "node:util"
const pbkdf2Async = promisify(pbkdf2)
async function verifyDjangoPbkdf2(password: string, encoded: string) {
const [algorithm, iterationsText, salt, digestText] = encoded.split("$")
if (
algorithm !== "pbkdf2_sha256" ||
!iterationsText ||
!salt ||
!digestText
) {
return false
}
const iterations = Number(iterationsText)
const expected = Buffer.from(digestText, "base64")
if (
!Number.isSafeInteger(iterations) ||
iterations <= 0 ||
expected.length === 0
) {
return false
}
const actual = await pbkdf2Async(
password,
salt,
iterations,
expected.length,
"sha256",
)
return timingSafeEqual(actual, expected)
}
/**
* 验密码。**pbkdf2 那条分支永远不能删** —— 生产库 1710 个账号全是 Django 写的
* pbkdf2,只会在各自下次登录时才升级成 argon2,删掉就是全站登不上。
* 迭代次数是从哈希串里读的,所以 120000 到 1200000 的老哈希都验得了。
*/
export async function verifyPassword(password: string, encoded: string) {
if (encoded.startsWith("pbkdf2_sha256$")) {
return {
valid: await verifyDjangoPbkdf2(password, encoded),
needsUpgrade: true,
}
}
if (encoded.startsWith("$argon2")) {
return {
valid: await Bun.password.verify(password, encoded),
needsUpgrade: false,
}
}
return { valid: false, needsUpgrade: false }
}
/**
* 参数是显式写死的,不用 Bun 的默认值(`m=65536, t=2, p=1`,即每次哈希占 64 MiB)。
* 取的是 OWASP 对 argon2id 的推荐下限 `m=19MiB, t=2, p=1`
*
* - 批量导入一个班要连算几十次哈希,64 MiB 那档单次 ~140ms,而 `oj-api` 的
* mem_limit 只有 512mdocker/compose.debian.yml),并发度被内存卡死。
* 19 MiB 这档单次 ~20ms,并发 4 路的峰值也才 76 MiB。
* - 参数是编码进哈希串本身的(`$argon2id$v=19$m=19456,t=2,p=1$...`),所以**存量
* 账号一个都不用迁移**Bun.password.verify 读串里的参数验,改这里只影响此后新写的哈希。
*/
const ARGON2_OPTIONS = {
algorithm: "argon2id",
memoryCost: 19456,
timeCost: 2,
} as const
/**
* 写密码的**唯一入口**。五个调用方都走这里:注册、管理员改密码、批量导入用户、
* 重置密码、登录时升级存量 pbkdf2。
*
* 之所以特意收成一个函数:原来五处各写各的 `Bun.password.hash`,而当年那个
* 「回滚窗口内不要升级成 argon2」的开关只管住了登录
* 那一处,另外四处照写 argon2 不误 —— 老师给学生点一次「重置密码」,那个账号
* 就回不去旧站了,开关关着也拦不住。旧站 2026-08-26 下线,开关已经删掉,
* 但「只有一个地方写密码」这件事留下来了。
*
* 真要把旧站拉回来:切换手册「万一已经改坏了」那节的脚本才是正经退路 ——
* 它拿 `raw_password` 重算 Django 的 make_password,能修**已经**变成 argon2 的
* 账号;靠开关只能拦住将来,修不了已经发生的。
*/
export function hashPassword(password: string) {
return Bun.password.hash(password, ARGON2_OPTIONS)
}