Jordan Lee Obfuscation
Why String Encryption Wants Per-Call-Site Keys
Encrypting every string literal is the easy part. The design question is which key protects which string. HashGuard’s string transformer is pool-sharded, multi-algorithm, and can derive a contextual key per call site; this post explains why that last option exists.
One key for everything, or one per call site
If every string in a jar is decrypted with the same key, an analyst who recovers that key, or the decryptor that holds it, reads all of them at once. A contextual key narrows the damage: each call site gets its own, so recovering one does not hand over the rest.
HashGuard exposes this as the optional contextual key of the string transformer. The contextualStringKey option also derives the string crypto IVs from the owning class, so the class a string lives in is part of what protects it.
The question is not only how strong the cipher is, but what one recovered key is worth.
How the pool is stored
- Pool-sharded: the string pool is split into shards.
- Each shard is encrypted with AES-CTR and compressed with zlib.
multiAlgoStringsvaries the AES modes across classes to break signatures.cryptoKeyBitssets the AES key size,128in the example configuration.stringEncryptModesets the strength and the runtime decryptor mode,Balancedin the example.
"stringEncryptMode": "Balanced","cryptoKeyBits": 128,"contextualStringKey": true,"multiAlgoStrings": true- 01 string Pool-sharded, AES-CTR and zlib shards, optional contextual key
- 02 polyString Per-class XOR-chain polymorphic decryptor
- 03 jntPolyString Per-call-site XOR and key polymorphic decryptor
Lighter decryptors
When AES is more than you need, two XOR-based options exist. polyString is a per-class XOR-chain polymorphic string decryptor. jntPolyString is a per-call-site XOR plus key polymorphic decryptor in the jnt style. Both are false in the example configuration.
Java 8 and the decryptor
A decryptor has to reach its own class. On Java 9 and later HashGuard uses privateLookupIn, and a Java 8-safe invokedynamic fallback covers Java 8.
Around the strings
comparisonObfuscationreplacesString.equals()with ahashCodeplusInteger.equals()proxy, with a collision fallback.staticFieldInitmoves staticStringandIntegerConstantValuefields into<clinit>asPUTSTATIC.