This commit is contained in:
Jarek Krochmalski committed 2026-01-11 07:16:18 +01:00
1 parent bd7b832394
commit 7643807717
86 files changed
+3644 -1385

No files matched your search

+46 -10
View File
@@ -9,8 +9,9 @@
* - SameSite=Strict (CSRF protection)
*/
import { randomBytes } from 'node:crypto';
import os from 'node:os';
import { secureRandomBytes, usingFallback } from './crypto-fallback';
import { argon2id, argon2Verify } from 'hash-wasm';
import type { Cookies } from '@sveltejs/kit';
import {
getAuthSettings,
@@ -94,27 +95,62 @@ export interface LoginResult {
}
// ============================================
// Password Hashing (Argon2id via Bun.password)
// Password Hashing (Argon2id)
// ============================================
// Argon2id parameters (matching Bun.password defaults)
const ARGON2_MEMORY_COST = 65536; // 64 MB in kibibytes
const ARGON2_TIME_COST = 3; // 3 iterations
const ARGON2_PARALLELISM = 1; // Single-threaded
const ARGON2_HASH_LENGTH = 32; // 256-bit output
const ARGON2_SALT_LENGTH = 16; // 128-bit salt
/**
* Hash a password using Argon2id via Bun's native password API
* Hash a password using Argon2id
*
* On modern kernels (>=3.17): Uses Bun's native password API (faster)
* On old kernels (<3.17): Uses hash-wasm (WASM-based, no getrandom dependency)
*
* Argon2id is the recommended variant - resistant to both side-channel and GPU attacks
*/
export async function hashPassword(password: string): Promise<string> {
// On old kernels, Bun.password.hash() crashes because it internally uses getrandom()
// Use hash-wasm as a fallback which is pure WASM and doesn't depend on the syscall
if (usingFallback()) {
const salt = secureRandomBytes(ARGON2_SALT_LENGTH);
return argon2id({
password,
salt,
iterations: ARGON2_TIME_COST,
parallelism: ARGON2_PARALLELISM,
memorySize: ARGON2_MEMORY_COST,
hashLength: ARGON2_HASH_LENGTH,
outputType: 'encoded' // Returns PHC format: $argon2id$v=19$m=65536,t=3,p=1$...
});
}
// Modern kernels: use Bun's native implementation (faster)
return Bun.password.hash(password, {
algorithm: 'argon2id',
memoryCost: 65536, // 64 MB
timeCost: 3 // 3 iterations
memoryCost: ARGON2_MEMORY_COST,
timeCost: ARGON2_TIME_COST
});
}
/**
* Verify a password against a hash
* Uses constant-time comparison internally
*
* Both Bun.password and hash-wasm use the same PHC format, so hashes are compatible
*/
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
try {
// On old kernels, use hash-wasm for verification
if (usingFallback()) {
return await argon2Verify({ password, hash });
}
// Modern kernels: use Bun's native implementation
return await Bun.password.verify(password, hash);
} catch {
return false;
@@ -130,7 +166,7 @@ export async function verifyPassword(password: string, hash: string): Promise<bo
* 32 bytes = 256 bits of entropy
*/
function generateSessionToken(): string {
return randomBytes(32).toString('base64url');
return secureRandomBytes(32).toString('base64url');
}
/**
@@ -411,7 +447,7 @@ export async function authenticateLocal(
if (!user) {
// Use constant time to prevent timing attacks
await Bun.password.hash('dummy', { algorithm: 'argon2id' });
await hashPassword('dummy');
return { success: false, error: 'Invalid username or password' };
}
@@ -1127,7 +1163,7 @@ async function getOidcDiscovery(issuerUrl: string): Promise<OidcDiscoveryDocumen
* Generate PKCE code verifier and challenge
*/
function generatePkce(): { codeVerifier: string; codeChallenge: string } {
const codeVerifier = randomBytes(32).toString('base64url');
const codeVerifier = secureRandomBytes(32).toString('base64url');
const hasher = new Bun.CryptoHasher('sha256');
hasher.update(codeVerifier);
const codeChallenge = hasher.digest('base64url') as string;
@@ -1150,8 +1186,8 @@ export async function buildOidcAuthorizationUrl(
const discovery = await getOidcDiscovery(config.issuerUrl);
// Generate state, nonce, and PKCE
const state = randomBytes(32).toString('base64url');
const nonce = randomBytes(16).toString('base64url');
const state = secureRandomBytes(32).toString('base64url');
const nonce = secureRandomBytes(16).toString('base64url');
const { codeVerifier, codeChallenge } = generatePkce();
// Store state for callback verification (expires in 10 minutes)