Skip to content

Repository files navigation

ezcoder

Encrypt a message for someone, paste the result anywhere (chat, e-mail, a forum), and only they can read it — and they can see it really came from you. Optional sessions add forward secrecy with the Signal Double Ratchet. All of it is hybrid post-quantum: X25519 + ML-KEM-768 for key agreement, Ed25519 + ML-DSA-65 for signatures (FIPS 203 / 204), so an attacker has to break both the classical and the post-quantum algorithm.

ezcoder

What it is not

  • It does not send anything. You copy the ezc1:… / ezs1:… text and deliver it yourself.
  • It does not hide that you are talking, message sizes, or who holds which key. Only the contents.
  • It has nothing to do with Ethereum any more (1.x used Ethereum's ECIES; 3.0 reads nothing older than itself).

Install

Prebuilt Windows / Linux / macOS builds are on the latest release (no Python needed). From source:

pip install -r requirements.txt
python main.py            # GUI  (run.cmd / run.sh)
python ezcoder.py --help  # CLI  (frozen build: ezcoder --cli --help)

On first start a key pair is created in keys.json next to the app (or in $EZCODER_HOME). The Keys tab shows your public identity (ezid3:…, about 4.3 KB of text because it carries the post-quantum keys), its fingerprint and a QR code of the fingerprint — share the identity text, never the file.

Using it

  • Message tab (stateless): paste the recipient's ezid3: identity (or pick a contact), type, Encrypt. To read a message, paste it and Decrypt — ezcoder tells you who signed it and whether that person is in your contacts.
  • Sessions tab (forward secret): add contacts by name + identity. Encrypt in session produces a message that also carries the handshake the first time; the other side just decrypts it and can answer. Messages may arrive out of order or get lost (up to 1000 skipped keys are kept). Reset session starts over.
  • Keys tab: copy identity, protect keys.json with a passphrase (scrypt + ChaCha20-Poly1305), export / import, generate a new identity.

Compare fingerprints with your contact over a channel you trust (phone, in person). That is the only protection against someone handing you a swapped identity.

sessions

CLI

ezcoder.py keygen [--passphrase]                  ezcoder.py id
ezcoder.py fingerprint ezid3:...                   ezcoder.py contact add NAME ezid3:... | list | remove WHO
ezcoder.py encrypt ezid3:... -m "text" | -f file   ezcoder.py decrypt -m "ezc3:..." | -f file   (stdin works too)
ezcoder.py session send WHO -m "text"              ezcoder.py session list | reset WHO

--home DIR selects the key folder, EZCODER_PASSPHRASE supplies the passphrase non-interactively.

Cryptography

Everything uses primitives from the Python cryptography package (≥ 50, which ships ML-KEM and ML-DSA via OpenSSL); nothing is hand-rolled and there are no other crypto dependencies. All constants below are in ezcrypto.py (≈ 400 lines — please review it). Hybrid everywhere: never PQ-only, never classical-only.

Identity. X25519 + ML-KEM-768 (FIPS 203) for key agreement, Ed25519 + ML-DSA-65 (FIPS 204, NIST level 3 to match ML-KEM-768) for signatures. ezid3: + base64url(dh_pub₃₂ ‖ sign_pub₃₂ ‖ kem_pub₁₁₈₄ ‖ pqsign_pub₁₉₅₂) = 3200 bytes, 4273 characters. Fingerprint = first 64 bits of SHA-256 over those 3200 bytes as xxxx xxxx xxxx xxxx. Every signature is a pair Ed25519(data) ‖ ML-DSA-65(data, ctx="ezcoder/v3") (64 + 3309 bytes); both must verify.

Sealed message ezc3: + base64url(0x03 ‖ eph_pub₃₂ ‖ kem_ct₁₀₈₈ ‖ recipient_id₈ ‖ ciphertext)

recipient_id SHA-256(recipient dh_pub ‖ kem_pub)[:8] — a wrong recipient gets "not encrypted for your key", not a MAC failure
shared secret X25519(eph, recipient.dh) ‖ ML-KEM-768.Decaps(recipient.kem, kem_ct)
key ‖ nonce HKDF-SHA256(ikm = shared, salt = ∅, info = ezcoder/v3/sealed ‖ eph_pub ‖ kem_ct ‖ recipient.dh ‖ recipient.kem) → 32 + 12 B
ciphertext ChaCha20-Poly1305(key, nonce, aad = the 1129-byte header, plaintext = inner)
inner sender identity₃₂₀₀ ‖ sig₃₃₇₃ ‖ message
sig over ezcoder/v3/sealed-sig ‖ eph_pub ‖ kem_ct ‖ recipient.dh ‖ recipient.kem ‖ SHA-256(message)

Overhead: ~7.7 KB binary / ~10.3 KB of text per message (the sender's full identity travels inside so an unknown sender can still be verified and added as a contact). A fresh ephemeral key and KEM ciphertext per message mean the nonce is never reused under a key. The signature binds the message to this ephemeral key, this KEM ciphertext and this recipient, so a recipient cannot re-wrap it for someone else. The sender's identity is inside the ciphertext.

SessionSignal Double Ratchet with a PQXDH-style hybrid handshake (no prekey server, both sides only have identity bundles): SK = HKDF-SHA256(DH(IK_A, IK_B) ‖ DH(EK_A, IK_B) ‖ ML-KEM-768.Encaps(IK_B.kem), info = ezcoder/v3/pqxdh), Bob's initial ratchet key is IK_B.dh, session_id = SHA-256(ezcoder/v3/sid ‖ sorted(IK_A.dh, IK_B.dh) ‖ EK_A)[:8]. The initiator's messages carry 0x01 ‖ IK_A₃₂₀₀ ‖ EK_A₃₂ ‖ kem_ct₁₀₈₈ ‖ sig(ezcoder/v3/hs ‖ EK_A ‖ kem_ct ‖ IK_B.dh ‖ IK_B.kem ‖ sid)₃₃₇₃ ‖ ratchet message until a reply arrives (~10 KB of text), then 0x02 ‖ sid ‖ ratchet message (~120 bytes + message). Ratchet: X25519 DH ratchet; KDF_RK = HKDF-SHA256(salt = RK, ikm = DH, info = ezcoder/v3/rk, 64 B); KDF_CK = HMAC-SHA256(CK, 0x02) → CK′, HMAC-SHA256(CK, 0x01) → MK; message key → ChaCha20-Poly1305 key ‖ nonce via HKDF-SHA256(MK, info = ezcoder/v3/mk); AAD = session_id ‖ header (ratchet_pub₃₂ ‖ PN ‖ N); MAX_SKIP = 1000; state is only committed after the AEAD tag verifies. The ratchet is implemented in-house (≈ 120 lines, straight from the spec).

What post-quantum covers, honestly. Sealed messages: confidentiality holds as long as either X25519 or ML-KEM-768 is unbroken; authenticity as long as either Ed25519 or ML-DSA-65 is. Sessions: the initial secret SK is hybrid, so a quantum attacker recording traffic today cannot recover a session from its handshake — but the per-message DH ratchet steps are X25519 only (exactly like Signal's PQXDH deployment). A future quantum attacker who also obtains a ratchet state could therefore follow the session from that point; there is no periodic KEM re-encapsulation (deliberately left out to keep the design small). Start a new session now and then if that matters to you.

Other limits / threat model. Long-term keys live in keys.json (optionally passphrase-wrapped with scrypt n=2¹⁵ + ChaCha20-Poly1305) and ratchet state in sessions.json; whoever reads those can impersonate you and read future session messages and all sealed messages ever sent to you (sealed mode has no forward secrecy). Sealed messages are signed, so they are not deniable. The handshake has no one-time prekeys. Nothing protects metadata. ML-KEM / ML-DSA come from OpenSSL through cryptography and are checked against the official FIPS 203 / 204 known-answer vectors in the test suite, but this application has not been audited; the construction is documented above so it can be.

Changes in 3.1

  • The Windows taskbar shows the app's own icon. The taskbar button takes its icon from the process's Application User Model ID rather than from the window, and with none of its own the process was grouped under whatever launched it and wore that program's icon. One is now set before any window exists, and it carries no version number so a pinned button survives an upgrade.

Changes in 3.0 (incompatible with 2.0 and 1.x)

  • Hybrid post-quantum: ML-KEM-768 mixed into every key agreement (sealed messages and the session handshake), ML-DSA-65 added to every signature. New prefixes ezid3: / ezc3: / ezs3:, key file version 3. Nothing older is read — decrypt anything important with the old version first. Identities are now ~4.3 KB, messages ~10 KB.
  • Known-answer tests from the FIPS vectors; tests for tampering with each signature and with the KEM ciphertext.
  • QR code now encodes the fingerprint (the identity no longer fits); identity shown in a copyable text box and can be saved to a file; whitespace / line breaks inside pasted identities and messages are ignored.

2.0

  • X25519 / Ed25519 / ChaCha20-Poly1305 / HKDF replaced secp256k1 ECIES (eciespy); messages became signed and bound to the recipient; Double Ratchet sessions; contacts; passphrase-protected key file; PySide6 GUI (was tkinter); CLI; tests; PyInstaller builds. 1.x had no authentication at all: anyone could forge a message "from" anyone.

Build

pip install -r requirements.txt pyinstaller && python build.pydist/ezcoder-<version>-<os>-<arch>. Tests: python -m pytest tests. EZCODER_SELFTEST=1 runs the roundtrips headlessly (used by CI on the frozen build).

About

Message encryptor and decryptor based on an Ethereum implementation of ECIES with a GUI

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages