cross-crypto-ts
v2.0.0
Published
Cifrado híbrido AES-GCM + RSA-OAEP con interoperabilidad entre TypeScript y Python, con diseño compatible para Rust.
Maintainers
Readme
Cross Crypto TS
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:
{
"encryptedKey": "...",
"encryptedData": "...",
"nonce": "...",
"tag": "...",
"mode": "json",
"aad": "none",
"oaepHash": "sha256"
}Desde la versión 2.0.0, los paquetes cifrados incluyen el campo oaepHash:
{
"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,AADo 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
v8es 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
npm install cross-crypto-tsRequisitos
- Node.js >= 18
- TypeScript >= 5 recomendado
- Entorno Node.js CommonJS
Uso básico: JSON en memoria
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:
{
"mensaje": "Hola desde TypeScript",
"ok": true
}RSA-OAEP SHA-256 por defecto
Desde 2.0.0, el default es:
const encrypted = encryptHybrid(data, publicKey, "json", {
oaepHash: "sha256",
});Y el descifrado detecta automáticamente el campo oaepHash si está presente:
const decrypted = decryptHybrid(encrypted, privateKey);No necesitas pasar oaepHash manualmente cuando el paquete trae:
{
"oaepHash": "sha256"
}Compatibilidad legacy con SHA-1
Para cifrar en modo legacy:
const encrypted = encryptHybrid(
{ legacy: true },
publicKey,
"json",
{
oaepHash: "sha1",
}
);Si el paquete trae:
{
"oaepHash": "sha1"
}entonces decryptHybrid(...) lo detecta automáticamente.
Para paquetes viejos sin oaepHash, puedes forzar el hash al descifrar:
const decrypted = decryptHybrid(
encryptedLegacy,
privateKey,
{
oaepHash: "sha1",
}
);Modos soportados
JSON
const encrypted = encryptHybrid(
{ hello: "world" },
publicKey,
"json"
);
const decrypted = decryptHybrid(
encrypted,
privateKey
);Binario
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
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.
const aad = {
tenant: "acadyne",
purpose: "test",
};
const encrypted = encryptHybrid(
{ msg: "hola" },
publicKey,
"json",
{ aad }
);
const decrypted = decryptHybrid(
encrypted,
privateKey,
{ aad }
);AAD incorrecto:
decryptHybrid(
encrypted,
privateKey,
{ aad: { tenant: "otro" } }
);Debe fallar con error de autenticación.
Para interoperabilidad TypeScript ↔ Python, usa preferentemente AAD como string estable:
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.
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:
const encrypted = encryptFileHybrid(
["datos/"],
publicKey,
{
saveFile: true,
outputEnc: "datos.enc.json",
}
);equivale a:
const encrypted = encryptFileHybrid(
["datos/"],
publicKey,
{
saveFile: true,
outputEnc: "datos.enc.json",
oaepHash: "sha256",
}
);Para compatibilidad legacy:
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,aadycontentMode. - 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.
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:
const outputPath = decryptHybrid(
"video.mp4.ccenc",
privateKey,
"video_restaurado.mp4"
);También puedes devolver bytes en memoria para stream:
const bytes = decryptHybrid(
"video.mp4.ccenc",
privateKey,
{
returnBytes: true,
}
);
console.log(Buffer.isBuffer(bytes)); // trueStream en archivos/carpetas con encryptFileHybrid
Para archivos/carpetas, encryptFileHybrid primero crea un ZIP temporal y luego puede cifrar ese ZIP en modo stream .ccenc.
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:
outputEncdebe 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.
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); // trueVerificación con expiración
Puedes limitar la edad aceptada de una firma:
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
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
{
"encryptedKey": "base64",
"encryptedData": "base64",
"nonce": "base64",
"tag": "base64",
"mode": "json | binary | v8",
"aad": "present | none",
"oaepHash": "sha1 | sha256"
}Para interoperabilidad TypeScript ↔ Python, usa normalmente:
{
"mode": "json | binary"
}Stream portable .ccenc
Para stream portable .ccenc, el contrato va embebido dentro del archivo:
{
"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:
CCRYPT2\n
uint32_be(header_json_length)
header_json_utf8
ciphertextReglas importantes:
oaepHashdebe viajar en el sobre o header.- Si
aades"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.
v8solo es compatible con Node.js.- Para stream interoperable usa
.ccenc, normalmente concontentMode: "binary".
Ejemplo de roundtrip SHA-256
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
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
Licencia
MIT © Jose Fabian Soltero Escobar
