Container format v1
This page describes the baseline container format: fixed and public, at moderate strength. Production distribution builds run it through the closed polymorphism engine, which randomizes the layout and key derivation per build; this specification describes the fixed baseline that the open framework implements and tests.
All integers are little-endian. All encryption is AES-256-GCM (16-byte authentication tag).
Layout
Header (64 bytes)
The first 60 bytes of the header (every field except reserved) are the GCM AAD of the index, so the header cannot be altered without breaking index decryption.
Index
The index is a GCM-encrypted UTF-8 JSON object:
name: path relative to the Ren'Py game directory, with forward slashes and no leading slash (for exampleimages/bg.png)data_offset: absolute offset of the file's first encrypted chunksize: plaintext size in bytesfile_id: 16 random bytes (hex), unique per file, salting the file key
File payload
Each file's plaintext is split into chunks of chunk_size bytes (an empty file is one empty chunk, so it still carries a tag). Each chunk is encrypted independently and stored as ciphertext || tag (plaintext length + 16). Because every non-final chunk holds exactly chunk_size bytes of plaintext, chunk i of a file always starts at data_offset + i*(chunk_size + 16), which enables random access by seek without scanning.
Key derivation (HKDF-SHA256, RFC 5869)
master_keyis 32 bytes, and where it comes from at runtime is not part of this baseline specification: the open framework takes it as input, while in a shipped game the closed key-derivation logic hides and reconstructs it- Per-file keys scope the nonce scheme to one key per file
Nonce (12 bytes) and AAD per chunk
The counter makes each nonce unique within a file, and the unique per-file key makes it unique across files. The AAD binds each chunk to its position and flags the final chunk, so chunks cannot be reordered and a file cannot be silently truncated.
Security notes (baseline)
- Confidentiality and integrity per chunk via GCM: tampering anywhere causes decryption to fail
- The format is public and fixed, so on its own it offers baseline strength: a universal reader is possible only if the attacker also holds the master key; the real barriers are runtime key derivation (closed) and per-build polymorphism (closed), neither of which is covered by this specification
- Out of scope by design: memory ripping and screen capture (see Threat model)
Reference implementation
- Writer:
src/renmori/container/writer.py - Reader and streaming:
src/renmori/container/reader.py - AES-GCM backends:
src/renmori/container/crypto.py - Format constants and key derivation:
src/renmori/container/format.py