Back to blogs

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.
  • multiAlgoStrings varies the AES modes across classes to break signatures.
  • cryptoKeyBits sets the AES key size, 128 in the example configuration.
  • stringEncryptMode sets the strength and the runtime decryptor mode, Balanced in the example.
String options from the example configuration json
"stringEncryptMode":   "Balanced","cryptoKeyBits":       128,"contextualStringKey": true,"multiAlgoStrings":    true
  1. 01 string Pool-sharded, AES-CTR and zlib shards, optional contextual key
  2. 02 polyString Per-class XOR-chain polymorphic decryptor
  3. 03 jntPolyString Per-call-site XOR and key polymorphic decryptor
The three string transformers.

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

  • comparisonObfuscation replaces String.equals() with a hashCode plus Integer.equals() proxy, with a collision fallback.
  • staticFieldInit moves static String and Integer ConstantValue fields into <clinit> as PUTSTATIC.