Overview
PreEmptive Protection™ JSDefender™ (PJSD) is a tool that protects JavaScript files by transforming them, using several different code transformation and injection techniques.
Supported Platforms
The tool is built as npm packages with TypeScript, and supports all platform where NodeJS runs. Currently, PJSD requires NodeJS version 7.10.1 or higher.
Known limitations
eval()
function: PJSD cannot correctly protect the expression passed as a string to the JavaScript eval() function. We do not recommend using theeval()
function anyway because it is considered highly dangerous by the MDN team.
Packages
There are four different packages that you can install:
- @preemptive/pjsd-core: The core protection logic and public API of PJSD, which is dependency of all the other packages.
- @preemptive/pjsd-cli: A wrapper for PJSD which allows you to run the tool from the command line.
- @preemptive/pjsd-webpack-plugin: A webpack plugin which allows you to hook into the webpack pipeline and protect the output chunks.
- @preemptive/pjsd-metro-plugin: A Metro plugin which hooks into the bundling process to protect the output bundle.
Each package has a README file, where you can find more information.
Installation
Download the preemptive-pjsd-*-{version}.tgz (e.g. preemptive-pjsd-core-1.0.0.tgz) packages you want to install (where 'version' is the actual version you want) and put them outside of your project's directory (the best way is to place them next to your project's directory). After that install via your package manager:
npm install <package-directory>/preemptive-pjsd-*-{version}.tgz --save-dev
OR
yarn add file:<package-directory>/preemptive-pjsd-*-{version}.tgz --dev
Note: If you are working in a team environment, the downloaded packages should be in the same place on every developer's machine in order to make it work.
Note: You must install
@preemptive/pjsd-core
manually in order to use most of the other packages because it is a peer dependency of those packages.
Updating:
If you got a new version of preemptive-pjsd- in the form of a preemptive-pjsd--{newVersion}.tgz file then you should place it outside of your project's directory and run the install script again with the new package:
npm install <package-directory>/preemptive-pjsd-*-{newVersion}.tgz --save-dev
OR
yarn add file:<package-directory>/preemptive-pjsd-*-{newVersion}.tgz --dev
Installing from a Url:
You can upload a PJSD package to an online file store (e.g. Blob Storage) or to your own package registry. This makes it easier for your team members to install the package. In the case of a file store, use the url in the install script (in the case of your own registry, use its own method):
npm install https://<package-url> --save-dev
OR
yarn add https://<package-url> --dev
Installing Globally:
@preemptive/pjsd-cli
also works as a system-wide/global cli tool. To install it globally:
npm install <package-directory>/preemptive-pjsd-core-{version}.tgz <package-directory>/preemptive-pjsd-cli-{version}.tgz -g
OR
yarn global add <package-absolute-directory>/preemptive-pjsd-core-{version}.tgz <package-absolute-directory>/preemptive-pjsd-cli-{version}.tgz
Note: Due to Yarn limitations you must specify the absolute path of the package to be able to install it globally.
Note: If you have not done so already, you must add the output of the
yarn global bin
command to your path to be able to run global Yarn packages.
Note: The Yarn Team considers global installation of a package as a bad practice in most cases.
After that you can use the pjsd
command everywhere in your system like you would use it as a local dev package.
Licensing
To use PJSD, you must have a valid license key. PJSD must have access to the key when it runs, and requires a network connection in order to contact the licensing server. You can specify the license key in several ways:
Command-line Option
The command-line interface has a --license
option that you can use to pass your license key each time you run. For example:
pjsd --license C3B940E5A00D492AAB45DD28091E9C53 [other options]
Configuration Property
You can set the license
property of the configuration file/object to your license key. For example:
{
"license": "C3B940E5A00D492AAB45DD28091E9C53"
}
Environment Variable
You can set a PJSD_LICENSE
environment variable to the value of your license key.
If you have the license key specified multiple ways, PJSD will use the first key it finds, in this order:
- Command-line option
- Configuration file/object
- Environment variable
If you do not specify a license key, or if your license key is expired or otherwise invalid, PJSD emits an error message, and stops.
To obtain or renew a license, contact PreEmptive.
Telemetry
PJSD uses a system for reporting application usage. We use this data to help guide our development of new and existing features. Only high level usage data is gathered -- configuration details are not collected. You may opt-out of this collection and reporting at any time by setting the PJSD_TELEMETRY_OPT_OUT
environment variable to 1
or true
.
Using the PJSD command line tool
The PJSD command line tool has two mandatory arguments: the input JavaScript source file name (with optional path), and the output file name for the protected version. For example:
pjsd myJsBundle.js myProtectedScript.js
In this mode, the tool uses the default protection settings.
You can also run the tool with a configuration file that contains specific protection settings:
pjsd myJsBundle.js myProtectedScript.js -c myConfig.json
pjsd myJsBundle.js myProtectedScript.js --config myConfig.json
Note: The
-c
and--config
command line options are identical.
Instead of using a configuration file, you can apply settings from the command line. For example, any of the following commands run the tool with string literal extraction and encoding turned on:
pjsd myJsBundle.js myProtectedScript.js -e
pjsd myJsBundle.js myProtectedScript.js --extstr
Note: The
-e
and--extstr
command line options are identical.
When using both command line options and the configuration file, the command line options override the settings read from the file. For example, the following command turns on string literal extraction, even if this setting is turned off in the configuration file:
pjsd myJsBundle.js myProtectedScript.js -c myConfig.json --extstr
The command line provides options that can turn settings off. The next example uses the -E
and --extstroff
options (they are identical) to turn off string literal extraction (even if the configuration file turns them on):
pjsd myJsBundle.js myProtectedScript.js -c myConfig.json -E
pjsd myJsBundle.js myProtectedScript.js -c myConfig.json --extstroff
In the future, PJSD will allow declaring protection settings right in the source code using ECMAScript/TypeScript annotations. The -a
(or --anns
) command line options notify the tool that it should consider these annotations.
Annotations override the tool configuration. For example, consider this command:
pjsd myJsBundle.js myProtectedScript.js -c myConfig.json -E -a
pjsd myJsBundle.js myProtectedScript.js -c myConfig.json --extstroff --anns
In this case, PJSD determines the set of finally applied protection settings in these steps:
- The initial set it read from
myConfig.json
. - The
-E
(or--extstroff
) options overwrite the configuration set collected in the previous step. - The annotations in the source code overwrite the configuration set calculated in the steps above.
Note: No command line option turns annotations off. By default, the tool does not check annotations.
Protection modes
By default, the tool operates in "exclusive mode". It protects the entire source code in the input file. You can then use the -s
(or the identical --scope
) option, to define a list of function names (or more precisely, function name patterns). PJSD uses this list to omit those functions from protection, while protecting everything outside of them.
pjsd myJsBundle.js myProtectedScript.js --scope "myOmittedFunction, otherFunction"
Note: Of course, the tool can handle nested function declarations. It provides a function name pattern format to find nested functions.
You can change to "inclusive mode" with the --inclusive
command line option. This option tells the the tool to protect only the specified functions, while leaving everything else unchanged.
pjsd myJsBundle.js myProtectedScript.js --inclusive --scope "myIPFunction"
Hint: With the configuration file you can define fine-grained settings. You can define separate settings for each function in the scope (or even groups of functions), and different settings for the source code outside of the specified scope. As a result, you can apply particular settings for the entire source file and others for specific functions. PJSD also supports inline configuration. This mode allows you to add settings to the JavaScript source code with PJSD directives.
Command line options
The tool supports short and long notations to specify options. The short names start with a single dash (-
), while long names start with two dashes (--
). Please note, a few options have only long names.
PJSD supports these command line options:
-c
,--config
: Names the configuration file to read the settings from. The name of the file should follow the option.--license
: Provides the license key to run the PJSD CLI tool. See Licensing to learn about alternative ways to specify the license key.--inclusive
: Turns on inclusive mode, only the functions specified with-s
or--scope
will be considered.-s
,--scope
: Specifies the names of scoped functions to include or exclude.-b
,--repbool
: Replaces Boolean literals (false
andtrue
) with explicit expressions that result in the same Boolean value.-B
,--repbooloff
: Turns off replacing Boolean literals.-i
,--repint
: Replaces integer literals (such as 0, 1, 2, and a few others) with explicit expressions that results in the same integer value.-I
,--repintoff
: Turns off replacing integer literals.--radix
: Changes all integer literals to their representation with the specified radix. The radix value can beBinary
,Hexadecimal
,Octal
, orDecimal
. The value should follow the option. If used together with-i
(--repint
), only the integer literals not affected by-i
are changed.-r
,--randomize
: If you use any of the-b
(--repbool
) or-i
(--repint
) options, the expressions that replace the originals are selected randomly from a few predefined ones.-R
,--randomizeoff
: Turns off randomizing the replacement expressions for Boolean and integer literals.-e
,--extstr
: Extracts string literals from the code, encodes them, and assigns them to variables. The code will use the newly created variables in place of the original string literals.-E
,--extstroff
: Turns off extracting string literals.-d
,--domainlock
: Locks the code to the domain specified in the argument following the option. This argument can be a full domain name likewww.mydomain.net
, or a partial domain name like.mydomain.net
to allow the code to run in any subdomain.-p
,--propind
: Changes properties referenced via the dot (.
) operator to instead be referenced via the index ([
..]
) operator. This effectively replaces the property name reference with a string literal. Though this transformation technique can be used individually, it is more useful in tandem with-e
(-extstr
).-P
,--propindoff
: Turns off property indirection.-l
,--localdecl
: Renames all local declarations (variables, functions, arguments) and their references. By default, uses theSequential
method. See details in the description of the-n
(-names
) options.-L
,--localdecloff
: Turns off renaming local declarations.-n
,--names
: Sets the name mangling method. Even if you do not turn local declaration transformation on, this option has an effect. It uses the method specified here to transform names in source code injected by the tool (such as the code for domain locking). The tool takes care that the newly created names do not collide with existing ones. This option can use one of these values:Sequential
: Uses sequential hexadecimal strings in variable names. Useful when testing or troubleshooting.Hexadecimal
: Uses encoded hexadecimal strings in variable names.Base52
: Uses only the lowercase and uppercase letters of the English alphabet. This is the default value of this option.Base62
: Uses decimal digits, lowercase and uppercase letters of the English alphabet.
-f
,--flow
: Turns on control flow protection.-F
,--flowoff
: Turns off control flow protection.--debrem
: Turns ondebugger
statement removal.--debremoff
: Turns offdebugger
statement removal.--idprefix
: Names the bundle identifier prefix. When you intend to use PJSD with multiple bundles, the string literal extraction and local declaration renaming transformations may result in conflicting variable names. Though a single bundle does not have name collisions, separately protected bundles might have them. If you loaded these bundles, colliding declaration names may break running code. The--idprefix
command line options allows you to set up a separate identifier prefix for each bundle. The name of the identifier prefix should follow the option.-a
,--anns
: Use this option to turn on ECMAScript/TypeScript annotation parsing to support partial code protection configuration. This release does not implement this feature, the option is reserved for future use.-A
,--annsoff
: Turn off ECMAScript/TypeScript annotation parsing to support partial code protection configuration. This release does not not implement this feature, the option is reserved for future use.-q
,--quiet
: Turn on quiet mode. In this mode, the tool emits only a few messages while running.-m
,--mapout
: Names the local declaration mapping file to output the mappings from original identifier names to the Protected ones. This option is useful when troubleshooting. The name of the file should follow the option.--id-map-in
: Names the ID map input file to use when using PJSD in a multi-file scenario. The tool uses this file to obtain information about previous file-level declaration name mappings.--id-map-out
: Names the ID map output file to use when using PJSD in a multi-file scenario. When the protection process completes, the tool writes file-level declaration mappings into this file.--id-map
: A shortcut command-line option when the file names for--id-map-in
and--id-map-out
are the same — new ID mappings are added to the mappings file used in the previous session.
Note: You can learn more about ID mapping files in the Using PJSD with Multiple Files section.
Default command line options
By default, PJSD turns on string literal extraction and local declaration renaming. Thus, these command lines are equivalent:
pjsd myJsBundle.js myProtectedScript.js
pjsd myJsBundle.js myProtectedScript.js -e -l
Let's assume you intend to use only Boolean literal replacement without the default transform methods. You need to issue this command:
pjsd myJsBundle.js myProtectedScript.js -E -L -b
Here, -E
turns off string literal extraction, while -L
turns off local declaration renaming.
Configuration file format
The PJSD configuration file is JSON formatted. Though the tool does not require it, it is a good practice to apply the .json
extension to this file.
This JSON code snippet shows the overall schema of the configuration file:
{
"booleanLiterals": {},
"integerLiterals": {},
"stringLiterals": {},
"domainLock": {},
"propertyIndirection": {},
"localDeclarations": {},
"debuggerRemoval": {},
"controlFlow": {},
"localContexts": [
{
"namePattern": "myFunc",
"booleanLiterals": {},
"integerLiterals": {},
"stringLiterals": {},
"propertyIndirection": {},
"localDeclarations": {},
"debuggerRemoval": {},
"controlFlow": {},
},
{
"namePattern": "otherFunc",
"booleanLiterals": {},
"integerLiterals": {},
"stringLiterals": {},
"propertyIndirection": {},
"localDeclarations": {},
"debuggerRemoval": {},
"controlFlow": {},
},
],
"idPrefix": "",
"license": ""
}
All properties except namePattern
and idPrefix
may have multiple types, be null
, or be entirely omitted from the configuration. namePattern
, idPrefix
, and license
can be strings or left undefined.
License key in the configuration file
PJSD allows you to provide the license key in the configuration file, via the license
property. See Licensing to learn about alternative ways to specify the license key.
How PJSD uses the configuration file
When the tool runs, it applies transforms sequentially in a specific order. Each of the JSON properties at the top (from booleanLiterals
to localDeclarations
) sets the configuration of a particular transform. The set of objects behind these properties is called a transform set. You can see that the JSON description contains a global transform set, and has a localContexts
collection that specifies transform sets for a specific group of functions — determined by the namePattern
property.
When a transform runs, it visits elements within the semantic tree of the source code. If it finds an element it intends to transform, it follows these steps:
- If that element is in a function that matches with a single local context, the transform uses its associated property from the configuration set specified in that context.
- If the element is in a function that matches with multiple local contexts, the transform applies its associated property from the configuration set in the last matching context.
- If no matching local context is found, or that context does not specify the property associated with the transform, the tool picks up the property from the global transform set.
- If no configuration object is found for the transform, or the property has a
false
value, the transform keeps the element intact. - The transform carries out the modification of the semantic tree according to the settings in the configuration object.
Note: if the configuration property is omitted from the file, or its value is set to
null
, it is considered undefined.
Configuration objects
In the current release, the tool applies these transforms:
Transform | Description |
---|---|
domainLock |
Allows binding the code to a specific domain (or its subdomains). When the code running in the browser originates from a non-matching domain, it breaks with an error. Note: if the host page of the protected JavaScript file is opened from the file system (and not through an http/https request), the domain lock is not applied. |
booleanLiterals |
Transforms the false and true literals to other expressions that result in the same false and true values, respectively. (The list of expressions can be easily extended in the source code.) |
integerLiterals |
Transforms integer literals to other (less obvious) expressions that result in the same value when evaluated. It can also transform all integer literals to a specific radix (binary, decimal, hexadecimal, or octal). |
propertyIndirection |
Transforms direct property access to indirect property access. |
stringLiterals |
Extracts string literals into variables and initializes those variables from encoded string literals. Replaces the original string with the corresponding variables |
localDeclaration |
Mangles the names of local declarations. |
debuggerRemoval |
Removes all occurrences of the debugger statement from the source code. |
controlFlow |
Transforms control flow statements into state machines. |
Turning a transform on or off
Each transform's configuration object can be set to false
to indicate that the particular transform should not be applied. Setting the property to true
or an empty object ({}
) tells the protection engine that the corresponding transform is used with its default value. (Note, the domainLock
transform does not have a default value, so it cannot be applied with true
or an empty object.)
A configuration object may have several setups with different behavior (demonstrated with booleanLiterals
):
- Explicitly states that
booleanLiterals
is not configured:
{
"booleanLiterals": null
}
- Explicitly states that
booleanLiterals
should be applied with its default configuration:
{
"booleanLiterals": true
}
This is exactly the same as this:
{
"booleanLiterals": {}
}
- Explicitly turns off the transform:
{
"booleanLiterals": false
}
Turning off a transform is useful when you intend to apply it partially. For example, the next configuration disables the booleanLiterals
transform for the myFunc
function:
{
"booleanLiterals": true,
"localContext:": [
{
"namePattern": "myFunc",
"booleanLiterals": false
}
]
Similarly, you can disable this transform for the entire source code, save myFunc
:
{
"booleanLiterals": false,
"localContext:": [
{
"namePattern": "myFunc",
"booleanLiterals": true
}
]
Or, shorter:
{
"localContext:": [
{
"namePattern": "myFunc",
"booleanLiterals": true
}
]
domainLock
configuration
The domainLock
transform has two configuration properties, domainPattern
, and errorScript
, respectively. If domainPettern
is null, empty, or contains only whitespace, the domainLock
transform does not run. You can set domainPattern
to a full domain name like www.mydomain.net
, or a partial domain name like .mydomain.net
to allow the code run in any subdomain of mydomain.net
.
You can provide a valid JavaScript code snippet in errorScript
. This script is executed when the protection senses that the code runs from an invalid domain.
Example:
{
"domainLock": {
"domainPattern": "mysuperapp.azurewebsites.net"
}
}
Instead of using the domainPattern
property, you can write it shorter:
{
"domainLock": "mysuperapp.azurewebsites.net"
}
You can specify an error script:
{
"domainLock": {
"domainPattern": "mysuperapp.azurewebsites.net",
"errorScript": "alert(\"Invalid domain\");"
}
}
Note: Though technically you can configure
domainLock
in a local context, the tool applies only the global configuration.
Note: The PJSD command-line interface does not have a direct option for setting the
errorScript
property. You need to specify this setting in the configuration file, and use the-c
/--config
option.
booleanLiterals
configuration
This transform supports the randomize
property. If it is false
(this is the default value), the transform will always use the same expression to replace the false
and true
literals. Should it be true
, the transform will choose a replacement expression randomly from its predefined expression repository.
Example:
{
"booleanLiterals": {
"randomize": true
}
}
By default, randomize
is turned off. So if you write this:
{
"booleanLiterals": true
}
It is the same as its longer form:
{
"booleanLiterals": {
"randomize": false
}
}
integerLiterals
configuration
This transform supports the randomize
property. If it is false
(this is the default value), the transform will always the same expression to replace the predefined integer literals (0, 1, and a few others). Should it be true
, the transform will choose a replacement expression randomly from its predefined expression repository.
integerLiterals
has another property, radix
. You can set it to one of these values: Binary
, Decimal
, Hexadecimal
, or Octal
. When radix
has any of these values, it changes all integer literals to use the particular radix. Those integer literals that have already been replaced with an expression (e.g. 0, 1, and others) do not change their radix.
Example:
{
"integerLiterals": {
"randomize": false,
"radix": "Octal"
}
}
propertyIndirection
configuration
This transform does not have any additional configuration properties.
stringLiteral
configuration
This transform does not have any additional configuration properties.
localDeclarations
properties
localDeclarations
has a nameMangling
property that allows you to specify one of these values: sequential
, hexadecimal
, base52
; it works as discussed in the description of the -n
(--names
) command line option. With the excludeIDs
property, you can set a list of file-level declaration identifiers that should be excluded from name mangling.
Example:
{
"localDeclarations": {
"nameMangling": "base52",
"excludeIds": [
"getName",
"somethingOther"
]
}
}
These settings will use the base52
style name mangling, and disable the renaming of getName
and somethingOther
identifiers, provided, they are declared in the file-level scope.
debuggerRemoval
configuration
This transform does not have any additional configuration properties.
controlFlow
configuration
This transform does not have any additional configuration properties.
Using PJSD with Multiple Files
PJSD supports protecting a single file in one session: you have a single input file, and you get a single output file. Nonetheless, PJSD is prepared for scenarios where you load multiple files into a web page, and you want to protect them.
PJSD has three mechanisms you can use in such scenarios:
- The
--idprefix
option. This option is a good choice when you use a bundler (such as Webpack or Browserify) with multiple output chunks. - ID mapping mechanism (
--id-map
,--id-map-in
, and--id-map-out
options). Allows PJSD to work with ID mappings generated in a previous PJSD call. - ID renaming exclusion (
localDeclaration
configuration). You can configure a list of IDs to exclude from renaming.
Important: All of these mechanisms affect only declarations (variables and functions) at the JavaScript's file-level (the global scope), as only the identifiers in this declaration scope may cause name collisions in multi-file scenarios. For explanations, look at this sample:
var lowerVal = 1;
var upperVal = 10;
function displayNumbers(lower, upper) {
for (var i = lower; i <= upper; i++) {
console.log(i)
}
}
displayNumbers(lowerVal, upperVal);
These mechanisms can be used to prevent lowerVal
, upperVal
, and displayNumbers
from being renamed, while allowing lower
, upper
, and i
to be renamed as usual.
Issues with Multiple Files Loaded in a Single Page
Local declaration renaming (name mangling) is an excellent protection option, as it changes meaningful identifiers to a meaningless string of letters and digits. However, when you have multiple files, using PJSD without extra care may break your running code, as the following example demonstrates.
Assume, you have two files, file1.js
, and file2.js
. The browser loads these files into an HTML page in this order:
...
<script src="file1.js"></script>
<script src="file2.js"></script>
...
file1.js
:
function getGreeting() {
return "Hello from multifile demo!"
}
file2.js
:
var element = document.getElementById("msg");
element.textContent = getGreeting();
You can protect these two files by running the PJSD tool twice, once for each file. As a result, you get these files after local declaration renaming:
file1.js
(protected):
function _0x000000() {
return "Hello from multifile demo!"
}
file2.js
(protected):
var _0x000000 = document.getElementById("msg");
_0x000000.textContent = getGreeting();
There are two issues:
- Each module contains a declaration,
_0x000000
. Because of the JavaScript declaration hoisting mechanism, the second_0x000000
declaration (infile2.js
) overwrites the first one. - As
getGreeting
is not a local declaration infile2.js
(it is just an identifier reference), it is not renamed.
This code will break when the browser loads the page.
Take a look at the next scenario that again uses two JavaScript files:
file1.js
:
function getGreeting() {
var name = getName();
return "Hello, " + name + ", from multifile demo!"
}
file2.js
:
function getName() {
return "Developer";
}
var element = document.getElementById("msg");
element.textContent = getGreeting();
After invoking PJSD for each file, this is what you get:
file1.js
(protected):
function _0x000000() {
var _0x000001 = getName();
return "Hello, "+ _0x000001 + ", from multifile demo!"
}
file2.js
(protected):
function _0x000000_() {
return "Developer";
}
var _0x000001 = document.getElementById("msg");
_0x000001.textContent = getGreeting();
You can immediately see that this code breaks, too:
- Each file contains a declaration,
_0x000000
. Because of JavaScript declaration hoisting, the declaration infile2.js
overwrites the first one. - As
getName
is not a local declaration infile1.js
(it is just an identifier reference), it is not renamed. - As
getGreeting
is not a local declaration infile2.js
(it is just an identifier reference), it is not renamed.
Using --idprefix
When you use a JavaScript bundler and create multiple chunks, existing code may break when you load them — because of colliding identifiers. Though a single bundle does not have name collisions, separately protected bundles might have them. If you loaded these bundles, colliding declaration names might break running code. The --idprefix
option (idPrefix
configuration property) allows you to set up a separate identifier prefix for each bundle. Because name collision can happen only in the program's global declaration scope, this setting does not affect declarations in nested scopes.
You can set up a different value for each bundle with the --idprefix
command-line option:
pjsd bundle1.js bundle1-protected.js --idprefix _a
pjsd bundle2.js bundle2-protected.js --idprefix _b
Optionally, you can use two configuration files for the two runs of PJSD
First bundle:
{
"propertyIndirection": true,
"stringLiteral": true,
"localDeclarations": {
"nameMangling": "base52"
},
"idPrefix": "_1"
}
Second bundle:
{
"propertyIndirection": true,
"stringLiteral": true,
"localDeclarations": {
"nameMangling": "base52"
},
"idPrefix": "_2"
}
Note: The command-line
--idprefix
option overwrites the setting in the configuration file.
Using ID Mappings
The LocalDeclarationTransform
of PJSD renames all declarations in the JavaScript file to protect. PJSD has a mechanism, ID mapping, which allows you to convey identifier mapping information between subsequent executions of the command-line tool. With the --id-map-out
option you can save the mapping information into a file:
pjsd file1.js file1.js --id-map-out idmap.txt
In a subsequent PJSD session, you can use the --id-map-in
command-line option to read the ID map file created after the previous one:
pjsd file2.js file2.js --id-map-in idmap.txt
When you have more than two files, you need to store the ID mappings after each PJSD execution, and read the previous mappings in each session — except the first one. You can use the same file name to cascade ID mappings. The tool reuses the input file and appends new declarations to it.
If you had four files, you'd use PJSD like this:
pjsd file1.js file1.js --id-map-out idmap.txt
pjsd file2.js file2.js --id-map-in idmap.txt --id-map-out idmap.txt
pjsd file3.js file3.js --id-map-in idmap.txt --id-map-out idmap.txt
pjsd file4.js file4.js --id-map-in idmap.txt --id-map-out idmap.txt
PJSD allows applying a shortcut command-line option, --id-map
, to avoid the duplication of the map file names:
pjsd file1.js file1.js --id-map-out idmap.txt
pjsd file2.js file2.js --id-map idmap.txt
pjsd file3.js file3.js --id-map idmap.txt
pjsd file4.js file4.js --id-map idmap.txt
Note: ID mapping and the
--map
option are two separate features. The ID mapping mechanism persists only file-level declaration name pairs (original name — mangled name). The--map
option saves all identifier mappings, including every scope, with some extra information attached to names.
ID Exclusion
PJSD provides a mechanism that allows you to exclude file-level declaration identifiers from renaming. This feature is useful when you have a JavaScript file that refers to declarations that are located in a file the page will load only after this file. PJSD would leave these names in the first file (as the identifiers are just references and not declarations), but rename them in their declaring files. This behavior will break the code.
To avoid this issue, in the configuration file, you can set up a list of identifiers to exclude from renaming when you protect the files, in which those are declared:
In the configuration file, you can declare a configuration section with the list of identifiers to exclude from renaming. For example:
{
"localDeclarations": {
"nameMangling": "base52",
"excludeIds": [
"getName",
"somethingOther"
]
}
}
Note: The PJSD command-line does not support any options to exclude identifier names.
Inline configuration
As you learned earlier, PJSD supports partial protection. You can select the parts of your source code you intend to protect. If you have a large JavaScript file, it might be difficult to fine-tune which blocks of the code should be protected using just command-line options or a configuration file. PJSD also allows inline configuration: you can set the protection options right in the source code. You do not need to use command-line options or a configuration file, as the source code provides the necessary information. To define protection settings, you use JavaScript directives (similar to "use strict"
).
Let's see an example. This is the source code to protect (input.js
):
var x = true;
var y = false;
function w() {
var x = false;
var y = true;
}
Assume you want to apply Boolean literal replacement to the default options. With the CLI, you can issue this command:
pjsd input.js protected.js -b
The result (protected.js
) would be this:
var Acjgb=!![];
var cemgb=(NaN===NaN);
function wZcgb() {
var Yaggb=(NaN===NaN);
var sWWfb=!![];
}
With inline configuration, you can add PJSD directives to the source code:
"@pjsd -b";
var x = true;
var y = false;
function w() {
var x = false;
var y = true;
}
Now, you can omit the -b
switch from the command line to get the same result:
pjsd input.js protected.js
The "@pjsd -b"
in the first line of the input is a valid JavaScript construct, called directive. JavaScript uses it only in the "use string"
mode.
A Short Recap on JavaScript Directives
In JavaScript, directives are string literals placed at the beginning of the code or at the beginning of block statements. If you put any other statement into the code, subsequent string literals are not considered to be directives. This short code snippet demonstrates which literals are directives and which are not:
"directive #1";
"directive #2";
var counter = 0;
"this is not a directive";
"nor this"
function square(n) {
"directive #3";
var result = n*n;
"not a directive";
return result;
}
if (square(2) === 4) {
"directive #4"
console.log("test passes.");
"not a directive"
}
PJSD Inline Configuration Directives
PJSD directives start with the @pjsd
prefix. Although JavaScript allows directives at the beginning of every block statement, PJSD considers only these directives (and ignores the others):
- Program directives
- Directives that belong to functions (either declarations or expressions)
- Class method directives
PJSD accept two forms of directives:
- CLI-option-format directives which use the command-line option format (single line). They start with the
@pjsd
prefix followed by a space or tab character. - CLI-config-format directives that utilize the JSON format of the PJSD configuration file. They start with the
@pjsd:
prefix.
If you use both types of directives in a particular context, the CLI-config-format takes priority over the others with CLI-option-format .
While executing the protection process, PJSD parses the declaration of these directives. If there are any kind of issue with them, PJSD issues a warning and continues the protection, ignoring the faulty directives.
CLI-Option-Format Directives
You can use a single directive where command line switch declarations follow the @pjsd
prefix, like in this example:
"@pjsd -i --names sequential -i --radix octal";
When you specify multiple directives in the same location, PJSD uses only the first one, and ignores the others:
"@pjsd -i --names sequential -i --radix octal";
"@pjsd -p"; // --- This directive is ignored
CLI-Config-Format Directives
The protection engine combines the directives with the @pjsd:
prefix into a JSON string. For example, let's assume, you have these directives:
'@pjsd: {'
'@pjsd: "integerLiterals": {'
'@pjsd: "radix": "octal"'
'@pjsd: }'
'@pjsd: }'
PJSD extracts the contents of these directives to this JSON:
{
"integerLiterals": {
"radix": "octal"
}
}
Of course, you can write this configuration with a single directive:
'@pjsd: { "integerLiterals": { "radix": "octal" } }'
Configuration Merging
The inline configuration settings are merged with the command line and configuration file settings. The inline configuration always overrides the others. When executing the protection process, PJSD applies the closest inline configuration for each source code elements it transforms. This configuration mode allows setting up more complex (but still easy-to-follow) protection scenarios than the ones with command line options or configuration files.
For example, this input turns on Boolean literal replacement for the entire program:
"@pjsd -b";
var x = true;
var y = false;
function w() {
var x = false;
var y = true;
}
With this change, only the body of the w
function gets this protection:
var x = true;
var y = false;
function w() {
"@pjsd -b";
var x = false;
var y = true;
}
You can declare a scenario where everything, except the body of w
gets protected:
"@pjsd -b";
var x = true;
var y = false;
function w() {
"@pjsd -B";
var x = false;
var y = true;
}
Declaration map
One of the most potent protection technique PJSD uses is local declaration transform. PJSD visits all local declarations (variables and functions that are not visible outside of the protected module, and renames them with their references, including occurrences in template string literals.
Overview of local declaration transform
The easiest way to understand how this method works is to look at examples. So, here is one:
var myVar = 123;
function increment(arg) {
return arg+1;
}
console.log(increment(myVar));
When you run the tool with the -l
(--localdecl
) command line option, the protection process results in this code:
var _0x000000=123;
function _0x000001(_0x000002) {
return _0x000002+1;
}
console.log(_0x000001(_0x000000));
Note: Though here you see the result with multiple lines and indentations, PJSD automatically compacts the code.
As you see, the tool renamed myVar
, increment
, and arg
, nonetheless it did not touch console
. While the three renamed identifiers are declared in the original code snippet, console
is not.
PJSD recognizes declarations in ES 2015 string literals, too. Change the body of increment
to this:
function increment(arg) {
return `Result: ${arg+1}`;
}
Now, the tool emits this output:
var _0x000000=123;
function _0x000001(_0x000002) {
return `Result: ${_0x000002+1}`;
}
console.log(_0x000001(_0x000000));
Creating a declaration map
Sometimes you need to have a good understanding of how PJSD renames declarations. The -m
(--mapout
) command line option allows you to output a CSV text file that shows these mappings. You can view this file with a standard text editor, or load it into Excel to see the contents in a spreadsheet.
Note: Declaration map is an early preview feature. In the future, declaration maps will be changed with source maps.
Declaration map format
Each line of the declaration map file describes a map entry. PJSD emits several map entry types:
- Scopes. A lexical declaration scope that hosts (scope-local) variable and function declarations, and other nested scopes.
- Declarations. A variable or function (including
let
andconst
declarations, and IIFEs (Immediately Invoked Function Expressions). - References. References to variable or function declarations. When the protection process renames a declaration, it renames all identifier references, too.
- Unresolved declarations. The code may refer to identifiers that are defined outside of the source module.
Each line may contain several fields that are separated by a comma (,
) character:
- Entry type. A 5-character name that refers to the type of the map entry (
Scope
,Decln
,Refer
, andUnres
). - Subtype. Declaration and reference entry types have subtypes (4-character string).
- Depth. Indicates depth of the declaration scope. The module scope has a depth of 0; a function in the module has a depth of 1; a function nested into another function has 2; and so on.
- Original name. The name of the variable, function, or reference before renaming -- or the full name of the scope.
- Global index. Each declaration has a global index (zero-based sequential number) that is unique in the entire source code. This field indicates this value. Name mangling uses this value.
- Local index. Each declaration has a scope-specific index (zero-based sequential number) that is unique within the hosting scope. This field indicates this value. Name mangling uses this value.
- Mangled name. The name after renaming the original declaration. Scopes and unresolved entry types leave this field empty.
- Reference count. Declaration map entries indicate the number of map entries references them (the number where the corresponding declaration was referenced in the source code). Other entry types do not contain this field.
Declaration map sample
Let's see a sample to examine the declaration map output:
var myVar = 123;
function increment(arg) {
return `Result: ${arg+1}`;
}
console.log(increment(myVar));
When running PJSD with the -l -n sequential -m <mapfile>
command line options, it produces this transformed code:
var _0x000000=123;
function _0x000001(_0x000002) {
return `Result: ${_0x000002+1}`;
}
console.log(_0x000001(_0x000000));
The map file has 12 entries:
#01: scope,,0,(module),,,,
#02: decln,varD,0,myVar,0,0,_0x000000,2
#03: decln,func,0,increment,1,1,_0x000001,2
#04: unres,,0,console,,,,
#05: refer,,0,myVar,,,_0x000000,
#06: refer,,0,increment,,,_0x000001,
#07: refer,,0,increment,,,_0x000001,
#08: refer,,0,myVar,,,_0x000000,
#09: scope,,1,(module)::_0x000001,,,,
#10: decln,argD,1,arg,2,0,_0x000002,2
#11: refer,,1,arg,,,_0x000002,
#12: refer,,1,arg,,,_0x000002,
Note: The map file does not have line numbers; here, we use them for the sake of explanation.
The map contains two scopes: the module's scope in line #01, and the scope of the increment
function in line #09. From line #2 to #8, you can see the declarations and references in the module's scope. Meanwhile, lines #10-#12 contain the map items within the increment
function's scope.
The module scope has two declarations, myVar
with index 0
(later renamed to _0x000000
), and increment
with index 1
(renamed to _0x000001
). You can see that the subtypes of these items are varD
(variable declaration), and func
(function declaration) respectively.
The module scope contains four references to these declarations; two for each declaration. Two references stand for the identifiers in the variable and function declaration statements. The other two come from the console.log(increment(myVar));
source code line. Line #04 describes an unresolved reference to the console
identifier. Because this name is defined somewhere else, PJSD does not rename it.
Line #09 names the scope of increment
as (module)::_0x000001
suggesting this is a nested scope, one declared within a function that belongs to the module's root scope. Line #10 declares the arg
function argument (this is why it uses the argD
subtype). Lines #11 and #12 are references to arg (renamed to _0x000002).
Should you invoke PJSD with the -l -n Base52 -m <mapfile>
command line options, both the protected code and the map file would use different identifiers:
(Protected output, indented for the sake if readability)
var Acjgb=123;
function cemgb(wZcgb) {
return `Result: ${wZcgb+1}`;
}
console.log(cemgb(Acjgb));
(Map file)
scope,,0,(module),,,,
decln,varD,0,myVar,0,0,Acjgb,2
decln,func,0,increment,1,1,cemgb,2
unres,,0,console,,,,
refer,,0,myVar,,,Acjgb,
refer,,0,increment,,,cemgb,
refer,,0,increment,,,cemgb,
refer,,0,myVar,,,Acjgb,
scope,,1,(module)::cemgb,,,,
decln,argD,1,arg,2,0,wZcgb,2
refer,,1,arg,,,wZcgb,
refer,,1,arg,,,wZcgb,