UNPKG

cross-crypto-ts

Version:

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

660 lines (501 loc) 14.3 kB
# Cross Crypto TS ![NPM Version](https://img.shields.io/npm/v/cross-crypto-ts) ![License](https://img.shields.io/github/license/acadyne/cross-crypto-ts) ![Build](https://img.shields.io/badge/build-passing-brightgreen) ![TypeScript](https://img.shields.io/badge/language-TypeScript-blue) **Cifrado híbrido interoperable entre TypeScript y Python, con diseño compatible para Rust.** Cross Crypto TS combina: - **AES-256-GCM** para cifrado autenticado. - **RSA-OAEP** para envolver la clave simétrica. - **RSA-OAEP SHA-256 por defecto desde v2.0.0**. - Compatibilidad legacy con **RSA-OAEP SHA-1**. - Soporte opcional para **AAD**. - Firmas **Ed25519** para autenticidad de payloads JSON. - Soporte para JSON, binario, archivos, carpetas ZIP y stream portable `.ccenc`. --- ## Introducción Cross Crypto TS es una librería de cifrado híbrido diseñada para interoperar entre distintos lenguajes, especialmente **TypeScript ↔ Python**, con un contrato de formato pensado para extenderse a Rust. Permite cifrar datos en un lenguaje y descifrarlos en otro manteniendo un formato de sobre estable. Para datos en memoria, el sobre cifrado usa JSON: ```json { "encryptedKey": "...", "encryptedData": "...", "nonce": "...", "tag": "...", "mode": "json", "aad": "none", "oaepHash": "sha256" } ``` Desde la versión `2.0.0`, los paquetes cifrados incluyen el campo `oaepHash`: ```json { "oaepHash": "sha256" } ``` Esto permite que el receptor sepa con qué hash OAEP debe descifrar la clave simétrica. Para stream, desde `2.0.0`, se usa un archivo binario portable `.ccenc` con header embebido. ## Nota importante sobre seguridad Esta librería ofrece cifrado autenticado con **AES-GCM** y envoltura de clave con **RSA-OAEP**. Eso significa: - El contenido viaja cifrado. - Cualquier modificación del `ciphertext`, `tag`, `AAD` o clave cifrada debe fallar. - Solo quien tenga la clave privada RSA puede descifrar. - Con **AAD** puedes autenticar metadatos externos sin cifrarlos. - Con **Ed25519** puedes firmar payloads JSON para verificar identidad/autenticidad del emisor. Pero: - No es “seguridad bidireccional” automáticamente si ambas partes no gestionan sus propias claves correctamente. - No es un protocolo completo de mensajería E2E con doble ratchet, forward secrecy o rotación automática de claves. - El modo `v8` es específico de Node.js y no debe asumirse interoperable con Python/Rust. - Si usas AAD como objeto JSON entre lenguajes, ambos lados deben producir exactamente los mismos bytes. Para máxima interoperabilidad, usa AAD como string o bytes cuando sea crítico. ## Instalación ```bash npm install cross-crypto-ts ``` ## Requisitos - Node.js >= 18 - TypeScript >= 5 recomendado - Entorno Node.js CommonJS ## Uso básico: JSON en memoria ```ts import { generateRSAKeys, encryptHybrid, decryptHybrid, } from "cross-crypto-ts"; const { publicKey, privateKey } = generateRSAKeys(4096); const payload = { mensaje: "Hola desde TypeScript", ok: true, }; const encrypted = encryptHybrid(payload, publicKey); console.log(encrypted.mode); // json console.log(encrypted.oaepHash); // sha256 const decrypted = decryptHybrid(encrypted, privateKey); console.log(decrypted); ``` Salida esperada: ```json { "mensaje": "Hola desde TypeScript", "ok": true } ``` ## RSA-OAEP SHA-256 por defecto Desde `2.0.0`, el default es: ```ts const encrypted = encryptHybrid(data, publicKey, "json", { oaepHash: "sha256", }); ``` Y el descifrado detecta automáticamente el campo `oaepHash` si está presente: ```ts const decrypted = decryptHybrid(encrypted, privateKey); ``` No necesitas pasar `oaepHash` manualmente cuando el paquete trae: ```json { "oaepHash": "sha256" } ``` ## Compatibilidad legacy con SHA-1 Para cifrar en modo legacy: ```ts const encrypted = encryptHybrid( { legacy: true }, publicKey, "json", { oaepHash: "sha1", } ); ``` Si el paquete trae: ```json { "oaepHash": "sha1" } ``` entonces `decryptHybrid(...)` lo detecta automáticamente. Para paquetes viejos sin `oaepHash`, puedes forzar el hash al descifrar: ```ts const decrypted = decryptHybrid( encryptedLegacy, privateKey, { oaepHash: "sha1", } ); ``` ## Modos soportados ### JSON ```ts const encrypted = encryptHybrid( { hello: "world" }, publicKey, "json" ); const decrypted = decryptHybrid( encrypted, privateKey ); ``` ### Binario ```ts import fs from "fs"; import { encryptHybrid, decryptHybrid } from "cross-crypto-ts"; const data = fs.readFileSync("foto.png"); const encrypted = encryptHybrid( data, publicKey, "binary" ); const decrypted = decryptHybrid( encrypted, privateKey ); fs.writeFileSync("foto_restaurada.png", decrypted); ``` ### V8 ```ts const encrypted = encryptHybrid( { complex: true, items: [1, 2, 3] }, publicKey, "v8" ); const decrypted = decryptHybrid( encrypted, privateKey ); ``` `mode="v8"` usa serialización interna de Node.js. No está pensado como formato interoperable con Python/Rust. ## AAD: datos autenticados no cifrados Puedes pasar **AAD** para autenticar metadatos externos. El AAD no se cifra, pero sí queda protegido por el tag **AES-GCM**. Si el receptor usa un AAD diferente, el descifrado falla. ```ts const aad = { tenant: "acadyne", purpose: "test", }; const encrypted = encryptHybrid( { msg: "hola" }, publicKey, "json", { aad } ); const decrypted = decryptHybrid( encrypted, privateKey, { aad } ); ``` AAD incorrecto: ```ts decryptHybrid( encrypted, privateKey, { aad: { tenant: "otro" } } ); ``` Debe fallar con error de autenticación. Para interoperabilidad TypeScript ↔ Python, usa preferentemente AAD como string estable: ```ts const aad = "tenant=acadyne;purpose=test"; ``` ## Cifrado híbrido de archivos y carpetas `encryptFileHybrid` empaqueta archivos/carpetas en un ZIP y cifra ese ZIP. Por defecto usa modo no-stream: el ZIP se cifra como binario y se puede guardar en JSON `.enc.json`. ```ts import { encryptFileHybrid, decryptFileHybrid, } from "cross-crypto-ts"; const encrypted = encryptFileHybrid( ["datos/", "documento.pdf"], publicKey, { saveFile: true, outputEnc: "datos.enc.json", attachMetadata: true, } ); const outputDir = decryptFileHybrid( "datos.enc.json", privateKey, "datos_descifrados" ); console.log("Archivos restaurados en:", outputDir); ``` ## Cifrado de archivos con OAEP SHA-256 Por defecto: ```ts const encrypted = encryptFileHybrid( ["datos/"], publicKey, { saveFile: true, outputEnc: "datos.enc.json", } ); ``` equivale a: ```ts const encrypted = encryptFileHybrid( ["datos/"], publicKey, { saveFile: true, outputEnc: "datos.enc.json", oaepHash: "sha256", } ); ``` Para compatibilidad legacy: ```ts const encrypted = encryptFileHybrid( ["datos/"], publicKey, { saveFile: true, outputEnc: "datos_legacy.enc.json", oaepHash: "sha1", } ); ``` ## Modo streaming portable `.ccenc` para archivos grandes Desde `2.0.0`, el modo stream produce un archivo binario portable `.ccenc`. El archivo contiene: - Magic header `CCRYPT2\n`. - Longitud del header en 4 bytes big-endian. - Header JSON embebido con `encryptedKey`, `nonce`, `tag`, `oaepHash`, `streamFormat`, `aad` y `contentMode`. - Ciphertext **AES-GCM** después del header. Esto permite que Python y TypeScript puedan descifrar el mismo archivo stream sin depender de un JSON externo. ```ts const encrypted = encryptHybrid( "video.mp4", publicKey, "stream", { outputPath: "video.mp4.ccenc", contentMode: "binary", } ); console.log(encrypted.encryptedPath); // video.mp4.ccenc const outputPath = decryptHybrid( encrypted, privateKey, "video_restaurado.mp4" ); console.log("Restaurado en:", outputPath); ``` También puedes descifrar pasando directamente la ruta `.ccenc`: ```ts const outputPath = decryptHybrid( "video.mp4.ccenc", privateKey, "video_restaurado.mp4" ); ``` También puedes devolver bytes en memoria para stream: ```ts const bytes = decryptHybrid( "video.mp4.ccenc", privateKey, { returnBytes: true, } ); console.log(Buffer.isBuffer(bytes)); // true ``` ## Stream en archivos/carpetas con `encryptFileHybrid` Para archivos/carpetas, `encryptFileHybrid` primero crea un ZIP temporal y luego puede cifrar ese ZIP en modo stream `.ccenc`. ```ts const encrypted = encryptFileHybrid( ["datos/"], publicKey, { useStream: true, outputEnc: "datos.ccenc", attachMetadata: true, streamChunkSize: 64 * 1024, } ); const outputDir = decryptFileHybrid( "datos.ccenc", privateKey, "datos_descifrados" ); ``` En este modo: - `outputEnc` debe apuntar normalmente a `.ccenc`. - No necesitas `saveFile: true`, porque el stream escribe directamente el archivo binario. - El resultado de `encryptFileHybrid(...)` sigue devolviendo metadata del sobre para inspección. ## Firmas Ed25519 para payloads JSON Además del cifrado, puedes firmar payloads JSON con **Ed25519**. Esto sirve para verificar que un payload fue emitido por quien posee la clave privada de firma. ```ts import { generateEd25519Keys, signPayload, verifyPayload, } from "cross-crypto-ts"; const keys = generateEd25519Keys(); const payload = { user: "fabian", scope: "admin", }; const signature = await signPayload( payload, keys.privateKey, { keyId: "v1", } ); const ok = await verifyPayload( payload, signature, keys.publicKey ); console.log(ok); // true ``` ## Verificación con expiración Puedes limitar la edad aceptada de una firma: ```ts const ok = await verifyPayload( payload, signature, keys.publicKey, { maxAgeSeconds: 300, } ); ``` Esto rechaza firmas demasiado antiguas o con timestamps demasiado adelantados. ## Fingerprint de claves públicas ```ts import { fingerprintPublicKey } from "cross-crypto-ts"; const fp = fingerprintPublicKey(keys.publicKey); console.log(fp); ``` El fingerprint se calcula sobre los bytes DER contenidos en la clave pública PEM. ## Interoperabilidad TypeScript ↔ Python El subconjunto interoperable entre TypeScript y Python es: - `mode="json"` - `mode="binary"` - archivos/carpetas empaquetados como ZIP - stream portable `.ccenc` - AAD cuando ambos lados usan exactamente los mismos bytes - RSA-OAEP SHA-256 / SHA-1 según `oaepHash` - firmas Ed25519 sobre JSON canónico `mode="v8"` es solo Node.js. ### Sobre JSON para datos en memoria ```json { "encryptedKey": "base64", "encryptedData": "base64", "nonce": "base64", "tag": "base64", "mode": "json | binary | v8", "aad": "present | none", "oaepHash": "sha1 | sha256" } ``` Para interoperabilidad TypeScript ↔ Python, usa normalmente: ```json { "mode": "json | binary" } ``` ### Stream portable `.ccenc` Para stream portable `.ccenc`, el contrato va embebido dentro del archivo: ```json { "version": 2, "format": "cross-crypto-stream", "streamFormat": "envelope", "cipher": "AES-256-GCM", "keyWrap": "RSA-OAEP", "encryptedKey": "base64", "nonce": "base64", "tag": "base64", "mode": "stream", "contentMode": "binary", "aad": "present | none", "oaepHash": "sha1 | sha256" } ``` El formato binario del `.ccenc` es: ```text CCRYPT2\n uint32_be(header_json_length) header_json_utf8 ciphertext ``` Reglas importantes: - `oaepHash` debe viajar en el sobre o header. - Si `aad` es `"present"`, ambos lados deben usar exactamente el mismo AAD. - JSON cifrado en memoria se serializa como UTF-8. - Las firmas Ed25519 usan JSON canónico con claves ordenadas. - Binario debe tratarse como bytes crudos, no como texto UTF-8. - `v8` solo es compatible con Node.js. - Para stream interoperable usa `.ccenc`, normalmente con `contentMode: "binary"`. ## Ejemplo de roundtrip SHA-256 ```ts const keys = generateRSAKeys(2048); const encrypted = encryptHybrid( { ok: true }, keys.publicKey ); if (encrypted.oaepHash !== "sha256") { throw new Error("OAEP hash inesperado"); } const decrypted = decryptHybrid( encrypted, keys.privateKey ); console.log(decrypted); // { ok: true } ``` ## Ejemplo de roundtrip legacy SHA-1 ```ts const keys = generateRSAKeys(2048); const encrypted = encryptHybrid( { legacy: true }, keys.publicKey, "json", { oaepHash: "sha1", } ); if (encrypted.oaepHash !== "sha1") { throw new Error("OAEP hash inesperado"); } const decrypted = decryptHybrid( encrypted, keys.privateKey ); console.log(decrypted); // { legacy: true } ``` ## Características | Característica | Estado | | -------------------------------------- | --------------------------------------- | | AES-256-GCM | ✅ | | RSA-OAEP SHA-256 por defecto | ✅ | | RSA-OAEP SHA-1 legacy | ✅ | | Campo `oaepHash` en el sobre | ✅ | | JSON en memoria | ✅ | | Binario en memoria | ✅ | | Archivos y carpetas vía ZIP | ✅ | | Stream portable `.ccenc` | ✅ | | AAD para metadatos autenticados | ✅ | | Firmas Ed25519 para payloads JSON | ✅ | | Fingerprint SHA-256 de claves públicas | ✅ | | Tipos TypeScript incluidos | ✅ | | Interoperabilidad TypeScript ↔ Python | ✅ | | Modo `v8` | ⚠️ Solo Node.js | ## Ecosistema Cross-Crypto - [Cross Crypto Py (Python)](https://github.com/acadyne/cross-crypto-py) - [Cross Crypto TS (TypeScript)](https://github.com/acadyne/cross-crypto-ts) - [Cross Crypto RS (Rust)](https://github.com/acadyne/cross-crypto-rs) ## Licencia MIT © Jose Fabian Soltero Escobar