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
Markdown
# 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:
```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