This document provides a detailed theoretical plan for decrypting the payload produced by encryption_rewrite.js. It is based on the deep analysis in ENCRYPTION.md and describes how to reverse each step of the encryption pipeline to recover the original key-value data pairs.
| 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 |
- DECRYPTION.md
- Practical Decryption Challenges and Solutions
- DataDome Decryption: Implementation 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.
- Input: The encrypted payload string (custom base64-like encoded).
- Custom Base64-like Decoding: Decode the string into a byte array, reversing the salt-based obfuscation.
- PRNG XOR Reversal: XOR the decoded buffer with the same PRNG sequence (seeded by
cidandsalt) to recover the obfuscated buffer. - Buffer Parsing: Parse the buffer using the original PRNG logic to extract key-value pairs.
- Output: The recovered key-value data pairs.
- Goal: Reverse the custom base64-like encoding and salt-based obfuscation.
- Process:
- For each group of 4 characters in the payload string:
- Map each character back to a 6-bit value using the inverse of
_encode6Bits. - Combine the 6-bit values into a 24-bit chunk.
- XOR each byte with the salt (in reverse order) to recover the original bytes.
- Map each character back to a 6-bit value using the inverse of
- Remove any padding bytes if present.
- For each group of 4 characters in the payload string:
- Output: The PRNG-XORed buffer (ciphertext + marker byte).
- Goal: Reverse the PRNG XOR applied during encryption.
- Process:
- Initialize the PRNG with the same seed and salt as in encryption (
cidandsalt). - For each byte in the decoded buffer (excluding the marker byte):
- XOR the byte with the PRNG output to recover the obfuscated buffer.
- Advance the PRNG once more to simulate the marker byte.
- Initialize the PRNG with the same seed and salt as in encryption (
- Output: The obfuscated buffer (as produced by
_addSignalin encryption).
- Goal: Parse the obfuscated buffer to extract the original key-value pairs.
- Process:
- Initialize the PRNG with the same seed and salt as used in
_addSignalduring encryption (hashandsalt). - Iterate through the buffer:
- For each entry:
- The first byte is the start byte, XORed with the PRNG output and a constant (44 or 123).
- The key is a UTF-8 byte sequence, each byte XORed with the PRNG output, ending at the separator byte (58).
- The value is a UTF-8 byte sequence, each byte XORed with the PRNG output, ending at the next start byte or end of buffer.
- Decode the key and value from UTF-8 and parse as JSON if possible.
- For each entry:
- Initialize the PRNG with the same seed and salt as used in
- Output: Array of key-value pairs (original data).
- decode6Bits(charCode): Inverse of
_encode6Bits, maps character code to 6-bit value. - decodeCustomBase64(encoded, salt): Decodes the custom base64-like string, reversing salt obfuscation.
- createPrng(seed, salt): Recreates the PRNG logic from encryption for both XOR reversal and buffer parsing.
- utf8Decode(bytes): Decodes a UTF-8 byte array to a string.
- Buffer parsing logic: Mirrors the logic in
_addSignalfor extracting key-value pairs. - Challenge Type handling: Sets appropriate constants based on whether it's a 'captcha' or 'interstitial' challenge.
- Use
debug_encrypt_buffer.json,debug_encrypt_cidprng.json, anddebug_encrypt_final.txtfrom encryption for step-by-step validation. - At each decryption stage, compare the intermediate buffer to the corresponding debug file to ensure correctness.
- Use small test cases and detailed logs to pinpoint any mismatches.
- The PRNG logic is non-standard and must be replicated exactly, including state mixing and salt handling.
- The custom base64-like encoding uses a non-standard mapping; ensure the inverse mapping is correct.
- Buffer alignment and marker byte handling are critical for correct parsing.
- Any deviation in PRNG seeding or advancement will result in incorrect decryption.
[Encrypted String] --(Custom Base64 Decode)--> [Cipher Buffer + Marker] --(PRNG XOR Reversal)--> [Obfuscated Buffer] --(Buffer Parsing)--> [Original Data]
| Step | Input | Output | File for Validation |
|---|---|---|---|
| Base64-like Decode | Encrypted string | Cipher buffer + mark | debug_encrypt_cidprng.json |
| PRNG XOR Reversal | Cipher buffer + mark | Obfuscated buffer | debug_encrypt_buffer.json |
| Buffer Parsing | Obfuscated buffer | Key-value pairs | original_data.json |
Decryption is the exact reversal of the multi-stage, obfuscated encryption pipeline. By carefully mirroring each step and validating against debug files, the original data can be reliably recovered, provided all parameters and logic are correct.
💡 Need a Professional DataDome Bypass?
Successfully implementing DataDome decryption requires deep technical expertise and continuous maintenance. If you need a robust and reliable DataDome bypass solution for your business, TakionAPI.tech offers ready-to-use APIs built by the same researcher who reverse engineered this encryption system.
While the theoretical understanding of the encryption algorithm was a significant first step, implementing a working decryption solution involved several complex practical challenges. This section details the specific difficulties encountered and how they were overcome.
Before attempting to decode actual DataDome payloads, we needed a robust testing framework:
We developed a test harness that could:
- Generate test data with various data types and edge cases
- Encrypt data using both the original and rewritten algorithms
- Attempt decryption and compare with the original data
- Highlight any discrepancies down to the byte level
function runComparisonTest(testData) {
// Encrypt with original algorithm
const originalEncrypted = originalEncryptor.encrypt(testData);
// Encrypt with rewritten algorithm
const rewrittenEncrypted = rewrittenEncryptor.encrypt(testData);
// Compare encrypted outputs
console.log("Original encryption matches rewrite:",
originalEncrypted === rewrittenEncrypted);
// Attempt decryption
const decrypted = decryptor.decrypt(rewrittenEncrypted);
// Compare with original data
const matches = compareDataStructures(testData, decrypted);
console.log("Decryption successful:", matches.success);
if (!matches.success) {
console.log("Differences:", matches.differences);
}
}For troubleshooting, we created specialized debugging tools:
function debugPRNGSequence(seed, salt, count) {
const prng = createPrng(seed, salt);
console.log(`PRNG sequence for seed=${seed}, salt=${salt}:`);
const sequence = [];
for (let i = 0; i < count; i++) {
sequence.push(prng());
}
console.log(sequence.map(b => b.toString(16).padStart(2, '0')).join(' '));
return sequence;
}
function compareBuffers(buffer1, buffer2) {
const maxLen = Math.max(buffer1.length, buffer2.length);
let differences = 0;
let firstDiffPos = -1;
console.log("Buffer comparison:");
console.log("Idx | Buffer1 | Buffer2 | Match");
console.log("----|----------|----------|------");
for (let i = 0; i < maxLen; i++) {
const b1 = i < buffer1.length ? buffer1[i] : undefined;
const b2 = i < buffer2.length ? buffer2[i] : undefined;
const match = b1 === b2;
if (!match && firstDiffPos === -1) {
firstDiffPos = i;
differences++;
}
// Print only the first 10 differences to avoid flooding the console
if (!match && differences <= 10) {
console.log(`${i.toString().padStart(4)} | ${b1?.toString(16).padStart(8) || 'undefined'} | ${b2?.toString(16).padStart(8) || 'undefined'} | ❌`);
}
}
console.log(`Total differences: ${differences}/${maxLen} bytes`);
console.log(`First difference at position: ${firstDiffPos}`);
return { differences, firstDiffPos };
}The most critical issue was ensuring the PRNG produced identical sequences during encryption and decryption.
Problem: Subtle differences in PRNG implementation or state tracking caused decryption to fail completely:
// Initial implementation with subtle bug
function createPrng(seed, salt) {
let state = seed;
let round = 0; // Should be -1!
return function() {
if (round >= 3) { // Should be > 2!
state = mixInt(state);
round = 0;
}
// ...rest of PRNG logic
};
}Solution: Created a validation system that could directly compare PRNG sequences:
// PRNG validation test
function validatePRNG() {
const seed = 0x12345678;
const salt = 0xABCDEF;
// Create PRNGs from both encryption and decryption implementations
const encryptPRNG = encryptor._createPrng(seed, salt)[0];
const decryptPRNG = decryptor.prngHelper._createPrng(seed, salt)[0];
// Generate and compare 1000 values
let mismatchCount = 0;
let firstMismatchPos = -1;
console.log("Validating PRNG implementation...");
for (let i = 0; i < 1000; i++) {
const encValue = encryptPRNG();
const decValue = decryptPRNG();
if (encValue !== decValue) {
mismatchCount++;
if (firstMismatchPos === -1) {
firstMismatchPos = i;
console.log(`First mismatch at position ${i}: ` +
`encrypt=${encValue}, decrypt=${decValue}`);
}
}
}
console.log(`PRNG validation complete: ${mismatchCount} mismatches`);
return mismatchCount === 0;
}This validation helped identify specific issues in the PRNG implementation:
- The round counter initialization at -1 vs 0
- The exact conditions for state mixing
- The application of salt XOR
- The order of operations
The decrypted buffer contains a JSON-like structure, but with the complexity of being dynamically constructed.
Problem:
Initial attempts to use JSON.parse() failed because the structure wasn't standard JSON:
// This approach fails
try {
const jsonData = JSON.parse(jsonStr);
return jsonData;
} catch (e) {
console.error("Failed to parse JSON:", e);
return null;
}Solution: Implemented a character-by-character parser that could handle the DataDome JSON format:
/**
* Parse the JSON string specifically for DataDome format
* @param {string} jsonStr - The JSON-like string to parse
* @returns {Array} - Array of key-value pairs
*/
_parseJsonString(jsonStr) {
const result = [];
// Simplified version of the complex parsing logic
let i = 0;
let parsingKey = false;
let parsingValue = false;
let currentKey = "";
while (i < jsonStr.length) {
// Handle the start of an object or entry
if (jsonStr[i] === '{' || jsonStr[i] === ',') {
parsingKey = true;
i++;
continue;
}
// Handle key parsing
if (parsingKey && jsonStr[i] === '"') {
// Extract the key between quotes
const keyStart = i + 1;
i++;
while (i < jsonStr.length && jsonStr[i] !== '"') i++;
currentKey = jsonStr.substring(keyStart, i);
parsingKey = false;
i++;
continue;
}
// Handle the separator between key and value
if (!parsingKey && !parsingValue && jsonStr[i] === ':') {
parsingValue = true;
i++;
continue;
}
// Handle value parsing based on type
if (parsingValue) {
let value;
// Handle different value types
if (jsonStr[i] === '"') {
// String value
// ...string parsing logic
}
else if (jsonStr[i] === 'n' && jsonStr.substr(i, 4) === 'null') {
value = null;
i += 4;
}
else if (jsonStr[i] === 't' && jsonStr.substr(i, 4) === 'true') {
value = true;
i += 4;
}
else if (jsonStr[i] === 'f' && jsonStr.substr(i, 5) === 'false') {
value = false;
i += 5;
}
else if (jsonStr[i] === '-' || (jsonStr[i] >= '0' && jsonStr[i] <= '9')) {
// Number value
// ...number parsing logic
}
// Add the entry and reset
result.push([currentKey, value]);
parsingValue = false;
currentKey = "";
continue;
}
// Move to next character if no special handling
i++;
}
return result;
}Identifying the correct boundaries between key-value pairs in the buffer was challenging due to the dynamic nature of the data.
Problem: Without clear delimiters, parsing the buffer was error-prone:
// Problematic approach
let currentPos = 0;
while (currentPos < buffer.length) {
// How do we know where one entry ends and the next begins?
// ...parsing logic
}Solution: Reversed the original buffer construction logic to accurately find boundaries:
/**
* Parse the buffer into key-value pairs
* @param {Array<number>} buffer - The decrypted buffer
* @returns {Array<Array>} - Array of key-value pairs
*/
_parseBuffer(buffer) {
const result = [];
let i = 0;
// Initialize PRNG with the same seed used during encryption
const prng = this.prngHelper._createPrng(this.prngSeed, this.salt, true)[0];
while (i < buffer.length) {
// The first byte is a start marker ('{' or ',')
const startByte = buffer[i++] ^ prng();
if (startByte !== 123 && startByte !== 44) {
console.warn(`Unexpected start byte: ${startByte}`);
}
// Read and decode the key
let keyBytes = [];
let b;
while (i < buffer.length) {
b = buffer[i++] ^ prng();
if (b === 58) break; // ':' separator
keyBytes.push(b);
}
// Read and decode the value
let valueBytes = [];
while (i < buffer.length) {
const peekByte = buffer[i] ^ prng(true); // Use flag to peek
if (peekByte === 44 || peekByte === 125) break; // ',' or '}'
b = buffer[i++] ^ prng();
valueBytes.push(b);
}
// Convert bytes to strings
const keyStr = String.fromCharCode(...keyBytes);
const valueStr = String.fromCharCode(...valueBytes);
// Parse JSON values
let key, value;
try {
key = JSON.parse(keyStr);
value = JSON.parse(valueStr);
} catch (e) {
console.warn(`JSON parse error for: ${keyStr} / ${valueStr}`);
key = keyStr;
value = valueStr;
}
result.push([key, value]);
}
return result;
}Several specialized debugging techniques proved essential:
We created a tool to visualize the buffer at different stages:
function visualizeBuffer(buffer, width = 16) {
let output = "Offset | Hex | ASCII\n";
output += "-------|----------------------------------------------------------|----------------\n";
for (let i = 0; i < buffer.length; i += width) {
const slice = buffer.slice(i, i + width);
// Hex representation
const hex = slice.map(b => b.toString(16).padStart(2, '0')).join(' ');
// ASCII representation
const ascii = slice.map(b => {
if (b >= 32 && b <= 126) return String.fromCharCode(b);
return '.';
}).join('');
output += `${i.toString().padStart(6, '0')} | ${hex.padEnd(width*3, ' ')} | ${ascii}\n`;
}
return output;
}To debug PRNG synchronization issues, we traced the state evolution:
function tracePRNGState(seed, salt, steps) {
let state = seed;
let round = -1;
let saltState = salt;
console.log("PRNG state tracing:");
console.log("Step | Round | State (hex) | Output | Salt");
console.log("-----|-------|-----------------|--------|------");
for (let i = 0; i < steps; i++) {
let output;
if (++round > 2) {
state = mixInt(state);
round = 0;
}
output = state >> (16 - 8 * round);
output ^= --saltState;
output &= 255;
console.log(`${i.toString().padStart(4)} | ${round} | ${state.toString(16).padStart(16, '0')} | ${output.toString(16).padStart(2, '0')} | ${saltState}`);
}
}We injected debug code into both the original and our implementation to verify identical behavior:
// Injected into original code
var originalOutputs = [];
var originalPRNGValues = [];
// Hook the original PRNG function
var originalPRNG = I(seed, salt)[0];
var hookedPRNG = function() {
var result = originalPRNG.apply(this, arguments);
originalPRNGValues.push(result);
return result;
};
// Replace the original function
I = function() { return [hookedPRNG]; };
// Then in our implementation
const ourPRNGValues = [];
const ourPRNG = createPrng(seed, salt);
const hookedOurPRNG = function() {
const result = ourPRNG.apply(this, arguments);
ourPRNGValues.push(result);
return result;
};
// After running both
console.log("PRNG sequences identical:",
originalPRNGValues.length === ourPRNGValues.length &&
originalPRNGValues.every((v, i) => v === ourPRNGValues[i]));The decryption implementation revealed several valuable insights:
- Exact Replication is Essential: Even minor deviations in implementation causes complete decryption failures
- End-to-End Testing is Crucial: Unit tests alone were insufficient; full-system tests were needed
- Buffer Visualization Tools: Specialized debugging tools were critical for understanding complex data structures
- Testing with Real Data: Using captured real-world data was more effective than synthetic test cases
- Side-by-Side Comparison: Running both original and rewritten code with the same inputs revealed subtle differences
Overall, the decryption implementation required extreme precision and systematic debugging, but resulted in a robust solution that can decrypt any DataDome payload given the correct hash and client ID.
This document details the process of implementing a decryption solution for DataDome's encryption algorithm. While the encryption_rewrite.js file represents a clean implementation of the encryption process, decryption.js required us to develop a complementary system capable of reversing each step.
Implementing decryption for the DataDome algorithm presented several unique challenges:
- PRNG Sequence Replication: The encryption uses multiple PRNGs with specific state evolution patterns that must be exactly replicated
- Custom Encoding Reversal: The non-standard base64-like encoding needed to be precisely reversed
- Buffer Structure Recognition: The decrypted buffer contains a JSON-like structure that required custom parsing
- XOR Chain Reversal: Multiple layers of XOR operations needed to be applied in the correct sequence
Our approach to developing the decryption implementation involved:
- Full encryption understanding: First deeply understanding every step of the encryption process
- Step-by-step reversal: Designing each decryption step to precisely mirror its encryption counterpart
- Test-driven development: Creating test cases to verify each component's accuracy
The most critical component for successful decryption was precisely replicating the PRNG behavior. We created a helper class that exactly duplicates the encryption PRNG implementation:
class PRNGHelper {
/**
* Bitwise mixing function for PRNG state.
* @param {number} value
* @returns {number}
*/
_mixInt(value) {
value ^= value << 13;
value ^= value >> 17;
value ^= value << 5;
return value;
}
/**
* Creates a PRNG function with internal state, used for obfuscation.
* @param {number} seed
* @param {number} salt
* @param {boolean} useAlt
* @returns {Array<Function>}
*/
_createPrng(seed, salt, useAlt = true) {
let state = seed, round = -1, saltState = salt;
let useAltCopy = useAlt;
let cache = null;
return [function (flag) {
let result;
if (cache !== null) {
result = cache;
cache = null;
} else {
if (++round > 2) {
state = PRNGHelper.prototype._mixInt(state);
round = 0;
}
result = state >> (16 - 8 * round);
if (useAltCopy) {
result ^= --saltState;
}
result &= 255;
if (flag) {
cache = result;
}
}
return result;
}];
}
}Key implementation details:
- Identical state evolution: The round counter and state transformation logic must match exactly
- Same salt application: The salt decrement and XOR operation must occur at identical points
- Caching behavior: The caching mechanism must operate identically to the encryption version
The encryption uses a custom base64-like encoding scheme that needed to be precisely reversed:
/**
* Decode the custom base64-like string
* @param {string} encoded - Encoded string
* @returns {Array<number>} - Array of decoded bytes
* @private
*/
_decodeCustomBase64(encoded) {
let bytes = [];
let n = this.salt;
for (let i = 0; i < encoded.length; i += 4) {
if (i + 3 >= encoded.length) break;
let c1 = this._decode6Bits(encoded.charCodeAt(i));
let c2 = this._decode6Bits(encoded.charCodeAt(i + 1));
let c3 = this._decode6Bits(encoded.charCodeAt(i + 2));
let c4 = this._decode6Bits(encoded.charCodeAt(i + 3));
let chunk = (c1 << 18) | (c2 << 12) | (c3 << 6) | c4;
bytes.push(((chunk >> 16) & 255) ^ (--n & 255));
bytes.push(((chunk >> 8) & 255) ^ (--n & 255));
bytes.push((chunk & 255) ^ (--n & 255));
}
// Handle padding if needed
let mod = encoded.length % 4;
if (mod) {
bytes = bytes.slice(0, bytes.length - (3 - mod));
}
return bytes;
}Key implementation details:
- Character to 6-bit value mapping: Reverse of the encoding function
- Chunk reconstruction: Combining four 6-bit values into a 24-bit integer
- Byte extraction: Splitting the 24-bit integer back into three bytes
- Salt XOR reversal: Apply the same salt decrement and XOR pattern
- Padding handling: Properly handle non-standard truncated output
The custom 6-bit value decoding function:
/**
* Decode a 6-bit encoded character according to DataDome's custom encoding
* @param {number} charCode - Character code to decode
* @returns {number} - Decoded 6-bit value
* @private
*/
_decode6Bits(charCode) {
if (charCode >= 97 && charCode <= 122) return charCode - 59; // 'a'-'z' → 38-63
if (charCode >= 65 && charCode <= 90) return charCode - 53; // 'A'-'Z' → 12-37
if (charCode >= 48 && charCode <= 57) return charCode - 46; // '0'-'9' → 2-11
if (charCode === 45) return 0; // '-' → 0
if (charCode === 95) return 1; // '_' → 1
return 0; // fallback
}After decoding the base64 representation, we need to reverse the CID-PRNG XOR applied during encryption:
/**
* Decrypt the encoded data
* @param {string} encoded - Base64-like encoded data
* @returns {Array<Array>} - Array of key-value pairs
*/
decrypt(encoded) {
// Step 1: Decode the custom base64-like string to get the XORed buffer
const bufferCidPrng = this._decodeCustomBase64(encoded);
// Step 2: Reverse the cidPrng XOR to get the original buffer
const cidPrng = this.prngHelper._createPrng(this.cidPrngSeed, this.salt, false)[0];
const bufferWithMarker = bufferCidPrng.map(b => b ^ cidPrng());
// Step 3: Parse the buffer to extract key-value pairs
return this._parseBuffer(bufferWithMarker);
}Key implementation details:
- Identical CID-PRNG seeding: Must use the exact same seed derivation as encryption
- Byte-by-byte XOR: Apply XOR to each byte in the correct order
- Last byte handling: The last byte is a marker that needs special handling
The most complex part of decryption is parsing the recovered buffer, which contains a JSON-like structure with each key-value pair XORed with the main PRNG:
/**
* Parse the buffer to extract key-value pairs
* @param {Array<number>} bufferWithMarker - Buffer to parse (with marker byte)
* @returns {Array<Array>} - Array of key-value pairs
* @private
*/
_parseBuffer(bufferWithMarker) {
// The last byte is a marker, remove it
const buffer = bufferWithMarker.slice(0, -1);
// Create PRNG for buffer parsing
// This must match exactly how the encryption process creates its PRNG
const prng = this.prngHelper._createPrng(this.prngSeed, this.salt, true)[0];
// Decrypt the entire buffer to get the raw JSON structure
const decodedBytes = [];
for (let i = 0; i < buffer.length; i++) {
const b = buffer[i] ^ prng();
decodedBytes.push(b);
}
// Convert to string for easier processing
const jsonStr = String.fromCharCode(...decodedBytes);
// Now parse this string to extract entries
return this._parseJsonString(jsonStr);
}Key implementation details:
- Marker byte removal: The final byte is a closing brace marker, not part of the data
- Main PRNG recreation: Must generate the exact same PRNG sequence as during encryption
- XOR reversal: Apply XOR to each byte with the PRNG output
- String conversion: Convert the decoded bytes to a string for processing
- Custom JSON parsing: Use custom logic to extract key-value pairs from the JSON-like structure
The final step involves parsing the recovered JSON-like string into key-value pairs:
/**
* Parse the decrypted JSON string into key-value pairs
* @param {string} jsonStr - Decrypted JSON string
* @returns {Array<Array>} - Array of key-value pairs
* @private
*/
_parseJsonString(jsonStr) {
const result = [];
let i = 0;
// Process each character
while (i < jsonStr.length) {
try {
// Find the start of an entry ('{' or ',')
if (jsonStr[i] === '{' || jsonStr[i] === ',') {
i++; // Skip the start marker
// Skip whitespace
while (i < jsonStr.length && /\s/.test(jsonStr[i])) i++;
// Look for key (which should be a JSON string)
if (jsonStr[i] !== '"') {
i++; // Skip non-quote character
continue;
}
i++; // Skip the opening quote
const keyStart = i;
// Read the key content
while (i < jsonStr.length && jsonStr[i] !== '"') {
// Handle escaped characters
if (jsonStr[i] === '\\') {
i += 2; // Skip escape sequence
} else {
i++;
}
}
if (i >= jsonStr.length) break;
const key = jsonStr.substring(keyStart, i);
i++; // Skip the closing quote
// Look for the separator (':')
while (i < jsonStr.length && jsonStr[i] !== ':') i++;
if (i >= jsonStr.length) break;
i++; // Skip the separator
// Skip whitespace
while (i < jsonStr.length && /\s/.test(jsonStr[i])) i++;
if (i >= jsonStr.length) break;
// Process the value based on its type
let value;
const valueStart = i;
if (jsonStr[i] === '"') {
// String value
i++; // Skip opening quote
let valueContent = '';
let escaped = false;
while (i < jsonStr.length) {
if (escaped) {
valueContent += jsonStr[i];
escaped = false;
} else if (jsonStr[i] === '\\') {
valueContent += jsonStr[i];
escaped = true;
} else if (jsonStr[i] === '"') {
break;
} else {
valueContent += jsonStr[i];
}
i++;
}
if (i < jsonStr.length) i++; // Skip closing quote
value = valueContent;
} else if (jsonStr[i] === '{' || jsonStr[i] === '[') {
// Object or array value - specialized handling
const isObject = jsonStr[i] === '{';
const openChar = jsonStr[i];
const closeChar = isObject ? '}' : ']';
let nestLevel = 1;
let valueStr = openChar;
i++; // Skip opening brace/bracket
while (i < jsonStr.length && nestLevel > 0) {
if (jsonStr[i] === openChar) nestLevel++;
else if (jsonStr[i] === closeChar) nestLevel--;
if (nestLevel > 0 || jsonStr[i] === closeChar) {
valueStr += jsonStr[i];
}
i++;
}
try {
value = JSON.parse(valueStr);
} catch {
value = valueStr; // Fallback if parsing fails
}
} else if (/^-?\d/.test(jsonStr[i])) {
// Number value
let numStr = '';
while (i < jsonStr.length && /[-0-9.eE+]/.test(jsonStr[i])) {
numStr += jsonStr[i++];
}
value = parseFloat(numStr);
} else if (jsonStr.substring(i, i + 4) === 'true') {
value = true;
i += 4;
} else if (jsonStr.substring(i, i + 5) === 'false') {
value = false;
i += 5;
} else if (jsonStr.substring(i, i + 4) === 'null') {
value = null;
i += 4;
} else {
// Unknown value type - just skip this character
i++;
continue;
}
// Add entry to result
result.push([key, value]);
} else {
// Skip any other characters
i++;
}
} catch (error) {
// If any parsing error occurs, skip to the next character
i++;
}
}
return result;
}Key implementation details:
- Character-by-character parsing: Process the JSON character by character to handle the custom structure
- Multiple value types: Handle different value types (string, number, boolean, null, object, array)
- Nested structure tracking: Properly handle nested objects and arrays with their own depth counters
- Error resilience: Recover from parsing errors to handle potential anomalies
The complete decryption flow can be summarized as:
-
Initialize the decryptor:
- Set up with the same hash and CID used for encryption
- Calculate the same seed values used during encryption
-
Decode the custom base64:
- Convert the URL-safe characters back to 6-bit values
- Combine 6-bit values into 24-bit chunks
- Extract and un-XOR bytes using salt
-
Reverse the CID-PRNG XOR:
- Create identical CID-PRNG with same seed
- XOR each byte with the CID-PRNG output
-
Reverse the main PRNG XOR:
- Create identical main PRNG with same seed
- XOR each byte with main PRNG output
-
Parse the JSON-like structure:
- Process the structure character by character
- Extract key-value pairs
- Handle different value types
The result is an array of key-value pairs matching the original data that was encrypted.
To ensure the decryption implementation was correct, we implemented several validation mechanisms:
-
Unit tests for each component:
- PRNG sequence generation
- Custom base64 decoding
- JSON structure parsing
-
End-to-end tests with sample data:
- Encrypt known data using the rewritten encryption
- Decrypt using our decryption implementation
- Verify that the original data is recovered correctly
-
Edge case testing:
- Empty strings
- Special characters
- Nested objects and arrays
- Various numeric values and boolean values
One of the most difficult aspects was ensuring the PRNGs produced identical byte sequences.
Solution: We meticulously replicated every aspect of the PRNG implementation, including:
- The precise seeding formulas
- The round counter evolution
- The salt decrement and application
- The caching mechanism
Even a single-byte difference would cause cascading errors, so extreme precision was required.
The buffer contains a JSON-like structure but cannot be parsed with JSON.parse() directly.
Solution: We implemented a custom parser that:
- Processes character by character
- Handles different value types
- Maintains correct nesting levels for objects and arrays
- Implements error recovery mechanisms
UTF-8 handling was particularly challenging, especially for multi-byte characters and surrogate pairs.
Solution: We implemented a careful reversal of the UTF-8 encoding process, ensuring proper handling of:
- ASCII characters (1 byte)
- Common Unicode characters (2-3 bytes)
- Surrogate pairs and extended Unicode (4 bytes)
-
Constructor:
new DataDomeDecryptor(hash, cid, salt, challengeType = 'captcha')hash: Hash value used during encryptioncid: Client ID used during encryptionsalt: Salt value (optional)challengeType: 'captcha' (default) or 'interstitial'
-
Key Methods:
decrypt(encoded): Decrypts the encoded string to recover the original datasetChallengeType(challengeType): Updates the challenge type and recalculates PRNG seedsgetChallengeType(): Returns the current challenge type
The decryption implementation successfully reverses each step of DataDome's encryption process, allowing complete recovery of the original data. This was accomplished through meticulous analysis of the encryption algorithm and precise replication of its key components.
The success of this implementation demonstrates that even complex, multi-layered encryption schemes can be reversed when the algorithm is known, highlighting the importance of true cryptographic security rather than security through obscurity.
For a complete technical analysis of the encryption and decryption algorithms, 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.