JavaScript number obfuscater

Posted on

Problem

I got inspired to do this after seeing this repo containing a bunch of weird syntactical quirks in JS, namely this example called “It’s a fail!”. The example was essentially the following:

You would not believe, but …

(![] + [])[+[]] +
 (![] + [])[+!+[]] +
 ([![]] + [][[]])[+!+[] + [+[]]] +
 (![] + [])[!+[] + !+[]];
// -> 'fail'

I found this quite interesting; however, as revealed by the explanation later on, the actual characters were merely coincidentally obtained using the fact that certain array operations return true, false and undefined, from which we can grab certain indices. The issue with using this for text obfuscation, of course, it that it’s not applicable to most characters. However, the way we grab those indices is also using some array syntax, following just three rules:

// Number definitions
+[] // -> 0
+!+[] // -> 1

// Addition
+!+[] + +!+[] // -> 2
+!+[] + !+[] // -> also 2, but with one less character

// Concatenation by arrayifying
+!+[] + [+[]] // -> 10
+!+[] + [+!+[]] // -> 11

Using this, we could implement an algorithm to convert any number into such a format. I’ve also made some optimisations, so for example, instead of doing +!+[] + +!+[] + +!+[] + +!+[] + +!+[] + +!+[] + +!+[] + +!+[] just to do 9, we could simply do [+!+[] + [+[]] - !+[]] – making 10 by concatenating 1 and 0, then subtracting 1. The same process is done for any number > 5.

Below is my code. Any comments on readability, naming, etc. is obviously welcome, but I’m also open to any suggestions on further ways to make the output number more compact.

const obfuscateNumber = num => {
  let chars = [...num.toString()];

  let outputStr = "";
  let tokens = 0;
  chars.forEach(char => {
    let digit = parseInt(char);

    outputStr += (tokens > 0? " + " : "")
    if (digit === 0){
      outputStr += (tokens > 0? "[" : "") 
                      + "+[]" 
                      + (tokens > 0? "]" : "");
    } else if (digit === 1) {
      outputStr += (tokens > 0? "[" : "") 
                      + "+!+[]" 
                      + (tokens > 0? "]" : "");
    } else if (digit <= 5) {
      outputStr += "[";
      for (i = 0; i < digit; i++){
        outputStr += (i == 0? "" : " + ") + "!+[]";
      }
      outputStr += "]";
    } else {
      outputStr += "[+!+[] + [+[]]";
      for (i = 0; i < 10 - digit; i++){
        outputStr += " - !+[]";
      }
      outputStr += "]";
    }

    tokens++;
  });

  return outputStr;
}

Note: when I use the word ‘obfuscate’, I mean it in the sense that the number is unreadable to average humans. Obviously, it easily decodable, and I’m not planning to implement it into some production-grade obfuscation software therefore.

Solution

Nice little bit of fun code, and almost working. This is a long review as I got carried away.

First

A Bug

The variable i is undeclared and thus using the global (higher level) scope. This can create very hard to find bugs in code that uses your code. Always declare all the unique variables you use in a function.

You should be using

  1. strict mode via the directive "use strict" at the top of your code
  2. Use JavaScript modules as they live in there own isolated local scope (still access global scope) and automatically execute in strict mode

Some will argue that undeclared variable is not a bug, all will agree it is dangerous, and I prefer to call it at its worst, a BUG.

with that out the way there are some problems.

Why obfuscate?

Protect IP

Generally we obfuscate code to make the legal department relax, rather than think that the very expensive intellectual property (IP) is an open book for anyone visiting the site to read (thus steal). However any coder can read obfuscated code, it just needs a little more effort, with a good IDE it is near effortless compared to poorly written code.

Does your function help protect the IP? No! if it can run it can be understood.

Performance

Code obfuscation comes with a secondary benefit, and the main reason it is still widely used. Namely as a code minifier reducing load time and reducing JIT (compile time) a little (no long string searching during tokenization)

Your code does not minify the source. So we are left with no benefit.

As an exercise just for the hell of it I will continue

Reliability

Good obfuscation means that the obscured code will run exactly the same as the original. Even the slightest change in behavior means that the obfuscated code is useless.

Your code fails the reliability test as it changes the type of the value you are obscuring.

Behavior problems

// single digit 6 obscured becomes [+!+[] + [+[]] - !+[]]

[+!+[] + [+[]] - !+[]] == [+!+[] + [+[]] - !+[]]; // false
[+!+[] + [+[]] - !+[]] === [+!+[] + [+[]] - !+[]]; // false
[+!+[] + [+[]] - !+[]] == 6; // false
[+!+[] + [+[]] - !+[]] === 6; // false

// double digit 42 obscured [!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]] removed white spaces

[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]] == [!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]]; // true
[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]] === [!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]]; // true
[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]] == 42; // true
[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]] === 42; // false

The difference between the two is due to what they evaluate to.

  • The single digit returns an array containing the number it represents as the first item.
  • The double digit something more complex, an expression (which rightly it should) that evaluates to a string representation of the number

:

// using 42

console.log(typeof [!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]]); // >> "object2"
console.log(typeof ([!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]])); // >> "string"

Expressions are evaluated using operator precedence meaning that if I use your function to obscure an expression I get different results for the very same expression, just its form has changed.

EG…

// evaluating obfuscated left size only makes the following statements true
// See hidden snippet below
6 + 3 * 12 == 632 
(3 * 12) + 6 == 326
6 + 3 * (12) == 636
(12 * 3) + 6 = 76

Can be fixed

You function can be fixed by adding some addition syntax around the obfication. eg if single digit return outputStr + "[0]" and if more than one digit return "Number(" + outputStr + ".join(''))" but really why bother.

Numbers are obfuscated

I would argue that numbers are already obfuscated as numbers do not carry inherent meaning.

42 has no meaning (WHAT!!!), it requires context life: {universe: {everything: {meaningOf: 42}}}. Even then contemporary pop cult gives it meaning, that the vast majority of the world population will not understand.

The meaning is more than just context it must be used to give it full meaning.

Consider const name = "At the end of the universe the restaurant Milliways".slice(42); What does 42 mean? “Milliways”, yes easy guess, or a bad case of gastric and I might have meant “illiways”. To be sure most people will need to count the characters to find where the 43 character is (Don’t bother the food is always 5 star.)

I think you get the point.

Magic dem numerals are…

A number is meaningless without context and usage. Using numbers in code is generally frowned upon, we call them magic numbers and consider unnamed numbers as bad practice.

Example of magic numbers and meaning

 //almost meaningless unless you do math in your head very well
 const vect = {
    x: Math.cos(7.853981633974483),
    y: Math.sin(7.853981633974483)
 }
 
 // give it some meaning but still kind of ambiguous
 const deg90CW = 7.853981633974483; 

 // till we use it 
 const down = {
    x: Math.cos(deg90CW),
    y: Math.sin(deg90CW)
 }

So we obfuscate the number 7.853981633974483 but have we really lost the meaning when the code now looks like

 // hide da num form peiring eyeses
 const deg90CW = ([+!+[] + [+[]] - !+[] - !+[] - !+[]] + [+!+[] + [+[]] - !+[] - !+[]] + [!+[] + !+[] + !+[] + !+[] + !+[]] + [!+[] + !+[] + !+[]] + [+!+[] + [+[]] - !+[]] + [+!+[] + [+[]] - !+[] - !+[]] + [+!+[]] + [+!+[] + [+[]] - !+[] - !+[] - !+[] - !+[]] + [!+[] + !+[] + !+[]] + [!+[] + !+[] + !+[]] + [+!+[] + [+[]] - !+[]] + [+!+[] + [+[]] - !+[] - !+[] - !+[]] + [!+[] + !+[] + !+[] + !+[]] + [!+[] + !+[] + !+[] + !+[]] + [+!+[] + [+[]] - !+[] - !+[]] + [!+[] + !+[] + !+[]]) / 10; 

 const down = {
    x: Math.cos(deg90CW),
    y: Math.sin(deg90CW)
 };

The value is really irrelevant to the reader of the code, the meaning is in the name and use.

Meaning of life 0b101010, 052, 42, or 0x2A

In JavaScript you can obscure numbers using any of the native number bases, base (Hex) 16 will also give you a slight minification in some situations. Only the very best (practiced) can use hex as numbers in their heads without the need to convert to base 10. Yet obscured code needs only an IDE and some time to understand.

Your custom function is 36 lines of very hard to digest code. You are performing a battery of if-elseif-else conditionals on the same variable — for this reason, it is most appropriate to employ a switch case (even though I have a strong bias against them) as a matter of best practice. If the goal is to obfuscate the output AND the code, I reckon you’ve found a winner.


I’ll offer a comparison using tests 5, 11, and 987654321.

Test Results:

5 -> [!+[] + !+[] + !+[] + !+[] + !+[]]
11 -> +!+[] + [+!+[]]
987654321 -> [+!+[] + [+[]] - !+[]] + [+!+[] + [+[]] - !+[] - !+[]] + [+!+[] + [+[]] - !+[] - !+[] - !+[]] + [+!+[] + [+[]] - !+[] - !+[] - !+[] - !+[]] + [!+[] + !+[] + !+[] + !+[] + !+[]] + [!+[] + !+[] + !+[] + !+[]] + [!+[] + !+[] + !+[]] + [!+[] + !+[]] + [+!+[]]

Alternatively, you might leverage Javascript-native calls to base64 encode: btoa() and atob(). Because you are dealing purely with integers, you don’t need to concern yourself with “The Unicode Problem”.
To encode a number use btoa(num) and to decode it back use atob(num).

Test Results:

5 -> NQ==
11 -> MTE=
987654321 -> OTg3NjU0MzIx

Benefits include:

  1. Future developers of your code will be able to instantly research what your process is doing.
  2. There is no need to write a custom function
  3. The output is far, far better compressed
  4. The process of decrypting the generated string is just as simple as encrypting it
  5. If you want to further obfuscate the generated strings, you can cleanly add your own “special sauce” that will offer more “entertainment” for crackers.

Leave a Reply

Your email address will not be published. Required fields are marked *