mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
fix: update server hash prefix handling for password hashing and verification
This commit is contained in:
+8
-10
@@ -6,6 +6,7 @@ import { StorageService } from './storage';
|
|||||||
// The client already does heavy PBKDF2 (600k iterations).
|
// The client already does heavy PBKDF2 (600k iterations).
|
||||||
// This second layer only needs to be non-trivial, not expensive.
|
// This second layer only needs to be non-trivial, not expensive.
|
||||||
const SERVER_HASH_ITERATIONS = 100_000;
|
const SERVER_HASH_ITERATIONS = 100_000;
|
||||||
|
const SERVER_HASH_PREFIX = '$s$';
|
||||||
const AUTH_CONTEXT_CACHE_TTL_MS = 15 * 1000;
|
const AUTH_CONTEXT_CACHE_TTL_MS = 15 * 1000;
|
||||||
|
|
||||||
interface CachedUserEntry {
|
interface CachedUserEntry {
|
||||||
@@ -133,7 +134,7 @@ export class AuthService {
|
|||||||
|
|
||||||
// Second-layer hash: PBKDF2-SHA256(clientHash, email-salt, iterations).
|
// Second-layer hash: PBKDF2-SHA256(clientHash, email-salt, iterations).
|
||||||
// Ensures database contents alone cannot be used to authenticate (pass-the-hash defense).
|
// Ensures database contents alone cannot be used to authenticate (pass-the-hash defense).
|
||||||
// Result is prefixed with "$s$" to distinguish from legacy raw client hashes.
|
// Result is prefixed to distinguish server-hashed credentials from invalid legacy rows.
|
||||||
async hashPasswordServer(clientHash: string, email: string): Promise<string> {
|
async hashPasswordServer(clientHash: string, email: string): Promise<string> {
|
||||||
const keyMaterial = await crypto.subtle.importKey(
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
@@ -151,19 +152,16 @@ export class AuthService {
|
|||||||
const bytes = new Uint8Array(bits);
|
const bytes = new Uint8Array(bits);
|
||||||
let binary = '';
|
let binary = '';
|
||||||
for (const b of bytes) binary += String.fromCharCode(b);
|
for (const b of bytes) binary += String.fromCharCode(b);
|
||||||
return '$s$' + btoa(binary);
|
return SERVER_HASH_PREFIX + btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify password: hash the input the same way, then constant-time compare.
|
// Verify password: hash the input the same way, then constant-time compare.
|
||||||
async verifyPassword(inputHash: string, storedHash: string, email?: string): Promise<boolean> {
|
async verifyPassword(inputHash: string, storedHash: string, email: string): Promise<boolean> {
|
||||||
// New server-hashed passwords are prefixed with "$s$".
|
if (!storedHash.startsWith(SERVER_HASH_PREFIX)) {
|
||||||
// Legacy accounts (created before the upgrade) store raw client hashes without prefix.
|
return false;
|
||||||
if (email && storedHash.startsWith('$s$')) {
|
|
||||||
const serverHash = await this.hashPasswordServer(inputHash, email);
|
|
||||||
return this.constantTimeEquals(serverHash, storedHash);
|
|
||||||
}
|
}
|
||||||
// Legacy path: direct constant-time comparison of raw client hashes.
|
const serverHash = await this.hashPasswordServer(inputHash, email);
|
||||||
return this.constantTimeEquals(inputHash, storedHash);
|
return this.constantTimeEquals(serverHash, storedHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
private constantTimeEquals(a: string, b: string): boolean {
|
private constantTimeEquals(a: string, b: string): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user