Name the transformation before you trace it
An opaque request body can be encrypted, compressed, serialized or Base64-wrapped, and those are different problems. Confusing them wastes time, so decide which one you are looking at first. The content type, the magic bytes, the change in length and the code that creates the body tell you.
| Signal | What it usually is | Next check |
|---|---|---|
| Body starts with 1f 8b 08 | Gzip | Decompress, then inspect the inner bytes. |
| Body is a short set of printable characters | Base64 | Decode, then check the structure. |
| Body is tagged binary with field boundaries | Protobuf or a custom serializer | Map fields, not cipher. It is structured data, not a secret. |
| No readable header, length shifts with input | A cipher | Find the mode, key source, IV and authentication tag. |
Follow the bytes backward
Begin where the body enters the HTTP client and trace its inputs. The function that returns those bytes is the last place you need to read; work from it back to the plain object. That exposes the serializer, the compression step, the cipher mode, the key source, the IV or nonce and the final encoding. Order matters: JSON that is compressed then encrypted produces different bytes from JSON encrypted then compressed.
# compare your reconstruction with the app output, byte for byte
python3 -c 'import sys,binascii; d=sys.stdin.buffer.read(); print(len(d), binascii.hexlify(d[:16]).decode())'
# 7b 22 ... -> starts with { " plaintext JSON
# 1f 8b 08 -> gzip
# edef / 08 -> ciphertext or protobuf, not a text envelopeConstants in Java or Kotlin are only one source. A key or a salt can come from remote configuration, the Android Keystore, a native library or an earlier handshake. A native method that returns a byte array needs the same input-and-output tracing as a Java method, because that is often where the key or the derived value is created.
Reproduce exact bytes
- Preserve field order when signing or encryption uses the raw serialized bytes.
- Match character encoding, padding and newline rules.
- Track whether timestamps are seconds, milliseconds or formatted text.
- Confirm whether the IV is random, derived or sent beside the ciphertext.
- Keep binary data as bytes until the protocol explicitly encodes it.
The signer is rarely given a JSON object. It is given a byte array, so the reconstruction has to produce that byte array exactly. Reformatting the JSON, reordering keys or letting a serialiser add a space will change the signature even though the parsed object is identical.
Test with controlled inputs
Change one field in the app and compare the output. Repeated plaintext producing repeated ciphertext points to deterministic inputs. A different ciphertext on every run points to a fresh IV, nonce or session value. Confirm at the function boundary by logging the arguments and the return of the serialiser and the cipher, so you know which step produced what you are looking at.
A worked example
Say the app encrypts a body with AES-256-GCM and sends the IV and the tag beside the ciphertext as Base64 fields. The client must derive the same key, generate a fresh IV for each call, encrypt the exact serialised JSON, base64-encode the IV, the ciphertext and the tag and package them in the order the server expects. A fixed IV copied from the app looks correct, but the authentication tag will not validate on a second call because GCM binds the tag to the ciphertext and the IV.
The final client should accept normal business inputs and perform the full transformation itself. A pasted encrypted blob proves little, because it cannot generate the next valid request.
Some servers also reject a ciphertext or a nonce they have seen before, so a request that is correct can still fail if you reuse an old value. That is a per-call rule, not an algorithm error, and the client has to generate a fresh value each time it runs.
Keep a log of the order each step runs: plain object, serializer, compression, cipher, encoding. When you change one field and the output changes in a way you did not expect, the log tells you which step consumed that field and which one did not. That is usually faster than re-deriving the whole pipeline from scratch.
Reviewed 30 August 2026 · SReverse research desk