As part of a recent commercial project which included a paid software download, I had a mandate to "protect the software from unauthorized distribution".
That's one of those requirements that's easier said than done.
In this particular case, the problem was compounded by the fact that the the "software" was largely browser-based javascript and much of it involved no client-server interaction. In a client-server environment, it's possible to centralize the authentication process. But in a client-only environment where no server-handshake is possible, things are significantly more challenging.
As we all know, Javascript source code is impossible to truly protect. Even the most convoluted code obfuscation is reversible with enough determination. Which is why the goal of those who obfuscate is to discourage copying more than it is to outright prevent it.
But even in cases where code obfuscation successfully discourages reverse engineering (or "beautification" as it is called), obfuscated code doesn't generally offer much protection against code theft. Since most obfuscated code runs equally well on any domain, "borrowed" code may still run on unauthorized websites regardless of its readability.
From a publisher perspective, it's often overlooked point that obfuscated code can be just as tempting a target as non-obfuscated code.
A survey of obfuscation methods is beyond the scope of this post, and endless how-to's have already been written about the various approaches used to render code human-unreadable. These approaches include identifier mangling, dead code injection, XOR encryption, a complete renaming of globals and string literals, code flattening, unicode escaping, self defending code and more. I have even seen code hidden in images, stored as pixel information. Suffice to say that for every convolution, there's a way to undo it -- and to restore the code to some level of readability.
In this case I had employed at least half of the above techniques and had already warped the code into a deviously complicated mess. Within that code I had embedded a simple comparison of the current domain to the authorized one, whose string representation was in-turn scrambled into unrecognizable oblivion.
But then what?
The two problems that immediately hit me were how to fail invisibly and how to create a universal piece of protective code which could be dropped into any pre-existing .js file to "domain lock" the script to an authorized domain.
Invisible failure is important because the point of failure can betray important clues as to where the domain-lock is taking place.
Universal code is often important when one doesn't know the specific script needing protection.
There were two ways to go that satisfied both of those requirements. I'll discuss the first one here, and the second in a forthcoming post.
As stated above, the general problem with most methods of script termination (including stopping code execution, intentionally throwing errors, launching into an infinite loop or redirecting the page) all leave an obvious console trail which would allow any determined programmer to quickly zero in on the offending piece of code and neutralize it.
I needed something sneakier.
My first solution -- which still works pretty well -- and has the advantage of being a truly universal solution -- is to damage other functions on the page. The reasoning behind this approach (which you can play with at a simple generator I made at DomainLockJS) is that the script must execute without an error, but cause a cascade of errors in other, unrelated functions. This approach leads the unauthorized user down a rabbit hole of errors in seemingly well formed code.
The simplest way to do this was to malform the window object -- swapping values and deleting non visible items from memory. With enough random changes, deletions and swaps, some part of the existing page code invariably breaks (assuming there is some significant amount of javascript code used on the page to begin with).
The resulting console error-trail is a frustrating sea of red, pointing to a host of confusingly unrelated functions which should otherwise be operating normally.
This solution is still probably the best route for most sites wanting to domain lock javascript code. It's a decent deterrent. But I needed something more specific. I needed something that would die invisibly... leave no console errors. And better yet: Have a completely different point of failure every time.
It's a devious little solution that makes me think I'd probably have been an evil script kiddie had I been so inclined. It's still not possible to protect javascript, but obfuscating where javascript fails is another story.
That solution is going to have to wait until next week when I have more time ...
Top comments (6)
Please keep it friendly everyone.
This is a very interesting subject and I look forward to the next one.
Interesting but I don't know if you could stop a determined mind from undoing everything. Also have tried adding wasm to the mix?
There's no javascript security solution that is fullproof.
WASM is hard to use in this case since you can't access the DOM directly -- it's very difficult to conceal the error source.
Of course, if you write the whole application in WASM, that's a good route.
You can write parts that do not require dom in wasm
You can write parts that do not require dom in wasm. And parse in functions that perform dom operations as a callback