Skip to content

Latest commit

 

History

History
344 lines (248 loc) · 8.92 KB

File metadata and controls

344 lines (248 loc) · 8.92 KB

4. Running the demo

Everything here is copy-pasteable. You need Python 3 and one package, cryptography — everything else the project uses is standard library.

python3 -c "import cryptography; print(cryptography.__version__)"

If that prints a version, you are ready. If it raises ModuleNotFoundError:

pip install -r requirements.txt

Some Linux distributions package it as python3-cryptography and it is already installed; others give you a "externally-managed-environment" error from pip, in which case use a virtual environment:

python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

The fast path

./demo.sh

Fifteen seconds, and the whole story plays out. What follows is that same story step by step, so you can run the pieces yourself and poke at them.


Step 1 — the publisher sets up

python3 -m authority.provision init --bits 2048
Generating a 2048-bit RSA modulus...
  secrets   -> authority/secrets.json  (private, gitignored)
  public key-> dist/public.json

Two files. dist/public.json holds N and e and is meant to be published. authority/secrets.json holds d, λ(N) and the HMAC master key, is written mode 0600, and is in .gitignore. Anyone who reads it can mint licenses and can trace — it is the crown jewels of the whole scheme.


Step 2 — encrypt the content, once

python3 -m authority.provision package
Encrypted 1013 bytes of premium content
  premium_levels.json -> dist/content.enc
  scheme: RSA-OAEP-SHA256 + AES-256-GCM
  this single file is shipped to every player

content/premium_levels.json is the plaintext: three levels and three skins. dist/content.enc is what players get. This command runs once, and the same output goes to everybody. That is the paper's premise — one encryption process, many decryption processes.


Step 3 — enrol some players

python3 -m authority.provision enroll alice alice@example.com
python3 -m authority.provision enroll bob   bob@example.com
python3 -m authority.provision enroll carol carol@example.com
Enrolled alice <alice@example.com> as LIC-0001
  decoder      : dist/licenses/alice/decoder.json   (holds d_ID)
  product key  : RAAS-VQAF-ENJ4-OZ23-4HQK-LBPA-UV2Z-ZCA7-IBYU-CEXF-LKU5-R7AS-L5LQ   (holds sigma_ID)

Your product keys will differ — they come from a fresh master key.

This is the moment the scheme happens. One private exponent, cut two ways, and the two halves walk out of the building through different doors: one as a file, one as a string of characters.

Look at what Alice got:

cat dist/licenses/alice/decoder.json
{
  "license_id": "LIC-0001",
  "variant": "multiplicative",
  "n": "2419...",
  "d_id": "1885..."
}

Now compare two players' d_id values:

python3 -c "
import json
for name in ('alice','bob'):
    d = json.load(open(f'dist/licenses/{name}/decoder.json'))
    print(name, str(d['d_id'])[:40], '...')
"

Completely different numbers. Both decrypt the same file.


Step 4 — everybody unlocks the same ciphertext

python3 -m game.main --license dist/licenses/alice --check-only
python3 -m game.main --license dist/licenses/bob   --check-only
python3 -m game.main --license dist/licenses/carol --check-only
UNLOCKED: unlocked with license LIC-0001
levels available: Open Field, Crossroads, The Pit, Comb
UNLOCKED: unlocked with license LIC-0002
levels available: Open Field, Crossroads, The Pit, Comb
UNLOCKED: unlocked with license LIC-0003
levels available: Open Field, Crossroads, The Pit, Comb

Same dist/content.enc, three different decryption paths, three successes.


Step 5 — but the halves must match

Alice's decoder with Bob's product key:

python3 -m game.main \
    --decoder dist/licenses/alice/decoder.json \
    --key "$(cat dist/licenses/bob/productkey.txt)" \
    --check-only
LOCKED:   decryption returned bottom: this key does not match this decoder
levels available: Open Field

⊥. Alice's σ_ID and Bob's d_ID were never meant to combine, so the exponent they produce is not d, and the result is a random block that fails OAEP unpadding. The game still runs — you just get the free level.

And with no license at all:

python3 -m game.main --check-only
LOCKED:   no license supplied
levels available: Open Field

Step 6 — play it

python3 -m game.main --license dist/licenses/alice

A level-select menu with PREMIUM UNLOCKED at the top and four levels. Arrow keys or wasd to select, Enter to play, t to change skin, q to quit. Needs a terminal at least 42×24.

Then try it locked:

python3 -m game.main

PREMIUM LOCKED, one level. That is what the encryption is protecting.


Step 7 — the leak

Alice hands her copy to someone else. Both halves, because the decoder alone would be useless:

mkdir -p pirate
cp dist/licenses/alice/decoder.json   pirate/decoder.json
cp dist/licenses/alice/productkey.txt pirate/productkey.txt

python3 -m game.main --license pirate --check-only
UNLOCKED: unlocked with license LIC-0001

The pirate plays. Nothing stops them — this scheme is not copy protection, it is accountability. Read that difference carefully: the paper says its aim "is to deter users from building pirate decoders", not to make copying impossible.


Step 8 — the trace

Now you are the publisher. Someone sent you a file. You do not know whose it is, and you did not get their product key.

python3 -m authority.provision trace pirate/decoder.json
Tracing pirate/decoder.json
  tested 3 enrolled users with a valid ciphertext

  LIC-0001  alice        DECRYPTS
  LIC-0002  bob          bottom
  LIC-0003  carol        bottom

TRAITOR IDENTIFIED: alice  <alice@example.com>   license LIC-0001

For each registered player: recompute their σ_ID from the master key, hand it and a valid ciphertext to the seized decoder, and see what comes out. Exactly one succeeded.


Step 9 — the dashboard

python3 -m dashboard.server

Open http://localhost:8080.

  • Licenses — the registry
  • Trace — upload pirate/decoder.json and press the button
  • The scheme — the equations, with the modulus size in use

The trace page has one input: the file. There is no field for a product key, on purpose.


Prove it is not staged

Fair question. Enrol somebody the demo never mentions, copy their decoder in, and see whether the dashboard still gets it right — without you telling it who you copied.

python3 -m authority.provision enroll dave dave@example.com
cp dist/licenses/dave/decoder.json pirate/mystery.json
python3 -m authority.provision trace pirate/mystery.json
TRAITOR IDENTIFIED: dave  <dave@example.com>   license LIC-0004

Then try a forgery — a decoder with a real modulus, a random d_ID, and a lie about which license it is:

python3 - <<'EOF'
import json, os, pathlib
d = json.loads(pathlib.Path("dist/licenses/alice/decoder.json").read_text())
d["license_id"] = "LIC-0002"                       # blame bob
d["d_id"] = str(int.from_bytes(os.urandom(256), "big") % int(d["n"]))
pathlib.Path("pirate/forged.json").write_text(json.dumps(d, indent=2))
EOF

python3 -m authority.provision trace pirate/forged.json
Trace inconclusive: this decoder was not issued by this authority.

Nobody is accused, and the license_id claiming to be Bob's is ignored — a pirate can write anything in that field, so the trace never reads it.


The tests

python3 -m unittest discover tests -v

43 tests. The scheme itself: the three splits reassembling d, product keys surviving a round trip through user mangling, one ciphertext opening under several decoders, wrong keys returning ⊥, a trace selecting exactly one user, and both inconclusive verdicts. Around it: level-pack parsing, the snake spawning clear of walls, the license gate's decision table, and the dashboard's multipart upload parser.


Starting over

rm -rf dist pirate authority/secrets.json authority/registry.db

demo.sh does this itself on every run. All four paths are gitignored.


When something goes wrong

Symptom Cause
secrets.json not found Run init first.
dist/content.enc not found Run package first.
LOCKED: no product key supplied Pass --key, set $SNAKE_PRODUCT_KEY, or use --license on a directory containing productkey.txt.
decryption returned bottom The key and the decoder are from different licenses — or you re-ran init, which generates a new authority and invalidates every license issued by the old one.
Terminal is 80x20; this needs at least 42x24 Resize the window.
Curses draws nothing over ssh Set TERM, e.g. TERM=xterm-256color.

Next: 5. What this does not do — the limits, stated plainly.