UNPKG

cross-crypto-ts

Version:

Cifrado híbrido AES-GCM + RSA-OAEP con interoperabilidad entre TypeScript y Python, con diseño compatible para Rust.

285 lines (284 loc) 11.1 kB
"use strict"; // src/decrypt.ts var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.decryptHybrid = decryptHybrid; const node_forge_1 = __importDefault(require("node-forge")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const v8_1 = __importDefault(require("v8")); const GCM_TAG_BITS = 128; const STREAM_ENVELOPE_MAGIC = Buffer.from("CCRYPT2\n", "utf8"); const STREAM_ENVELOPE_VERSION = 2; function resolveOaepHash(encrypted, explicit) { const resolved = explicit ?? encrypted.oaepHash ?? "sha256"; if (resolved !== "sha1" && resolved !== "sha256") { throw new Error("oaepHash debe ser 'sha1' o 'sha256'."); } return resolved; } function getForgeMd(hash) { if (hash === "sha256") return node_forge_1.default.md.sha256.create(); return node_forge_1.default.md.sha1.create(); } function normalizeAad(aad) { if (aad === undefined || aad === null) return undefined; if (typeof aad === "string") { return Buffer.from(aad, "utf8").toString("binary"); } if (aad instanceof Uint8Array) { return Buffer.from(aad).toString("binary"); } return Buffer.from(JSON.stringify(aad), "utf8").toString("binary"); } function isStreamEnvelopeFile(filePath) { try { if (!fs_1.default.existsSync(filePath) || !fs_1.default.statSync(filePath).isFile()) { return false; } const fd = fs_1.default.openSync(filePath, "r"); const magic = Buffer.alloc(STREAM_ENVELOPE_MAGIC.length); try { const n = fs_1.default.readSync(fd, magic, 0, magic.length, 0); return n === magic.length && magic.equals(STREAM_ENVELOPE_MAGIC); } finally { fs_1.default.closeSync(fd); } } catch { return false; } } function readStreamEnvelopeHeader(filePath) { const fd = fs_1.default.openSync(filePath, "r"); try { const magic = Buffer.alloc(STREAM_ENVELOPE_MAGIC.length); const magicRead = fs_1.default.readSync(fd, magic, 0, magic.length, 0); if (magicRead !== magic.length || !magic.equals(STREAM_ENVELOPE_MAGIC)) { throw new Error("Archivo stream inválido: magic no coincide."); } const lenBuf = Buffer.alloc(4); const lenRead = fs_1.default.readSync(fd, lenBuf, 0, 4, STREAM_ENVELOPE_MAGIC.length); if (lenRead !== 4) { throw new Error("Archivo stream inválido: header length incompleto."); } const headerLen = lenBuf.readUInt32BE(0); if (headerLen <= 0) { throw new Error("Archivo stream inválido: header vacío."); } const headerBuf = Buffer.alloc(headerLen); const headerRead = fs_1.default.readSync(fd, headerBuf, 0, headerLen, STREAM_ENVELOPE_MAGIC.length + 4); if (headerRead !== headerLen) { throw new Error("Archivo stream inválido: header incompleto."); } const parsed = JSON.parse(headerBuf.toString("utf8")); if (!parsed || typeof parsed !== "object") { throw new Error("Archivo stream inválido: header no es objeto JSON."); } const header = parsed; if (header.version !== STREAM_ENVELOPE_VERSION) { throw new Error(`Versión de stream no soportada: ${header.version}`); } if (header.format !== "cross-crypto-stream") { throw new Error("Formato de stream no soportado."); } if (header.streamFormat !== "envelope") { throw new Error("streamFormat no soportado."); } return { header, ciphertextOffset: STREAM_ENVELOPE_MAGIC.length + 4 + headerLen, }; } finally { fs_1.default.closeSync(fd); } } function decryptAesKey(params) { const privateKey = node_forge_1.default.pki.privateKeyFromPem(params.privateKeyPem); const md = getForgeMd(params.oaepHash); return privateKey.decrypt(node_forge_1.default.util.decode64(params.encryptedKeyB64), "RSA-OAEP", { md, mgf1: { md }, }); } function decryptStreamEnvelope(params) { const { header, ciphertextOffset } = readStreamEnvelopeHeader(params.envelopePath); const oaepHash = resolveOaepHash(header, params.options.oaepHash); const aadBytes = normalizeAad(params.options.aad); const aesKey = decryptAesKey({ encryptedKeyB64: header.encryptedKey, privateKeyPem: params.privateKeyPem, oaepHash, }); const nonce = node_forge_1.default.util.decode64(header.nonce); const tag = node_forge_1.default.util.decode64(header.tag); const decipher = node_forge_1.default.cipher.createDecipher("AES-GCM", aesKey); decipher.start({ iv: node_forge_1.default.util.createBuffer(nonce), tag: node_forge_1.default.util.createBuffer(tag), tagLength: GCM_TAG_BITS, additionalData: aadBytes, }); const fdIn = fs_1.default.openSync(params.envelopePath, "r"); const chunkSize = 64 * 1024; const buffer = Buffer.alloc(chunkSize); if (params.options.returnBytes) { const chunks = []; try { let pos = ciphertextOffset; while (true) { const n = fs_1.default.readSync(fdIn, buffer, 0, buffer.length, pos); if (!n) break; pos += n; decipher.update(node_forge_1.default.util.createBuffer(buffer.subarray(0, n).toString("binary"))); const outBytes = decipher.output.getBytes(); if (outBytes.length > 0) { chunks.push(Buffer.from(outBytes, "binary")); } } const success = decipher.finish(); if (!success) throw new Error("Tag inválido."); const finalBytes = decipher.output.getBytes(); if (finalBytes.length > 0) { chunks.push(Buffer.from(finalBytes, "binary")); } return Buffer.concat(chunks); } finally { fs_1.default.closeSync(fdIn); } } const finalPath = params.outPath || params.envelopePath.replace(/\.ccenc$/, ".dec"); fs_1.default.mkdirSync(path_1.default.dirname(finalPath) || ".", { recursive: true }); const tmpPath = `${finalPath}.tmp-${Date.now()}-${Math.random() .toString(16) .slice(2)}`; const fdOut = fs_1.default.openSync(tmpPath, "w"); try { let pos = ciphertextOffset; while (true) { const n = fs_1.default.readSync(fdIn, buffer, 0, buffer.length, pos); if (!n) break; pos += n; decipher.update(node_forge_1.default.util.createBuffer(buffer.subarray(0, n).toString("binary"))); const outBytes = decipher.output.getBytes(); if (outBytes.length > 0) { fs_1.default.writeSync(fdOut, Buffer.from(outBytes, "binary")); } } const success = decipher.finish(); if (!success) throw new Error("Tag inválido."); const finalBytes = decipher.output.getBytes(); if (finalBytes.length > 0) { fs_1.default.writeSync(fdOut, Buffer.from(finalBytes, "binary")); } fs_1.default.closeSync(fdOut); fs_1.default.closeSync(fdIn); fs_1.default.renameSync(tmpPath, finalPath); return finalPath; } catch (error) { try { fs_1.default.closeSync(fdOut); } catch { // noop } try { fs_1.default.closeSync(fdIn); } catch { // noop } if (fs_1.default.existsSync(tmpPath)) { try { fs_1.default.unlinkSync(tmpPath); } catch { // noop } } throw error; } } function decryptHybrid(encrypted, privateKeyPem, outPathOrOptions, maybeOptions = {}) { try { const outPath = typeof outPathOrOptions === "string" ? outPathOrOptions : undefined; const options = typeof outPathOrOptions === "object" && outPathOrOptions !== null ? outPathOrOptions : maybeOptions; if (typeof encrypted === "string") { if (!isStreamEnvelopeFile(encrypted)) { throw new Error("Ruta stream inválida: no es un envelope .ccenc."); } return decryptStreamEnvelope({ envelopePath: encrypted, privateKeyPem, outPath, options, }); } if (encrypted.mode === "stream") { if (!encrypted.encryptedPath) { throw new Error("Path no encontrado en payload stream."); } if (!isStreamEnvelopeFile(encrypted.encryptedPath)) { throw new Error("El modo stream v2 requiere un archivo envelope .ccenc válido."); } return decryptStreamEnvelope({ envelopePath: encrypted.encryptedPath, privateKeyPem, outPath, options, }); } const oaepHash = resolveOaepHash(encrypted, options.oaepHash); const aadBytes = normalizeAad(options.aad); const aesKey = decryptAesKey({ encryptedKeyB64: encrypted.encryptedKey, privateKeyPem, oaepHash, }); const nonce = node_forge_1.default.util.decode64(encrypted.nonce); const tag = node_forge_1.default.util.decode64(encrypted.tag); const decipher = node_forge_1.default.cipher.createDecipher("AES-GCM", aesKey); decipher.start({ iv: node_forge_1.default.util.createBuffer(nonce), tag: node_forge_1.default.util.createBuffer(tag), tagLength: GCM_TAG_BITS, additionalData: aadBytes, }); const encryptedDataBytes = node_forge_1.default.util.decode64(encrypted.encryptedData); decipher.update(node_forge_1.default.util.createBuffer(encryptedDataBytes)); const success = decipher.finish(); if (!success) throw new Error("Tag inválido."); if (encrypted.mode === "json") { return JSON.parse(decipher.output.toString()); } if (encrypted.mode === "binary") { return Buffer.from(decipher.output.getBytes(), "binary"); } if (encrypted.mode === "v8") { const buffer = Buffer.from(decipher.output.getBytes(), "binary"); return v8_1.default.deserialize(buffer); } throw new Error(`Modo desconocido: ${encrypted}`); } catch (error) { throw new Error(error instanceof Error ? `Desencriptación fallida: ${error.message}` : "Desencriptación fallida"); } }