Add frontend service with auth, MFA, and content management
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
- Node.js/Express backend with TypeScript - SQLite database for users, sessions, and content metadata - Authentication with TOTP and WebAuthn MFA support - Admin user auto-created on first startup - User content gallery with view/delete functionality - RunPod API proxy (keeps API keys server-side) - Docker setup with CI/CD for Gitea registry 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
81
frontend/src/utils/crypto.ts
Normal file
81
frontend/src/utils/crypto.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { randomBytes, createCipheriv, createDecipheriv, scrypt } from 'crypto';
|
||||
import { promisify } from 'util';
|
||||
import argon2 from 'argon2';
|
||||
import { config } from '../config.js';
|
||||
|
||||
const scryptAsync = promisify(scrypt);
|
||||
|
||||
// Password hashing with Argon2id
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return argon2.hash(password, {
|
||||
type: argon2.argon2id,
|
||||
memoryCost: 65536,
|
||||
timeCost: 3,
|
||||
parallelism: 4,
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifyPassword(hash: string, password: string): Promise<boolean> {
|
||||
try {
|
||||
return await argon2.verify(hash, password);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate cryptographically secure random strings
|
||||
export function generateSecureToken(length: number = 32): string {
|
||||
return randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
export function generateSessionId(): string {
|
||||
return randomBytes(32).toString('base64url');
|
||||
}
|
||||
|
||||
// AES-256-GCM encryption for TOTP secrets
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const IV_LENGTH = 16;
|
||||
const AUTH_TAG_LENGTH = 16;
|
||||
|
||||
async function deriveKey(): Promise<Buffer> {
|
||||
const keyHex = config.encryptionKey;
|
||||
if (keyHex.length !== 64) {
|
||||
throw new Error('ENCRYPTION_KEY must be 64 hex characters (32 bytes)');
|
||||
}
|
||||
return Buffer.from(keyHex, 'hex');
|
||||
}
|
||||
|
||||
export async function encrypt(plaintext: string): Promise<string> {
|
||||
const key = await deriveKey();
|
||||
const iv = randomBytes(IV_LENGTH);
|
||||
const cipher = createCipheriv(ALGORITHM, key, iv);
|
||||
|
||||
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Format: iv:authTag:ciphertext (all hex)
|
||||
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
||||
}
|
||||
|
||||
export async function decrypt(ciphertext: string): Promise<string> {
|
||||
const key = await deriveKey();
|
||||
const parts = ciphertext.split(':');
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid ciphertext format');
|
||||
}
|
||||
|
||||
const iv = Buffer.from(parts[0], 'hex');
|
||||
const authTag = Buffer.from(parts[1], 'hex');
|
||||
const encrypted = parts[2];
|
||||
|
||||
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
Reference in New Issue
Block a user