Added stringArrayCallsTransform and stringArrayCallsTransformThreshold options

This commit is contained in:
sanex
2021-08-08 21:23:11 +03:00
parent 0c1aa60d81
commit 6b3539773a
34 changed files with 628 additions and 96 deletions
+1
View File
@@ -2,6 +2,7 @@ Change Log
v2.19.0
---
* **New options**: `stringArrayCallsTransform` and `stringArrayCallsTransformThreshold`
* Fixed very rare cases when `rotateStringArray` couldn't rotate array properly
* Improved `selfDefending` option
* Installed `npm` package now has `types` directory and doesn't have `src` directory
+47 -21
View File
@@ -381,10 +381,12 @@ Following options are available for the JS Obfuscator:
splitStrings: false,
splitStringsChunkLength: 10,
stringArray: true,
stringArrayCallsTransform: true,
stringArrayCallsTransformThreshold: 0.5,
stringArrayEncoding: [],
stringArrayIndexesType: [
'hexadecimal-number'
],
stringArrayEncoding: [],
stringArrayIndexShift: true,
stringArrayWrappersCount: 1,
stringArrayWrappersChainedCalls: true,
@@ -443,8 +445,10 @@ Following options are available for the JS Obfuscator:
--split-strings <boolean>
--split-strings-chunk-length <number>
--string-array <boolean>
--string-array-indexes-type '<list>' (comma separated) [hexadecimal-number, hexadecimal-numeric-string]
--string-array-calls-transform <boolean>
--string-array-calls-transform-threshold <number>
--string-array-encoding '<list>' (comma separated) [none, base64, rc4]
--string-array-indexes-type '<list>' (comma separated) [hexadecimal-number, hexadecimal-numeric-string]
--string-array-index-shift <boolean>
--string-array-wrappers-count <number>
--string-array-wrappers-chained-calls <boolean>
@@ -1111,10 +1115,24 @@ Type: `boolean` Default: `true`
Removes string literals and place them in a special array. For instance, the string `"Hello World"` in `var m = "Hello World";` will be replaced with something like `var m = _0x12c456[0x1];`
### `stringArrayCallsTransform`
Type: `boolean` Default: `true`
##### :warning: [`stringArray`](#stringarray) option must be enabled
Enables the transformation of calls to the [`stringArray`](#stringarray). All arguments of these calls may be extracted to different variables depending on [`stringArrayCallsTransformThreshold`](#stringarraycallstransformthreshold) value. For large source code, the internal threshold also could be applied.
### `stringArrayCallsTransformThreshold`
Type: `number` Default: `0.5`
##### :warning: [`stringArray`](#stringarray) and [`stringArrayCallsTransformThreshold`](#stringarraycallstransformthreshold) options must be enabled
You can use this setting to adjust the probability (from 0 to 1) that calls to the string array will be transformed.
### `stringArrayEncoding`
Type: `string[]` Default: `[]`
##### :warning: `stringArray` option must be enabled
##### :warning: [`stringArray`](#stringarray) option must be enabled
This option can slow down your script.
@@ -1163,6 +1181,17 @@ Type: `boolean` Default: `true`
Enables additional index shift for all string array calls
### `stringArrayThreshold`
Type: `number` Default: `0.8` Min: `0` Max: `1`
##### :warning: [`stringArray`](#stringarray) option must be enabled
You can use this setting to adjust the probability (from 0 to 1) that a string literal will be inserted into the `stringArray`.
This setting is especially useful for large code size because it repeatedly calls to the `string array` and can slow down your code.
`stringArrayThreshold: 0` equals to `stringArray: false`.
### `stringArrayWrappersCount`
Type: `number` Default: `1`
@@ -1340,17 +1369,6 @@ function d (c, g) {
}
test();
```
### `stringArrayThreshold`
Type: `number` Default: `0.8` Min: `0` Max: `1`
##### :warning: [`stringArray`](#stringarray) option must be enabled
You can use this setting to adjust the probability (from 0 to 1) that a string literal will be inserted into the `stringArray`.
This setting is especially useful for large code size because it repeatedly calls to the `string array` and can slow down your code.
`stringArrayThreshold: 0` equals to `stringArray: false`.
### `target`
Type: `string` Default: `browser`
@@ -1417,9 +1435,9 @@ Allows to enable/disable string conversion to unicode escape sequence.
Unicode escape sequence increases code size greatly and strings easily can be reverted to their original view. Recommended to enable this option only for small source code.
## Preset Options
### High obfuscation, low performance
### High obfuscation
Performance will 50-100% slower than without obfuscation
The performance will be much slower than without obfuscation
```javascript
{
@@ -1442,6 +1460,8 @@ Performance will 50-100% slower than without obfuscation
splitStrings: true,
splitStringsChunkLength: 5,
stringArray: true,
stringArrayCallsTransform: true,
stringArrayCallsTransformThreshold: 1,
stringArrayEncoding: ['rc4'],
stringArrayIndexShift: true,
stringArrayWrappersCount: 5,
@@ -1454,9 +1474,9 @@ Performance will 50-100% slower than without obfuscation
}
```
### Medium obfuscation, optimal performance
### Medium obfuscation
Performance will 30-35% slower than without obfuscation
The performance will be slower than without obfuscation
```javascript
{
@@ -1479,6 +1499,8 @@ Performance will 30-35% slower than without obfuscation
splitStrings: true,
splitStringsChunkLength: 10,
stringArray: true,
stringArrayCallsTransform: true,
stringArrayCallsTransformThreshold: 0.75,
stringArrayEncoding: ['base64'],
stringArrayIndexShift: true,
stringArrayWrappersCount: 2,
@@ -1491,9 +1513,9 @@ Performance will 30-35% slower than without obfuscation
}
```
### Low obfuscation, High performance
### Low obfuscation
Performance will slightly slower than without obfuscation
The performance will be at a relatively normal level
```javascript
{
@@ -1513,6 +1535,8 @@ Performance will slightly slower than without obfuscation
simplify: true,
splitStrings: false,
stringArray: true,
stringArrayCallsTransform: true,
stringArrayCallsTransformThreshold: 0.5,
stringArrayEncoding: [],
stringArrayIndexShift: true,
stringArrayWrappersCount: 1,
@@ -1524,7 +1548,7 @@ Performance will slightly slower than without obfuscation
}
```
### Default preset, High performance
### Default preset
```javascript
{
@@ -1544,6 +1568,8 @@ Performance will slightly slower than without obfuscation
simplify: true,
splitStrings: false,
stringArray: true,
stringArrayCallsTransform: true,
stringArrayCallsTransformThreshold: 0.5,
stringArrayEncoding: [],
stringArrayIndexShift: true,
stringArrayWrappersCount: 1,
+1
View File
@@ -90,6 +90,7 @@ export class JavaScriptObfuscator implements IJavaScriptObfuscator {
NodeTransformer.ScopeIdentifiersTransformer,
NodeTransformer.ScopeThroughIdentifiersTransformer,
NodeTransformer.SplitStringTransformer,
NodeTransformer.StringArrayCallsTransformer,
NodeTransformer.StringArrayRotateFunctionTransformer,
NodeTransformer.StringArrayScopeCallsWrapperTransformer,
NodeTransformer.StringArrayTransformer,
+11 -1
View File
@@ -371,9 +371,19 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
)
.option(
'--string-array <boolean>',
'Disables gathering of all literal strings into an array and replacing every literal string with an array call',
'Enables gathering of all literal strings into an array and replacing every literal string with the call to the string array',
BooleanSanitizer
)
.option(
'--string-array-calls-transform <boolean>',
'Enables the transformation of calls to the string array',
BooleanSanitizer
)
.option(
'--string-array-calls-transform-threshold <number>',
'The probability that that calls to the string array will be transformed',
parseFloat
)
.option(
'--string-array-encoding <list> (comma separated, without whitespaces)',
'Encodes each string in strings array using base64 or rc4 (this option can slow down your code speed). ' +
@@ -9,6 +9,7 @@ import { ControlFlowCustomNode } from '../../../enums/custom-nodes/ControlFlowCu
import { DeadCodeInjectionCustomNode } from '../../../enums/custom-nodes/DeadCodeInjectionCustomNode';
import { ObjectExpressionKeysTransformerCustomNode } from '../../../enums/custom-nodes/ObjectExpressionKeysTransformerCustomNode';
import { StringArrayCustomNode } from '../../../enums/custom-nodes/StringArrayCustomNode';
import { StringArrayIndexNode } from '../../../enums/custom-nodes/string-array-index-nodes/StringArrayIndexNode';
import { BinaryExpressionFunctionNode } from '../../../custom-nodes/control-flow-flattening-nodes/BinaryExpressionFunctionNode';
import { BlockStatementControlFlowFlatteningNode } from '../../../custom-nodes/control-flow-flattening-nodes/BlockStatementControlFlowFlatteningNode';
@@ -22,7 +23,7 @@ import { ObjectExpressionVariableDeclarationHostNode } from '../../../custom-nod
import { StringArrayCallNode } from '../../../custom-nodes/string-array-nodes/StringArrayCallNode';
import { StringArrayHexadecimalNumberIndexNode } from '../../../custom-nodes/string-array-nodes/string-array-index-nodes/StringArrayHexadecimalNumberIndexNode';
import { StringArrayHexadecimalNumericStringIndexNode } from '../../../custom-nodes/string-array-nodes/string-array-index-nodes/StringArrayHexadecimalNumericStringIndexNode';
import { StringArrayIndexNode } from '../../../enums/custom-nodes/string-array-index-nodes/StringArrayIndexNode';
import { StringArrayIndexHostNode } from '../../../custom-nodes/string-array-nodes/StringArrayIndexHostNode';
import { StringArrayScopeCallsWrapperFunctionNode } from '../../../custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperFunctionNode';
import { StringArrayScopeCallsWrapperVariableNode } from '../../../custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperVariableNode';
import { StringLiteralControlFlowStorageCallNode } from '../../../custom-nodes/control-flow-flattening-nodes/control-flow-storage-nodes/StringLiteralControlFlowStorageCallNode';
@@ -81,6 +82,10 @@ export const customNodesModule: interfaces.ContainerModule = new ContainerModule
.toConstructor(StringArrayCallNode)
.whenTargetNamed(StringArrayCustomNode.StringArrayCallNode);
bind<interfaces.Newable<ICustomNode>>(ServiceIdentifiers.Newable__ICustomNode)
.toConstructor(StringArrayIndexHostNode)
.whenTargetNamed(StringArrayCustomNode.StringArrayIndexHostNode);
bind<interfaces.Newable<ICustomNode>>(ServiceIdentifiers.Newable__ICustomNode)
.toConstructor(StringArrayScopeCallsWrapperFunctionNode)
.whenTargetNamed(StringArrayCustomNode.StringArrayScopeCallsWrapperFunctionNode);
@@ -5,12 +5,17 @@ import { INodeTransformer } from '../../../interfaces/node-transformers/INodeTra
import { NodeTransformer } from '../../../enums/node-transformers/NodeTransformer';
import { StringArrayCallsTransformer } from '../../../node-transformers/string-array-transformers/StringArrayCallsTransformer';
import { StringArrayRotateFunctionTransformer } from '../../../node-transformers/string-array-transformers/StringArrayRotateFunctionTransformer';
import { StringArrayScopeCallsWrapperTransformer } from '../../../node-transformers/string-array-transformers/StringArrayScopeCallsWrapperTransformer';
import { StringArrayTransformer } from '../../../node-transformers/string-array-transformers/StringArrayTransformer';
export const stringArrayTransformersModule: interfaces.ContainerModule = new ContainerModule((bind: interfaces.Bind) => {
// strings transformers
bind<INodeTransformer>(ServiceIdentifiers.INodeTransformer)
.to(StringArrayCallsTransformer)
.whenTargetNamed(NodeTransformer.StringArrayCallsTransformer);
bind<INodeTransformer>(ServiceIdentifiers.INodeTransformer)
.to(StringArrayRotateFunctionTransformer)
.whenTargetNamed(NodeTransformer.StringArrayRotateFunctionTransformer);
@@ -105,7 +105,7 @@ export abstract class AbstractStringArrayCallNode extends AbstractCustomNode {
const stringArrayCallIndexNode: ESTree.Expression = this.stringArrayIndexNodeFactory(stringArrayIndexNodeName)
.getNode(normalizedIndex);
NodeMetadata.set(stringArrayCallIndexNode, { replacedLiteral: true });
NodeMetadata.set(stringArrayCallIndexNode, { stringArrayCallLiteralNode: true });
const hexadecimalNode: ESTree.Expression = isPositive
? stringArrayCallIndexNode
@@ -126,7 +126,7 @@ export abstract class AbstractStringArrayCallNode extends AbstractCustomNode {
protected getRc4KeyLiteralNode (decodeKey: string): ESTree.Literal {
const rc4KeyLiteralNode: ESTree.Literal = NodeFactory.literalNode(decodeKey);
NodeMetadata.set(rc4KeyLiteralNode, { replacedLiteral: true });
NodeMetadata.set(rc4KeyLiteralNode, { stringArrayCallLiteralNode: true });
return rc4KeyLiteralNode;
}
@@ -0,0 +1,94 @@
import { inject, injectable, } from 'inversify';
import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
import type * as ESTree from 'estree';
import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
import { TStatement } from '../../types/node/TStatement';
import { TStringArrayIndexNodeFactory } from '../../types/container/custom-nodes/string-array-index-nodes/TStringArrayIndexNodeFactory';
import { IArrayUtils } from '../../interfaces/utils/IArrayUtils';
import { ICustomCodeHelperFormatter } from '../../interfaces/custom-code-helpers/ICustomCodeHelperFormatter';
import { IOptions } from '../../interfaces/options/IOptions';
import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
import { IStringArrayStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayStorage';
import { initializable } from '../../decorators/Initializable';
import { AbstractStringArrayCallNode } from './AbstractStringArrayCallNode';
import { NodeFactory } from '../../node/NodeFactory';
@injectable()
export class StringArrayIndexHostNode extends AbstractStringArrayCallNode {
/**
* @type {Literal}
*/
@initializable()
private stringArrayCallIndexNode!: ESTree.Literal;
/**
* @type {number}
*/
@initializable()
private variableName!: string;
/**
* @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
* @param {TStringArrayIndexNodeFactory} stringArrayIndexNodeFactory
* @param {ICustomCodeHelperFormatter} customCodeHelperFormatter
* @param {IStringArrayStorage} stringArrayStorage
* @param {IArrayUtils} arrayUtils
* @param {IRandomGenerator} randomGenerator
* @param {IOptions} options
*/
public constructor (
@inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
@inject(ServiceIdentifiers.Factory__IStringArrayIndexNode)
stringArrayIndexNodeFactory: TStringArrayIndexNodeFactory,
@inject(ServiceIdentifiers.ICustomCodeHelperFormatter) customCodeHelperFormatter: ICustomCodeHelperFormatter,
@inject(ServiceIdentifiers.IStringArrayStorage) stringArrayStorage: IStringArrayStorage,
@inject(ServiceIdentifiers.IArrayUtils) arrayUtils: IArrayUtils,
@inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
@inject(ServiceIdentifiers.IOptions) options: IOptions
) {
super(
identifierNamesGeneratorFactory,
stringArrayIndexNodeFactory,
customCodeHelperFormatter,
stringArrayStorage,
arrayUtils,
randomGenerator,
options
);
}
/**
* @param {Literal} stringArrayCallIndexNode
* @param {string} variableName
*/
public initialize (
stringArrayCallIndexNode: ESTree.Literal,
variableName: string
): void {
this.stringArrayCallIndexNode = stringArrayCallIndexNode;
this.variableName = variableName;
}
/**
* @returns {TStatement[]}
*/
protected getNodeStructure (): TStatement[] {
const indexHostVariableNode: ESTree.VariableDeclaration = NodeFactory.variableDeclarationNode(
[
NodeFactory.variableDeclaratorNode(
NodeFactory.identifierNode(this.variableName),
this.stringArrayCallIndexNode
)
],
'const'
);
return [indexHostVariableNode];
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ declare module 'estree' {
}
export interface LiteralNodeMetadata extends BaseNodeMetadata {
replacedLiteral?: boolean;
stringArrayCallLiteralNode?: boolean;
}
/**
@@ -1,5 +1,6 @@
export enum StringArrayCustomNode {
StringArrayCallNode = 'StringArrayCallNode',
StringArrayIndexHostNode = 'StringArrayIndexHostNode',
StringArrayScopeCallsWrapperFunctionNode = 'StringArrayScopeCallsWrapperFunctionNode',
StringArrayScopeCallsWrapperVariableNode = 'StringArrayScopeCallsWrapperVariableNode'
}
@@ -28,6 +28,7 @@ export enum NodeTransformer {
ScopeIdentifiersTransformer = 'ScopeIdentifiersTransformer',
ScopeThroughIdentifiersTransformer = 'ScopeThroughIdentifiersTransformer',
SplitStringTransformer = 'SplitStringTransformer',
StringArrayCallsTransformer = 'StringArrayCallsTransformer',
StringArrayTransformer = 'StringArrayTransformer',
StringArrayRotateFunctionTransformer = 'StringArrayRotateFunctionTransformer',
StringArrayScopeCallsWrapperTransformer = 'StringArrayScopeCallsWrapperTransformer',
+2
View File
@@ -51,6 +51,8 @@ export interface IOptions {
readonly splitStringsChunkLength: number;
readonly stringArray: boolean;
readonly stringArrayEncoding: TStringArrayEncoding[];
readonly stringArrayCallsTransform: boolean;
readonly stringArrayCallsTransformThreshold: number;
readonly stringArrayIndexesType: TStringArrayIndexesType[];
readonly stringArrayIndexShift: boolean;
readonly stringArrayWrappersChainedCalls: boolean;
+7
View File
@@ -24,6 +24,13 @@ export interface IArrayUtils {
*/
getLastElement <T> (array: T[]): T | undefined;
/**
* @param {T[]} array
* @param {number} index
* @returns {T | undefined}
*/
getLastElementByIndex <T> (array: T[], index: number): T | undefined;
/**
* @param array
* @param times
@@ -23,6 +23,8 @@ import { NodeUtils } from '../../node/NodeUtils';
*/
@injectable()
export class SplitStringTransformer extends AbstractNodeTransformer {
private static readonly maxStringLengthForSecondPass: number = 30000;
/**
* @type {number}
*/
@@ -80,14 +82,14 @@ export class SplitStringTransformer extends AbstractNodeTransformer {
* @returns {IVisitor | null}
*/
public getVisitor (nodeTransformationStage: NodeTransformationStage): IVisitor | null {
if (!this.options.splitStrings) {
return null;
}
switch (nodeTransformationStage) {
case NodeTransformationStage.Converting:
return {
enter: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => {
if (!this.options.splitStrings) {
return;
}
leave: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => {
if (parentNode && NodeGuards.isLiteralNode(node)) {
return this.transformNode(node, parentNode);
}
@@ -113,48 +115,55 @@ export class SplitStringTransformer extends AbstractNodeTransformer {
}
// pass #1: split string on a large chunks with length of `firstPassChunkLength`
const firstPassChunksNode: ESTree.Node = this.transformLiteralNodeByChunkLength(
const [firstPassChunksNode, stringChunks]: [ESTree.Node, string[]] = this.transformLiteralNodeByChunkLength(
literalNode,
parentNode,
SplitStringTransformer.firstPassChunkLength
);
// pass #2: split large chunks on a chunks with length of `splitStringsChunkLength`
const stringChunksCount: number = stringChunks.length;
const isLargeString: boolean = SplitStringTransformer.maxStringLengthForSecondPass
/ SplitStringTransformer.firstPassChunkLength
<= stringChunksCount;
const minSecondPathChunkLength = isLargeString
? Math.floor(stringChunksCount / this.options.splitStringsChunkLength)
: this.options.splitStringsChunkLength;
// pass #2: split large chunks on a chunks with length of min length === `splitStringsChunkLength`
const secondPassChunksNode: ESTree.Node = estraverse.replace(firstPassChunksNode, {
// eslint-disable-next-line @typescript-eslint/no-shadow
enter: (node: ESTree.Node, parentNode: ESTree.Node | null) => {
if (parentNode && NodeGuards.isLiteralNode(node)) {
enter: (node: ESTree.Node) => {
if (NodeGuards.isLiteralNode(node)) {
return this.transformLiteralNodeByChunkLength(
node,
parentNode,
this.options.splitStringsChunkLength
);
minSecondPathChunkLength
)[0];
}
}
});
NodeUtils.parentizeNode(secondPassChunksNode, parentNode);
NodeUtils.parentizeAst(secondPassChunksNode);
return secondPassChunksNode;
}
/**
* @param {Literal} literalNode
* @param {Node} parentNode
* @param {number} chunkLength
* @returns {Node}
* @returns {[resultNode: Node, stringChunks: string[]]}
*/
private transformLiteralNodeByChunkLength (
literalNode: ESTree.Literal,
parentNode: ESTree.Node,
chunkLength: number
): ESTree.Node {
): [resultNode: ESTree.Node, stringChunks: string[]] {
if (!NodeLiteralUtils.isStringLiteralNode(literalNode)) {
return literalNode;
return [literalNode, []];
}
const valueLength: number = stringz.length(literalNode.value);
if (chunkLength >= valueLength) {
return literalNode;
return [literalNode, []];
}
const stringChunks: string[] = SplitStringTransformer.chunkString(
@@ -163,13 +172,10 @@ export class SplitStringTransformer extends AbstractNodeTransformer {
chunkLength
);
const binaryExpressionNode: ESTree.BinaryExpression =
this.transformStringChunksToBinaryExpressionNode(stringChunks);
NodeUtils.parentizeAst(binaryExpressionNode);
NodeUtils.parentizeNode(binaryExpressionNode, parentNode);
return binaryExpressionNode;
return [
this.transformStringChunksToBinaryExpressionNode(stringChunks),
stringChunks
];
}
/**
@@ -65,7 +65,7 @@ export class MetadataTransformer extends AbstractNodeTransformer {
NodeMetadata.set(node, { ignoredNode: false });
if (NodeGuards.isLiteralNode(node)) {
NodeMetadata.set(node, { replacedLiteral: false });
NodeMetadata.set(node, { stringArrayCallLiteralNode: false });
}
return node;
@@ -14,6 +14,7 @@ import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
import { NodeGuards } from '../../node/NodeGuards';
import { NodeLiteralUtils } from '../../node/NodeLiteralUtils';
import { RenamePropertiesMode } from '../../enums/node-transformers/rename-properties-transformers/RenamePropertiesMode';
import { NodeUtils } from '../../node/NodeUtils';
@injectable()
export class RenamePropertiesTransformer extends AbstractNodeTransformer {
@@ -106,20 +107,20 @@ export class RenamePropertiesTransformer extends AbstractNodeTransformer {
* @returns {Node}
*/
public transformNode (node: ESTree.Node, parentNode: ESTree.Node): ESTree.Node {
let propertyNode: ESTree.Node | null = null;
if (NodeGuards.isPropertyNode(node)) {
return this.transformPropertyNode(node);
propertyNode = this.transformPropertyNode(node);
} else if (NodeGuards.isPropertyDefinitionNode(node)) {
propertyNode = this.transformPropertyDefinitionNode(node);
} else if (NodeGuards.isMemberExpressionNode(node)) {
propertyNode = this.transformMemberExpressionNode(node);
} else if (NodeGuards.isMethodDefinitionNode(node)) {
propertyNode = this.transformMethodDefinitionNode(node);
}
if (NodeGuards.isPropertyDefinitionNode(node)) {
return this.transformPropertyDefinitionNode(node);
}
if (NodeGuards.isMemberExpressionNode(node)) {
return this.transformMemberExpressionNode(node);
}
if (NodeGuards.isMethodDefinitionNode(node)) {
return this.transformMethodDefinitionNode(node);
if (propertyNode) {
NodeUtils.parentizeNode(propertyNode, parentNode);
}
return node;
@@ -0,0 +1,196 @@
import { inject, injectable, } from 'inversify';
import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
import * as ESTree from 'estree';
import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
import { TInitialData } from '../../types/TInitialData';
import { TNodeWithLexicalScopeStatements } from '../../types/node/TNodeWithLexicalScopeStatements';
import { TStatement } from '../../types/node/TStatement';
import { TStringArrayCustomNodeFactory } from '../../types/container/custom-nodes/TStringArrayCustomNodeFactory';
import { ICustomNode } from '../../interfaces/custom-nodes/ICustomNode';
import { IIdentifierNamesGenerator } from '../../interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator';
import { IOptions } from '../../interfaces/options/IOptions';
import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
import { NodeTransformer } from '../../enums/node-transformers/NodeTransformer';
import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage';
import { StringArrayCustomNode } from '../../enums/custom-nodes/StringArrayCustomNode';
import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
import { NodeAppender } from '../../node/NodeAppender';
import { NodeFactory } from '../../node/NodeFactory';
import { NodeGuards } from '../../node/NodeGuards';
import { NodeMetadata } from '../../node/NodeMetadata';
import { NodeUtils } from '../../node/NodeUtils';
import { StringArrayIndexHostNode } from '../../custom-nodes/string-array-nodes/StringArrayIndexHostNode';
@injectable()
export class StringArrayCallsTransformer extends AbstractNodeTransformer {
/**
* @type {number}
*/
private static readonly lexicalScopeBodyLengthThreshold: number = 20000;
/**
* @type {NodeTransformer[]}
*/
public override readonly runAfter: NodeTransformer[] = [
NodeTransformer.StringArrayTransformer
];
/**
* @type {IIdentifierNamesGenerator}
*/
private readonly identifierNamesGenerator: IIdentifierNamesGenerator;
/**
* @type {TStringArrayCustomNodeFactory}
*/
private readonly stringArrayTransformerCustomNodeFactory: TStringArrayCustomNodeFactory;
/**
* @type {TNodeWithLexicalScopeStatements[]}
*/
private readonly visitedLexicalScopeNodesStack: TNodeWithLexicalScopeStatements[] = [];
/**
* @param {IRandomGenerator} randomGenerator
* @param {IOptions} options
* @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
* @param {TStringArrayCustomNodeFactory} stringArrayTransformerCustomNodeFactory
*/
public constructor (
@inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
@inject(ServiceIdentifiers.IOptions) options: IOptions,
@inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
@inject(ServiceIdentifiers.Factory__IStringArrayCustomNode)
stringArrayTransformerCustomNodeFactory: TStringArrayCustomNodeFactory
) {
super(randomGenerator, options);
this.identifierNamesGenerator = identifierNamesGeneratorFactory(options);
this.stringArrayTransformerCustomNodeFactory = stringArrayTransformerCustomNodeFactory;
this.stringArrayTransformerCustomNodeFactory = stringArrayTransformerCustomNodeFactory;
}
/**
* @param {NodeTransformationStage} nodeTransformationStage
* @returns {IVisitor | null}
*/
public getVisitor (nodeTransformationStage: NodeTransformationStage): IVisitor | null {
if (!this.options.stringArrayCallsTransform) {
return null;
}
switch (nodeTransformationStage) {
case NodeTransformationStage.StringArray:
return {
enter: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => {
if (parentNode && NodeGuards.isNodeWithLexicalScopeStatements(node, parentNode)) {
this.onLexicalScopeNodeEnter(node);
return node;
}
},
leave: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => {
if (parentNode && NodeGuards.isNodeWithLexicalScopeStatements(node, parentNode)) {
this.onLexicalScopeNodeLeave();
return node;
}
if (
parentNode
&& NodeGuards.isLiteralNode(node)
&& NodeMetadata.isStringArrayCallLiteralNode(node)
) {
return this.transformNode(node, parentNode);
}
}
};
default:
return null;
}
}
/**
* @param {Literal} literalNode
* @param {Node} parentNode
* @returns {Literal | Identifier}
*/
public transformNode (literalNode: ESTree.Literal, parentNode: ESTree.Node): ESTree.Literal | ESTree.Identifier {
const randomLexicalScopeNode: TNodeWithLexicalScopeStatements = this.randomGenerator
.getRandomGenerator()
.pickone(this.visitedLexicalScopeNodesStack);
if (!randomLexicalScopeNode) {
return literalNode;
}
const randomNumber: number = this.randomGenerator.getMathRandom();
const isOptionThresholdPassed: boolean = randomNumber <= this.options.stringArrayCallsTransformThreshold;
const isLargeLexicalScopeBodyLengthThresholdPassed: boolean = randomNumber > (
randomLexicalScopeNode.body.length / StringArrayCallsTransformer.lexicalScopeBodyLengthThreshold
);
if (!isOptionThresholdPassed || !isLargeLexicalScopeBodyLengthThresholdPassed) {
return literalNode;
}
const hostVariableName: string = this.identifierNamesGenerator.generateNext();
const hostVariableNode: ESTree.VariableDeclaration = this.getStringArrayIndexHostNode(
literalNode,
hostVariableName
);
NodeUtils.parentizeAst(hostVariableNode);
NodeAppender.prepend(randomLexicalScopeNode, [hostVariableNode]);
const identifierNode: ESTree.Identifier = NodeFactory.identifierNode(hostVariableName);
NodeUtils.parentizeNode(identifierNode, parentNode);
return identifierNode;
}
/**
* @param {Literal} stringArrayCallIndexNode
* @param {string} variableName
* @returns {VariableDeclaration}
*/
private getStringArrayIndexHostNode (
stringArrayCallIndexNode: ESTree.Literal,
variableName: string
): ESTree.VariableDeclaration {
const stringArrayCallCustomNode: ICustomNode<TInitialData<StringArrayIndexHostNode>> =
this.stringArrayTransformerCustomNodeFactory(StringArrayCustomNode.StringArrayIndexHostNode);
stringArrayCallCustomNode.initialize(
stringArrayCallIndexNode,
variableName
);
const statementNode: TStatement = stringArrayCallCustomNode.getNode()[0];
if (!NodeGuards.isVariableDeclarationNode(statementNode)) {
throw new Error('`stringArrayIndexHostNode.getNode()[0]` should returns array with `isVariableDeclarationNode` node');
}
return statementNode;
}
/**
* @param {TNodeWithLexicalScopeStatements} lexicalScopeBodyNode
*/
private onLexicalScopeNodeEnter (lexicalScopeBodyNode: TNodeWithLexicalScopeStatements): void {
this.visitedLexicalScopeNodesStack.push(lexicalScopeBodyNode);
}
private onLexicalScopeNodeLeave (): void {
this.visitedLexicalScopeNodesStack.pop();
}
}
@@ -139,7 +139,11 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
this.prepareNode(node);
}
if (parentNode && NodeGuards.isLiteralNode(node) && !NodeMetadata.isReplacedLiteral(node)) {
if (
parentNode
&& NodeGuards.isLiteralNode(node)
&& !NodeMetadata.isStringArrayCallLiteralNode(node)
) {
return this.transformNode(node, parentNode);
}
}
@@ -187,16 +191,19 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
const cacheKey: string = this.literalNodesCacheStorage.buildKey(literalValue, stringArrayStorageItemData);
const useCachedValue: boolean = this.literalNodesCacheStorage.shouldUseCachedValue(cacheKey, stringArrayStorageItemData);
let resultNode: ESTree.Node;
if (useCachedValue) {
return <ESTree.Node>this.literalNodesCacheStorage.get(cacheKey);
const nodeFromCache: ESTree.Node = <ESTree.Node>this.literalNodesCacheStorage.get(cacheKey);
resultNode = NodeUtils.clone(nodeFromCache);
} else {
resultNode = stringArrayStorageItemData
? this.getStringArrayCallNode(stringArrayStorageItemData)
: literalNode;
this.literalNodesCacheStorage.set(cacheKey, resultNode);
}
const resultNode: ESTree.Node = stringArrayStorageItemData
? this.getStringArrayCallNode(stringArrayStorageItemData)
: literalNode;
this.literalNodesCacheStorage.set(cacheKey, resultNode);
NodeUtils.parentizeNode(resultNode, parentNode);
return resultNode;
+1 -1
View File
@@ -63,7 +63,7 @@ export class NodeFactory {
public static binaryExpressionNode (
operator: ESTree.BinaryOperator,
left: ESTree.Expression,
right: ESTree.Expression,
right: ESTree.Expression
): ESTree.BinaryExpression {
return {
type: NodeType.BinaryExpression,
+5 -2
View File
@@ -43,7 +43,10 @@ export class NodeMetadata {
* @param {Node} literalNode
* @returns {boolean}
*/
public static isReplacedLiteral (literalNode: ESTree.Literal): boolean {
return NodeMetadata.get<ESTree.LiteralNodeMetadata, 'replacedLiteral'>(literalNode, 'replacedLiteral') === true;
public static isStringArrayCallLiteralNode (literalNode: ESTree.Literal): boolean {
return NodeMetadata.get<
ESTree.LiteralNodeMetadata,
'stringArrayCallLiteralNode'
>(literalNode, 'stringArrayCallLiteralNode') === true;
}
}
+14
View File
@@ -340,6 +340,20 @@ export class Options implements IOptions {
@IsBoolean()
public readonly stringArray!: boolean;
/**
* @type {boolean}
*/
@IsBoolean()
public readonly stringArrayCallsTransform!: boolean;
/**
* @type {number}
*/
@IsNumber()
@Min(0)
@Max(1)
public readonly stringArrayCallsTransformThreshold!: number;
/**
* @type {TStringArrayEncoding[]}
*/
+2
View File
@@ -20,6 +20,7 @@ import { SplitStringsChunkLengthRule } from './normalizer-rules/SplitStringsChun
import { StringArrayRule } from './normalizer-rules/StringArrayRule';
import { StringArrayEncodingRule } from './normalizer-rules/StringArrayEncodingRule';
import { StringArrayWrappersChainedCallsRule } from './normalizer-rules/StringArrayWappersChainedCalls';
import { StringArrayCallsTransformRule } from './normalizer-rules/StringArrayCallsTransform';
@injectable()
export class OptionsNormalizer implements IOptionsNormalizer {
@@ -39,6 +40,7 @@ export class OptionsNormalizer implements IOptionsNormalizer {
SourceMapBaseUrlRule,
SourceMapFileNameRule,
SplitStringsChunkLengthRule,
StringArrayCallsTransformRule,
StringArrayRule,
StringArrayEncodingRule,
StringArrayWrappersChainedCallsRule,
@@ -0,0 +1,19 @@
import { TOptionsNormalizerRule } from '../../types/options/TOptionsNormalizerRule';
import { IOptions } from '../../interfaces/options/IOptions';
/**
* @param {IOptions} options
* @returns {IOptions}
*/
export const StringArrayCallsTransformRule: TOptionsNormalizerRule = (options: IOptions): IOptions => {
if (!options.stringArrayCallsTransform) {
options = {
...options,
stringArrayCallsTransform: false,
stringArrayCallsTransformThreshold: 0
};
}
return options;
};
@@ -15,6 +15,8 @@ export const StringArrayRule: TOptionsNormalizerRule = (options: IOptions): IOpt
rotateStringArray: false,
shuffleStringArray: false,
stringArray: false,
stringArrayCallsTransform: false,
stringArrayCallsTransformThreshold: 0,
stringArrayEncoding: [
StringArrayEncoding.None
],
+2
View File
@@ -51,6 +51,8 @@ export const DEFAULT_PRESET: TInputOptions = Object.freeze({
splitStrings: false,
splitStringsChunkLength: 10,
stringArray: true,
stringArrayCallsTransform: true,
stringArrayCallsTransformThreshold: 0.5,
stringArrayEncoding: [
StringArrayEncoding.None
],
+1
View File
@@ -13,6 +13,7 @@ export const HIGH_OBFUSCATION_PRESET: TInputOptions = Object.freeze({
debugProtectionInterval: true,
optionsPreset: OptionsPreset.HighObfuscation,
splitStringsChunkLength: 5,
stringArrayCallsTransformThreshold: 1,
stringArrayEncoding: [
StringArrayEncoding.Rc4
],
+1
View File
@@ -14,6 +14,7 @@ export const MEDIUM_OBFUSCATION_PRESET: TInputOptions = Object.freeze({
optionsPreset: OptionsPreset.MediumObfuscation,
splitStrings: true,
splitStringsChunkLength: 10,
stringArrayCallsTransformThreshold: 0.75,
stringArrayEncoding: [
StringArrayEncoding.Base64
],
+2
View File
@@ -47,6 +47,8 @@ export const NO_ADDITIONAL_NODES_PRESET: TInputOptions = Object.freeze({
splitStrings: false,
splitStringsChunkLength: 0,
stringArray: false,
stringArrayCallsTransform: false,
stringArrayCallsTransformThreshold: 0,
stringArrayEncoding: [
StringArrayEncoding.None
],
@@ -43,9 +43,7 @@ export class VisitedLexicalScopeNodesStackStorage extends ArrayStorage <TNodeWit
* @returns {TNodeWithLexicalScopeStatements | undefined}
*/
public getPenultimateElement (): TNodeWithLexicalScopeStatements | undefined {
const storageLength: number = this.getLength();
return this.get(storageLength - 2) ?? undefined;
return this.arrayUtils.getLastElementByIndex(this.getStorage(), 1);
}
/**
+10 -1
View File
@@ -85,9 +85,18 @@ export class ArrayUtils implements IArrayUtils {
* @returns {T | undefined}
*/
public getLastElement <T> (array: T[]): T | undefined {
return this.getLastElementByIndex(array, 0);
}
/**
* @param {T[]} array
* @param {number} index
* @returns {T | undefined}
*/
public getLastElementByIndex <T> (array: T[], index: number): T | undefined {
const arrayLength: number = array.length;
return array[arrayLength - 1] ?? undefined;
return array[arrayLength - 1 - index] ?? undefined;
}
/**
+38 -11
View File
@@ -1,25 +1,52 @@
'use strict';
import { readFileAsString } from '../helpers/readFileAsString';
import { StringArrayWrappersType } from '../../src/enums/node-transformers/string-array-transformers/StringArrayWrappersType';
import { StringArrayEncoding } from '../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
(function () {
const JavaScriptObfuscator: any = require('../../index');
const code: string = readFileAsString(__dirname + '/../functional-tests/javascript-obfuscator/fixtures/custom-nodes-identifier-names-collision.js');
let obfuscationResult = JavaScriptObfuscator.obfuscate(
code,
let obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
`
class Test {
constructor () {
let test = {}
}
static methodA = () => {
console.log('method_A');
}
methodB () {
console.log('method_B');
console.log('method_C');
console.log('method_D');
console.log('method_E');
Test.methodA();
}
}
const instance = new Test();
Test.methodA();
instance.methodB();
`,
{
identifierNamesGenerator: 'mangled',
compact: false,
simplify: false,
stringArray: true,
seed: 429105580
stringArrayThreshold: 1,
stringArrayCallsTransform: true,
stringArrayCallsTransformThreshold: 1,
stringArrayWrappersCount: 2,
stringArrayWrappersParametersMaxCount: 5,
stringArrayWrappersType: StringArrayWrappersType.Function,
stringArrayEncoding: [StringArrayEncoding.Rc4]
}
);
let obfuscatedCode: string = obfuscationResult.getObfuscatedCode();
let identifierNamesCache = obfuscationResult.getIdentifierNamesCache();
).getObfuscatedCode();
console.log(obfuscatedCode);
console.log(eval(obfuscatedCode));
console.log(identifierNamesCache);
})();
})();
@@ -741,6 +741,8 @@ describe('OptionsNormalizer', () => {
...getDefaultOptions(),
shuffleStringArray: true,
stringArray: false,
stringArrayCallsTransform: true,
stringArrayCallsTransformThreshold: 1,
stringArrayEncoding: [StringArrayEncoding.Rc4],
stringArrayIndexShift: true,
stringArrayWrappersChainedCalls: true,
@@ -753,6 +755,8 @@ describe('OptionsNormalizer', () => {
...getDefaultOptions(),
shuffleStringArray: false,
stringArray: false,
stringArrayCallsTransform: false,
stringArrayCallsTransformThreshold: 0,
stringArrayEncoding: [StringArrayEncoding.None],
stringArrayIndexShift: false,
stringArrayWrappersChainedCalls: false,
@@ -767,6 +771,26 @@ describe('OptionsNormalizer', () => {
});
});
describe('stringArrayCallsTransformRule', () => {
before(() => {
optionsPreset = getNormalizedOptions({
...getDefaultOptions(),
stringArrayCallsTransform: false,
stringArrayCallsTransformThreshold: 1
});
expectedOptionsPreset = {
...getDefaultOptions(),
stringArrayCallsTransform: false,
stringArrayCallsTransformThreshold: 0
};
});
it('should normalize options preset', () => {
assert.deepEqual(optionsPreset, expectedOptionsPreset);
});
});
describe('stringArrayEncodingRule', () => {
before(() => {
optionsPreset = getNormalizedOptions({
@@ -11,7 +11,7 @@ describe('NodeMetadata', () => {
describe('set', () => {
const expectedMetadata: ESTree.LiteralNodeMetadata = {
ignoredNode: true,
replacedLiteral: true
stringArrayCallLiteralNode: true
};
let node: ESTree.Literal;
@@ -20,7 +20,7 @@ describe('NodeMetadata', () => {
node = NodeFactory.literalNode('foo');
NodeMetadata.set(node, {
ignoredNode: true,
replacedLiteral: true
stringArrayCallLiteralNode: true
})
});
@@ -38,8 +38,11 @@ describe('NodeMetadata', () => {
before(() => {
node = NodeFactory.literalNode('foo');
node.metadata = {};
node.metadata.replacedLiteral = true;
value = NodeMetadata.get<ESTree.LiteralNodeMetadata, 'replacedLiteral'>(node, 'replacedLiteral');
node.metadata.stringArrayCallLiteralNode = true;
value = NodeMetadata.get<ESTree.LiteralNodeMetadata, 'stringArrayCallLiteralNode'>(
node,
'stringArrayCallLiteralNode'
);
});
it('should get metadata value of the node', () => {
@@ -92,8 +95,8 @@ describe('NodeMetadata', () => {
before(() => {
node = NodeFactory.literalNode('foo');
node.metadata = {};
node.metadata.replacedLiteral = true;
value = NodeMetadata.isReplacedLiteral(node);
node.metadata.stringArrayCallLiteralNode = true;
value = NodeMetadata.isStringArrayCallLiteralNode(node);
});
it('should return metadata value', () => {
+62
View File
@@ -215,6 +215,68 @@ describe('ArrayUtils', () => {
});
});
describe('getLastElementByIndex', () => {
describe('empty array', () => {
const array: string[] = [];
const expectedLastElement: undefined = undefined;
let lastElement: string | undefined;
before(() => {
lastElement = arrayUtils.getLastElementByIndex(array, 1);
});
it('should return undefined if array is empty', () => {
assert.equal(lastElement, expectedLastElement);
});
});
describe('array length: `1` and index is out of array boundary', () => {
const array: string[] = ['foo'];
const expectedLastElement: undefined = undefined;
let lastElement: string | undefined;
before(() => {
lastElement = arrayUtils.getLastElementByIndex(array, 2);
});
it('should return undefined', () => {
assert.equal(lastElement, expectedLastElement);
});
});
describe('array length: `3` and index is `0`', () => {
const array: string[] = ['foo', 'bar', 'baz'];
const expectedLastElement: string = 'baz';
let lastElement: string | undefined;
before(() => {
lastElement = arrayUtils.getLastElementByIndex(array, 0);
});
it('should return element with a correct index', () => {
assert.equal(lastElement, expectedLastElement);
});
});
describe('array length: `3` and index is `1`', () => {
const array: string[] = ['foo', 'bar', 'baz'];
const expectedLastElement: string = 'bar';
let lastElement: string | undefined;
before(() => {
lastElement = arrayUtils.getLastElementByIndex(array, 1);
});
it('should return element with a correct index', () => {
assert.equal(lastElement, expectedLastElement);
});
});
});
describe('rotate', () => {
let array: number[],
rotatedArray: number[];