# 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