File size: 4,603 Bytes
3eeb0cf 0f84d64 3eeb0cf 0f84d64 3eeb0cf 0f84d64 3eeb0cf 0f84d64 3eeb0cf 0f84d64 3eeb0cf 0f84d64 3eeb0cf f3eb8b7 0f84d64 3eeb0cf 0f84d64 f3eb8b7 0f84d64 3eeb0cf f3eb8b7 0f84d64 3eeb0cf 0f84d64 f3eb8b7 0f84d64 3eeb0cf 0f84d64 3eeb0cf 0f84d64 3eeb0cf 0f84d64 | 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 | 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);
}
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;
}
}
|