Преглед на файлове

Improved integration between `renameProperties` and `controlFlowFlattening` options (#1060)

Timofey Kachalov преди 3 години
родител
ревизия
b99b6a46ee
променени са 18 файла, в които са добавени 270 реда и са изтрити 179 реда
  1. 4 0
      CHANGELOG.md
  2. 1 1
      package.json
  3. 9 1
      src/declarations/ESTree.d.ts
  4. 15 16
      src/generators/identifier-names-generators/MangledIdentifierNamesGenerator.ts
  5. 9 8
      src/node-transformers/control-flow-transformers/control-flow-replacers/AbstractControlFlowReplacer.ts
  6. 1 1
      src/node-transformers/control-flow-transformers/control-flow-replacers/CallExpressionControlFlowReplacer.ts
  7. 1 1
      src/node-transformers/control-flow-transformers/control-flow-replacers/StringArrayCallControlFlowReplacer.ts
  8. 1 1
      src/node-transformers/control-flow-transformers/control-flow-replacers/StringLiteralControlFlowReplacer.ts
  9. 26 78
      src/node-transformers/rename-properties-transformers/RenamePropertiesTransformer.ts
  10. 11 0
      src/node/NodeMetadata.ts
  11. 25 42
      test/dev/dev.ts
  12. 59 25
      test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/string-litertal-control-flow-replacer/StringLiteralControlFlowReplacer.spec.ts
  13. 7 0
      test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/string-litertal-control-flow-replacer/fixtures/same-storage-key-for-same-string-values.js
  14. 64 0
      test/functional-tests/node-transformers/rename-properties-transformers/rename-properties-transformer/RenamePropertiesTransformer.spec.ts
  15. 6 0
      test/functional-tests/node-transformers/rename-properties-transformers/rename-properties-transformer/fixtures/control-flow-flattening-integration.js
  16. 4 0
      test/functional-tests/node-transformers/rename-properties-transformers/rename-properties-transformer/fixtures/transform-object-keys-integration.js
  17. 9 5
      test/functional-tests/node-transformers/string-array-transformers/string-array-rotate-function-transformer/StringArrayRotateFunctionTransformer.spec.ts
  18. 18 0
      test/unit-tests/node/node-metadata/NodeMetadata.spec.ts

+ 4 - 0
CHANGELOG.md

@@ -1,5 +1,9 @@
 Change Log
 
+v3.2.6
+---
+* Improved integration between `renameProperties` and `controlFlowFlattening` options. Fixed https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1053
+
 v3.2.5
 ---
 * Fixed https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1056

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "javascript-obfuscator",
-  "version": "3.2.5",
+  "version": "3.2.6",
   "description": "JavaScript obfuscator",
   "keywords": [
     "obfuscator",

+ 9 - 1
src/declarations/ESTree.d.ts

@@ -3,7 +3,6 @@
 import * as acorn from 'acorn';
 import * as escodegen from '@javascript-obfuscator/escodegen';
 import * as eslintScope from 'eslint-scope';
-import { BlockStatement } from 'estree';
 
 declare module 'estree' {
     /**
@@ -14,8 +13,13 @@ declare module 'estree' {
         ignoredNode?: boolean;
     }
 
+    export interface IdentifierNodeMetadata extends BaseNodeMetadata {
+        propertyKeyToRenameNode?: boolean
+    }
+
     export interface LiteralNodeMetadata extends BaseNodeMetadata {
         stringArrayCallLiteralNode?: boolean;
+        propertyKeyToRenameNode?: boolean
     }
 
     /**
@@ -40,6 +44,10 @@ declare module 'estree' {
         scope?: eslintScope.Scope | null;
     }
 
+    interface Identifier extends BaseNode {
+        metadata?: IdentifierNodeMetadata;
+    }
+
     interface BigIntLiteral extends BaseNode {
         metadata?: LiteralNodeMetadata;
         'x-verbatim-property'?: escodegen.XVerbatimProperty;

+ 15 - 16
src/generators/identifier-names-generators/MangledIdentifierNamesGenerator.ts

@@ -27,11 +27,6 @@ export class MangledIdentifierNamesGenerator extends AbstractIdentifierNamesGene
      */
     private static readonly initMangledNameCharacter: string = '9';
 
-    /**
-     * @type {WeakMap<TNodeWithLexicalScope, string>}
-     */
-    private static readonly lastMangledNameInScopeMap: WeakMap <TNodeWithLexicalScope, string> = new WeakMap();
-
     /**
      * @type {string[]}
      */
@@ -48,14 +43,19 @@ export class MangledIdentifierNamesGenerator extends AbstractIdentifierNamesGene
     private static readonly reservedNamesSet: Set<string> = new Set(reservedIdentifierNames);
 
     /**
-     * @type {WeakMap<string, string>}
+     * @type {string}
      */
-    private readonly lastMangledNameForLabelMap: Map <string, string> = new Map();
+    private lastMangledName: string = MangledIdentifierNamesGenerator.initMangledNameCharacter;
 
     /**
-     * @type {string}
+     * @type {WeakMap<TNodeWithLexicalScope, string>}
      */
-    private previousMangledName: string = MangledIdentifierNamesGenerator.initMangledNameCharacter;
+    private readonly lastMangledNameForScopeMap: WeakMap <TNodeWithLexicalScope, string> = new WeakMap();
+
+    /**
+     * @type {WeakMap<string, string>}
+     */
+    private readonly lastMangledNameForLabelMap: Map <string, string> = new Map();
 
     /**
      * @type {ISetUtils}
@@ -85,7 +85,7 @@ export class MangledIdentifierNamesGenerator extends AbstractIdentifierNamesGene
      * @returns {string}
      */
     public generateNext (nameLength?: number): string {
-        const identifierName: string = this.generateNewMangledName(this.previousMangledName);
+        const identifierName: string = this.generateNewMangledName(this.lastMangledName);
 
         this.updatePreviousMangledName(identifierName);
         this.preserveName(identifierName);
@@ -103,7 +103,7 @@ export class MangledIdentifierNamesGenerator extends AbstractIdentifierNamesGene
             : '';
 
         const identifierName: string = this.generateNewMangledName(
-            this.previousMangledName,
+            this.lastMangledName,
             (newIdentifierName: string) => {
                 const identifierNameWithPrefix: string = `${prefix}${newIdentifierName}`;
 
@@ -136,7 +136,7 @@ export class MangledIdentifierNamesGenerator extends AbstractIdentifierNamesGene
                 this.isValidIdentifierNameInLexicalScopes(newIdentifierName, lexicalScopes)
         );
 
-        MangledIdentifierNamesGenerator.lastMangledNameInScopeMap.set(lexicalScopeNode, identifierName);
+        this.lastMangledNameForScopeMap.set(lexicalScopeNode, identifierName);
 
         this.updatePreviousMangledName(identifierName);
         this.preserveNameForLexicalScope(identifierName, lexicalScopeNode);
@@ -216,11 +216,11 @@ export class MangledIdentifierNamesGenerator extends AbstractIdentifierNamesGene
      * @param {string} name
      */
     protected updatePreviousMangledName (name: string): void {
-        if (!this.isIncrementedMangledName(name, this.previousMangledName)) {
+        if (!this.isIncrementedMangledName(name, this.lastMangledName)) {
             return;
         }
 
-        this.previousMangledName = name;
+        this.lastMangledName = name;
     }
 
     /**
@@ -309,8 +309,7 @@ export class MangledIdentifierNamesGenerator extends AbstractIdentifierNamesGene
      */
     private getLastMangledNameForScopes (lexicalScopeNodes: TNodeWithLexicalScope[]): string {
         for (const lexicalScope of lexicalScopeNodes) {
-            const lastMangledName: string | null = MangledIdentifierNamesGenerator.lastMangledNameInScopeMap
-                .get(lexicalScope) ?? null;
+            const lastMangledName: string | null = this.lastMangledNameForScopeMap.get(lexicalScope) ?? null;
 
             if (!lastMangledName) {
                 continue;

+ 9 - 8
src/node-transformers/control-flow-transformers/control-flow-replacers/AbstractControlFlowReplacer.ts

@@ -36,9 +36,9 @@ export abstract class AbstractControlFlowReplacer implements IControlFlowReplace
     protected readonly randomGenerator: IRandomGenerator;
 
     /**
-     * @type {Map<string, Map<string, string[]>>}
+     * @type {Map<string, Map<string | number, string[]>>}
      */
-    protected readonly replacerDataByControlFlowStorageId: Map <string, Map<string, string[]>> = new Map();
+    protected readonly replacerDataByControlFlowStorageId: Map <string, Map<string | number, string[]>> = new Map();
 
     /**
      * @param {TControlFlowCustomNodeFactory} controlFlowCustomNodeFactory
@@ -80,23 +80,23 @@ export abstract class AbstractControlFlowReplacer implements IControlFlowReplace
     /**
      * @param {ICustomNode} customNode
      * @param {IControlFlowStorage} controlFlowStorage
-     * @param {string} replacerId
+     * @param {string | number} replacerId
      * @param {number} usingExistingIdentifierChance
      * @returns {string}
      */
     protected insertCustomNodeToControlFlowStorage (
         customNode: ICustomNode,
         controlFlowStorage: IControlFlowStorage,
-        replacerId: string,
+        replacerId: string | number,
         usingExistingIdentifierChance: number
     ): string {
         const controlFlowStorageId: string = controlFlowStorage.getStorageId();
-        const storageKeysById: Map<string, string[]> = this.replacerDataByControlFlowStorageId.get(controlFlowStorageId)
+        const storageKeysById: Map<string | number, string[]> = this.replacerDataByControlFlowStorageId.get(controlFlowStorageId)
             ?? new Map <string, string[]>();
-        const storageKeysForCurrentId: string[] | null = storageKeysById.get(replacerId) ?? null;
+        const storageKeysForCurrentId: string[] = storageKeysById.get(replacerId) ?? [];
 
         const shouldPickFromStorageKeysById = this.randomGenerator.getMathRandom() < usingExistingIdentifierChance
-            && storageKeysForCurrentId?.length;
+            && storageKeysForCurrentId.length;
 
         if (shouldPickFromStorageKeysById) {
             return this.randomGenerator.getRandomGenerator().pickone(storageKeysForCurrentId);
@@ -104,7 +104,8 @@ export abstract class AbstractControlFlowReplacer implements IControlFlowReplace
 
         const storageKey: string = this.generateStorageKey(controlFlowStorage);
 
-        storageKeysById.set(replacerId, [storageKey]);
+        storageKeysForCurrentId.push(storageKey);
+        storageKeysById.set(replacerId, storageKeysForCurrentId);
         this.replacerDataByControlFlowStorageId.set(controlFlowStorageId, storageKeysById);
         controlFlowStorage.set(storageKey, customNode);
 

+ 1 - 1
src/node-transformers/control-flow-transformers/control-flow-replacers/CallExpressionControlFlowReplacer.ts

@@ -66,7 +66,7 @@ export class CallExpressionControlFlowReplacer extends AbstractControlFlowReplac
             return callExpressionNode;
         }
 
-        const replacerId: string = String(callExpressionNode.arguments.length);
+        const replacerId: number = callExpressionNode.arguments.length;
         const callExpressionFunctionCustomNode: ICustomNode<TInitialData<CallExpressionFunctionNode>> =
             this.controlFlowCustomNodeFactory(ControlFlowCustomNode.CallExpressionFunctionNode);
         const expressionArguments: (ESTree.Expression | ESTree.SpreadElement)[] = callExpressionNode.arguments;

+ 1 - 1
src/node-transformers/control-flow-transformers/control-flow-replacers/StringArrayCallControlFlowReplacer.ts

@@ -74,7 +74,7 @@ export class StringArrayCallControlFlowReplacer extends AbstractControlFlowRepla
             return literalNode;
         }
 
-        const replacerId: string = String(literalNode.value);
+        const replacerId: string | number = literalNode.value;
         const literalCustomNode: ICustomNode<TInitialData<LiteralNode>> =
             this.controlFlowCustomNodeFactory(ControlFlowCustomNode.LiteralNode);
 

+ 1 - 1
src/node-transformers/control-flow-transformers/control-flow-replacers/StringLiteralControlFlowReplacer.ts

@@ -69,7 +69,7 @@ export class StringLiteralControlFlowReplacer extends AbstractControlFlowReplace
             return literalNode;
         }
 
-        const replacerId: string = String(literalNode.value);
+        const replacerId: string = literalNode.value;
         const literalCustomNode: ICustomNode<TInitialData<LiteralNode>> =
             this.controlFlowCustomNodeFactory(ControlFlowCustomNode.LiteralNode);
 

+ 26 - 78
src/node-transformers/rename-properties-transformers/RenamePropertiesTransformer.ts

@@ -14,7 +14,7 @@ import { RenamePropertiesMode } from '../../enums/node-transformers/rename-prope
 import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
 import { NodeGuards } from '../../node/NodeGuards';
 import { NodeLiteralUtils } from '../../node/NodeLiteralUtils';
-import { NodeUtils } from '../../node/NodeUtils';
+import { NodeMetadata } from '../../node/NodeMetadata';
 
 @injectable()
 export class RenamePropertiesTransformer extends AbstractNodeTransformer {
@@ -48,7 +48,7 @@ export class RenamePropertiesTransformer extends AbstractNodeTransformer {
             | ESTree.PropertyDefinition
             | ESTree.MemberExpression
             | ESTree.MethodDefinition
-    > (
+        > (
         propertyNode: TNode,
         propertyKeyNode: ESTree.Expression | ESTree.PrivateIdentifier
     ): propertyKeyNode is ESTree.Identifier | ESTree.Literal {
@@ -56,7 +56,7 @@ export class RenamePropertiesTransformer extends AbstractNodeTransformer {
             return false;
         }
 
-        return NodeGuards.isIdentifierNode(propertyKeyNode) || NodeGuards.isLiteralNode(propertyKeyNode);
+        return true;
     }
 
     /**
@@ -96,6 +96,15 @@ export class RenamePropertiesTransformer extends AbstractNodeTransformer {
         node: ESTree.Node,
         parentNode: ESTree.Node
     ): void {
+        if ((NodeGuards.isPropertyNode(parentNode) && parentNode.key === node)
+            || NodeGuards.isMemberExpressionNode(parentNode) && parentNode.property === node
+            || NodeGuards.isMethodDefinitionNode(parentNode) && parentNode.key === node
+            || NodeGuards.isPropertyDefinitionNode(parentNode) && parentNode.key === node) {
+            NodeMetadata.set(node, {propertyKeyToRenameNode: true});
+
+            return;
+        }
+
         if (this.options.renamePropertiesMode === RenamePropertiesMode.Safe) {
             this.analyzeAutoExcludedPropertyNames(node, parentNode);
         }
@@ -107,87 +116,35 @@ 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)) {
-            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 (propertyNode) {
-            NodeUtils.parentizeNode(propertyNode, parentNode);
+        if (!NodeGuards.isIdentifierNode(node) && !NodeGuards.isLiteralNode(node)) {
+            return node;
         }
 
-        return node;
-    }
-
-    /**
-     * @param {Property} propertyNode
-     * @returns {Property}
-     */
-    private transformPropertyNode (propertyNode: ESTree.Property): ESTree.Property {
-        const propertyKeyNode: ESTree.Expression | ESTree.PrivateIdentifier = propertyNode.key;
-
-        if (RenamePropertiesTransformer.isValidPropertyNode(propertyNode, propertyKeyNode)) {
-            propertyNode.key = this.renamePropertiesReplacer.replace(propertyKeyNode);
-            propertyNode.shorthand = false;
-        }
-
-        return propertyNode;
-    }
-
-    /**
-     * @param {PropertyDefinition} propertyNode
-     * @returns {PropertyDefinition}
-     */
-    private transformPropertyDefinitionNode (propertyNode: ESTree.PropertyDefinition): ESTree.PropertyDefinition {
-        const propertyKeyNode: ESTree.Expression | ESTree.PrivateIdentifier = propertyNode.key;
-
-        if (RenamePropertiesTransformer.isValidPropertyNode(propertyNode, propertyKeyNode)) {
-            propertyNode.key = this.renamePropertiesReplacer.replace(propertyKeyNode);
+        if (!NodeMetadata.isPropertyKeyToRenameNode(node)) {
+            return node;
         }
 
-        return propertyNode;
-    }
+        const isPropertyNode = NodeGuards.isPropertyNode(parentNode);
+        const isPropertyLikeNode = isPropertyNode
+            || NodeGuards.isPropertyDefinitionNode(parentNode)
+            || NodeGuards.isMemberExpressionNode(parentNode)
+            || NodeGuards.isMethodDefinitionNode(parentNode);
 
-    /**
-     * @param {Property} memberExpressionNode
-     * @returns {Property}
-     */
-    private transformMemberExpressionNode (memberExpressionNode: ESTree.MemberExpression): ESTree.MemberExpression {
-        const propertyKeyNode: ESTree.Expression | ESTree.PrivateIdentifier = memberExpressionNode.property;
-
-        if (RenamePropertiesTransformer.isValidPropertyNode(memberExpressionNode, propertyKeyNode)) {
-            memberExpressionNode.property = this.renamePropertiesReplacer.replace(propertyKeyNode);
+        if (isPropertyLikeNode && !RenamePropertiesTransformer.isValidPropertyNode(parentNode, node)) {
+            return node;
         }
 
-        return memberExpressionNode;
-    }
-
-    /**
-     * @param {MethodDefinition} methodDefinitionNode
-     * @returns {MethodDefinition}
-     */
-    private transformMethodDefinitionNode (methodDefinitionNode: ESTree.MethodDefinition): ESTree.MethodDefinition {
-        const propertyKeyNode: ESTree.Expression | ESTree.PrivateIdentifier = methodDefinitionNode.key;
-
-        if (RenamePropertiesTransformer.isValidPropertyNode(methodDefinitionNode, propertyKeyNode)) {
-            methodDefinitionNode.key = this.renamePropertiesReplacer.replace(propertyKeyNode);
+        if (isPropertyNode) {
+            parentNode.shorthand = false;
         }
 
-        return methodDefinitionNode;
+        return this.renamePropertiesReplacer.replace(node);
     }
 
     /**
      * @param {Node} node
      * @param {Node} parentNode
      */
-    // eslint-disable-next-line complexity
     private analyzeAutoExcludedPropertyNames (
         node: ESTree.Node,
         parentNode: ESTree.Node
@@ -196,15 +153,6 @@ export class RenamePropertiesTransformer extends AbstractNodeTransformer {
             return;
         }
 
-        if (
-            (NodeGuards.isPropertyNode(parentNode) && parentNode.key === node)
-            || NodeGuards.isMemberExpressionNode(parentNode) && parentNode.property === node
-            || NodeGuards.isMethodDefinitionNode(parentNode) && parentNode.key === node
-            || NodeGuards.isPropertyDefinitionNode(parentNode) && parentNode.key === node
-        ) {
-            return;
-        }
-
         this.renamePropertiesReplacer.excludePropertyName(node.value);
     }
 }

+ 11 - 0
src/node/NodeMetadata.ts

@@ -39,6 +39,17 @@ export class NodeMetadata {
         return NodeMetadata.get<ESTree.BaseNodeMetadata, 'ignoredNode'>(node, 'ignoredNode') === true;
     }
 
+    /**
+     * @param {Identifier | Literal} node
+     * @returns {boolean}
+     */
+    public static isPropertyKeyToRenameNode (node: ESTree.Identifier | ESTree.Literal): boolean {
+        return NodeMetadata.get<ESTree.IdentifierNodeMetadata | ESTree.LiteralNodeMetadata, 'propertyKeyToRenameNode'>(
+            node,
+            'propertyKeyToRenameNode'
+        ) === true;
+    }
+
     /**
      * @param {Node} literalNode
      * @returns {boolean}

+ 25 - 42
test/dev/dev.ts

@@ -1,55 +1,38 @@
 'use strict';
 
-import { StringArrayWrappersType } from '../../src/enums/node-transformers/string-array-transformers/StringArrayWrappersType';
-
 (function () {
     const JavaScriptObfuscator: any = require('../../index');
 
     let obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
         `
-            function foo () {
-                function bar() {
-                    var string1 = 'string1';
-                    var string2 = 'string2';
-                    var string3 = 'string3';
-                    var string4 = 'string4';
-                    var string5 = 'string5';
-                    var string6 = 'string6';
-                    
-                    function bark () {
-                        var string1 = 'string1';
-                        var string2 = 'string2';
-                        var string3 = 'string3';
-                        var string4 = 'string4';
-                        var string5 = 'string5';
-                        var string6 = 'string6';
-                    }
-                }
-                
-                bar()
+            var obj = {
+                foo: 1
             }
-            
-            console.log(foo());
         `,
         {
-            identifierNamesGenerator: 'mangled',
-            compact: false,
-            controlFlowFlattening: false,
-            controlFlowFlatteningThreshold: 1,
-            simplify: false,
-            stringArrayRotate: false,
-            stringArray: true,
-            stringArrayIndexesType: [
-                'hexadecimal-number',
-                'hexadecimal-numeric-string'
-            ],
-            stringArrayThreshold: 1,
-            stringArrayCallsTransform: true,
-            stringArrayCallsTransformThreshold: 1,
-            rotateStringArray: true,
-            stringArrayWrappersType: StringArrayWrappersType.Function,
-            transformObjectKeys: false,
-            seed: 1
+            "compact": false,
+            "controlFlowFlattening": true,
+            "controlFlowFlatteningThreshold": 1,
+            "disableConsoleOutput": false,
+            "identifierNamesGenerator": "mangled",
+            "log": true,
+            "numbersToExpressions": true,
+            "renameProperties": true,
+            "renamePropertiesMode": "safe",
+            "simplify": false,
+            "stringArray": true,
+            "stringArrayCallsTransform": true,
+            "stringArrayIndexShift": true,
+            "stringArrayRotate": false,
+            "stringArrayShuffle": false,
+            "stringArrayWrappersCount": 5,
+            "stringArrayWrappersChainedCalls": true,
+            "stringArrayWrappersParametersMaxCount": 5,
+            "stringArrayWrappersType": "function",
+            "stringArrayThreshold": 0,
+            "transformObjectKeys": true,
+            "unicodeEscapeSequence": false,
+            "ignoreRequireImports": false
         }
     ).getObfuscatedCode();
 

+ 59 - 25
test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/string-litertal-control-flow-replacer/StringLiteralControlFlowReplacer.spec.ts

@@ -3,6 +3,7 @@ import { assert } from 'chai';
 import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../../src/options/presets/NoCustomNodes';
 
 import { readFileAsString } from '../../../../../helpers/readFileAsString';
+import { getRegExpMatch } from '../../../../../helpers/getRegExpMatch';
 
 import { JavaScriptObfuscator } from '../../../../../../src/JavaScriptObfuscatorFacade';
 
@@ -10,34 +11,67 @@ describe('StringLiteralControlFlowReplacer', () => {
     describe('replace', () => {
         const variableMatch: string = '_0x([a-f0-9]){4,6}';
 
-        const controlFlowStorageStringLiteralRegExp: RegExp = new RegExp(
-            `var ${variableMatch} *= *\\{'\\w{5}' *: *'test'\\};`
-        );
-        const controlFlowStorageCallRegExp: RegExp = new RegExp(
-            `var ${variableMatch} *= *${variableMatch}\\['\\w{5}'\\];`
-        );
-
-        let obfuscatedCode: string;
-
-        before(() => {
-            const code: string = readFileAsString(__dirname + '/fixtures/input-1.js');
-
-            obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                code,
-                {
-                    ...NO_ADDITIONAL_NODES_PRESET,
-                    controlFlowFlattening: true,
-                    controlFlowFlatteningThreshold: 1
-                }
-            ).getObfuscatedCode();
-        });
+        describe('Variant #1 - base behavior', () => {
+
+            const controlFlowStorageStringLiteralRegExp: RegExp = new RegExp(
+                `var ${variableMatch} *= *\\{'\\w{5}' *: *'test'\\};`
+            );
+            const controlFlowStorageCallRegExp: RegExp = new RegExp(
+                `var ${variableMatch} *= *${variableMatch}\\['\\w{5}'\\];`
+            );
+
+            let obfuscatedCode: string;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/input-1.js');
+
+                obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                    code,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        controlFlowFlattening: true,
+                        controlFlowFlatteningThreshold: 1
+                    }
+                ).getObfuscatedCode();
+            });
+
+            it('should add string literal node as property of control flow storage node', () => {
+                assert.match(obfuscatedCode, controlFlowStorageStringLiteralRegExp);
+            });
 
-        it('should add string literal node as property of control flow storage node', () => {
-            assert.match(obfuscatedCode, controlFlowStorageStringLiteralRegExp);
+            it('should replace string literal node with call to control flow storage node', () => {
+                assert.match(obfuscatedCode, controlFlowStorageCallRegExp);
+            });
         });
 
-        it('should replace string literal node with call to control flow storage node', () => {
-            assert.match(obfuscatedCode, controlFlowStorageCallRegExp);
+        describe('Variant #2 - same storage key for same string values', () => {
+            const storageKeyRegExp: RegExp = /'(\w{5})': 'value'/;
+            const expectedStorageCallsMatchesCount: number = 5;
+
+            let storageCallsMatchesCount: number;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/same-storage-key-for-same-string-values.js');
+
+                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                    code,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        compact: false,
+                        controlFlowFlattening: true,
+                        controlFlowFlatteningThreshold: 1
+                    }
+                ).getObfuscatedCode();
+
+                const storageKeyMatch = getRegExpMatch(obfuscatedCode, storageKeyRegExp);
+                const storageCallsRegExp = new RegExp(`${variableMatch}\\[\'${storageKeyMatch}\']`, 'g')
+
+                storageCallsMatchesCount = obfuscatedCode.match(storageCallsRegExp)?.length ?? 0;
+            });
+
+            it('should add string literal nodes with same values under same storage item', () => {
+                assert.equal(storageCallsMatchesCount, expectedStorageCallsMatchesCount);
+            });
         });
     });
 });

+ 7 - 0
test/functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/string-litertal-control-flow-replacer/fixtures/same-storage-key-for-same-string-values.js

@@ -0,0 +1,7 @@
+(function () {
+    var string1 = 'value';
+    var string2 = 'value';
+    var string3 = 'value';
+    var string4 = 'value';
+    var string5 = 'value';
+})();

+ 64 - 0
test/functional-tests/node-transformers/rename-properties-transformers/rename-properties-transformer/RenamePropertiesTransformer.spec.ts

@@ -411,6 +411,70 @@ describe('RenamePropertiesTransformer', () => {
                         assert.match(obfuscatedCode, propertyRegExp);
                     });
                 });
+
+                describe('Variant #10: integration with `controlFlowFlattening` option', () => {
+                    const propertyRegExp: RegExp = new RegExp(
+                        'const b *= *{ *' +
+                            '\'\\w{5}\' *: *\'a\' *' +
+                        '}; *' +
+                        'const c *= *{' +
+                            '\'a\': *0x1' +
+                        '};' +
+                        'c\\[b\\[\'\\w{5}\']];'
+                    );
+
+                    let obfuscatedCode: string;
+
+                    before(() => {
+                        const code: string = readFileAsString(__dirname + '/fixtures/control-flow-flattening-integration.js');
+
+                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                            code,
+                            {
+                                ...NO_ADDITIONAL_NODES_PRESET,
+                                renameProperties: true,
+                                renamePropertiesMode: RenamePropertiesMode.Unsafe,
+                                identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
+                                controlFlowFlattening: true,
+                                controlFlowFlatteningThreshold: 1
+                            }
+                        ).getObfuscatedCode();
+                    });
+
+                    it('Should correctly rename property when `controlFlowFlattening` option is enabled', () => {
+                        assert.match(obfuscatedCode, propertyRegExp);
+                    });
+                });
+
+                describe('Variant #11: integration with `transformObjectKeys` option', () => {
+                    const propertyRegExp: RegExp = new RegExp(
+                        'const b *= *{}; *' +
+                        'b\\[\'a\'] *= *0x1;' +
+                        'const foo *= *b;' +
+                        'foo\\[\'a\'];'
+                    );
+
+                    let obfuscatedCode: string;
+
+                    before(() => {
+                        const code: string = readFileAsString(__dirname + '/fixtures/transform-object-keys-integration.js');
+
+                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                            code,
+                            {
+                                ...NO_ADDITIONAL_NODES_PRESET,
+                                renameProperties: true,
+                                renamePropertiesMode: RenamePropertiesMode.Unsafe,
+                                identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
+                                transformObjectKeys: true
+                            }
+                        ).getObfuscatedCode();
+                    });
+
+                    it('Should correctly rename property when `transformObjectKeys` option is enabled', () => {
+                        assert.match(obfuscatedCode, propertyRegExp);
+                    });
+                });
             });
 
             describe('Variant #3: Ignored literal node type', () => {

+ 6 - 0
test/functional-tests/node-transformers/rename-properties-transformers/rename-properties-transformer/fixtures/control-flow-flattening-integration.js

@@ -0,0 +1,6 @@
+(() => {
+    const foo = {
+        bar: 1
+    };
+    foo['bar'];
+})();

+ 4 - 0
test/functional-tests/node-transformers/rename-properties-transformers/rename-properties-transformer/fixtures/transform-object-keys-integration.js

@@ -0,0 +1,4 @@
+const foo = {
+    bar: 1
+};
+foo['bar'];

+ 9 - 5
test/functional-tests/node-transformers/string-array-transformers/string-array-rotate-function-transformer/StringArrayRotateFunctionTransformer.spec.ts

@@ -132,13 +132,14 @@ describe('StringArrayRotateFunctionTransformer', function () {
         });
 
         describe('Code evaluation', function () {
-            this.timeout(100000);
+            const samplesCount: number = 50;
+            const evaluationTimeout: number = 5000;
 
-            const samplesCount: number = 100;
+            this.timeout(samplesCount * evaluationTimeout);
 
             let hasRuntimeErrors: boolean = false;
 
-            before(() => {
+            before(async() => {
                 const code: string = readFileAsString(__dirname + '/fixtures/code-evaluation.js');
 
                 const obfuscateFunc = () => {
@@ -186,7 +187,10 @@ describe('StringArrayRotateFunctionTransformer', function () {
 
                 for (let i = 0; i < samplesCount; i++) {
                     try {
-                        const evaluationResult = eval(obfuscateFunc());
+                        const evaluationResult = await evaluateInWorker(
+                            obfuscateFunc(),
+                            evaluationTimeout
+                        );
 
                         if (evaluationResult !== 'fooooooo') {
                             hasRuntimeErrors = true;
@@ -205,7 +209,7 @@ describe('StringArrayRotateFunctionTransformer', function () {
         });
 
         describe('Prevent early successful comparison', () => {
-            const evaluationTimeout:  number = 1000;
+            const evaluationTimeout: number = 1000;
             const samplesCount: number = 100;
 
             let numberNumericalExpressionAnalyzerAnalyzeStub: sinon.SinonStub;

+ 18 - 0
test/unit-tests/node/node-metadata/NodeMetadata.spec.ts

@@ -86,6 +86,24 @@ describe('NodeMetadata', () => {
         });
     });
 
+    describe('propertyKeyToRenameNode', () => {
+        const expectedValue: boolean = true;
+
+        let node: ESTree.Identifier,
+            value: boolean | undefined;
+
+        before(() => {
+            node = NodeFactory.identifierNode('foo');
+            node.metadata = {};
+            node.metadata.propertyKeyToRenameNode = true;
+            value = NodeMetadata.isPropertyKeyToRenameNode(node);
+        });
+
+        it('should return metadata value', () => {
+            assert.equal(value, expectedValue);
+        });
+    });
+
     describe('isStringArrayCallLiteralNode', () => {
         const expectedValue: boolean = true;