Spaces:
Runtime error
Runtime error
File size: 4,793 Bytes
7f5e478 15464c7 7f5e478 15464c7 7f5e478 15464c7 7f5e478 bff1056 15464c7 7f5e478 15464c7 7f5e478 15464c7 7f5e478 15464c7 7f5e478 15464c7 7f5e478 15464c7 7f5e478 15464c7 7f5e478 15464c7 7f5e478 15464c7 7f5e478 15464c7 7f5e478 15464c7 7f5e478 15464c7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | 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;
}
}
|