End-to-end encryption helper built on elliptic-curve Diffie-Hellman (ECDH), PBKDF2 key derivation, and Fernet symmetric encryption. The E2EE class coordinates server/client key exchange, mutual authentication, and message confidentiality.
- ECDH (SECP521R1) key agreement for both a public key and a salt key-pair
- Server-signed public material to prevent tampering (ECDSA with SHA-224)
- PBKDF2-HMAC-SHA256 derivation of a 256-bit Fernet key from the dual shared secrets
- Simple
encrypt/decrypthelpers that emit URL-safe Base64 strings
- Python
>=3.9.10 cryptographylibrary (pulled automatically viapyproject.toml)- DER-encoded ECDSA key pair available on disk:
PRIVATE_KEYenvironment variable -> path to the server signing key (PKCS#8 DER)PUBLIC_KEYenvironment variable -> path to the public key distributed to clients (SubjectPublicKeyInfo DER)
These files are used exactly as in the tests/conftest.py fixture.
- Server bootstrap: instantiate
E2EE()with no arguments. It becomes the authoritative peer, loading its private signing key and generating ephemeral public key/salt pairs. - Publish public material: the server exposes
server.public_keyandserver.public_salt. Each property returns the Base32-encoded point alongside a Base32 signature. - Client bootstrap: instantiate
E2EE(server.public_key, server.public_salt)on the client. The client verifies the signatures with the server's public signing key (pointed to byPUBLIC_KEY), generates its own ephemeral key material, and sets up symmetric encryption. - Mutual exchange: the client shares its unsigned public key/salt. The server calls
load_peer_public_key(client.public_key, client.public_salt)to complete ECDH. - Secure channel: both peers derive the same Fernet key from the combined secrets and can call
encrypt/decryptto exchange ciphertexts.
The round-trip mirrors tests/test_e2ee.py and can be summarized as:
server = E2EE()
client = E2EE(server.public_key, server.public_salt)
server.load_peer_public_key(client.public_key, client.public_salt)
ciphertext = server.encrypt("hello")
plaintext = client.decrypt(ciphertext)- Generate signing keys (dev only):
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat
priv = ec.generate_private_key(ec.SECP521R1())
pub = priv.public_key()
open("/tmp/ecdsa_private_key.pem", "wb").write(
priv.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption())
)
open("/tmp/ecdsa_public_key.pem", "wb").write(
pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
)- Export the paths:
export PRIVATE_KEY=/tmp/ecdsa_private_key.pem
export PUBLIC_KEY=/tmp/ecdsa_public_key.pem- Exchange messages:
from e2ee import E2EE
server = E2EE()
client = E2EE(server.public_key, server.public_salt)
server.load_peer_public_key(client.public_key, client.public_salt)
secret_from_server = server.encrypt("SECRET MESSAGE FROM SERVER!")
print(client.decrypt(secret_from_server))
secret_from_client = client.encrypt("SECRET MESSAGE FROM CLIENT!")
print(server.decrypt(secret_from_client))E2EE(public_key=None, public_salt=None): Creates a server instance when called with no arguments; otherwise acts as a client that immediately verifies and loads the remote public key material.public_key/public_salt: Base32-encoded strings (plus signatures on the server). Client values do not include signatures.load_peer_public_key(public_key, public_salt, exchange=True): Imports peer material. For clients, the tuple must include(encoded_key, signature)pairs; servers expect plain Base32 strings. Whenexchangeis true (default) the instance computes shared secrets and arms Fernet encryption.encrypt(message: str) -> str: Encrypts UTF-8 strings and returns URL-safe Base64 ciphertext, ideal for transport over JSON/HTTP.decrypt(encoded_ciphertext: str) -> str: Reversesencryptand returns the original plaintext string.
pip install -e .[dev]
pytestThe unit test in tests/test_e2ee.py performs the full server/client dance, so it serves as an executable example as well as a regression check.