Status: proof of concept, under active development. This is not a production-grade secure messaging system. It has not been independently audited, and several of its design goals (see Known limitations below) are not fully met yet. Do not use it to protect anything where a real compromise would hurt you.
GBC turns a short text message into a 3D point cloud. The points that actually encode the message ("wheat") are mixed in with random-looking decoy points ("chaff", "decoy", "entropy") so that, without the right keys, every point in the cloud looks equally meaningless. The real points are placed using a keyed pseudo-random function (HKDF/HMAC-SHA256), and a short piece of metadata (nonce, message length, an integrity digest) is hidden redundantly inside a few of the decoy-looking points using RSA-OAEP, so the receiver can find it by trial decryption instead of by position.
In short: it's a steganographic packaging layer on top of standard crypto primitives (HKDF, HMAC, RSA-OAEP). The primitives do the actual security work; the geometry's job is obfuscation, deniability, and — frankly — being a fun format to look at.
gbc_core.py -- the protocol itself: geometry, HKDF key schedule,
RSA-OAEP envelope, HMAC integrity, encode_message()
/ decode_message(). Single source of truth; both
CLI scripts below just call into this.
gbc_encode.py -- CLI: reads input.json, writes public/ + private/
gbc_decode.py -- CLI: reads public/message.json + private/secret.json
tests/ -- pytest suite (round-trip, tamper detection, edge
cases, CLI integration) -- see "Running the tests"
.github/workflows/ -- CI: runs the test suite on push/PR
requirements.txt, requirements-dev.txt, pytest.ini, LICENSE, CHANGELOG.md
A message round-trip produces exactly two artifacts:
public/message.json -- the point cloud + receiver's public RSA key.
Safe to publish / share over an open channel.
private/secret.json -- the shared master key + receiver's private RSA
key. Must travel over a separate, confidential
channel (USB, encrypted email, Signal, etc.)
Both files carry a matching id field so the decoder can catch "wrong
secret.json for this message.json" as an explicit error instead of a
confusing crypto failure.
pip install -r requirements.txt
python gbc_encode.py # reads input.json, writes public/ and private/
python gbc_decode.py # reads public/message.json + private/secret.jsoninput.json:
{ "message": "hello world", "language": "en" }pip install -r requirements-dev.txt
pytest # full suite
pytest -m "not slow" # skip the real-subprocess smoke test, ~2x fasterThe suite covers: round-trips (including every character in the
alphabet, one at a time and all together), negative paths (wrong
master key, wrong RSA private key, tampered HMAC, tampered points,
tampered entropy nodes, truncated clouds, malformed input), boundary
conditions (empty message, single character, the length-field ceiling),
and the CLI layer itself (input.json parsing, file layout, the id-match
check). See tests/ for the individual cases.
- Teaching / demoing protocol design — it's a compact example of combining HKDF domain separation, a keyed-PRF steganographic layout, a hybrid RSA-OAEP metadata envelope, and whole-cloud HMAC integrity in one project.
- CTF challenges / ARGs / puzzle boxes — "this file is a meaningless 3D point cloud... or is it?" is a natural fit for capture-the-flag or alternate-reality-game content.
- Steganographic / artistic message delivery — the cloud can be rendered as an actual 3D asset (point-cloud file, mesh viewer, etc.) and shared as if it were just an odd piece of generative art. Deniability is a genuine, working property here: without both keys, an observer cannot tell which points (if any) are real.
- Learning material for the "self-destructing message" problem — GBC is a good jumping-off point for discussing why client-checked expiry doesn't work (see below) and what actually does (Ephemerizer-style threshold key release, delete-on-first-fetch hosting, etc.).
If you need actual secure messaging, use an established, audited tool (age, Signal, PGP/GPG, etc.). GBC's geometric packaging does not add cryptographic strength beyond its underlying primitives, and it adds real complexity and attack surface that a plain authenticated-encryption scheme (e.g. XChaCha20-Poly1305) wouldn't have.
This list is deliberately honest, not a list of "todos we'll casually get to" — treat every point here as a real gap in the current design:
master_keyis per-message, not a shared long-term secret. Every run ofgbc_encode.pygenerates a brand new, randommaster_key(secrets.token_bytes(32)) and ships it inside that message's ownprivate/secret.json— it is never reused across messages. This is a genuinely good property: compromising one message'ssecret.jsonexposes only that one message, not any others. The one thing to still watch for: if you manually reuse asecret.json/master_keyacross more than one message — the scripts don't do this, but nothing stops a user from copy-pasting one — you'd be reintroducing exactly the risk a per-message key is meant to avoid. Treat eachsecret.jsonas single-message, single-use.- No sender authentication / non-repudiation. Anyone holding
master_keycan produce a message that looks legitimate. There's no separate signing identity for "who actually sent this." - Message length is not fully hidden. The published point count is
dominated by the number of "wheat" points, which is exactly
3 × message_length. Chaff/decoy counts are log-scaled specifically to avoid compounding this leak, but they do not close it — an observer can still estimate roughly how long the plaintext is just from the size ofmessage.json, even without any keys. (This is documented in the code as a known, structural consequence of the space-sizing formula, not something the log-scaling "fixes.") - RSA-OAEP is not post-quantum secure. If "harvest now, decrypt later" matters for your threat model, this needs a hybrid PQC KEM instead of (or alongside) RSA.
- No forward-looking access control beyond key possession. Once
private/secret.jsonis compromised, the message is fully readable — there is no second factor (e.g. a passphrase) required in addition to the files themselves. - Alphabet is restricted. The character set is Turkish + English letters, digits, basic punctuation, space, and newline — no arbitrary binary data without an extra encoding layer (e.g. hex).
- Poor scalability. Point count grows with message length; this is workable for short messages (a sentence or two) and impractical for anything paragraph-length or longer.
- Character matching is not constant-time.
decode_message()tries alphabet characters in order and stops at the first match, so wall-clock decode time varies slightly with which character matched. The receiver already holds every secret needed to decode locally, so this isn't an oracle against a remote attacker today -- but it would become one if this function were ever exposed as a network service without also being made constant-time.
There is no TTL, expiry, or "self-destruct" mechanism in this protocol.
A public/message.json remains fully decodable for as long as it exists
and the matching private/secret.json is available — there is no
built-in way to make it stop being readable after some deadline.
If you need a message that can only be read once, or only within a
window, that property has to be enforced by whoever hosts
public/message.json — e.g. a Privnote/Yopass/OneTimeSecret-style
service that deletes its copy the moment it's first fetched — not by
this project. Even then, be aware this is fundamentally a best-effort
guarantee: once a legitimate receiver has decoded the message, nothing
stops them from copying the plaintext (screenshot, copy-paste, etc.).
This is the same "analog hole" every self-destructing-message product
(Snapchat included) runs into — it's a UX/policy layer, not a
cryptographic one.
MIT licensed (see LICENSE) -- proof-of-concept code, provided as-is,
for learning and experimentation. No warranty, no audit, no guarantees
about confidentiality, integrity, or availability of anything encoded
with it.