PSA/shared/utils/encryption.ts
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

120 lines
3.6 KiB
TypeScript

import { getSecret } from '../core/getSecret';
// Utility: encode string to Uint8Array
const te = new TextEncoder();
function toHex(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let hex = '';
for (let i = 0; i < bytes.length; i++) {
const h = bytes[i].toString(16).padStart(2, '0');
hex += h;
}
return hex;
}
function randomHex(byteLength: number): string {
const arr = new Uint8Array(byteLength);
globalThis.crypto.getRandomValues(arr);
return toHex(arr.buffer);
}
async function pbkdf2Hex(password: string, salt: string, iterations: number, keyLength: number, digest: string): Promise<string> {
// Map Node digest names to WebCrypto ones
const algo = digest.toUpperCase() === 'SHA512' || digest.toUpperCase() === 'SHA-512' ? 'SHA-512'
: digest.toUpperCase() === 'SHA256' || digest.toUpperCase() === 'SHA-256' ? 'SHA-256'
: 'SHA-512';
const keyMaterial = await globalThis.crypto.subtle.importKey(
'raw',
te.encode(password),
'PBKDF2',
false,
['deriveBits']
);
const bits = await globalThis.crypto.subtle.deriveBits(
{
name: 'PBKDF2',
hash: algo,
iterations,
salt: te.encode(salt),
},
keyMaterial,
keyLength * 8
);
return toHex(bits);
}
/**
* Hash a password using PBKDF2 with a random salt
* @param password - The plain text password to hash
* @returns A string in the format "salt:hash"
*/
export async function hashPassword(password: string): Promise<string> {
const key = await getSecret('nextauth_secret', 'NEXTAUTH_SECRET');
if (!key) {
throw new Error('Failed to retrieve the encryption key from the secret provider');
}
const saltBytes = Number(process.env.SALT_BYTES) || 12;
const iterations = Number(process.env.ITERATIONS) || 10000;
const keyLength = Number(process.env.KEY_LENGTH) || 64;
const digest = process.env.ALGORITHM || 'sha512';
const salt = randomHex(saltBytes);
const hash = await pbkdf2Hex(password, key + salt, iterations, keyLength, digest);
return `${salt}:${hash}`;
}
/**
* Verify a password against a stored hash
* @param password - The plain text password to verify
* @param storedHash - The stored hash in the format "salt:hash"
* @returns True if the password matches, false otherwise
*/
export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
const key = await getSecret('nextauth_secret', 'NEXTAUTH_SECRET');
if (!key) {
throw new Error('Failed to retrieve the encryption key from the secret provider');
}
const iterations = Number(process.env.ITERATIONS) || 10000;
const keyLength = Number(process.env.KEY_LENGTH) || 64;
const digest = process.env.ALGORITHM || 'sha512';
if (!password || !storedHash) {
return false;
}
try {
const [salt, originalHash] = storedHash.split(':');
if (!salt || !originalHash) return false;
const hash = await pbkdf2Hex(password, key + salt, iterations, keyLength, digest);
return hash === originalHash;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('Error during password verification');
}
return false;
}
}
/**
* Generate a secure random password
* @param length - The length of the password (default: 16)
* @returns A secure random password
*/
export function generateSecurePassword(length: number = 16): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%^&';
const buf = new Uint8Array(length);
globalThis.crypto.getRandomValues(buf);
let out = '';
for (let i = 0; i < length; i++) {
out += chars[buf[i] % chars.length];
}
return out;
}