UNPKG

cross-crypto-ts

Version:

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

236 lines (235 loc) 8.92 kB
"use strict"; // src/encrypt.ts var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.encryptHybrid = encryptHybrid; const node_forge_1 = __importDefault(require("node-forge")); const fs_1 = __importDefault(require("fs")); const os_1 = __importDefault(require("os")); const path_1 = __importDefault(require("path")); const v8_1 = __importDefault(require("v8")); const AES_KEY_SIZE = 32; const AES_NONCE_SIZE = 12; const GCM_TAG_BITS = 128; const STREAM_ENVELOPE_MAGIC = Buffer.from("CCRYPT2\n", "utf8"); const STREAM_ENVELOPE_VERSION = 2; function resolveOaepHash(value) { const resolved = value ?? "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 bufferToBinaryString(data) { return Buffer.from(data).toString("binary"); } function serializeData(data, mode) { if (mode === "json") { if (typeof data !== "object" || data instanceof Uint8Array || Buffer.isBuffer(data)) { throw new TypeError("En modo 'json', data debe ser un objeto JSON."); } return Buffer.from(JSON.stringify(data), "utf8").toString("binary"); } if (mode === "v8") { return bufferToBinaryString(v8_1.default.serialize(data)); } if (mode === "binary") { if (typeof data === "string") { return Buffer.from(data, "utf8").toString("binary"); } if (data instanceof Uint8Array || Buffer.isBuffer(data)) { return bufferToBinaryString(data); } throw new TypeError("En modo 'binary', data debe ser string, Buffer o Uint8Array."); } throw new Error(`Modo de cifrado no soportado: ${mode}`); } function writeStreamEnvelope(params) { const header = { version: STREAM_ENVELOPE_VERSION, format: "cross-crypto-stream", streamFormat: "envelope", cipher: "AES-256-GCM", keyWrap: "RSA-OAEP", encryptedKey: params.encryptedKey, nonce: params.nonce, tag: params.tag, mode: "stream", contentMode: params.contentMode, aad: params.aadPresent ? "present" : "none", oaepHash: params.oaepHash, }; const headerBytes = Buffer.from(JSON.stringify(header), "utf8"); if (headerBytes.byteLength > 0xffffffff) { throw new Error("Header de stream demasiado grande."); } fs_1.default.mkdirSync(path_1.default.dirname(params.outputPath) || ".", { recursive: true }); const headerLen = Buffer.alloc(4); headerLen.writeUInt32BE(headerBytes.byteLength, 0); const fdOut = fs_1.default.openSync(params.outputPath, "w"); const fdCipher = fs_1.default.openSync(params.ciphertextPath, "r"); try { fs_1.default.writeSync(fdOut, STREAM_ENVELOPE_MAGIC); fs_1.default.writeSync(fdOut, headerLen); fs_1.default.writeSync(fdOut, headerBytes); const buffer = Buffer.alloc(1024 * 1024); while (true) { const n = fs_1.default.readSync(fdCipher, buffer, 0, buffer.length, null); if (!n) break; fs_1.default.writeSync(fdOut, buffer.subarray(0, n)); } } finally { fs_1.default.closeSync(fdCipher); fs_1.default.closeSync(fdOut); } } function makeTempCipherPath() { const name = `cross_crypto_stream_${Date.now()}_${Math.random() .toString(16) .slice(2)}.cipher`; return path_1.default.join(os_1.default.tmpdir(), name); } function encryptFileToTempCiphertext(params) { const cipher = node_forge_1.default.cipher.createCipher("AES-GCM", params.aesKey); cipher.start({ iv: params.nonce, tagLength: GCM_TAG_BITS, additionalData: params.aadBytes, }); const fdIn = fs_1.default.openSync(params.inputPath, "r"); const fdOut = fs_1.default.openSync(params.tempCipherPath, "w"); const buffer = Buffer.alloc(params.chunkSize); try { while (true) { const n = fs_1.default.readSync(fdIn, buffer, 0, buffer.length, null); if (!n) break; const chunk = buffer.subarray(0, n); cipher.update(node_forge_1.default.util.createBuffer(chunk.toString("binary"))); const outBytes = cipher.output.getBytes(); if (outBytes.length > 0) { fs_1.default.writeSync(fdOut, Buffer.from(outBytes, "binary")); } } const success = cipher.finish(); if (!success) { throw new Error("No se pudo finalizar AES-GCM stream."); } const finalBytes = cipher.output.getBytes(); if (finalBytes.length > 0) { fs_1.default.writeSync(fdOut, Buffer.from(finalBytes, "binary")); } return cipher.mode.tag.getBytes(); } finally { fs_1.default.closeSync(fdIn); fs_1.default.closeSync(fdOut); } } function encryptHybrid(data, publicKeyPem, mode = "json", options = {}) { const oaepHash = resolveOaepHash(options.oaepHash); const aadBytes = normalizeAad(options.aad); const aesKey = node_forge_1.default.random.getBytesSync(AES_KEY_SIZE); const nonce = node_forge_1.default.random.getBytesSync(AES_NONCE_SIZE); const publicKey = node_forge_1.default.pki.publicKeyFromPem(publicKeyPem); const md = getForgeMd(oaepHash); const encryptedAesKey = publicKey.encrypt(aesKey, "RSA-OAEP", { md, mgf1: { md }, }); const encryptedKeyB64 = node_forge_1.default.util.encode64(encryptedAesKey); const nonceB64 = node_forge_1.default.util.encode64(nonce); if (mode === "stream") { if (typeof data !== "string") { throw new TypeError("Stream mode requiere path al archivo."); } if (!fs_1.default.existsSync(data) || !fs_1.default.statSync(data).isFile()) { throw new Error(`Archivo no encontrado para stream: ${data}`); } const inputPath = data; const finalPath = options.outputPath ?? inputPath + ".ccenc"; const contentMode = options.contentMode ?? "binary"; const chunkSize = options.streamChunkSize ?? 64 * 1024; const tempCipherPath = makeTempCipherPath(); try { const tag = encryptFileToTempCiphertext({ inputPath, tempCipherPath, aesKey, nonce, aadBytes, chunkSize, }); const tagB64 = node_forge_1.default.util.encode64(tag); writeStreamEnvelope({ outputPath: finalPath, encryptedKey: encryptedKeyB64, nonce: nonceB64, tag: tagB64, oaepHash, aadPresent: aadBytes !== undefined, contentMode, ciphertextPath: tempCipherPath, }); return { encryptedKey: encryptedKeyB64, nonce: nonceB64, tag: tagB64, encryptedPath: finalPath, mode: "stream", contentMode, streamFormat: "envelope", aad: aadBytes ? "present" : "none", oaepHash, }; } finally { if (fs_1.default.existsSync(tempCipherPath)) { fs_1.default.unlinkSync(tempCipherPath); } } } const plaintext = serializeData(data, mode); const cipher = node_forge_1.default.cipher.createCipher("AES-GCM", aesKey); cipher.start({ iv: nonce, tagLength: GCM_TAG_BITS, additionalData: aadBytes, }); cipher.update(node_forge_1.default.util.createBuffer(plaintext)); const success = cipher.finish(); if (!success) { throw new Error("No se pudo finalizar AES-GCM."); } const encrypted = cipher.output.getBytes(); const tag = cipher.mode.tag.getBytes(); return { encryptedKey: encryptedKeyB64, encryptedData: node_forge_1.default.util.encode64(encrypted), nonce: nonceB64, tag: node_forge_1.default.util.encode64(tag), mode, aad: aadBytes ? "present" : "none", oaepHash, }; }