feat(db): PostgreSQL + PBKDF2 Password Hashing
- PostgreSQL 16 Schema (12 Tabellen) - Multi-Tenant mit org_id - 40+ Performance Indexes - Full-Text Search für Kontakte - DSGVO Audit Logging - PBKDF2 statt Argon2 (Web Crypto API) - Auto-Update Triggers - Views für Pipeline & Activity Stats Deployed: https://api.crm.kronos-soulution.de
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
"name": "pulse-crm-backend",
|
||||
"version": "0.1.0",
|
||||
"tasks": {
|
||||
"dev": "deno run --allow-net --allow-env --allow-read --allow-ffi --watch src/main.ts",
|
||||
"start": "deno run --allow-net --allow-env --allow-read --allow-ffi src/main.ts",
|
||||
"test": "deno test --allow-net --allow-env --allow-read --allow-ffi",
|
||||
"dev": "deno run --allow-net --allow-env --allow-read --watch src/main.ts",
|
||||
"start": "deno run --allow-net --allow-env --allow-read src/main.ts",
|
||||
"test": "deno test --allow-net --allow-env --allow-read",
|
||||
"check": "deno check src/main.ts",
|
||||
"lint": "deno lint",
|
||||
"fmt": "deno fmt",
|
||||
@@ -16,7 +16,6 @@
|
||||
"@std/dotenv": "jsr:@std/dotenv@^0.225.0",
|
||||
"postgres": "https://deno.land/x/postgres@v0.19.3/mod.ts",
|
||||
"zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
|
||||
"argon2": "https://deno.land/x/argon2@v0.10.1/lib/mod.ts",
|
||||
"djwt": "https://deno.land/x/djwt@v3.0.2/mod.ts"
|
||||
},
|
||||
"compilerOptions": {
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
import { hash, verify } from "argon2";
|
||||
|
||||
// ============================================
|
||||
// PASSWORD HASHING SERVICE (Argon2)
|
||||
// PASSWORD HASHING SERVICE (PBKDF2 via Web Crypto)
|
||||
// ============================================
|
||||
// Using Web Crypto API - no external dependencies
|
||||
|
||||
// Argon2id configuration (OWASP recommended)
|
||||
const HASH_OPTIONS = {
|
||||
memoryCost: 65536, // 64 MB
|
||||
timeCost: 3, // 3 iterations
|
||||
parallelism: 4,
|
||||
hashLength: 32,
|
||||
};
|
||||
const ITERATIONS = 100000;
|
||||
const HASH_LENGTH = 64; // bytes
|
||||
const SALT_LENGTH = 32; // bytes
|
||||
const ALGORITHM = "PBKDF2";
|
||||
const HASH_ALGORITHM = "SHA-512";
|
||||
|
||||
/**
|
||||
* Hash a password using Argon2id
|
||||
* Hash a password using PBKDF2
|
||||
* Returns: salt:hash (both hex encoded)
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return await hash(password, HASH_OPTIONS);
|
||||
const encoder = new TextEncoder();
|
||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
encoder.encode(password),
|
||||
ALGORITHM,
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
|
||||
const hashBuffer = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: ALGORITHM,
|
||||
salt: salt,
|
||||
iterations: ITERATIONS,
|
||||
hash: HASH_ALGORITHM,
|
||||
},
|
||||
keyMaterial,
|
||||
HASH_LENGTH * 8 // bits
|
||||
);
|
||||
|
||||
const saltHex = Array.from(salt)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
const hashHex = Array.from(new Uint8Array(hashBuffer))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
|
||||
return `${saltHex}:${hashHex}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,19 +51,61 @@ export async function hashPassword(password: string): Promise<string> {
|
||||
*/
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
hashedPassword: string
|
||||
storedHash: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
return await verify(hashedPassword, password);
|
||||
const [saltHex, hashHex] = storedHash.split(":");
|
||||
if (!saltHex || !hashHex) return false;
|
||||
|
||||
const salt = new Uint8Array(
|
||||
saltHex.match(/.{2}/g)!.map((byte) => parseInt(byte, 16))
|
||||
);
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
encoder.encode(password),
|
||||
ALGORITHM,
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
|
||||
const hashBuffer = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: ALGORITHM,
|
||||
salt: salt,
|
||||
iterations: ITERATIONS,
|
||||
hash: HASH_ALGORITHM,
|
||||
},
|
||||
keyMaterial,
|
||||
HASH_LENGTH * 8
|
||||
);
|
||||
|
||||
const computedHashHex = Array.from(new Uint8Array(hashBuffer))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
|
||||
// Timing-safe comparison
|
||||
return timingSafeEqual(hashHex, computedHashHex);
|
||||
} catch {
|
||||
// Invalid hash format or other error
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timing-safe string comparison
|
||||
*/
|
||||
function timingSafeEqual(a: string, b: string): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
let result = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
||||
}
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check password strength
|
||||
* Returns { valid: boolean, errors: string[] }
|
||||
*/
|
||||
export function validatePasswordStrength(password: string): {
|
||||
valid: boolean;
|
||||
@@ -67,7 +136,7 @@ export function validatePasswordStrength(password: string): {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a secure random token (for password reset, email verification)
|
||||
* Generate a secure random token
|
||||
*/
|
||||
export function generateSecureToken(length = 32): string {
|
||||
const array = new Uint8Array(length);
|
||||
|
||||
Reference in New Issue
Block a user