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",
|
"name": "pulse-crm-backend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --allow-net --allow-env --allow-read --allow-ffi --watch src/main.ts",
|
"dev": "deno run --allow-net --allow-env --allow-read --watch src/main.ts",
|
||||||
"start": "deno run --allow-net --allow-env --allow-read --allow-ffi src/main.ts",
|
"start": "deno run --allow-net --allow-env --allow-read src/main.ts",
|
||||||
"test": "deno test --allow-net --allow-env --allow-read --allow-ffi",
|
"test": "deno test --allow-net --allow-env --allow-read",
|
||||||
"check": "deno check src/main.ts",
|
"check": "deno check src/main.ts",
|
||||||
"lint": "deno lint",
|
"lint": "deno lint",
|
||||||
"fmt": "deno fmt",
|
"fmt": "deno fmt",
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
"@std/dotenv": "jsr:@std/dotenv@^0.225.0",
|
"@std/dotenv": "jsr:@std/dotenv@^0.225.0",
|
||||||
"postgres": "https://deno.land/x/postgres@v0.19.3/mod.ts",
|
"postgres": "https://deno.land/x/postgres@v0.19.3/mod.ts",
|
||||||
"zod": "https://deno.land/x/zod@v3.22.4/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"
|
"djwt": "https://deno.land/x/djwt@v3.0.2/mod.ts"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"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 ITERATIONS = 100000;
|
||||||
const HASH_OPTIONS = {
|
const HASH_LENGTH = 64; // bytes
|
||||||
memoryCost: 65536, // 64 MB
|
const SALT_LENGTH = 32; // bytes
|
||||||
timeCost: 3, // 3 iterations
|
const ALGORITHM = "PBKDF2";
|
||||||
parallelism: 4,
|
const HASH_ALGORITHM = "SHA-512";
|
||||||
hashLength: 32,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash a password using Argon2id
|
* Hash a password using PBKDF2
|
||||||
|
* Returns: salt:hash (both hex encoded)
|
||||||
*/
|
*/
|
||||||
export async function hashPassword(password: string): Promise<string> {
|
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(
|
export async function verifyPassword(
|
||||||
password: string,
|
password: string,
|
||||||
hashedPassword: string
|
storedHash: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
// Invalid hash format or other error
|
|
||||||
return false;
|
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
|
* Check password strength
|
||||||
* Returns { valid: boolean, errors: string[] }
|
|
||||||
*/
|
*/
|
||||||
export function validatePasswordStrength(password: string): {
|
export function validatePasswordStrength(password: string): {
|
||||||
valid: boolean;
|
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 {
|
export function generateSecureToken(length = 32): string {
|
||||||
const array = new Uint8Array(length);
|
const array = new Uint8Array(length);
|
||||||
|
|||||||
Reference in New Issue
Block a user