| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import crypto from 'crypto'; |
|
|
| const ALG = 'aes-256-gcm'; |
| const IV_LEN = 12; |
| const VERSION = 'v1'; |
|
|
| let _cachedKey: Buffer | null = null; |
|
|
| function getMasterKey(): Buffer { |
| if (_cachedKey) return _cachedKey; |
| const raw = |
| process.env.ENCRYPTION_KEY || |
| process.env.ADMIN_PASSWORD || |
| 'medos-default-dev-key-please-set-ENCRYPTION_KEY'; |
| if (!process.env.ENCRYPTION_KEY) { |
| |
| |
| console.warn( |
| '[crypto] ENCRYPTION_KEY is not set — derived a fallback key from ADMIN_PASSWORD. Set ENCRYPTION_KEY (32 random bytes hex) before going to production.', |
| ); |
| } |
| _cachedKey = crypto.createHash('sha256').update(raw).digest(); |
| return _cachedKey; |
| } |
|
|
| |
| export function encryptString(plain: string): string { |
| if (!plain) return ''; |
| const key = getMasterKey(); |
| const iv = crypto.randomBytes(IV_LEN); |
| const cipher = crypto.createCipheriv(ALG, key, iv); |
| const enc = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]); |
| const tag = cipher.getAuthTag(); |
| return `${VERSION}:${iv.toString('base64')}:${tag.toString('base64')}:${enc.toString('base64')}`; |
| } |
|
|
| |
| |
| |
| |
| |
| export function decryptString(payload: string): string { |
| if (!payload) return ''; |
| const parts = payload.split(':'); |
| if (parts[0] !== VERSION || parts.length !== 4) return payload; |
| try { |
| const [, ivB, tagB, dataB] = parts; |
| const key = getMasterKey(); |
| const iv = Buffer.from(ivB, 'base64'); |
| const tag = Buffer.from(tagB, 'base64'); |
| const data = Buffer.from(dataB, 'base64'); |
| const decipher = crypto.createDecipheriv(ALG, key, iv); |
| decipher.setAuthTag(tag); |
| const dec = Buffer.concat([decipher.update(data), decipher.final()]); |
| return dec.toString('utf8'); |
| } catch (e: any) { |
| console.error('[crypto] decryptString failed:', e?.message); |
| return ''; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function redact(value: string | undefined | null, keepLast = 4): string { |
| if (!value) return ''; |
| if (value.length <= keepLast) return '•'.repeat(8); |
| return `${'•'.repeat(8)}${value.slice(-keepLast)}`; |
| } |
|
|
| |
| export function safeEqual(a: string, b: string): boolean { |
| const ab = Buffer.from(a, 'utf8'); |
| const bb = Buffer.from(b, 'utf8'); |
| if (ab.length !== bb.length) return false; |
| return crypto.timingSafeEqual(ab, bb); |
| } |
|
|