Skip to main content

Cifrado híbrido AES-GCM + RSA-OAEP con firma Ed25519 e interoperabilidad entre Python, TypeScript y Rust

Project description

Cross Crypto Py PyPI License Python Versions Build

Cifrado híbrido interoperable entre Python y TypeScript, con diseño compatible para Rust.

Cross Crypto Py 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 y objetos serializados con dill.
  • Stream portable .ccenc para archivos grandes.

Introducción

Cross Crypto Py es una librería de cifrado híbrido diseñada para interoperar entre distintos lenguajes, especialmente Python ↔ TypeScript, 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, 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.
  • dill puede ejecutar código durante deserialización si el payload no es confiable.
  • No uses mode="dill" con datos de origen no confiable, incluso si están cifrados o firmados.

Instalación

pip install cross-crypto-py

Requisitos

  • Python ≥ 3.7
  • cryptography
  • pycryptodome
  • dill

Uso básico: JSON en memoria

from cross_crypto_py import generateRSAKeys, encryptHybrid, decryptHybrid

keys = generateRSAKeys(bits=4096)

public_key = keys["publicKey"]
private_key = keys["privateKey"]

payload = {
    "mensaje": "Hola desde Python",
    "ok": True,
}

encrypted = encryptHybrid(payload, public_key)

print(encrypted["mode"])      # json
print(encrypted["oaepHash"])  # sha256

decrypted = decryptHybrid(encrypted, private_key)

print(decrypted)

Salida esperada:

{
  "mensaje": "Hola desde Python",
  "ok": true
}

RSA-OAEP SHA-256 por defecto

Desde 2.0.0, el default es:

encrypted = encryptHybrid(
    data,
    public_key,
    oaep_hash="sha256",
)

Y el descifrado detecta automáticamente el campo oaepHash si está presente:

decrypted = decryptHybrid(encrypted, private_key)

No necesitas pasar oaep_hash manualmente cuando el paquete trae:

{
  "oaepHash": "sha256"
}

Compatibilidad legacy con SHA-1

Versiones anteriores usaban RSA-OAEP con SHA-1 por compatibilidad con algunas implementaciones.

Para cifrar en modo legacy:

encrypted = encryptHybrid(
    {"legacy": True},
    public_key,
    oaep_hash="sha1",
)

Para descifrar paquetes viejos que no traen oaepHash:

decrypted = decryptHybrid(
    encrypted_legacy,
    private_key,
    oaep_hash="sha1",
)

Si el paquete viejo trae:

{
  "oaepHash": "sha1"
}

entonces decryptHybrid(...) lo detecta automáticamente.

Modos soportados

JSON

encrypted = encryptHybrid(
    {"hello": "world"},
    public_key,
    mode="json",
)

decrypted = decryptHybrid(
    encrypted,
    private_key,
    mode="json",
)

Binario

data = b"\x00\x01\x02hello\xff"

encrypted = encryptHybrid(
    data,
    public_key,
    mode="binary",
)

decrypted = decryptHybrid(
    encrypted,
    private_key,
    mode="binary",
)

assert decrypted == data

Dill

dill permite serializar objetos Python, pero no es interoperable con TypeScript/Rust.

Además, deserializar dill puede ejecutar código. Úsalo solo con datos confiables y en flujos completamente controlados.

En esta versión, mode="dill" espera una firma embebida válida para descifrar correctamente. Para interoperabilidad Python ↔ TypeScript usa json, binary, archivos ZIP o stream .ccenc.

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.

aad = {
    "tenant": "acadyne",
    "purpose": "test",
}

encrypted = encryptHybrid(
    {"msg": "hola"},
    public_key,
    aad=aad,
)

decrypted = decryptHybrid(
    encrypted,
    private_key,
    aad=aad,
)

AAD incorrecto:

decryptHybrid(
    encrypted,
    private_key,
    aad={"tenant": "otro"},
)

Debe fallar con error de autenticación.

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 guarda en JSON .enc.json.

from cross_crypto_py.file_crypto import encryptFileHybrid, decryptFileHybrid

encrypted = encryptFileHybrid(
    paths=["datos/", "documento.pdf"],
    public_key=public_key,
    save_file=True,
    output_enc="datos.enc.json",
)

output_dir = decryptFileHybrid(
    "datos.enc.json",
    private_key,
    extract_to="datos_descifrados",
)

print("Archivos restaurados en:", output_dir)

Cifrado de archivos con OAEP SHA-256

Por defecto:

encrypted = encryptFileHybrid(
    paths=["datos/"],
    public_key=public_key,
    save_file=True,
    output_enc="datos.enc.json",
)

equivale a:

encrypted = encryptFileHybrid(
    paths=["datos/"],
    public_key=public_key,
    save_file=True,
    output_enc="datos.enc.json",
    oaep_hash="sha256",
)

Para compatibilidad legacy:

encrypted = encryptFileHybrid(
    paths=["datos/"],
    public_key=public_key,
    save_file=True,
    output_enc="datos_legacy.enc.json",
    oaep_hash="sha1",
)

Y para descifrar un paquete viejo sin oaepHash:

output_dir = decryptFileHybrid(
    "datos_legacy.enc.json",
    private_key,
    oaep_hash="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, contentMode, aad, cipher y keyWrap.
  • Ciphertext AES-GCM después del header.
from cross_crypto_py import encryptHybrid, decryptHybrid

encrypted = encryptHybrid(
    "video.mp4",
    public_key,
    mode="binary",
    stream=True,
    output_path="video.mp4.ccenc",
)

output_path = decryptHybrid(
    "video.mp4.ccenc",
    private_key,
    stream=True,
    decrypted_output_path="video_restaurado.mp4",
)

print(output_path)

También puedes descifrar usando el objeto retornado:

output_path = decryptHybrid(
    encrypted,
    private_key,
    stream=True,
    decrypted_output_path="video_restaurado.mp4",
)

También puedes recuperar bytes directamente:

data = decryptHybrid(
    "video.mp4.ccenc",
    private_key,
    stream=True,
    return_bytes=True,
)

assert isinstance(data, bytes)

Archivos/carpetas con stream .ccenc

encryptFileHybrid(..., use_stream=True) empaqueta primero los archivos en un ZIP y luego cifra ese ZIP como .ccenc.

from cross_crypto_py.file_crypto import encryptFileHybrid, decryptFileHybrid

encrypted = encryptFileHybrid(
    paths=["datos/"],
    public_key=public_key,
    output_enc="datos.ccenc",
    use_stream=True,
    attach_metadata=True,
)

print(encrypted["streamFormat"])  # envelope
print(encrypted["encryptedPath"]) # datos.ccenc

output_dir = decryptFileHybrid(
    "datos.ccenc",
    private_key,
    extract_to="datos_descifrados",
)

print(output_dir)

En este modo:

  • output_enc debe apuntar normalmente a .ccenc.
  • No necesitas save_file=True, porque el stream escribe directamente el archivo binario.
  • El resultado de encryptFileHybrid(...) devuelve 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.

from cross_crypto_py import (
    generateEd25519Keys,
    signPayload,
    verifyPayload,
)

keys = generateEd25519Keys()

payload = {
    "user": "fabian",
    "scope": "admin",
}

signature = signPayload(
    payload,
    keys["privateKey"],
    key_id="v1",
)

ok = verifyPayload(
    payload,
    signature,
    keys["publicKey"],
)

print(ok)  # True

Verificación con expiración

Puedes limitar la edad aceptada de una firma:

ok = verifyPayload(
    payload,
    signature,
    keys["publicKey"],
    max_age_seconds=300,
)

Esto rechaza firmas demasiado antiguas o con timestamps demasiado adelantados.

Fingerprint de claves públicas

from cross_crypto_py import fingerprintPublicKey

fp = fingerprintPublicKey(keys["publicKey"])

print(fp)

El fingerprint se calcula normalizando la clave pública a DER y aplicando SHA-256.

Interoperabilidad Python ↔ TypeScript

El subconjunto interoperable entre Python y TypeScript es:

  • mode="json"
  • mode="binary"
  • Archivos/carpetas vía ZIP cifrado
  • Stream portable .ccenc
  • AAD
  • OAEP SHA-256 / SHA-1 según oaepHash
  • Firmas Ed25519 sobre payloads JSON canónicos

mode="dill" es solo Python.

Sobre JSON para datos en memoria

{
  "encryptedKey": "base64",
  "encryptedData": "base64",
  "nonce": "base64",
  "tag": "base64",
  "mode": "json | binary | dill",
  "aad": "present | none",
  "oaepHash": "sha1 | sha256"
}

Para interoperabilidad Python ↔ TypeScript, 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
ciphertext

Reglas importantes:

  • oaepHash debe viajar en el sobre o en el header del .ccenc.
  • Si aad es "present", ambos lados deben usar exactamente el mismo AAD.
  • JSON cifrado se serializa como UTF-8 compacto, sin espacios innecesarios.
  • Las firmas Ed25519 usan JSON canónico con claves ordenadas.
  • Binario debe tratarse como bytes crudos, no como texto UTF-8.
  • dill solo es compatible con Python.
  • El stream .ccenc no depende de archivos secundarios: header y ciphertext viajan juntos.
  • Para stream interoperable Python ↔ TypeScript, usa contentMode="binary".

Ejemplo de roundtrip SHA-256

from cross_crypto_py import generateRSAKeys, encryptHybrid, decryptHybrid

keys = generateRSAKeys(bits=2048)

encrypted = encryptHybrid(
    {"ok": True},
    keys["publicKey"],
)

assert encrypted["oaepHash"] == "sha256"

decrypted = decryptHybrid(
    encrypted,
    keys["privateKey"],
)

assert decrypted == {"ok": True}

Ejemplo de roundtrip legacy SHA-1

from cross_crypto_py import generateRSAKeys, encryptHybrid, decryptHybrid

keys = generateRSAKeys(bits=2048)

encrypted = encryptHybrid(
    {"legacy": True},
    keys["publicKey"],
    oaep_hash="sha1",
)

assert encrypted["oaepHash"] == "sha1"

decrypted = decryptHybrid(
    encrypted,
    keys["privateKey"],
)

assert 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
Modo streaming portable .ccenc
AAD para metadatos autenticados
Firmas Ed25519 para payloads JSON
Fingerprint SHA-256 de claves públicas
Stubs .pyi incluidos
Interoperabilidad Python ↔ TypeScript
dill para objetos Python ⚠️ Solo fuentes confiables

Ecosistema Cross-Crypto

Licencia

MIT © Jose Fabian Soltero Escobar

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cross_crypto_py-2.0.0.tar.gz (27.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cross_crypto_py-2.0.0-py3-none-any.whl (30.4 kB view details)

Uploaded Python 3

File details

Details for the file cross_crypto_py-2.0.0.tar.gz.

File metadata

  • Download URL: cross_crypto_py-2.0.0.tar.gz
  • Upload date:
  • Size: 27.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for cross_crypto_py-2.0.0.tar.gz
Algorithm Hash digest
SHA256 5aab576cf5f805d1712e9937a9f641fe35507154e62cac940a61346e963c0f51
MD5 bab99705a7a2dcb956155dd0b6b739ac
BLAKE2b-256 9a0cc0e939a458ddb2a9003265aff3cc196954bccc16450204c40ac33062ff58

See more details on using hashes here.

File details

Details for the file cross_crypto_py-2.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for cross_crypto_py-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9b41f9b0641a03756f9cd2d83f8e7e3f1a8fe5755fc38582bddbae67328cc73b
MD5 7fdf87e1ef60977b7b5b13f1e637838a
BLAKE2b-256 b512ba5065d9593bb26c17bfee3301ea41fbe1a041a5cfa20e42d6d6430fe4aa

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page