This document provides a deep-dive analysis of the encryption logic implemented in encryption_rewrite.js. The script is designed to obfuscate and encrypt key-value data pairs using a custom, multi-stage process involving PRNGs, custom base64-like encoding, and several layers of XOR and mixing operations. The logic is intentionally complex and mimics real-world obfuscated payload builders.
| Document | Description |
|---|---|
| README.md | Project overview and introduction |
| ENCRYPTION.md | Detailed analysis of the encryption algorithm |
| DECRYPTION.md | Implementation of the decryption process |
| technical_analysis.md | Technical deep-dive into cryptographic properties |
| LEARN.md | Educational guide on reverse engineering techniques |
- Initialization: The encryptor is initialized with a
hash,cid, and optionalsalt. - Buffer Construction: Each key-value pair is encoded and obfuscated, then appended to an internal buffer.
- PRNG XOR: The buffer is XORed with a PRNG sequence seeded by the
cidandsalt. - Marker Byte: A marker byte is appended, also obfuscated with the PRNG.
- Custom Base64-like Encoding: The resulting byte array is encoded into a string using a custom 6-bit mapping and further obfuscated with the salt.
- Output: The final string is the encrypted payload.
Note What the salt is doing (in really simplified terms) is to change the last char of the encryption string. It will be so "based on timestamp" for this reason when comparing 2 encrypted strings made with the same cid, hash and signals, be sure to remove the last char before comparing them (unless you are using the same salt). Take a look into validity_check.js
- ENCRYPTION.md
- Navigation
- High-Level Encryption Pipeline
- Table of Contents
- Class:
DataDomeEncryptor- Constructor
- Key Methods
_generateHsv()_customHash(str)_encode6Bits(value)_mixInt(value)_createPrng(seed, salt)_utf8Xor(str, prng)_safeJson(value)_encodePayload(byteArr, salt, encode6Bits)_resetEncryptionState()_addSignal(key, value)_buildPayload(cid)add(key, value)encrypt()setChallengeType(challengeType)getChallengeType()
- Step-by-Step Encryption Example
- Debug Files
- Notes on Obfuscation
- Diagrams
- Summary Table
- Conclusion
- Challenges and Methodologies in Reverse Engineering DataDome
- DataDome Encryption: Reverse Engineering Process
Need DataDome Bypass Solutions?
If you need a reliable DataDome bypass solution for your project, turn to the experts who truly understand the technology. My company, TakionAPI, offers professional anti-bot bypass APIs with proven effectiveness against DataDome and other bot-defense systems.
No more worrying about understanding, reversing, and solving the challenge yourself, or about keeping it up to date every day. One simple API call does it all.
We provide free trials, example implementations, and setup assistance to make the entire process easy and smooth.
- 📄 Check our straightforward documentation
- 🚀 Start your trial
- 💬 Contact us on Discord for custom development and support.
Visit TakionAPI.tech for real, high-quality anti-bot bypass solutions — we know what we're doing.
- Inputs:
hash,cid,salt(optional),challengeType(optional, default: 'captcha') - Purpose: Sets up the encryption state, including PRNG seeds and internal buffer.
- Challenge Types:
captcha: Default challenge type, uses standard DataDome CAPTCHA parametersinterstitial: Alternative challenge type with different XOR constants and fixed HSV value
- Generates a pseudo-random HSV string based on the hash and random values.
- Used as a hidden value in the encryption process (not directly in the buffer).
- Custom string hashing function, returns a 32-bit integer.
- Used to seed PRNGs.
- Maps a 6-bit value (0-63) to a custom character code for the payload.
- The mapping is non-standard and intentionally obfuscated.
- Bitwise mixing function for PRNG state.
- Used to further obfuscate PRNG state transitions.
- Returns a PRNG function with internal state.
- The PRNG is used for both buffer obfuscation and the final XOR step.
- The PRNG state is advanced and mixed in a non-standard way, with salt affecting the output.
- Converts a string to a UTF-8 byte array, then XORs each byte with the PRNG output.
- Used for both keys and values.
- Safely JSON-stringifies a value, returns undefined on error.
- Encodes an array of bytes into a custom base64-like string.
- Each group of 3 bytes is obfuscated with the salt, then split into 4 groups of 6 bits, and mapped to characters.
- Initializes or resets the encryption state (PRNG, buffer, etc.).
- Sets up the PRNG seeds and buffer.
- Encodes and obfuscates a key-value pair, then appends it to the buffer.
- Steps:
- Compute a start byte:
prng() ^ (buffer.length ? 44 : 123) - Encode the key as UTF-8, XOR with PRNG, append to buffer
- Add a separator byte:
58 ^ prng() - Encode the value as UTF-8, XOR with PRNG, append to buffer
- Compute a start byte:
- Finalizes the buffer and produces the encrypted payload string.
- Steps:
- Create a PRNG seeded with
cidandsalt - XOR each buffer byte with the PRNG output
- Append a marker byte:
125 ^ prng(true) ^ cidPrng() - Encode the result with
_encodePayload - Write debug files for each stage
- Create a PRNG seeded with
- Public method to add a key-value pair (calls
_addSignal).
- Public method to build the encrypted payload (calls
_buildPayload).
- Updates the challenge type and resets the encryption state with appropriate parameters.
- Valid values are 'captcha' or 'interstitial'.
- Returns the current challenge type being used.
-
Initialization
- PRNG seeds are derived from the hash, cid, and salt.
- Internal buffer is empty.
-
Adding Key-Value Pairs
- For each pair:
- Start byte is computed and added.
- Key is UTF-8 encoded, XORed with PRNG, and added.
- Separator byte is added.
- Value is UTF-8 encoded, XORed with PRNG, and added.
- For each pair:
-
Finalizing Buffer
- Buffer is written to
debug_encrypt_buffer.json(plaintext, obfuscated but not yet PRNG-XORed).
- Buffer is written to
-
PRNG XOR
- Each buffer byte is XORed with a PRNG seeded by
cidandsalt. - Result is written to
debug_encrypt_cidprng.json(ciphertext, ready for encoding). - Marker byte is appended (also PRNG-XORed).
- Each buffer byte is XORed with a PRNG seeded by
-
Custom Base64-like Encoding
- The PRNG-XORed buffer is encoded using
_encodePayload:- Each group of 3 bytes is obfuscated with the salt, then split into 4 groups of 6 bits.
- Each 6-bit group is mapped to a character using
_encode6Bits.
- The final string is written to
debug_encrypt_final.txt.
- The PRNG-XORed buffer is encoded using
-
Output
- The encrypted payload string is returned.
debug_encrypt_buffer.json: Buffer after all key-value pairs are added, before PRNG XOR.debug_encrypt_cidprng.json: Buffer after PRNG XOR and marker byte, before base64-like encoding.debug_encrypt_final.txt: Final encoded string (the encrypted payload).
- The PRNG logic is intentionally non-standard, with state mixing and salt affecting output.
- The custom base64-like encoding uses a non-standard 6-bit to char mapping.
- Multiple layers of XOR and mixing make reverse engineering non-trivial.
[Input Data] --(addSignal)--> [Obfuscated Buffer] --(PRNG XOR)--> [Cipher Buffer + Marker] --(Custom Base64)--> [Final String]
| Step | Input | Output | File (if any) |
|---|---|---|---|
| Buffer Construction | key-value pairs | Obfuscated buffer | debug_encrypt_buffer.json |
| PRNG XOR | Obfuscated buffer | Cipher buffer + mark | debug_encrypt_cidprng.json |
| Base64-like Encode | Cipher buffer + mark | Final string | debug_encrypt_final.txt |
The encryption process in encryption_rewrite.js is a multi-stage, obfuscated pipeline involving custom PRNGs, XOR, and encoding. Each step is carefully designed to make decryption non-trivial without full knowledge of the process and parameters.
The reverse engineering of DataDome's encryption algorithm required a systematic approach involving multiple stages of testing and validation:
- Dynamic Capture: Intercepted encrypted payloads during live DataDome captcha challenges
- Client-Side Debugging: Used browser developer tools to locate the encryption functions in obfuscated JavaScript
- Format Identification: Analyzed patterns in the encrypted outputs to identify the encoding scheme
- Excessive Code Obfuscation: Variable names like
Le,qe,Ta, andyaprovided no semantic meaning - Dead Code Injection: Large portions of the code were never executed but existed to confuse analysis
- Control Flow Obfuscation: Function calls were wrapped in complex conditions and indirect references
- Dynamic Value Generation: Many critical constants were generated through mathematical operations rather than directly stated
- Function Isolation: Extracted key functions and tested them individually with controlled inputs
- Breakpoint Analysis: Set strategic breakpoints to observe internal values at critical points
- Input-Output Mapping: Created test cases to map specific inputs to their encrypted outputs
- Differential Analysis: Made small changes to inputs to observe how they affected the output
- State Tracking Complexity: The PRNG maintains internal state across multiple calls, making single function analysis insufficient
- Dual PRNG Interaction: The interaction between the main PRNG and CID-PRNG was particularly difficult to trace
- Cache Timing Issues: The PRNG caching mechanism caused non-obvious output patterns that initially appeared random
- Mixed Encoding Types: Both strings and numeric values had different encoding paths
- Invisible Markers: The buffer contains invisible markers (start bytes, separators) that were difficult to identify
- Nested XOR Operations: Each byte undergoes multiple XOR operations, making it hard to isolate the effect of each
- Exact Reproduction Requirement: Even a single-byte difference in any stage would cascade into completely different output
- Parameter Sensitivity: Small changes to inputs like salt or hash would drastically alter the output
- Edge Case Handling: Special cases like empty values, null values, and complex nested structures required specific testing
To ensure our reverse engineering was accurate, we developed a comprehensive testing framework:
Each function was isolated and tested with controlled inputs:
// Example test for the _customHash function
const testHash = (input) => {
const originalResult = originalCode.hash(input);
const rewrittenResult = rewrite._customHash(input);
console.log(`Input: "${input}"`);
console.log(`Original hash: ${originalResult}`);
console.log(`Rewritten hash: ${rewrittenResult}`);
console.log(`Match: ${originalResult === rewrittenResult}`);
};
// Test cases
testHash(""); // Empty string
testHash("abc"); // Simple string
testHash("!@#$%^&*()"); // Special characters
testHash("A".repeat(1000)); // Long stringFor multi-stage processes, we verified each intermediate step:
// Add debug outputs at each stage
fs.writeFileSync('debug_encrypt_buffer.json', JSON.stringify(this._buffer));
fs.writeFileSync('debug_encrypt_cidprng.json', JSON.stringify(output));
fs.writeFileSync('debug_encrypt_final.txt', encoded);For complete validation, we:
- Created test fixtures with known inputs and outputs
- Processed a variety of data types and structures
- Compared outputs with the original algorithm byte-by-byte
- Verified that our decryption could reverse both the original and rewritten encryption
We identified and tested numerous edge cases:
- Empty strings and null values
- Unicode characters including multi-byte and surrogate pairs
- Very large numbers and scientific notation
- Nested objects and arrays
- Boolean values and special JSON values
The reverse engineering process yielded several key insights about DataDome's approach:
- Intentional Complexity: The algorithm appears deliberately over-engineered to deter analysis
- Layered Obfuscation: Rather than relying on strong cryptography, multiple layers of weak obfuscation are stacked
- Performance Constraints: The algorithm makes trade-offs that favor execution speed over security
- Browser Compatibility: The implementation avoids modern APIs, likely for broader compatibility with older browsers
- Evolution Patterns: Comparing versions across time showed minimal changes to the core algorithm, suggesting a lack of security iterations
These methodical testing approaches and careful analysis were essential to successfully reverse engineering the complete encryption system, enabling us to create both a clean implementation and a working decryption solution.
This document details the step-by-step process of reverse engineering DataDome's obfuscated encryption algorithm from encryption_original.js to the clean implementation in encryption_rewrite.js.
The original code employs multiple layers of sophisticated obfuscation:
//const _hsv = "93440349C6C6";
let cid = "f5plrtBvWRjaAknic6efnfeti1YCvwXlZYI_tK~4OUEKH_w6OgQEA6NyAMgf1CeWCTaKdySlOVR_uIbSV2yv~WwLUperWQAqpqOWFZyfnM2RYuPz5LgCLTKo4Khv~bdv"
let hash = "14D062F60A4BDE8CE8647DFC720349"
var n = hash.slice(-4),
t = Math.floor(Math.random() * 9),
e = Math.random().toString(16).slice(2, 10).toUpperCase();
const _hsv = e.slice(0, t) + n + e.slice(t);
var Le = Math.ceil(2554.92),
qe = Math.floor(1.69);
var ra = parseInt(747.75);
var Ma = parseInt(-816.33);The most confusing elements include:
- Nonsensical variable names (
Le,qe,ra,Ma, etc.) - Floating-point constants that get rounded to integers, making their purpose unclear
- Dead code branches with complex conditions that always evaluate to the same result
- Multiple nested closures to hide the actual functionality
The core encryption functionality is buried in a complex closure pattern:
var Ea = function () {
// Unreachable condition check
if (Ta && s[150][460] != s[467][62]) return ya;
// Meaningless operations
Math.ceil(4.1), Math.ceil(0.25), Ta = 1;
// Important constants hidden among noise
var e = 1789537805, // Default hash result
t = Math.floor(1.29), // Useless
a = parseInt(1567.97), // Useless
M = 9959949970, // PRNG seed component
g = !0; // Alt mode flag
// Function d: Hash function
function d(r) { /* ... */ }
// Function D: Mixing function
function D(e) { /* ... */ }
// Function N: Encoding function
function N(e) { /* ... */ }
// Function I: PRNG creator
function I(e, t) { /* ... */ }
// The actual returned encryption function
return ya = function (e, t) { /* ... */ };
}()The main encryption function is created by the IIFE (Immediately Invoked Function Expression) and stored in ya, which is then accessed through Ea.
function d(r) {
if (!r) return e;
for (var M = 0, g = Math.ceil(-981.04), d = Number(-1638), h = 0; h < r.length; h++)
M = (M << 5) - M + r.charCodeAt(h) | 0;
return (2 * (Le | a) + 3 * ~(Le | a) - 2 * (~Le | a) - ~(Le & a) >
-1 * (ra & ~s) + 2 * ~(ra & s) + 1 * ~(ra ^ s) - 3 * ~(ra | s) - 2 * ~(ra | ~s) ?
0 != M : (d | g) - 2 * (~d & g) + ~g - (d | ~g) > 331) ? M : e;
}Reverse engineering steps:
- Identifying the algorithm: This is a variant of the djb2 hash algorithm
- Removing noise variables:
ganddare unused in the actual computation - Simplifying the conditional: The complex condition evaluates to
0 != M ? M : e - Determining the fallback value:
e(1789537805) is returned if the input is empty or the hash is 0
Rewritten as:
/**
* Hashes a string using a custom algorithm, returns a 32-bit integer or a fallback constant.
* @param {string} str
* @returns {number}
*/
_customHash(str) {
if (!str) return 1789537805;
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i) | 0;
}
return hash !== 0 ? hash : 1789537805;
}function D(e) {
e ^= e << 13;
e ^= e >> 17;
return e ^ e << 5;
}This function was relatively straightforward - it's a bitwise mixing function using XOR and shifts. It creates non-linear transformations of the input value.
Rewritten as:
/**
* Bitwise mixing function for PRNG state.
* @param {number} value
* @returns {number}
*/
_mixInt(value) {
value ^= value << 13;
value ^= value >> 17;
return value ^ value << 5;
}function N(e) {
return e > 37 ? 59 + e : e > 11 ? 53 + e : e > 1 ? 46 + e : 50 * e + 45;
}This function maps 6-bit values (0-63) to ASCII character codes.
Analysis:
- Values 0-1: Special cases mapping to '-' (45) and '_' (95)
- Values 2-11: Map to ASCII '0'-'9' (48-57)
- Values 12-37: Map to ASCII 'A'-'Z' (65-90)
- Values 38-63: Map to ASCII 'a'-'z' (97-122)
Rewritten as:
/**
* Encodes a 6-bit value into a custom character code for the payload.
* @param {number} value
* @returns {number}
*/
_encode6Bits(value) {
if (value > 37) {
return 59 + value;
} else if (value > 11) {
return 53 + value;
} else if (value > 1) {
return 46 + value;
} else {
return 50 * value + 45;
}
}function I(e, t) {
var a = e,
n = -1,
c = t,
i = g;
g = !1;
var r = null;
return [function (e) {
var t;
if (null !== r) {
t = r;
r = null;
} else {
++n > 2 && (a = D(a), n = 0);
t = a >> 16 - 8 * n;
t ^= i ? --c : 0;
t &= 255;
e && (r = t);
}
return t;
}];
}This was one of the most complex functions to understand. It creates a stateful PRNG that:
- Maintains an internal state
athat evolves through theDfunction - Uses a round counter
nto extract different 8-bit segments of the state - Optionally XORs the output with a decrementing salt counter
c - Includes a caching mechanism triggered by the
eparameter
Rewritten as:
/**
* Creates a PRNG function with internal state, used for obfuscation.
* @param {number} seed
* @param {number} salt
* @returns {function(boolean): number}
*/
_createPrng(seed, salt) {
let state = seed, round = -1, saltState = salt, useAlt = this._useAlt;
this._useAlt = false;
let cache = null;
return [function (flag) {
let result;
if (cache !== null) {
result = cache;
cache = null;
} else {
if (++round > 2) {
state = DataDomeEncryptor.prototype._mixInt(state);
round = 0;
}
result = state >> (16 - 8 * round);
if (useAlt) {
result ^= --saltState;
}
result &= 255;
if (flag) {
cache = result;
}
}
return result;
}];
}The core encryption functions were nested inside another function:
return ya = function (e, t) {
var a = M ^ d(e) ^ t,
s = D(D(Date.now() >> 3 ^ 11027890091) * M),
g = I(a, s)[0],
h = [],
j = !0,
p = 0,
x = function (e) { /* UTF-8 conversion and XOR */ },
z = function (e) { /* JSON stringify */ },
A = function (e) { /* Custom base64 encoding */ };
function y(e, t) { /* Adds a key-value pair to the buffer */ }
var T = new Set();
return [y, function (e, t) { /* Deduplicating key-value addition */ },
function (e) { /* Builds the final payload */ }];
}x = function (e) {
for (var t = [], a = 0, c = 0; c < e.length; c++) {
var r = e.charCodeAt(c);
r < 128 ? t[a++] = r : r < 2048 ? (t[a++] = r >> 6 | 192, t[a++] = 63 & r | 128) :
55296 == (64512 & r) && c + 1 < e.length && 56320 == (64512 & e.charCodeAt(c + 1)) ?
(r = 65536 + ((1023 & r) << 10) + (1023 & e.charCodeAt(++c)), t[a++] = r >> 18 | 240,
t[a++] = r >> 12 & 63 | 128, t[a++] = r >> 6 & 63 | 128, t[a++] = 63 & r | 128) :
(t[a++] = r >> 12 | 224, t[a++] = r >> 6 & 63 | 128, t[a++] = 63 & r | 128);
}
for (var o = 0; o < t.length; o++) t[o] ^= g();
return t;
}This function:
- Converts a JavaScript string to UTF-8 bytes
- XORs each byte with the output of the PRNG function
g
Rewritten as:
/**
* Converts a string to a UTF-8 byte array and XORs each byte with the PRNG.
* @param {string} str
* @param {function(): number} prng
* @returns {number[]}
*/
_utf8Xor(str, prng) {
let utf8Bytes = [];
let idx = 0;
for (let i = 0; i < str.length; i++) {
let code = str.charCodeAt(i);
if (code < 128) {
utf8Bytes[idx++] = code;
} else if (code < 2048) {
utf8Bytes[idx++] = code >> 6 | 192;
utf8Bytes[idx++] = 63 & code | 128;
} else if (55296 == (64512 & code) && i + 1 < str.length && 56320 == (64512 & str.charCodeAt(i + 1))) {
// Surrogate pair
code = 65536 + ((1023 & code) << 10) + (1023 & str.charCodeAt(++i));
utf8Bytes[idx++] = code >> 18 | 240;
utf8Bytes[idx++] = code >> 12 & 63 | 128;
utf8Bytes[idx++] = code >> 6 & 63 | 128;
utf8Bytes[idx++] = 63 & code | 128;
} else {
utf8Bytes[idx++] = code >> 12 | 224;
utf8Bytes[idx++] = code >> 6 & 63 | 128;
utf8Bytes[idx++] = 63 & code | 128;
}
}
// XOR each byte with prng()
for (let j = 0; j < utf8Bytes.length; j++) {
utf8Bytes[j] ^= prng();
}
return utf8Bytes;
}z = function (e) {
try {
return JSON.stringify(e);
} catch (e) {
return;
}
}A simple wrapper around JSON.stringify with error handling.
Rewritten as:
/**
* Safely JSON-stringifies a value, returns undefined on error.
* @param {any} value
* @returns {string|undefined}
*/
_safeJson(value) {
try {
return JSON.stringify(value);
} catch (e) {
return;
}
}function y(e, t) {
if (`string` == typeof e && 0 != e.length && (!t || -1 != [`number`, `string`, `boolean`].indexOf(ga(t)))) {
var a, c = z(e), s = z(t);
if (e && void 0 !== s && e !== String.fromCharCode(120, 116, 49)) {
h.push(g() ^ (h.length ? 44 : 123));
Array.prototype.push.apply(h, x(c));
h.push(58 ^ g());
Array.prototype.push.apply(h, x(s));
if (j) {
j = !1;
(`string` == typeof _hsv && _hsv.length > 0 ||
`number` == typeof _hsv && !isNaN(_hsv)) && (a = _hsv);
}
}
}
}This is the core function that adds a key-value pair to the encryption buffer. It:
- Validates the key and value types
- Adds a start marker ('{'/',' XORed with PRNG)
- Adds the key as UTF-8 bytes, XORed with PRNG
- Adds a separator (':' XORed with PRNG)
- Adds the value as UTF-8 bytes, XORed with PRNG
- Has special handling for the first entry
Rewritten as:
/**
* Adds a key-value pair to the buffer, obfuscated and encoded.
* @param {string} key
* @param {string|number|boolean} value
*/
_addSignal(key, value) {
const allowedTypes = ['number', 'string', 'boolean'];
if (typeof key === 'string' && key.length !== 0 && (!value || allowedTypes.includes(typeof value))) {
let hsvTemp;
const keyStr = this._safeJson(key);
const valueStr = this._safeJson(value);
if (key && valueStr !== undefined && key !== 'xt1') {
const startByte = this._prng() ^ (this._buffer.length ? 44 : 123);
this._buffer.push(startByte);
const keyBytes = this._utf8Xor(keyStr, this._prng);
Array.prototype.push.apply(this._buffer, keyBytes);
const sepByte = 58 ^ this._prng();
this._buffer.push(sepByte);
const valueBytes = this._utf8Xor(valueStr, this._prng);
Array.prototype.push.apply(this._buffer, valueBytes);
if (this._isFirst) {
this._isFirst = false;
if ((typeof this._hsv === 'string' && this._hsv.length > 0) ||
(typeof this._hsv === 'number' && !isNaN(this._hsv))) {
hsvTemp = this._hsv;
}
}
}
}
}function (e) {
var t = I(1809053797 ^ d(e), s)[0];
for (var a = [], n = 0; n < h.length; n++) a.push(h[n] ^ t());
a.push(125 ^ g(!0) ^ t());
return A(a);
}This function finalizes the encryption process:
- Creates a new PRNG based on the client ID
- XORs each byte in the buffer with this CID-PRNG
- Adds a closing brace ('}' XORed with both PRNGs)
- Encodes the result using the custom base64-like function
A
Rewritten as:
/**
* Builds the final encrypted payload string for a given cid.
* @param {string} cid
* @returns {string}
*/
_buildPayload(cid) {
const cidPrng = this._createPrng(1809053797 ^ this._customHash(cid), this._salt)[0];
let output = [];
for (let i = 0; i < this._buffer.length; i++) output.push(this._buffer[i] ^ cidPrng());
output.push(125 ^ this._prng(true) ^ cidPrng());
const encoded = this._encodePayload(output, this._salt, this._encode6Bits.bind(this));
return encoded;
}A = function (e) {
for (var t = 0, a = [], n = s; t < e.length;) {
var r = (255 & --n ^ e[t++]) << 16 | (255 & --n ^ e[t++]) << 8 | 255 & --n ^ e[t++];
a.push(
String.fromCharCode(N(r >> 18 & 63)),
String.fromCharCode(N(r >> 12 & 63)),
String.fromCharCode(N(r >> 6 & 63)),
String.fromCharCode(N(63 & r))
);
}
var M = e.length % 3;
return M && (a.length -= 3 - M), a.join('');
}This function performs the final encoding:
- Groups bytes into 3-byte chunks, XORing each with a decrementing salt value
- Converts each chunk to 4 characters using the 6-bit encoding function
N - Handles padding for incomplete chunks through truncation
Rewritten as:
/**
* Encodes an array of bytes into a custom base64-like string.
* @param {number[]} byteArr
* @param {number} salt
* @param {function(number): number} encode6Bits
* @returns {string}
*/
_encodePayload(byteArr, salt, encode6Bits) {
let i = 0;
let output = [];
let n = salt;
// Process each group of 3 bytes
while (i < byteArr.length) {
// Combine 3 bytes into a 24-bit number, with obfuscation
let chunk = (255 & --n ^ byteArr[i++]) << 16 |
(255 & --n ^ byteArr[i++]) << 8 |
(255 & --n ^ byteArr[i++]);
// Split into 4 groups of 6 bits and encode
output.push(
String.fromCharCode(encode6Bits((chunk >> 18) & 63)),
String.fromCharCode(encode6Bits((chunk >> 12) & 63)),
String.fromCharCode(encode6Bits((chunk >> 6) & 63)),
String.fromCharCode(encode6Bits(chunk & 63))
);
}
// Handle padding if input length is not a multiple of 3
let mod = byteArr.length % 3;
if (mod) output.length -= 3 - mod;
return output.join('');
}After understanding all the components, the final step was to structure them into a proper class:
/**
* DataDomeEncryptor
* Implements a custom encryption/obfuscation routine for key-value data pairs.
* The encryption logic is intentionally complex and mimics a real-world obfuscated payload builder.
*/
class DataDomeEncryptor {
/**
* @param {string} hash - The hash string used as part of the encryption seed.
* @param {string} cid - The client/session identifier used in the payload.
* @param {number|null} salt - Optional external salt for the encryption process.
*/
constructor(hash, cid, salt = null) {
this.hash = hash;
this.cid = cid;
this._hsv = this._generateHsv();
this._externalSalt = salt; // Store the externally provided salt
this._initEncryptor();
}
// ... all the rewritten methods added here ...
/**
* Initializes the encryption engine and sets up the addSignal and buildPayload methods.
*/
_initEncryptor() {
this._resetEncryptionState();
this.addSignal = this._addSignal.bind(this);
this.buildPayload = this._buildPayload.bind(this);
}
/**
* Adds a key-value pair to the encryption buffer (public method).
* @param {string} key
* @param {string|number|boolean} value
*/
add(key, value) {
this.addSignal(key, value);
}
/**
* Builds the encrypted payload for the current cid (public method).
* @returns {string}
*/
encrypt() {
return this.buildPayload(this.cid);
}
}To ensure the rewritten code was functionally identical to the original, several verification steps were taken:
- Debug logging: Adding logging points in both versions to compare intermediate values
- Test data encryption: Encrypting the same data with both versions and comparing outputs
- Roundtrip testing: Encrypting data, then decrypting it to verify the original values are recovered
- Edge case testing: Testing with various input types, empty values, and special characters
The verification confirmed that the clean implementation produces identical results to the original obfuscated code, while being much more readable and maintainable.
The reverse engineering process transformed a heavily obfuscated, difficult-to-understand implementation into a clean, well-structured class that preserves the exact functionality while making it clear how each part of the encryption process works.
This demonstrates that even sophisticated obfuscation techniques can be methodically reversed with careful analysis, and that complex algorithms can be rewritten in a more maintainable form without sacrificing functionality.
For a detailed technical analysis of the encryption algorithm itself, including cryptographic properties and security considerations, see technical_analysis.md.
If you found this project helpful or interesting, consider starring the repo and following me for more security research and tools, or buy me a coffee to keep me up.
Stay in touch with me via discord or mail or anything.
