Skip to content

Add config obfuscation (T9f) - #6

Merged
kirre-bylund merged 2 commits into
devfrom
feat/config-obfuscation
Nov 13, 2026
Merged

kirre-bylund merged 2 commits into
devfrom
feat/config-obfuscation

Conversation

@kirre-bylund

@kirre-bylund kirre-bylund commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Implements T9f. A config file can now be plain JSON or obfuscated Base64, matching the other SDKs' "plain or encrypted" behaviour.

The decision I had to make, and why

"Port it" turned out to be ambiguous. Both engine SDKs use AES with a hardcoded key, and they are not compatible with each other: Unity is AES-128-CBC with an IV prefix, Unreal is AES-256-ECB with PKCS7. So there was no single thing to port — the real question was whether to take on a crypto dependency at all.

I decided against AES. AES is not in the C++ standard library, so matching either engine means adding a crypto implementation to core, which every adopter links. That is against D2's source-only drop-in promise and the spec's refusal to require a package manager.

A vendored AES would be worse than no AES, because it is a security-shaped dependency that invites the reader to believe the result is secure. That is precisely the misreading R29 exists to prevent. A construction that looks like obfuscation and is documented as obfuscation is safer than one that looks like encryption and is not.

So it is a fixed-key XOR stream plus Base64: honest, small, no dependency, no pretence. The header says "this is obfuscation, not encryption" in its first paragraph, and says that an adopter who needs real protection should get OS key storage or an injected seam rather than a stronger cipher with a key in the binary.

If you would rather have AES, say so and I will redo it — but it needs a decision on which format to match, since the two engines disagree.

Other decisions worth knowing

  • Deterministic on purpose. A random IV would change the file on every write and show up in every diff. Identical plaintext producing identical output is fine for obfuscation and would be a defect in encryption — which is a useful illustration of the difference.
  • The loader accepts either form, matching Unreal: JSON first, because a JSON document is never valid Base64, then deobfuscate. A file that is neither reports both possibilities, because the adopter does not know which one they were supposed to have.
  • The empty string is asymmetric and that is deliberate. Base64Encode("") is "", which is correct Base64, but IsValidBase64("") is false because its caller is the config loader and an empty file is not a config. Asserted explicitly rather than left as a surprise.

Verification

  • 11 test functions; all 8 suites pass; zero warnings on MSVC.
  • Verified by breaking the XOR (9 assertions fail) and the Base64 padding (14 fail).
  • Both boundary checks clean; -DLOOTLOCKER_MODULE_ONLY still configures for core and auth;broadcasts.

Spec

lootlocker/index#1901 records the decision, and closes out T9d and T9e which merged in #5 but were still marked "awaiting merge".

A config file can now be plain JSON or obfuscated Base64, matching the other
SDKs' "plain or encrypted" behaviour. The loader tries JSON first, because a
JSON document is never valid Base64, then falls back to deobfuscating.

This is obfuscation, not encryption, and the header says so in the first
paragraph. The key is compiled into the binary, so anyone with a debugger can
recover it. Its only job is to stop a player casually reading an API key out of
a text file. R29 exists because the other SDKs describe this as encryption and
adopters believe them.

Not AES, deliberately. Both engine SDKs use AES with a hardcoded key and they
are not compatible with each other - Unity is AES-128-CBC with an IV prefix,
Unreal is AES-256-ECB with PKCS7 - so "port it" has no single answer. Matching
either means adding a crypto implementation to core, which every adopter links,
against D2's source-only drop-in promise and the spec's refusal to require a
package manager. A vendored AES would also be a security-shaped dependency that
invites the reader to believe the result is secure, which is the exact
misreading R29 is about.

So it is a fixed-key XOR stream plus Base64: honest, small, no dependency, no
pretence. If an adopter needs real protection the answer is OS key storage or
an injected seam, not a stronger cipher with a key in the binary.

Deterministic on purpose. A random IV would mean the file changes every time it
is written, which shows up in every diff. Identical plaintext producing
identical output is fine for obfuscation and would not be for encryption.

The empty string is asymmetric and that is deliberate: Base64Encode("") is "",
which is correct Base64, but IsValidBase64("") is false because its caller is
the config loader and an empty file is not a config. Asserted explicitly rather
than left as a surprise.

Verified by breaking the XOR (9 assertions fail) and the Base64 padding (14
fail). Both boundary checks clean, all 8 suites pass, zero warnings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings November 13, 2026 11:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Active issues remain: obfuscated loads return decoded JSON instead of the documented original raw body, and the public API documentation does not describe the newly accepted Base64 format or its errors.

2 open issues
What changed in this PR

Adds optional, dependency-free XOR-plus-Base64 obfuscation for configuration files while preserving plain JSON support and integrating the behavior into the core library.

Changes:

  • Added Base64 encoding, decoding, validation, and fixed-key XOR obfuscation utilities.
  • Updated configuration loading to accept plain JSON or obfuscated Base64 input.
  • Added build integration and unit tests covering round trips, malformed input, loader behavior, and error handling.
File Description
tests/​test_config_obfuscation.cpp Adds unit tests for Base64 processing, XOR obfuscation, validation, configuration loading, and error handling.
tests/​CMakeLists.txt Registers the configuration obfuscation test executable and its CTest suite.
src/​core/​config_obfuscation.cpp Implements dependency-free Base64 processing and fixed-key XOR obfuscation.
src/​core/​config_file.cpp Adds support for loading plain JSON or obfuscated Base64 configuration files.
include/​lootlocker/​config_obfuscation.h Defines the public Base64 and configuration obfuscation APIs with documented security limitations.
CMakeLists.txt Adds the configuration obfuscation implementation to the core library target.

Review effort: Lite

Comment thread src/core/config_file.cpp Resolved
Comment thread src/core/config_file.cpp Resolved
RawBody() returned the decoded JSON on the obfuscated path, contradicting the
contract in config_file.h that promises the file's original text. The plain-JSON
path honoured it; the obfuscated path did not. Worse, the test asserted the
deviation, so it locked in the wrong behaviour rather than catching it.

The contract wins: an adopter can decode Base64 themselves, but cannot recover
the original bytes once the loader replaces them. The obfuscated path now passes
the original input through, and the test asserts equality with the Base64 rather
than merely checking that the decoded JSON appears. Verified by reverting the fix
and watching two assertions fail.

The header also under-described the format. It said the file is JSON and listed
three error cases, never mentioning that Base64-obfuscated input is now accepted
or the "neither JSON nor Base64" error. It now documents both accepted forms, the
full error list, and what RawBody() carries on each path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kirre-bylund
kirre-bylund force-pushed the feat/config-obfuscation branch from ad52cbc to 980891c Compare November 13, 2026 12:01
@kirre-bylund
kirre-bylund merged commit 59f678d into dev Nov 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants