feat: add password salting with SHA-256 + random salt - #1082
feat: add password salting with SHA-256 + random salt#1082daoischain-bot wants to merge 1 commit into
Conversation
- Add hashPasswordWithSalt / verifyPassword to utils.ts - Add password_salt column to address and users tables - Update user registration, login, password change to use salted hash - Update admin user/address password reset to use salted hash - Auto-generated address passwords now use salted hash - Backward compatible: verifyPassword falls back to direct comparison when salt is null
📝 WalkthroughWalkthrough新增 Changes密码加盐流程
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant AuthAPI
participant PasswordUtils
participant Database
Client->>AuthAPI: 注册或修改密码
AuthAPI->>PasswordUtils: hashPasswordWithSalt(password)
PasswordUtils-->>AuthAPI: hash, salt
AuthAPI->>Database: 保存 password 和 password_salt
Client->>AuthAPI: 登录
AuthAPI->>Database: 查询 password 和 password_salt
AuthAPI->>PasswordUtils: verifyPassword(input, storedHash, salt)
PasswordUtils-->>AuthAPI: 校验结果
AuthAPI-->>Client: 登录响应
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
worker/src/utils.ts (1)
485-496: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win建议使用恒定时间比较
hash === storedHash(及 legacy 分支的===)为非恒定时间比较,存在时序侧信道风险。Cloudflare 最佳实践建议对秘密值使用crypto.subtle.timingSafeEqual()(先将两侧编码为等长缓冲区,避免按长度短路)。此处比较的是哈希值、可利用性较低,但作为认证路径的安全卫生仍建议改用恒定时间比较。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@worker/src/utils.ts` around lines 485 - 496, 在 verifyPassword 中将哈希比较改为恒定时间比较:使用 crypto.subtle.timingSafeEqual() 比较两侧经过统一编码且长度一致的字节缓冲区,覆盖带盐分支的 hash 与 storedHash 以及 legacy 分支的 incomingHash 与 storedHash;长度不一致时先安全返回 false,避免直接使用 ===。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@worker/src/utils.ts`:
- Around line 470-479: Replace the single SHA-256 implementation in
hashPasswordWithSalt with Web Crypto PBKDF2 using crypto.subtle.importKey and
deriveBits, a cryptographically random salt, and at least 100,000 iterations;
update the output format to preserve the algorithm parameters. Extend
verifyPassword to recognize and validate existing SHA-256 hashes, then rehash
and persist them with PBKDF2 after successful login.
---
Nitpick comments:
In `@worker/src/utils.ts`:
- Around line 485-496: 在 verifyPassword 中将哈希比较改为恒定时间比较:使用
crypto.subtle.timingSafeEqual() 比较两侧经过统一编码且长度一致的字节缓冲区,覆盖带盐分支的 hash 与 storedHash
以及 legacy 分支的 incomingHash 与 storedHash;长度不一致时先安全返回 false,避免直接使用 ===。
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9136569b-2581-4213-8a3a-5ce030b1a35a
📒 Files selected for processing (7)
db/2026-07-10-password-salt.sqlworker/src/admin_api/address_api.tsworker/src/admin_api/admin_user_api.tsworker/src/common.tsworker/src/mails_api/address_auth.tsworker/src/user_api/user.tsworker/src/utils.ts
| export const hashPasswordWithSalt = async ( | ||
| password: string, | ||
| salt?: string | ||
| ): Promise<{ hash: string, salt: string }> => { | ||
| const useSalt = salt || crypto.randomUUID(); | ||
| const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password + useSalt)); | ||
| const hashArray = Array.from(new Uint8Array(digest)); | ||
| const hash = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); | ||
| return { hash, salt: useSalt }; | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🌐 Web query:
Cloudflare Workers WebCrypto PBKDF2 deriveBits password hashing example
💡 Result:
Cloudflare Workers fully support the standard Web Crypto API, which is the recommended way to perform PBKDF2 password hashing [1][2]. Because the Workers runtime implements the standard SubtleCrypto interface, the process follows a two-step asynchronous pattern [2][3]. ### Implementation Steps 1. Import the Password: Use crypto.subtle.importKey to convert your raw password string (encoded as a Uint8Array) into a cryptographic key object [4][3]. 2. Derive Bits: Use crypto.subtle.deriveBits with the PBKDF2 algorithm parameters to perform the hashing [4][2]. ### Code Example The following example demonstrates how to derive bits from a password using PBKDF2: async function derivePasswordHash(password, salt) { const encoder = new TextEncoder; const passwordData = encoder.encode(password); // 1. Import the password as a key material object const keyMaterial = await crypto.subtle.importKey( "raw", passwordData, { name: "PBKDF2" }, false, ["deriveBits"]); // 2. Derive bits using PBKDF2 const derivedBits = await crypto.subtle.deriveBits( { name: "PBKDF2", salt: salt, // Must be a Uint8Array, preferably cryptographically random iterations: 100000, // Adjust based on security requirements hash: "SHA-256" }, keyMaterial, 256 // Length of the derived bits in bits); return new Uint8Array(derivedBits); } // Helper: Generating a random salt const salt = crypto.getRandomValues(new Uint8Array(16)); ### Best Practices * Salt: Always use a unique, cryptographically random salt for every password [2]. Generate it using crypto.getRandomValues() [2]. * Iterations: Use a high iteration count (e.g., 100,000 or more) to protect against brute-force attacks [4][2]. * Asynchronous Handling: Since Web Crypto API methods are asynchronous, ensure you await the results to avoid blocking the event loop [3]. * Output Conversion: The result of deriveBits is an ArrayBuffer. If you need to store it as a string (e.g., for a database), convert it to a hexadecimal or Base64 format [2][3].
Citations:
- 1: https://developers.cloudflare.com/workers/runtime-apis/web-crypto/
- 2: https://deepwiki.com/cloudflare/worker-performance-examples/2.4-cloudflare-worker-implementation
- 3: https://deepwiki.com/cloudflare/worker-performance-examples/2-pbkdf2-key-generation-examples
- 4: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveBits
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the target file and inspect the relevant section with line numbers.
wc -l worker/src/utils.ts
sed -n '430,520p' worker/src/utils.ts | cat -n
# Search for other password hashing / verification helpers.
rg -n "hashPasswordWithSalt|verifyPassword|PBKDF2|scrypt|bcrypt|argon2|subtle\.deriveBits|crypto\.subtle\.digest\('SHA-256'" worker/src -SRepository: dreamhunter2333/cloudflare_temp_email
Length of output: 6602
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the current file context around the helper and its callers/usages.
rg -n "hashPasswordWithSalt|verifyPassword" worker/src -SRepository: dreamhunter2333/cloudflare_temp_email
Length of output: 2488
单轮 SHA-256 不适合作为密码哈希
Workers 原生支持 Web Crypto 的 PBKDF2,建议改成 crypto.subtle.importKey + deriveBits,使用随机盐和至少 100,000 次迭代;当前实现只能防彩虹表,挡不住弱口令的离线爆破。旧数据可在 verifyPassword 里按格式兼容,并在登录后逐步升级。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@worker/src/utils.ts` around lines 470 - 479, Replace the single SHA-256
implementation in hashPasswordWithSalt with Web Crypto PBKDF2 using
crypto.subtle.importKey and deriveBits, a cryptographically random salt, and at
least 100,000 iterations; update the output format to preserve the algorithm
parameters. Extend verifyPassword to recognize and validate existing SHA-256
hashes, then rehash and persist them with PBKDF2 after successful login.
|
Thanks for working on this. Before this can be merged, please take one of the following approaches:
I prefer option 2 because it keeps deployments backward-compatible and stores the algorithm, version, cost factor, salt, and digest together. Please also use a slow password KDF such as PBKDF2 rather than a single SHA-256 round. |
Summary
Adds password salting for stored passwords in both
usersandaddresstables using SHA-256 + random UUID salt.Changes
hashPasswordWithSalt(password, salt?)andverifyPassword(incomingHash, storedHash, salt)db/2026-07-10-password-salt.sql): Adds nullablepassword_saltTEXT column tousersandaddresstablesverifyPasswordfalls back to direct SHA-256 comparison when salt is null, so existing records continue to workNotes
nodejs_compatflag in wrangler.toml (forcrypto.randomUUID())Summary by CodeRabbit