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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
120 lines
3.6 KiB
TypeScript
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;
|
|
}
|