Spaces:
Runtime error
Runtime error
| import crypto from 'crypto'; | |
| import fs from 'fs/promises'; | |
| import path from 'path'; | |
| const JSON_FORMAT_VERSION = 2; | |
| const BINARY_FORMAT_VERSION = 1; | |
| const ALGORITHM = 'aes-256-gcm'; | |
| const KEY_LENGTH = 32; | |
| const IV_LENGTH = 12; | |
| const AUTH_TAG_LENGTH = 16; | |
| function getKey() { | |
| const keyEnv = process.env.DATA_ENCRYPTION_KEY; | |
| if (!keyEnv) throw new Error('DATA_ENCRYPTION_KEY environment variable not set'); | |
| return crypto.createHash('sha256').update(keyEnv).digest().subarray(0, KEY_LENGTH); | |
| } | |
| export function createLookupHash(value, namespace = 'lookup') { | |
| return crypto | |
| .createHmac('sha256', getKey()) | |
| .update(`${namespace}:${String(value ?? '')}`) | |
| .digest('hex'); | |
| } | |
| function normalizeAad(aad = '') { | |
| if (Buffer.isBuffer(aad)) return aad; | |
| return Buffer.from(String(aad || ''), 'utf8'); | |
| } | |
| export function encryptBuffer(buffer, aad = '') { | |
| const key = getKey(); | |
| const iv = crypto.randomBytes(IV_LENGTH); | |
| const cipher = crypto.createCipheriv(ALGORITHM, key, iv); | |
| const aadBuffer = normalizeAad(aad); | |
| if (aadBuffer.length) cipher.setAAD(aadBuffer); | |
| const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]); | |
| const authTag = cipher.getAuthTag(); | |
| return { | |
| version: BINARY_FORMAT_VERSION, | |
| iv, | |
| authTag, | |
| encrypted, | |
| }; | |
| } | |
| export function decryptBuffer(payload, aad = '') { | |
| const key = getKey(); | |
| const iv = Buffer.isBuffer(payload?.iv) ? payload.iv : Buffer.from(payload?.iv || '', 'hex'); | |
| const authTag = Buffer.isBuffer(payload?.authTag) | |
| ? payload.authTag | |
| : Buffer.from(payload?.authTag || '', 'hex'); | |
| const encrypted = Buffer.isBuffer(payload?.encrypted) | |
| ? payload.encrypted | |
| : Buffer.from(payload?.encrypted || '', 'hex'); | |
| const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); | |
| const aadBuffer = normalizeAad(aad); | |
| if (aadBuffer.length) decipher.setAAD(aadBuffer); | |
| decipher.setAuthTag(authTag); | |
| return Buffer.concat([decipher.update(encrypted), decipher.final()]); | |
| } | |
| export function packEncryptedBuffer(payload) { | |
| const header = Buffer.allocUnsafe(1 + 1 + payload.iv.length + payload.authTag.length); | |
| header.writeUInt8(payload.version || BINARY_FORMAT_VERSION, 0); | |
| header.writeUInt8(payload.iv.length, 1); | |
| payload.iv.copy(header, 2); | |
| payload.authTag.copy(header, 2 + payload.iv.length); | |
| return Buffer.concat([header, payload.encrypted]); | |
| } | |
| export function unpackEncryptedBuffer(buffer) { | |
| const version = buffer.readUInt8(0); | |
| if (version !== BINARY_FORMAT_VERSION) { | |
| throw new Error(`Unsupported encrypted buffer version: ${version}`); | |
| } | |
| const ivLength = buffer.readUInt8(1); | |
| const ivStart = 2; | |
| const ivEnd = ivStart + ivLength; | |
| const tagEnd = ivEnd + AUTH_TAG_LENGTH; | |
| return { | |
| version, | |
| iv: buffer.subarray(ivStart, ivEnd), | |
| authTag: buffer.subarray(ivEnd, tagEnd), | |
| encrypted: buffer.subarray(tagEnd), | |
| }; | |
| } | |
| export async function writeEncryptedFile(filePath, buffer, aad = '') { | |
| await fs.mkdir(path.dirname(filePath), { recursive: true }); | |
| const payload = encryptBuffer(buffer, aad); | |
| await fs.writeFile(filePath, packEncryptedBuffer(payload)); | |
| } | |
| export async function readEncryptedFile(filePath, aad = '') { | |
| const packed = await fs.readFile(filePath); | |
| return decryptBuffer(unpackEncryptedBuffer(packed), aad); | |
| } | |
| function legacyDecryptJson(encryptedData) { | |
| const key = getKey(); | |
| const decipher = crypto.createDecipher(ALGORITHM, key); | |
| decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex')); | |
| let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8'); | |
| decrypted += decipher.final('utf8'); | |
| return JSON.parse(decrypted); | |
| } | |
| export function encryptJson(data, aad = '') { | |
| const payload = encryptBuffer(Buffer.from(JSON.stringify(data), 'utf8'), aad); | |
| return { | |
| version: JSON_FORMAT_VERSION, | |
| iv: payload.iv.toString('hex'), | |
| authTag: payload.authTag.toString('hex'), | |
| encrypted: payload.encrypted.toString('hex'), | |
| }; | |
| } | |
| export function decryptJson(encryptedData, aad = '') { | |
| if (!encryptedData) return null; | |
| if ((encryptedData.version || 0) >= JSON_FORMAT_VERSION) { | |
| const decrypted = decryptBuffer(encryptedData, aad); | |
| return JSON.parse(decrypted.toString('utf8')); | |
| } | |
| return legacyDecryptJson(encryptedData); | |
| } | |
| export async function saveEncryptedJson(filePath, data, aad = '') { | |
| const encrypted = encryptJson(data, aad); | |
| await fs.mkdir(path.dirname(filePath), { recursive: true }); | |
| await fs.writeFile(filePath, JSON.stringify(encrypted, null, 2), 'utf8'); | |
| } | |
| export async function loadEncryptedJson(filePath, aad = '') { | |
| try { | |
| const content = await fs.readFile(filePath, 'utf8'); | |
| const encrypted = JSON.parse(content); | |
| return decryptJson(encrypted, aad); | |
| } catch (err) { | |
| if (err.code === 'ENOENT') return null; | |
| throw err; | |
| } | |
| } | |