Pak encryption and decryption
Dawn Engine pak encryption protects shipped asset packs (.pak files) so that file data, file names, and the zip central directory are ciphertext on disk. Ordinary players and standard zip tools (7-Zip, WinRAR, unzip, Python zipfile) cannot list or extract the real assets. At runtime the Archive layer decrypts on demand; game code, FileIO, and asset loading do not need changes.
Maturity: Delivered. End-to-end packaging, runtime decryption, key hardening, and shipping validation are in place. Encryption is a build- and pack-time opt-in: tools and launchers must be built with LY_PAK_ENCRYPTION=ON, and bundles must be produced with --encrypt / --encrypt-paks.
This is hardening, not absolute DRM. Because the client must decrypt offline, a determined reverse engineer with full control of the binary can eventually recover keys or dump plaintext from memory. The goal is to stop casual extraction and raise the cost of amateur reverse engineering above the value of the assets.
What problem this solves
O3DE pak files are zip archives. Without encryption, anyone who obtains game_<platform>.pak can open it with common tools and pull meshes, textures, configs, and other content. Dawn Engine reuses the engine’s existing zip encryption header skeleton and implements AES-256-CTR encryption of both the central directory and each stored file, so the pack is unreadable outside the matching launcher.
It does not encrypt the engine executable itself, replace the pak/zip container format, or require online key servers in v1.
How it works
- Pack time:
AssetBundlerBatch(orexport-project --encrypt-paks) compresses assets as usual, then encrypts each file’s stored bytes and the central directory (CDR). Each pack gets its own salt; keys are derived from a 32-byte master key. - Optional integrity: from format v2, HMAC-SHA256 detects tampering. From v4, optional Ed25519 CDR signatures stop forged packs even if the symmetric master key were extracted.
- Ship time: the release launcher embeds an obfuscated copy of the same master key (and optionally the signature public key). No key environment variables are required on the player machine.
- Run time: opening a pack decrypts the CDR; reading a file decrypts that file’s ciphertext in place, then decompresses. Mixed mounts of plaintext and encrypted packs are supported when not in strict mode.
Seed / asset list → AssetBundler (--encrypt) → encrypted .pak
↑
DAWN_PAK_MASTER_KEY (env / file)
↓
Launcher build (embedded obfuscated key)
↓
OpenPack → decrypt CDR → ReadFile → decrypt → decompress → assets
Prerequisites
| Requirement | Notes |
|---|---|
| Encryption-enabled toolchain | Build AssetBundlerBatch and the shipping launcher with LY_PAK_ENCRYPTION=ON. An OFF build that receives --encrypt fails instead of writing a plaintext pack. |
| Master key | 32 bytes (64 hex characters), injected via environment variable or key file—never via git, setreg, or CLI plaintext. |
| Optional sign key | 32-byte Ed25519 private seed (DAWN_PAK_SIGN_KEY) for v4 signed packs; public key is embedded at launcher configure time. |
| Same key for pack and embed | Packaging and launcher embed must use the same master key, or runtime rejects the pack with a master-key mismatch (KCV). |
Recommended shipping CMake flags for the launcher:
-DLY_PAK_ENCRYPTION=ON
-DLY_PAK_ENCRYPTION_STRICT=ON
-DLY_PAK_EMBED_MASTER_KEY=ON
| Switch | Role |
|---|---|
LY_PAK_ENCRYPTION | Compiles encrypt/decrypt paths |
LY_PAK_ENCRYPTION_STRICT | Fail closed on decrypt/HMAC/signature failures; if a sign public key is embedded, unsigned packs are rejected |
LY_PAK_EMBED_MASTER_KEY | At configure time, generate obfuscated key material into the build tree (never commit the generated header) |
Day-to-day tip: use plaintext packs in profile/debug for iteration; encrypt only release PAK shipping builds.
Prepare keys (never commit)
Generate a master key:
openssl rand -hex 32
# or
python -c "import secrets; print(secrets.token_hex(32))"
Inject at pack (and launcher configure) time:
# Linux / macOS
export DAWN_PAK_MASTER_KEY=<64-hex>
# or
export DAWN_PAK_MASTER_KEY_FILE=/secure/dawn_pak_master_key.hex
# Windows PowerShell
$env:DAWN_PAK_MASTER_KEY = "<64-hex>"
Optional CDR signing (v4):
export DAWN_PAK_SIGN_KEY=<64-hex> # or DAWN_PAK_SIGN_KEY_FILE=<path>
# Dev/test only for the public key:
export DAWN_PAK_SIGN_PUBLIC_KEY=<64-hex>
If DAWN_PAK_SIGN_KEY is set, packs are signed; if not, packs remain encrypted but unsigned and still load (unless STRICT + embedded public key requires signatures).
Hard rules
- Do not put master or sign keys in git, CI logs, crash dumps, or command-line arguments.
- Prefer CI secrets / a vault; use the same secret for both “bundle” and “embed launcher” steps.
- Repository guards (
.gitignore,scripts/pak_encryption/check_no_keys.pyon PRs) block accidental key check-ins.
Produce encrypted packs
Pick one path. The master key is always read from the environment or a key file—there is no CLI flag that accepts the raw key.
Path A — export-project (recommended for full ship)
export DAWN_PAK_MASTER_KEY=<64-hex>
o3de export-project -es ExportScripts/export_source_built_project.py \
--project-path <ProjectPath> \
-assets --build-tools --config release --archive-output zip \
--seedlist <ProjectPath>/Assets/seedlist.seed -out <OutputPath> \
--encrypt-paks
--encrypt-paks/-epenables encryption;--no-encrypt-paks/-nepturns it off.- Missing master key with encryption requested aborts early; no plaintext pack is written.
Path B — AssetBundlerBatch bundles
export DAWN_PAK_MASTER_KEY=<64-hex>
AssetBundlerBatch bundles \
--assetListFile game_pc.assetlist \
--outputBundlePath Cache/pc/game_pc.pak \
--maxSize 2048 \
--encrypt \
--platform pc \
--allowOverwrites
Path C — AssetBundlerBatch bundleSeed
export DAWN_PAK_MASTER_KEY=<64-hex>
AssetBundlerBatch bundleSeed \
--seedListFile game.seed \
--outputBundlePath Cache/pc/game_pc.pak \
--encrypt --platform pc
| Flag | Meaning |
|---|---|
--encrypt | Mark the bundle for Dawn pak encryption |
--outputBundlePath | Output path (must end in .pak) |
--maxSize <MB> | Split into dependent volumes when exceeded (default 2048) |
--platform | Target platform |
--allowOverwrites | Overwrite existing outputs (typical in CI) |
Multi-volume packs
When size exceeds --maxSize, the bundler writes <name>.pak, <name>__1.pak, <name>__2.pak, … (not .pak.001). Each volume is a full encrypted pack with its own salt and CDR crypto. Runtime mounts volumes from the primary manifest. Verify every volume, not only the primary.
Runtime decryption
With an ON launcher and a matching embedded (or env-provided) key:
- Encrypted packs open and load like normal packs.
- Call sites under
IArchive/ FileIO stay unchanged. - Decrypt is streaming and offset-friendly (AES-256-CTR); packs are not fully decrypted to disk.
- In non-strict configurations, plaintext and encrypted packs can be mounted together.
For shipping, prefer embedded obfuscated key + STRICT. Environment variables are for pack tools and local/dev launchers, not for end-user machines.
Verify after packaging
| Check | Command | Expect |
|---|---|---|
| Not extractable by common tools | python scripts/pak_encryption/verify_pak_unreadable.py Cache/pc/game_pc.pak --expect-name <known-asset> | Exit 0 |
| Script sanity on plaintext | Same with --plaintext on a non-encrypted pack | Exit 0 |
| Pack key matches embed key | python scripts/pak_encryption/verify_key_fingerprint.py --expected-file <embed-fingerprint> | Fingerprints match |
| No plaintext key in binaries | python scripts/pak_encryption/scan_binary_for_key.py <install-or-layout>/ | Exit 0 |
| OFF vs ON-without-encrypt | python scripts/pak_encryption/compare_pak_regression.py <off.pak> <on-plain.pak> | Equivalent aside from timestamps |
For multi-volume packs, run the unreadable check on the primary and each __N volume.
Minimal CI sketch:
export DAWN_PAK_MASTER_KEY="$PAK_MASTER_KEY_SECRET"
export DAWN_PAK_SIGN_KEY="$PAK_SIGN_KEY_SECRET" # optional
cmake --preset windows-mono \
-DLY_PAK_ENCRYPTION=ON -DLY_PAK_ENCRYPTION_STRICT=ON -DLY_PAK_EMBED_MASTER_KEY=ON
cmake --build build/windows_mono --config release
o3de export-project -es ExportScripts/export_source_built_project.py \
--project-path <ProjectPath> -assets --build-tools --config release \
--archive-output zip --seedlist <ProjectPath>/Assets/seedlist.seed \
-out <OutputPath> --encrypt-paks
python scripts/pak_encryption/verify_pak_unreadable.py <layout>/Cache/pc/game_pc.pak --expect-name game.cfg
python scripts/pak_encryption/scan_binary_for_key.py <layout>/
Limits and related features
Safe to rely on:
- AES-256-CTR encryption of CDR and per-file data; HMAC integrity (v2+); optional Ed25519 CDR signing (v4);
- Transparent runtime load; multi-volume
__Nencryption; mixed plaintext/encrypted mounts when not STRICT; - Key via env / file only; obfuscated embed for shipping; PR key-leak scan.
Do not assume:
- Absolute protection against professional reverse engineering;
- Online DRM or server-side key delivery in v1;
- Compatibility with external zip tools (they are expected to fail);
- Encryption without rebuilding tools/launcher with
LY_PAK_ENCRYPTION=ON; - Successful load when pack and launcher keys differ.
Troubleshooting
| Symptom | Likely cause | What to try |
|---|---|---|
| “pak encryption support is not compiled in” | Tools built with encryption OFF | Rebuild AssetBundlerBatch with LY_PAK_ENCRYPTION=ON |
| “no pak master key is available” | Missing DAWN_PAK_MASTER_KEY(_FILE) | Set the env var or key file before packing |
| Runtime “master key mismatch” | Pack key ≠ embedded key | Align secrets; run verify_key_fingerprint.py; rebuild pack and launcher |
| “signature is invalid” / “requires signed paks” | Sign key/public mismatch, or STRICT + unsigned pack | Use matching DAWN_PAK_SIGN_KEY / embedded public key, or ship signed packs |
| Configure: “embedding the pak master key failed” | Embed ON but no key at configure | Inject DAWN_PAK_MASTER_KEY(_FILE) into the configure environment |
check_no_keys.py hits | Key or generated header staged | Remove from the commit; confirm .gitignore |
| 7-Zip can still list real names | Pack was not encrypted | Confirm --encrypt / --encrypt-paks and ON tools |
Next steps
- Introduction: Dawn Engine capabilities, including asset protection.
- World Streaming: large-world streaming that may also ship as packs/DLC.