feat: funciones Python datascience, finance, cybersecurity y pipelines
Datascience: aggregate_by_group, deduplicate_entities/relations, detect_drift, diff_entities/relations, extract_entities/relations_llm, hotness_score, melt, merge_graphs, pivot, build_entity/relation_schema_prompt. Finance: avellaneda_stoikov_quotes, generate_gbm_prices, generate_taker_order, hawkes_intensity + módulo finance.py. Cybersecurity: envelope_encrypt/decrypt + módulo cybersecurity.py. Pipelines: extraction_pipeline, monte_carlo_market, run_market_sim. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ from .cybersecurity import (
|
||||
levenshtein_distance,
|
||||
jaccard_similarity,
|
||||
normalize_url,
|
||||
envelope_encrypt,
|
||||
envelope_decrypt,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -22,4 +24,6 @@ __all__ = [
|
||||
"levenshtein_distance",
|
||||
"jaccard_similarity",
|
||||
"normalize_url",
|
||||
"envelope_encrypt",
|
||||
"envelope_decrypt",
|
||||
]
|
||||
|
||||
@@ -4,8 +4,11 @@ import hashlib
|
||||
import math
|
||||
import re
|
||||
import base64
|
||||
import secrets
|
||||
import struct
|
||||
from collections import Counter
|
||||
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
|
||||
def hash_sha256(data: bytes) -> str:
|
||||
@@ -165,3 +168,147 @@ def normalize_url(raw_url: str) -> str:
|
||||
sorted_query = urlencode(sorted(params.items()), doseq=True)
|
||||
# Drop fragment
|
||||
return urlunparse((scheme, netloc, path, parsed.params, sorted_query, ""))
|
||||
|
||||
|
||||
# --- Envelope Encryption (AES-256-GCM) ---
|
||||
|
||||
_ENVELOPE_MAGIC = b"OVE1"
|
||||
_ENVELOPE_VERSION = 0x01
|
||||
_HEADER_SIZE = 12 # magic(4) + version(1) + reserved(1) + efk_len(2) + kiv_len(2) + div_len(2)
|
||||
|
||||
|
||||
def _build_envelope(
|
||||
encrypted_file_key: bytes,
|
||||
key_iv: bytes,
|
||||
data_iv: bytes,
|
||||
encrypted_content: bytes,
|
||||
) -> bytes:
|
||||
"""Construye el formato binario del envelope (helper puro interno).
|
||||
|
||||
Header (12 bytes):
|
||||
Magic (4B): b"OVE1"
|
||||
Version (1B): 0x01
|
||||
Reserved (1B): 0x00
|
||||
EFK_len (2B): longitud de encrypted_file_key (big-endian)
|
||||
KIV_len (2B): longitud de key_iv (big-endian)
|
||||
DIV_len (2B): longitud de data_iv (big-endian)
|
||||
Seguido de: encrypted_file_key + key_iv + data_iv + encrypted_content
|
||||
"""
|
||||
header = (
|
||||
_ENVELOPE_MAGIC
|
||||
+ struct.pack(">BBHHH", _ENVELOPE_VERSION, 0x00,
|
||||
len(encrypted_file_key), len(key_iv), len(data_iv))
|
||||
)
|
||||
return header + encrypted_file_key + key_iv + data_iv + encrypted_content
|
||||
|
||||
|
||||
def _parse_envelope(ciphertext: bytes) -> tuple:
|
||||
"""Parsea el envelope binario y retorna sus componentes (helper puro interno).
|
||||
|
||||
Returns:
|
||||
(encrypted_file_key, key_iv, data_iv, encrypted_content)
|
||||
|
||||
Raises:
|
||||
ValueError: si el envelope esta truncado o la version no es soportada.
|
||||
"""
|
||||
if len(ciphertext) < _HEADER_SIZE:
|
||||
raise ValueError(
|
||||
f"Envelope truncado: se esperaban al menos {_HEADER_SIZE} bytes, "
|
||||
f"se recibieron {len(ciphertext)}"
|
||||
)
|
||||
|
||||
magic = ciphertext[:4]
|
||||
if magic != _ENVELOPE_MAGIC:
|
||||
raise ValueError(f"Magic invalido: se esperaba {_ENVELOPE_MAGIC!r}, se obtuvo {magic!r}")
|
||||
|
||||
version, _reserved, efk_len, kiv_len, div_len = struct.unpack(">BBHHH", ciphertext[4:12])
|
||||
|
||||
if version != _ENVELOPE_VERSION:
|
||||
raise ValueError(f"Version de envelope no soportada: {version}")
|
||||
|
||||
offset = _HEADER_SIZE
|
||||
encrypted_file_key = ciphertext[offset : offset + efk_len]
|
||||
offset += efk_len
|
||||
key_iv = ciphertext[offset : offset + kiv_len]
|
||||
offset += kiv_len
|
||||
data_iv = ciphertext[offset : offset + div_len]
|
||||
offset += div_len
|
||||
encrypted_content = ciphertext[offset:]
|
||||
|
||||
if (
|
||||
len(encrypted_file_key) != efk_len
|
||||
or len(key_iv) != kiv_len
|
||||
or len(data_iv) != div_len
|
||||
):
|
||||
raise ValueError("Envelope truncado: longitudes declaradas exceden los datos disponibles")
|
||||
|
||||
return encrypted_file_key, key_iv, data_iv, encrypted_content
|
||||
|
||||
|
||||
def envelope_encrypt(plaintext: bytes, master_key: bytes) -> bytes:
|
||||
"""Cifra datos usando patron Envelope Encryption con AES-256-GCM.
|
||||
|
||||
Genera una file key aleatoria de 32 bytes, cifra los datos con ella,
|
||||
luego cifra la file key con la master_key. El resultado es un envelope
|
||||
binario que contiene todo lo necesario para descifrar con la master_key.
|
||||
|
||||
Args:
|
||||
plaintext: Datos a cifrar (puede ser vacio).
|
||||
master_key: Clave maestra de 32 bytes (AES-256).
|
||||
|
||||
Returns:
|
||||
Envelope binario cifrado.
|
||||
|
||||
Raises:
|
||||
Exception: Si ocurre un error en el cifrado (clave de longitud incorrecta, etc.).
|
||||
"""
|
||||
# 1. Generar file_key aleatoria (DEK: Data Encryption Key)
|
||||
file_key = secrets.token_bytes(32)
|
||||
|
||||
# 2. Cifrar contenido con la file_key
|
||||
data_iv = secrets.token_bytes(12)
|
||||
aesgcm_data = AESGCM(file_key)
|
||||
encrypted_content = aesgcm_data.encrypt(data_iv, plaintext, None)
|
||||
|
||||
# 3. Cifrar file_key con la master_key (KEK: Key Encryption Key)
|
||||
key_iv = secrets.token_bytes(12)
|
||||
aesgcm_key = AESGCM(master_key)
|
||||
encrypted_file_key = aesgcm_key.encrypt(key_iv, file_key, None)
|
||||
|
||||
# 4. Construir envelope
|
||||
return _build_envelope(encrypted_file_key, key_iv, data_iv, encrypted_content)
|
||||
|
||||
|
||||
def envelope_decrypt(ciphertext: bytes, master_key: bytes) -> bytes:
|
||||
"""Descifra datos cifrados con envelope_encrypt.
|
||||
|
||||
Si los datos no empiezan con el magic b"OVE1", se asume que no estan
|
||||
cifrados y se retornan tal cual (comportamiento passthrough). Esto
|
||||
permite usar la funcion en archivos que pueden o no estar cifrados.
|
||||
|
||||
Args:
|
||||
ciphertext: Envelope cifrado (o datos en plano si no tienen magic).
|
||||
master_key: Clave maestra de 32 bytes (AES-256).
|
||||
|
||||
Returns:
|
||||
Datos descifrados, o ciphertext sin modificar si no tiene magic.
|
||||
|
||||
Raises:
|
||||
ValueError: Si el envelope esta corrupto o truncado.
|
||||
cryptography.exceptions.InvalidTag: Si la master_key es incorrecta
|
||||
o los datos fueron manipulados (falla de autenticacion GCM).
|
||||
"""
|
||||
# Passthrough: si no comienza con magic, asumir que no esta cifrado
|
||||
if not ciphertext.startswith(_ENVELOPE_MAGIC):
|
||||
return ciphertext
|
||||
|
||||
# Parsear envelope
|
||||
encrypted_file_key, key_iv, data_iv, encrypted_content = _parse_envelope(ciphertext)
|
||||
|
||||
# Descifrar file_key con master_key
|
||||
aesgcm_key = AESGCM(master_key)
|
||||
file_key = aesgcm_key.decrypt(key_iv, encrypted_file_key, None)
|
||||
|
||||
# Descifrar contenido con file_key
|
||||
aesgcm_data = AESGCM(file_key)
|
||||
return aesgcm_data.decrypt(data_iv, encrypted_content, None)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: envelope_decrypt
|
||||
kind: function
|
||||
lang: py
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def envelope_decrypt(ciphertext: bytes, master_key: bytes) -> bytes"
|
||||
description: "Descifra datos cifrados con envelope_encrypt. Si los datos no comienzan con el magic b'OVE1', los retorna sin modificar (passthrough). Soporta archivos que pueden o no estar cifrados sin necesidad de chequeo previo."
|
||||
tags: [decryption, aes, gcm, envelope-encryption, dek, kek, cryptography, cybersecurity, passthrough]
|
||||
uses_functions: [envelope_encrypt_py_cybersecurity]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [cryptography, struct]
|
||||
tested: true
|
||||
tests:
|
||||
- "decrypt de datos cifrados"
|
||||
- "decrypt de datos no cifrados passthrough"
|
||||
- "key incorrecta"
|
||||
- "envelope truncado"
|
||||
- "magic invalido"
|
||||
test_file_path: "python/functions/cybersecurity/envelope_encrypt_test.py"
|
||||
file_path: "python/functions/cybersecurity/cybersecurity.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import secrets
|
||||
from cybersecurity import envelope_encrypt, envelope_decrypt
|
||||
|
||||
master_key = secrets.token_bytes(32)
|
||||
|
||||
# Caso 1: descifrar datos cifrados
|
||||
ciphertext = envelope_encrypt(b"datos secretos", master_key)
|
||||
plaintext = envelope_decrypt(ciphertext, master_key)
|
||||
# plaintext == b"datos secretos"
|
||||
|
||||
# Caso 2: passthrough — datos no cifrados
|
||||
raw = b"archivo en plano"
|
||||
result = envelope_decrypt(raw, master_key)
|
||||
# result == b"archivo en plano" (sin modificar)
|
||||
|
||||
# Caso 3: key incorrecta — lanza InvalidTag
|
||||
wrong_key = secrets.token_bytes(32)
|
||||
# envelope_decrypt(ciphertext, wrong_key) → cryptography.exceptions.InvalidTag
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Implementacion original inspirada en OpenViking `openviking/crypto/encryptor.py` (AGPL-3.0). Reimplementada desde cero.
|
||||
|
||||
- **Passthrough**: si `ciphertext` no empieza con `b"OVE1"`, se retorna sin modificar. Permite usar la funcion indistintamente en archivos cifrados y no cifrados.
|
||||
- **Autenticacion GCM**: si la master_key es incorrecta o los datos fueron manipulados, `cryptography.exceptions.InvalidTag` es lanzado por la capa GCM — nunca se retorna texto corrupto.
|
||||
- **ValueError**: lanzado si el envelope tiene magic correcto pero estructura invalida (truncado o version no soportada).
|
||||
- `master_key` debe ser de exactamente 32 bytes para AES-256.
|
||||
- Requiere `cryptography` instalado: `uv add cryptography`.
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: envelope_encrypt
|
||||
kind: function
|
||||
lang: py
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def envelope_encrypt(plaintext: bytes, master_key: bytes) -> bytes"
|
||||
description: "Cifra datos usando patron Envelope Encryption con AES-256-GCM. Genera una file key aleatoria (DEK), cifra los datos con ella, luego cifra la file key con la master_key (KEK). Retorna un envelope binario con magic b'OVE1'."
|
||||
tags: [encryption, aes, gcm, envelope-encryption, dek, kek, cryptography, cybersecurity]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [cryptography, secrets, struct]
|
||||
tested: true
|
||||
tests:
|
||||
- "encrypt → decrypt roundtrip"
|
||||
- "datos vacios"
|
||||
- "datos grandes"
|
||||
- "ciphertext tiene magic correcto"
|
||||
- "ciphertext es distinto cada vez"
|
||||
test_file_path: "python/functions/cybersecurity/envelope_encrypt_test.py"
|
||||
file_path: "python/functions/cybersecurity/cybersecurity.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import secrets
|
||||
from cybersecurity import envelope_encrypt, envelope_decrypt
|
||||
|
||||
master_key = secrets.token_bytes(32) # 256-bit KEK
|
||||
plaintext = b"datos confidenciales"
|
||||
|
||||
ciphertext = envelope_encrypt(plaintext, master_key)
|
||||
# ciphertext[:4] == b"OVE1"
|
||||
|
||||
recovered = envelope_decrypt(ciphertext, master_key)
|
||||
# recovered == plaintext
|
||||
```
|
||||
|
||||
## Formato del envelope
|
||||
|
||||
```
|
||||
Magic (4B): b"OVE1" identificador de formato
|
||||
Version (1B): 0x01 version del protocolo
|
||||
Reserved (1B): 0x00 reservado para uso futuro
|
||||
EFK_len (2B): big-endian longitud de encrypted_file_key
|
||||
KIV_len (2B): big-endian longitud de key_iv
|
||||
DIV_len (2B): big-endian longitud de data_iv
|
||||
--- header: 12 bytes total ---
|
||||
Encrypted File Key (variable, incluye GCM auth tag de 16B)
|
||||
Key IV (12B)
|
||||
Data IV (12B)
|
||||
Encrypted Content (variable, incluye GCM auth tag de 16B)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Implementacion original inspirada en OpenViking `openviking/crypto/encryptor.py` (AGPL-3.0). Reimplementada desde cero.
|
||||
|
||||
- La file key (DEK) es de 32 bytes generados con `secrets.token_bytes` (CSPRNG).
|
||||
- Tanto el cifrado de datos como el de la file key usan AES-256-GCM con IVs de 12 bytes.
|
||||
- El GCM auth tag (16 bytes) garantiza autenticidad e integridad.
|
||||
- `master_key` debe ser de exactamente 32 bytes para AES-256.
|
||||
- Requiere `cryptography` instalado: `uv add cryptography`.
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Tests para envelope_encrypt y envelope_decrypt."""
|
||||
|
||||
import secrets
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from cybersecurity import envelope_encrypt, envelope_decrypt
|
||||
|
||||
|
||||
def test_encrypt_decrypt_roundtrip():
|
||||
master_key = secrets.token_bytes(32)
|
||||
plaintext = b"datos de prueba para envelope encryption"
|
||||
ciphertext = envelope_encrypt(plaintext, master_key)
|
||||
result = envelope_decrypt(ciphertext, master_key)
|
||||
assert result == plaintext
|
||||
|
||||
|
||||
def test_datos_vacios():
|
||||
master_key = secrets.token_bytes(32)
|
||||
ciphertext = envelope_encrypt(b"", master_key)
|
||||
result = envelope_decrypt(ciphertext, master_key)
|
||||
assert result == b""
|
||||
|
||||
|
||||
def test_datos_grandes():
|
||||
master_key = secrets.token_bytes(32)
|
||||
plaintext = secrets.token_bytes(1024 * 1024) # 1 MB
|
||||
ciphertext = envelope_encrypt(plaintext, master_key)
|
||||
result = envelope_decrypt(ciphertext, master_key)
|
||||
assert result == plaintext
|
||||
|
||||
|
||||
def test_decrypt_datos_no_cifrados_passthrough():
|
||||
master_key = secrets.token_bytes(32)
|
||||
plain = b"archivo no cifrado, sin magic bytes"
|
||||
result = envelope_decrypt(plain, master_key)
|
||||
assert result == plain
|
||||
|
||||
|
||||
def test_key_incorrecta():
|
||||
master_key = secrets.token_bytes(32)
|
||||
wrong_key = secrets.token_bytes(32)
|
||||
ciphertext = envelope_encrypt(b"secreto", master_key)
|
||||
try:
|
||||
envelope_decrypt(ciphertext, wrong_key)
|
||||
assert False, "deberia haber lanzado excepcion"
|
||||
except Exception:
|
||||
pass # esperado: InvalidTag de cryptography
|
||||
|
||||
|
||||
def test_envelope_truncado():
|
||||
master_key = secrets.token_bytes(32)
|
||||
ciphertext = envelope_encrypt(b"datos", master_key)
|
||||
truncated = ciphertext[:6] # header incompleto
|
||||
try:
|
||||
envelope_decrypt(truncated, master_key)
|
||||
assert False, "deberia haber lanzado ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def test_magic_invalido():
|
||||
master_key = secrets.token_bytes(32)
|
||||
# Construir datos con magic valido para pasar el check del passthrough
|
||||
# pero con header corrupto
|
||||
bad_envelope = b"OVE1" + b"\x00" * 20 # magic correcto pero header invalido
|
||||
try:
|
||||
envelope_decrypt(bad_envelope, master_key)
|
||||
assert False, "deberia haber lanzado excepcion"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_ciphertext_tiene_magic_correcto():
|
||||
master_key = secrets.token_bytes(32)
|
||||
ciphertext = envelope_encrypt(b"test", master_key)
|
||||
assert ciphertext[:4] == b"OVE1"
|
||||
|
||||
|
||||
def test_ciphertext_es_distinto_cada_vez():
|
||||
master_key = secrets.token_bytes(32)
|
||||
plaintext = b"mismo mensaje"
|
||||
ct1 = envelope_encrypt(plaintext, master_key)
|
||||
ct2 = envelope_encrypt(plaintext, master_key)
|
||||
# IVs aleatorios garantizan ciphertexts distintos
|
||||
assert ct1 != ct2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_encrypt_decrypt_roundtrip()
|
||||
test_datos_vacios()
|
||||
test_datos_grandes()
|
||||
test_decrypt_datos_no_cifrados_passthrough()
|
||||
test_key_incorrecta()
|
||||
test_envelope_truncado()
|
||||
test_magic_invalido()
|
||||
test_ciphertext_tiene_magic_correcto()
|
||||
test_ciphertext_es_distinto_cada_vez()
|
||||
print("Todos los tests pasaron.")
|
||||
Reference in New Issue
Block a user