Skip to content
Lesson 10.10Game Files, Mods, and Artifact Trust

Encryption and Key Lifecycles

Compare encoding, hashing, MACs, and encryption, then follow the keys and metadata needed to protect a save.

Lesson 10.10
Read or listenPrint chapter or book
About 46 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 10.10Key ideas, reference and questions

A one-page summary of “Encryption and Key Lifecycles” you can keep beside your notes. Plain Markdown.

Lesson 10.10 — Encryption and Key Lifecycles: cheatsheet

  • Remember: Store the nonce beside the ciphertext, but keep the key separate and avoid reusing a nonce with that key.
  • Also useful: Associated data is a readable envelope label sealed into the authentication check: swapping the slot or version invalidates the package.

Reading map

  • Five tools with different jobs
  • The parts of an encrypted envelope
  • Complete sealing code
  • Opening must fail closed
  • Run and test the lab
  • Choose and validate an authenticated format

Formulas and patterns

  • 3.4 × 10³⁸ keys ÷ 3.2 × 10²⁸ keys per year ≈ 1.1 × 10¹⁰ years

Review

  • Q: What does AEAD associated data provide? A: Authentication for readable context, such as a slot number and format version
  • Q: Which tool is reversible and needs no secret? A: Encoding, such as Base64
Download cheatsheet (.md)

Lesson 10.8 used a hash to compare exact files and a signature to check a publisher’s approval. Suppose instead that a save contains private data and the owner wants to keep its contents unreadable on disk. That calls for encryption and a plan for the key. The related words are easy to mix up, but their jobs are not interchangeable:

Five tools, five jobs

Tool Reversible? Secret required? Main job
Encoding Yes No Represent data, such as Base64 or UTF-8
Hash No No Produce a fixed-size fingerprint
MAC No Yes Detect changes made without the secret key
Encryption Yes, with key Yes Hide the plaintext
AEAD Yes, with key Yes Hide plaintext and authenticate it and its context

AEAD means authenticated encryption with associated data. It is usually the right starting point for a new encrypted file or message because confidentiality without tamper detection is incomplete.

The lab uses symmetric encryption: one key both locks and unlocks, so whoever encrypts and whoever decrypts must hold the same secret. It is efficient for large payloads, but it leaves a problem: the key has to reach the other side without anyone copying it on the way.

Public-key cryptography uses a public/private key pair, as Lesson 10.8 showed for signatures. In a public-key encryption scheme, anyone with your public key can protect a small message for you, while your private key is needed to open it. Picture giving out open padlocks and keeping their key. That picture does not explain key agreement, where two sides contribute information and derive shared secret material without sending the finished secret. Nor does the padlock tell you whose public key you received: identity must be checked separately.

Network protocols combine these jobs. In a certificate-authenticated TLS 1.3 connection, a handshake authenticates the server and establishes shared secret material; both sides derive symmetric traffic keys from it, and authenticated encryption protects the records. The handshake does not simply send a browser-generated traffic key inside the server’s public-key padlock. See the TLS 1.3 specification. The save lab has no remote recipient to agree with: the owner seals and opens the file on the same PC. Its harder question is where to keep the long-lived key, not how to transport it across a network.

Start with the threat model, not the cipher name

Section titled “Start with the threat model, not the cipher name”

Before choosing an algorithm, write four plain-English statements:

  1. Asset: what information or game state needs protection?
  2. Adversary: what can the person you are defending against read, change, or execute?
  3. Property: do you need confidentiality, integrity, authenticity, availability, or some combination?
  4. Trust boundary: which program, account, or server is allowed to hold the key and make the final decision?

For an offline single-player save, the player controls the same PC that must decrypt the save. Encryption can protect the file from casual disclosure or accidental editing, but it cannot create a server-like secret boundary by itself. If the game ships a key and must use it locally, a determined owner can observe that use. For a server-authoritative multiplayer profile, the server can keep the authoritative key and state outside the client’s control.

This does not make local encryption useless. It makes its promise precise. Security engineering starts by choosing a realistic promise, then checks whether the mechanism actually enforces it.

The lab uses XChaCha20-Poly1305 from the RustCrypto chacha20poly1305 crate. One envelope contains:

+0x00nonce, 24 bytes
+0x18ciphertext, same length as the plaintext
end − 0x1016-byte Poly1305 tag
One sealed envelope, in the order seal writes it. split_at_checked(24) in the decryption code is what separates the nonce from everything after it.

The nonce is not a password and does not need to be hidden. It must be unique for messages protected by the same key. XChaCha20’s larger nonce makes random generation practical, but the program still stores it with the ciphertext so decryption can use the same value.

Some information should remain readable but must not be silently swapped. The lab authenticates this context:

save-slot:3|format:1

If someone moves the ciphertext to slot four or claims it uses another format, opening it fails.

context does not match
what was authenticated
seal with context
save-slot:3, format:1
envelope stored on disk
open with context
save-slot:4
decryption fails

The associated data is not encrypted; it is included in the authentication calculation.

Put together, sealing the 19-byte GSAV save and opening it twice looks like this:

Seal the 19-byte save, then open it twiceSealing writes nonce, ciphertext and tag. Opening recomputes the tag before it returns a single byte.
  1. 1 Seal: four inputs, one envelope

    What seal takes

    Save bytes (plaintext)
    47 53 41 56 … 4A 03 00 0019 bytes
    Secret key
    32 bytes, kept apart from the file
    Nonce
    24 random bytes, new for every seal
    Context (associated data)
    save-slot:3|format:1readable, but authenticated

    Envelope on disk, bytes 0 to 58

    nonce, ciphertext, tag
    nonce24 bytes
    ciphertext19 bytes
    tag16 bytes

    The envelope is 24 + 19 + 16 = 59 bytes, offsets 0 to 58. The key is not in it.

  2. 2 Open the untouched envelope

    Envelope on disk, bytes 0 to 58

    nonce, ciphertext, tag
    nonce24 bytes
    ciphertext19 bytes
    tag16 bytes

    open, with the same key and context

    Tag recomputed from the envelope
    equals the stored tag
    open returns
    the 19 save bytes

    The recomputed tag matches, so the save bytes come back.

  3. 3 Open after one ciphertext byte changed

    Envelope on disk, bytes 0 to 58

    nonce, ciphertext, tag
    nonce24 bytes
    ciphertext, 1 byte changed19 bytes
    tag16 bytes

    open, with the same key and context

    Tag recomputed from the envelope
    differs from the stored tag
    open returns
    AuthenticationFailed, and no bytes

    The recomputed tag no longer matches, so open refuses and returns none of the bytes.

Sealing writes nonce, ciphertext and tag. Opening recomputes the tag before it returns a single byte.

Authenticated context belongs to one envelope. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

saved_context = slot 3, format 1
requested_context = selected_slot, format 1
compare the contexts used by authentication
combine with the assumed byte-tag result
return plaintext only after authentication
report failure without partial plaintext

Result: the original context and assumed valid bytes permit opening.

Final values: slot = 3; tag = valid; saved_context = slot 3:format 1; requested_context = slot3:format 1; plaintext = fixture saved data; context_matches = true; authenticated = true.

Read every step of the lesson’s example
  1. Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.

    slot = 3; tag = valid.

  2. Step 2, line 2. The stored envelope was sealed with readable context slot 3, format 1.

    slot = 3; tag = valid; saved_context = slot 3:format 1; requested_context = slot3:format 1; plaintext = not returned.

  3. Step 3, line 3. A different context must not be substituted silently.

    slot = 3; tag = valid; saved_context = slot 3:format 1; requested_context = slot3:format 1; plaintext = not returned; context_matches = true.

  4. Step 4, line 4. This teaching model takes the byte-tag result as an input; a real AEAD implementation computes and checks it.

    slot = 3; tag = valid; saved_context = slot 3:format 1; requested_context = slot3:format 1; plaintext = not returned; context_matches = true; authenticated = true.

  5. Step 5, line 5. Release the illustrative saved data only after the whole check succeeds.

    slot = 3; tag = valid; saved_context = slot 3:format 1; requested_context = slot3:format 1; plaintext = fixture saved data; context_matches = true; authenticated = true.

  6. Step 6, line 6. Result: the original context and assumed valid bytes permit opening.

    slot = 3; tag = valid; saved_context = slot 3:format 1; requested_context = slot3:format 1; plaintext = fixture saved data; context_matches = true; authenticated = true.

Here the inputs are a 32-byte key, the save bytes, and context such as save slot and format version. seal generates a fresh nonce, encrypts and authenticates the save plus its context, then stores the nonce before the ciphertext so the opener can use the same nonce later. The key stays outside the returned envelope:

use chacha20poly1305::{
XChaCha20Poly1305, XNonce,
aead::{Aead, Generate, KeyInit, Payload},
};
fn seal(key: &[u8; 32], plaintext: &[u8], context: &[u8]) -> Result<Vec<u8>, ()> {
let cipher = XChaCha20Poly1305::new(key.into());
let nonce = XNonce::generate(); // 🎲 New nonce for this message.
let ciphertext = cipher
.encrypt(&nonce, Payload { msg: plaintext, aad: context })
.map_err(|_| ())?;
let mut envelope = Vec::with_capacity(24 + ciphertext.len());
envelope.extend_from_slice(&nonce);
envelope.extend_from_slice(&ciphertext);
Ok(envelope)
}

The complete lab returns descriptive errors and includes tests. Its dependency is pinned by Cargo.lock:

chacha20poly1305 = { version = "0.11", features = ["getrandom"] }

This uses the current 0.11 API documented by RustCrypto.

Decryption splits the nonce from the envelope, then authenticates both ciphertext and context:

let (nonce_bytes, ciphertext) = envelope
.split_at_checked(24)
.ok_or(CryptoError::TruncatedEnvelope)?;
let nonce = XNonce::try_from(nonce_bytes)
.map_err(|_| CryptoError::TruncatedEnvelope)?;
cipher
.decrypt(&nonce, Payload { msg: ciphertext, aad: context })
.map_err(|_| CryptoError::AuthenticationFailed)

Do not return partly decrypted data after an authentication error. Do not explain whether the key, nonce, tag, context, or ciphertext was wrong to an untrusted caller; one generic failure avoids turning error messages into an oracle.

Perfect encryption code with a key committed to Git is still broken. A save editor or local game tool that protects player data needs answers to:

  • Where is the key created?
  • Is it unique per user, device, or save?
  • Where is it stored?
  • Which process may request it?
  • How is it rotated?
  • What happens to old saves?
  • How is it erased from temporary buffers?

For a Windows game tool, protected operating-system storage may be more appropriate than a constant in the executable. A server-authoritative game may keep important secrets on the server instead of trusting every client. The correct choice depends on what game data the key protects and who must be able to open it.

The demo uses a visible repeated key only to make one local round trip repeatable. A real save or profile tool must load protected key material instead of compiling the key into the executable.

Taking the key is the realistic attack, because guessing it is hopeless. Trying every possible key, a brute-force attack, always works in the end; the question is when the end arrives. Work it out for a 128-bit key.

Each extra bit doubles the number of keys, so 128 bits give 2¹²⁸ keys. To size that, use 2¹⁰ = 1,024, which is a little more than 1,000: every 10 bits multiply the count by about 1,000. 128 bits is twelve groups of 10 bits plus 8 more, so 2¹²⁸ is roughly 1,000¹² × 2⁸ = 256 × 10³⁶. Each 1,024 is slightly more than 1,000, which lifts the exact value to about 3.4 × 10³⁸.

Now be generous to the attacker: a billion computers, each testing a trillion keys every second, is 10⁹ × 10¹² = 10²¹ keys per second. A year has 60 × 60 × 24 × 365 = 31,536,000 seconds, about 3.2 × 10⁷, so that is about 3.2 × 10²⁸ keys a year. Dividing gives

3.4 × 10³⁸ keys ÷ 3.2 × 10²⁸ keys per year ≈ 1.1 × 10¹⁰ years

about 11 billion years to try them all, close to the 13.8-billion-year age of the universe. The lab’s key is 256 bits, and 2²⁵⁶ is 2¹²⁸ multiplied by itself, so its count is that enormous number squared.

Brute force stops being hopeless when the secret is small. A six-letter lowercase password has 26⁶ possibilities: 26 × 26 × 26 = 17,576, and 17,576 × 17,576 = 308,915,776. At a billion guesses per second, that is all of them in about a third of a second. That is why encryption keys are random 128- or 256-bit values rather than passwords, and why a password that must protect a key is first run through a deliberately slow key-derivation function, so each guess costs the attacker real time.

Think of a key as having a lifecycle rather than merely a location:

cryptographically secure RNG
access-controlled storage
load for one operation
erase from memory
when the operation ends
rotation policy
old envelopes still
readable or migrated
retirement policy
old backups handled
key lost
recovery path
Generated
Stored
Loaded
Rotated
Retired
Recovered

Generation needs a cryptographically secure random source. Storage needs an access boundary. Use should keep the plaintext and key alive for no longer than necessary. Rotation needs a way to distinguish old envelopes and either read or migrate them. Retirement needs a policy for old backups. Recovery needs an answer for a lost key; otherwise “secure” may simply mean “the player’s save is gone forever.”

Availability is part of the design. An authenticated file that correctly rejects one flipped bit still needs backups, version metadata, and a recovery path. Integrity without recovery can detect damage while leaving the player unable to continue.

Any computer

From the repository root, the first command runs the small encryption demo; the second checks that valid data opens and changed data is rejected.

Terminal window
cargo run --manifest-path advanced-memory-labs/Cargo.toml --bin crypto_demo
cargo test --manifest-path advanced-memory-labs/Cargo.toml crypto

The tests verify:

  • a valid envelope opens to the original bytes;
  • flipping one ciphertext bit fails authentication;
  • changing associated data fails authentication.

That third test is easy to forget and is exactly why tests should describe the security contract.

Analysis does not magically reveal plaintext

Section titled “Analysis does not magically reveal plaintext”

During the course fixture’s execution, you may see plaintext where the program uses it, the key where the crypto API receives it, and ciphertext where the file is written. That does not mean encryption “does nothing.” Encryption protects data outside the trusted processing moment, such as at rest or in transit.

Use breakpoints and data-flow tracing to follow the fixture’s complete key lifecycle: creation, use, storage, rotation, and destruction. A partial trace can make a correct design look broken.

Also keep a checksum separate from a cryptographic authenticator. A checksum detects accidental corruption, but anyone who can edit the file can recompute an unkeyed one, as Lesson 10.8 did for the GSAV save. A valid MAC or AEAD tag requires the secret key.

Choose and validate an authenticated format

Section titled “Choose and validate an authenticated format”
  • ✅ Use a reviewed AEAD implementation.
  • ✅ Generate a fresh nonce as required by the algorithm.
  • ✅ Authenticate file version, slot, identity, or message type as associated data.
  • ✅ Treat authentication failure as a hard stop.
  • ❌ Do not invent a cipher from XOR, rotation, and a checksum.
  • ❌ Do not hardcode real save/profile keys in source code.
  • ❌ Do not reuse a nonce with the same key.
  • ❌ Do not log keys or decrypted secrets.

The file lessons have now taken a saved value from readable text to binary fields, mod assets, archives, integrity checks, and encrypted payloads. Each format defines its own fields, boundaries, and validation checks. The shared habit is to identify the bytes, parse them within bounds, verify the expected form, and keep a way back. Chapter 10 follows a different kind of game mod: a Lua script that the game deliberately loads and runs through a limited host interface.

Knowledge check

Protect the context

What does AEAD associated data provide?

Finished Lesson 10.10?

Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.

Chapter 10: 10 lessons

This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.