├── .npmignore ├── src ├── __tests__ │ ├── files │ │ ├── es5.js │ │ ├── numberedLines.js │ │ └── hello.ts │ ├── getPositionOfSubstring.ts │ ├── extendFileExtension.test.ts │ ├── obfuscateCode.test.ts │ ├── __snapshots__ │ │ └── composeSourceMaps.test.ts.snap │ └── composeSourceMaps.test.ts ├── index.ts ├── extendFileExtension.ts ├── getCallerFile.ts ├── obfuscateCode.ts ├── composeSourceMaps.ts ├── obfuscatingTransformer.ts └── getMetroTransformer.ts ├── .vscode └── settings.json ├── tsconfig.json ├── LICENSE ├── .gitignore ├── typings └── javascript-obfuscator.d.ts ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !dist/**/* 3 | !README.md 4 | !LICENSE 5 | !typings/**/* 6 | 7 | -------------------------------------------------------------------------------- /src/__tests__/files/es5.js: -------------------------------------------------------------------------------- 1 | var y = "yes" 2 | module.exports = function banana() { 3 | return y 4 | } 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { obfuscatingTransformer } from "./obfuscatingTransformer" 2 | 3 | export = obfuscatingTransformer 4 | -------------------------------------------------------------------------------- /src/__tests__/files/numberedLines.js: -------------------------------------------------------------------------------- 1 | export const line1 = 1 2 | export const line2 = 2 3 | export const line3 = 3 4 | 5 | export const line5 = 5 6 | -------------------------------------------------------------------------------- /src/__tests__/files/hello.ts: -------------------------------------------------------------------------------- 1 | export interface Hello { 2 | readonly line2: string 3 | line3: number 4 | } 5 | 6 | console.log("line6") 7 | 8 | export const line8 = true 9 | -------------------------------------------------------------------------------- /src/extendFileExtension.ts: -------------------------------------------------------------------------------- 1 | export function extendFileExtension(filename: string, extensionPart: string) { 2 | const parts = filename.split(".") 3 | parts.splice(1, 0, extensionPart) 4 | return parts.join(".") 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "**/bower_components": true, 6 | "dist": true 7 | }, 8 | "editor.formatOnPaste": true, 9 | "prettier.semi": false, 10 | "prettier.trailingComma": "all", 11 | "editor.formatOnSave": true 12 | } 13 | -------------------------------------------------------------------------------- /src/__tests__/getPositionOfSubstring.ts: -------------------------------------------------------------------------------- 1 | export function getPositionOfSubstring( 2 | text: string, 3 | substring: string, 4 | ): { line: number; column: number } | null { 5 | const lines = text.split(/\r?\n/) 6 | 7 | for (let line = 0; line < lines.length; line++) { 8 | const column = lines[line].indexOf(substring) 9 | 10 | if (column >= 0) { 11 | return { line: line + 1, column } 12 | } 13 | } 14 | 15 | return null 16 | } 17 | -------------------------------------------------------------------------------- /src/__tests__/extendFileExtension.test.ts: -------------------------------------------------------------------------------- 1 | import { extendFileExtension } from "../extendFileExtension" 2 | 3 | describe("extendFileExtension", () => { 4 | it("adds an extension part", () => { 5 | expect(extendFileExtension("blah.js", "ext")).toBe("blah.ext.js") 6 | expect(extendFileExtension("blah.tsx", "obfuscated")).toBe( 7 | "blah.obfuscated.tsx", 8 | ) 9 | expect(extendFileExtension("blah", "js")).toBe("blah.js") 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/__tests__/obfuscateCode.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | 3 | jest.mock("javascript-obfuscator", () => ({ 4 | obfuscate() { 5 | return { 6 | getObfuscatedCode() { 7 | return "this code is obfuscated" 8 | }, 9 | getSourceMap() { 10 | return "" 11 | }, 12 | } 13 | }, 14 | })) 15 | 16 | import { obfuscateCode } from "../obfuscateCode" 17 | 18 | describe("obfuscateCode", () => { 19 | it("obfuscates code", () => { 20 | const filename = require.resolve("./files/es5.js") 21 | const es5code = fs.readFileSync(filename).toString() 22 | expect(obfuscateCode(es5code, {})).toBe("this code is obfuscated") 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2015" 7 | ], 8 | "strict": true, 9 | "outDir": "dist", 10 | "moduleResolution": "node", 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "declaration": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "javascript-obfuscator": [ 17 | "typings/javascript-obfuscator.d.ts" 18 | ] 19 | }, 20 | "experimentalDecorators": true, 21 | }, 22 | "include": [ 23 | "typings", 24 | "src" 25 | ], 26 | "exclude": [ 27 | "node_modules", 28 | "src/__test__/**/*" 29 | ], 30 | "compileOnSave": true 31 | } 32 | -------------------------------------------------------------------------------- /src/getCallerFile.ts: -------------------------------------------------------------------------------- 1 | // This code taken from https://stackoverflow.com/a/29581862/1382997 2 | // Question by einstein https://stackoverflow.com/users/449132/einstein 3 | // Answer by Rhionin https://stackoverflow.com/users/1751376/rhionin 4 | export function getCallerFile() { 5 | const originalFunc = Error.prepareStackTrace 6 | 7 | let callerfile 8 | 9 | try { 10 | const err = new Error() as any 11 | var currentfile 12 | 13 | Error.prepareStackTrace = function(_, stack) { 14 | return stack 15 | } 16 | 17 | currentfile = err.stack.shift().getFileName() 18 | 19 | // go up two places in the stack trace. i.e. out of this file, and out of index.ts 20 | for (let i = 0; i < 2; i++) { 21 | while (err.stack.length) { 22 | callerfile = err.stack.shift().getFileName() 23 | 24 | if (currentfile !== callerfile) break 25 | } 26 | 27 | currentfile = callerfile 28 | } 29 | } catch (e) {} 30 | 31 | Error.prepareStackTrace = originalFunc 32 | 33 | return callerfile 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 David Sheldrick 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional eslint cache 43 | .eslintcache 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | # Output of 'npm pack' 49 | *.tgz 50 | 51 | # Yarn Integrity file 52 | .yarn-integrity 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | dist 58 | package 59 | *.tar.gz 60 | 61 | # IntelliJ 62 | .idea 63 | -------------------------------------------------------------------------------- /typings/javascript-obfuscator.d.ts: -------------------------------------------------------------------------------- 1 | declare module "javascript-obfuscator" { 2 | namespace JavaScriptObfuscator { 3 | interface Options { 4 | compact?: boolean 5 | controlFlowFlattening?: boolean 6 | controlFlowFlatteningThreshold?: 0.75 7 | deadCodeInjection?: boolean 8 | deadCodeInjectionThreshold?: 0.4 9 | debugProtection?: boolean 10 | debugProtectionInterval?: boolean 11 | disableConsoleOutput?: boolean 12 | domainLock?: string[] 13 | identifierNamesGenerator?: "hexadecimal" | "mangled" 14 | log?: boolean 15 | renameGlobals?: boolean 16 | reservedNames?: string[] 17 | rotateStringArray?: true 18 | seed?: 0 19 | selfDefending?: boolean 20 | sourceMap?: boolean 21 | sourceMapBaseUrl?: string 22 | sourceMapFileName?: string 23 | sourceMapMode?: "separate" | "inline" 24 | stringArray?: boolean 25 | stringArrayEncoding?: string[] 26 | stringArrayThreshold?: 0.75 27 | target?: "browser" | "extension" | "node" 28 | unicodeEscapeSequence?: boolean 29 | } 30 | 31 | function obfuscate( 32 | sourceCode: string, 33 | options: Options, 34 | ): { 35 | getObfuscatedCode(): string 36 | getSourceMap(): string | null 37 | } 38 | } 39 | 40 | export = JavaScriptObfuscator 41 | } 42 | -------------------------------------------------------------------------------- /src/obfuscateCode.ts: -------------------------------------------------------------------------------- 1 | import * as Obfuscator from "javascript-obfuscator" 2 | import { 3 | convertMetroRawSourceMapToStandardSourceMap, 4 | composeSourceMaps, 5 | MetroRawSourceMap, 6 | } from "./composeSourceMaps" 7 | import { RawSourceMap } from "source-map/source-map" 8 | 9 | export function obfuscateCode( 10 | code: string, 11 | options: Obfuscator.Options, 12 | ): string { 13 | return Obfuscator.obfuscate(code, options).getObfuscatedCode() 14 | } 15 | 16 | export function obfuscateCodePreservingSourceMap( 17 | code: string, 18 | map: string | RawSourceMap | MetroRawSourceMap, 19 | originlFilename: string, 20 | originalSource: string, 21 | options: Obfuscator.Options, 22 | ): { code: string; map: string } { 23 | const obfuscationResult = Obfuscator.obfuscate(code, options) 24 | const obfuscationResultMap = obfuscationResult.getSourceMap() 25 | 26 | if (!obfuscationResultMap) { 27 | throw new Error( 28 | "javascript-obfuscator did not return a source map for file " + 29 | originlFilename, 30 | ) 31 | } 32 | 33 | if (Array.isArray(map)) { 34 | map = convertMetroRawSourceMapToStandardSourceMap( 35 | map, 36 | originlFilename, 37 | originalSource, 38 | ) 39 | } 40 | 41 | return { 42 | code: obfuscationResult.getObfuscatedCode(), 43 | map: composeSourceMaps( 44 | map, 45 | obfuscationResultMap, 46 | originlFilename, 47 | originalSource, 48 | ), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-obfuscating-transformer", 3 | "version": "1.0.0", 4 | "description": "Obfuscating transformer for react-native", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/ds300/react-native-obfuscating-transformer", 7 | "author": "David Sheldrick", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@types/app-root-path": "^1.2.4", 11 | "@types/babel__core": "^7.1.14", 12 | "@types/babel__generator": "^7.6.2", 13 | "@types/node": "^14.14.37", 14 | "@types/semver": "^7.3.4", 15 | "app-root-path": "^2.0.1", 16 | "@babel/generator": "^7.13.9", 17 | "@babel/traverse": "^7.13.13", 18 | "@babel/parser": "^7.13.13", 19 | "javascript-obfuscator": "^2.11.0", 20 | "jju": "^1.3.0", 21 | "semver": "^5.4.1", 22 | "source-map": "^0.6.1" 23 | }, 24 | "peerDependencies": { 25 | "react-native": ">=0.45.0" 26 | }, 27 | "scripts": { 28 | "build": "rm -rf dist && tsc", 29 | "precommit": "tsc && lint-staged", 30 | "test": "jest" 31 | }, 32 | "typings": "dist/index.d.ts", 33 | "lint-staged": { 34 | "**/*.ts": [ 35 | "prettier --no-semi --trailing-comma all" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "@types/jest": "^22.0.1", 40 | "babel-preset-react-native": "^4.0.1", 41 | "husky": "^0.13.3", 42 | "jest": "^26.0.22", 43 | "lint-staged": "^3.4.1", 44 | "metro": "^0.65.2", 45 | "metro-bundler": "^0.22.1", 46 | "np": "^4.0.2", 47 | "prettier": "^1.10.2", 48 | "react-native": "^0.64.0", 49 | "ts-jest": "^22.0.1", 50 | "ts-node": "^4.1.0", 51 | "typescript": "^4.0.7" 52 | }, 53 | "jest": { 54 | "transform": { 55 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 56 | }, 57 | "transformIgnorePatterns": [ 58 | "node_modules/(?!(javascript-obfuscator)/)" 59 | ], 60 | "moduleNameMapper": { 61 | "javascript-obfuscator": "/node_modules/javascript-obfuscator/index.ts" 62 | }, 63 | "testRegex": "src/.*(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 64 | "moduleFileExtensions": [ 65 | "ts", 66 | "tsx", 67 | "js", 68 | "json" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/composeSourceMaps.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`composeSourceMaps composes two source maps together 1`] = ` 4 | "console.log(\\"line6\\"); 5 | export const line8 = true; 6 | //# sourceMappingURL=hello.js.map" 7 | `; 8 | 9 | exports[`composeSourceMaps composes two source maps together 2`] = ` 10 | "Object.defineProperty(exports, \\"__esModule\\", { 11 | value: true 12 | }); 13 | console.log(\\"line6\\"); 14 | var line8 = exports.line8 = true;" 15 | `; 16 | 17 | exports[`convertMetroRawSourceMapToStandardSourceMap takes a raw source map and converts it to a non-raw source map 1`] = ` 18 | Array [ 19 | Array [ 20 | 4, 21 | 0, 22 | 1, 23 | 7, 24 | ], 25 | Array [ 26 | 4, 27 | 4, 28 | 1, 29 | 13, 30 | "line1", 31 | ], 32 | Array [ 33 | 4, 34 | 28, 35 | 1, 36 | 21, 37 | ], 38 | Array [ 39 | 4, 40 | 29, 41 | 1, 42 | 7, 43 | ], 44 | Array [ 45 | 5, 46 | 0, 47 | 2, 48 | 7, 49 | ], 50 | Array [ 51 | 5, 52 | 4, 53 | 2, 54 | 13, 55 | "line2", 56 | ], 57 | Array [ 58 | 5, 59 | 28, 60 | 2, 61 | 21, 62 | ], 63 | Array [ 64 | 5, 65 | 29, 66 | 2, 67 | 7, 68 | ], 69 | Array [ 70 | 6, 71 | 0, 72 | 3, 73 | 7, 74 | ], 75 | Array [ 76 | 6, 77 | 4, 78 | 3, 79 | 13, 80 | "line3", 81 | ], 82 | Array [ 83 | 6, 84 | 28, 85 | 3, 86 | 21, 87 | ], 88 | Array [ 89 | 6, 90 | 29, 91 | 3, 92 | 7, 93 | ], 94 | Array [ 95 | 8, 96 | 0, 97 | 5, 98 | 7, 99 | ], 100 | Array [ 101 | 8, 102 | 4, 103 | 5, 104 | 13, 105 | "line5", 106 | ], 107 | Array [ 108 | 8, 109 | 28, 110 | 5, 111 | 21, 112 | ], 113 | Array [ 114 | 8, 115 | 29, 116 | 5, 117 | 7, 118 | ], 119 | ] 120 | `; 121 | 122 | exports[`convertMetroRawSourceMapToStandardSourceMap takes a raw source map and converts it to a non-raw source map 2`] = `"{\\"version\\":3,\\"sources\\":[\\"/Users/dshe/code/react-native-obfuscating-transformer/src/__tests__/numberedLines.js\\"],\\"names\\":[\\"line1\\",\\"line2\\",\\"line3\\",\\"line5\\"],\\"mappings\\":\\";;;AAAO,IAAMA,wBAAQ,CAAd;AACA,IAAMC,wBAAQ,CAAd;AACA,IAAMC,wBAAQ,CAAd;;AAEA,IAAMC,wBAAQ,CAAd\\",\\"sourcesContent\\":[\\"export const line1 = 1\\\\nexport const line2 = 2\\\\nexport const line3 = 3\\\\n\\\\nexport const line5 = 5\\\\n\\"]}"`; 123 | -------------------------------------------------------------------------------- /src/composeSourceMaps.ts: -------------------------------------------------------------------------------- 1 | import { SourceMapConsumer, SourceMapGenerator, RawSourceMap } from "source-map" 2 | 3 | export type MetroRawSourceMap = Array< 4 | [number, number, number, number, string | undefined] 5 | > 6 | 7 | export function convertMetroRawSourceMapToStandardSourceMap( 8 | map: MetroRawSourceMap, 9 | originalFileName: string, 10 | originalFileContent: string, 11 | ): string { 12 | const outputMap = new SourceMapGenerator() 13 | 14 | outputMap.setSourceContent(originalFileName, originalFileContent) 15 | 16 | map.forEach(args => { 17 | const [generatedLine, generatedColumn, originalLine, originalColumn] = args 18 | outputMap.addMapping({ 19 | generated: { 20 | line: generatedLine, 21 | column: generatedColumn, 22 | }, 23 | original: { 24 | line: originalLine, 25 | column: originalColumn, 26 | }, 27 | source: originalFileName, 28 | name: args.length === 5 ? (args[4] as string) : undefined, 29 | }) 30 | }) 31 | 32 | return outputMap.toString() 33 | } 34 | 35 | export function convertStandardSourceMapToMetroRawSourceMap( 36 | map: RawSourceMap | string, 37 | ) { 38 | const consumer = new SourceMapConsumer(map as any) // upstream types are wrong 39 | 40 | const outputMap: MetroRawSourceMap = [] 41 | 42 | consumer.eachMapping(mapping => { 43 | outputMap.push([ 44 | mapping.generatedLine, 45 | mapping.generatedColumn, 46 | mapping.originalLine, 47 | mapping.originalColumn, 48 | mapping.name, 49 | ]) 50 | }) 51 | 52 | return outputMap 53 | } 54 | 55 | export function composeSourceMaps( 56 | sourceMap: string | RawSourceMap, 57 | targetMap: string | RawSourceMap, 58 | sourceFileName: string, 59 | sourceContent: string, 60 | ) { 61 | const tsConsumer = new SourceMapConsumer(sourceMap as any) // upstreeam types are wrong 62 | const babelConsumer = new SourceMapConsumer(targetMap as any) 63 | const map = new SourceMapGenerator() 64 | map.setSourceContent(sourceFileName, sourceContent) 65 | babelConsumer.eachMapping( 66 | ({ 67 | generatedLine, 68 | generatedColumn, 69 | originalLine, 70 | originalColumn, 71 | name, 72 | }) => { 73 | if (originalLine) { 74 | const original = tsConsumer.originalPositionFor({ 75 | line: originalLine, 76 | column: originalColumn, 77 | }) 78 | if (original.line) { 79 | map.addMapping({ 80 | generated: { 81 | line: generatedLine, 82 | column: generatedColumn, 83 | }, 84 | original: { 85 | line: original.line, 86 | column: original.column, 87 | }, 88 | source: sourceFileName, 89 | name: name, 90 | }) 91 | } 92 | } 93 | }, 94 | ) 95 | return map.toString() 96 | } 97 | -------------------------------------------------------------------------------- /src/__tests__/composeSourceMaps.test.ts: -------------------------------------------------------------------------------- 1 | import { getMetroTransformer } from "../getMetroTransformer" 2 | import { 3 | convertMetroRawSourceMapToStandardSourceMap, 4 | MetroRawSourceMap, 5 | composeSourceMaps, 6 | } from "../composeSourceMaps" 7 | import * as fs from "fs" 8 | import * as path from "path" 9 | import { SourceMapConsumer } from "source-map/source-map" 10 | import { getPositionOfSubstring } from "./getPositionOfSubstring" 11 | import * as ts from "typescript" 12 | 13 | const numberedLines = fs 14 | .readFileSync(require.resolve("./files/numberedLines.js")) 15 | .toString() 16 | 17 | describe("convertMetroRawSourceMapToStandardSourceMap", () => { 18 | it("takes a raw source map and converts it to a non-raw source map", () => { 19 | const transformer = getMetroTransformer(47) 20 | 21 | const { map, code } = transformer.transform({ 22 | filename: require.resolve("./files/numberedLines.js"), 23 | src: numberedLines, 24 | options: { 25 | retainLines: false, 26 | }, 27 | }) 28 | 29 | if (typeof code !== "string") { 30 | // use this rather than expect for typescript's sake 31 | throw new Error("code must be a string") 32 | } 33 | 34 | expect(Array.isArray(map)).toBe(true) 35 | expect(map).toMatchSnapshot() 36 | 37 | const standardMap = convertMetroRawSourceMapToStandardSourceMap( 38 | map as MetroRawSourceMap, 39 | path.join(__dirname, "numberedLines.js"), 40 | numberedLines, 41 | ) 42 | 43 | const standardMapConsumer = new SourceMapConsumer(standardMap as any) // upstream types are wrong 44 | 45 | for (const substring of ["line1", "line2", "line3", "line5"]) 46 | expect( 47 | standardMapConsumer.originalPositionFor( 48 | getPositionOfSubstring(code, substring)!, 49 | ), 50 | ).toMatchObject(getPositionOfSubstring(numberedLines, substring)!) 51 | 52 | expect(standardMap).toMatchSnapshot() 53 | }) 54 | }) 55 | 56 | describe("composeSourceMaps", () => { 57 | it("composes two source maps together", () => { 58 | const filename = require.resolve("./files/hello.ts") 59 | const hello = fs.readFileSync(filename).toString() 60 | 61 | const tsTranspileResult = ts.transpileModule(hello, { 62 | fileName: filename, 63 | compilerOptions: { 64 | sourceMap: true, 65 | target: ts.ScriptTarget.ES2015, 66 | }, 67 | }) 68 | 69 | expect(tsTranspileResult.outputText).toMatchSnapshot() 70 | 71 | const upstreamTransformResult = getMetroTransformer(47).transform({ 72 | filename, 73 | src: tsTranspileResult.outputText, 74 | options: { 75 | retainLines: false, 76 | }, 77 | }) 78 | 79 | expect(upstreamTransformResult.code).toMatchSnapshot() 80 | 81 | const composedMap = new SourceMapConsumer(composeSourceMaps( 82 | tsTranspileResult.sourceMapText!, 83 | convertMetroRawSourceMapToStandardSourceMap( 84 | upstreamTransformResult.map as MetroRawSourceMap, 85 | filename, 86 | hello, 87 | ), 88 | filename, 89 | hello, 90 | ) as any) // upstream types are wrong 91 | 92 | expect( 93 | composedMap.originalPositionFor( 94 | getPositionOfSubstring(upstreamTransformResult.code!, "line6")!, 95 | ), 96 | ).toMatchObject({ line: 6 }) 97 | 98 | expect( 99 | composedMap.originalPositionFor( 100 | getPositionOfSubstring(upstreamTransformResult.code!, "line8")!, 101 | ), 102 | ).toMatchObject({ line: 8 }) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /src/obfuscatingTransformer.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto" 2 | import * as fs from "fs" 3 | import * as path from "path" 4 | import * as JavaScriptObfuscator from "javascript-obfuscator" 5 | import { path as appRootPath } from "app-root-path" 6 | 7 | import { getCallerFile } from "./getCallerFile" 8 | import { 9 | MetroTransformer, 10 | generateAndConvert, 11 | getMetroTransformer, 12 | MetroTransformerResult, 13 | maybeTransformMetroResult, 14 | } from "./getMetroTransformer" 15 | import { 16 | obfuscateCode, 17 | obfuscateCodePreservingSourceMap, 18 | } from "./obfuscateCode" 19 | import { extendFileExtension } from "./extendFileExtension" 20 | 21 | function getOwnCacheKey(upstreamCacheKey: string, configFilename: string) { 22 | var key = crypto.createHash("md5") 23 | key.update(upstreamCacheKey) 24 | key.update(fs.readFileSync(__filename)) 25 | key.update(fs.readFileSync(configFilename)) 26 | return key.digest("hex") 27 | } 28 | 29 | export interface ObfuscatingTransformerOptions { 30 | filter?(filename: string, source: string): boolean 31 | upstreamTransformer?: MetroTransformer 32 | obfuscatorOptions?: JavaScriptObfuscator.Options 33 | trace?: boolean 34 | emitObfuscatedFiles?: boolean 35 | enableInDevelopment?: boolean 36 | } 37 | 38 | const sourceDir = path.join(appRootPath, "src") 39 | 40 | export function obfuscatingTransformer({ 41 | filter = filename => filename.startsWith(sourceDir), 42 | upstreamTransformer = getMetroTransformer(), 43 | obfuscatorOptions: _obfuscatorOptions, 44 | ...otherOptions 45 | }: ObfuscatingTransformerOptions): MetroTransformer { 46 | const callerFilename = getCallerFile() 47 | 48 | const obfuscatorOptions: JavaScriptObfuscator.Options = { 49 | ..._obfuscatorOptions, 50 | sourceMap: true, 51 | sourceMapMode: "separate", 52 | stringArray: false, 53 | } 54 | 55 | return { 56 | transform(props) { 57 | const result = upstreamTransformer.transform(props) 58 | 59 | if (props.options.dev && !otherOptions.enableInDevelopment) { 60 | return result 61 | } 62 | 63 | const resultCanBeObfuscated = result.code || result.ast 64 | 65 | if (resultCanBeObfuscated && filter(props.filename, props.src)) { 66 | if (otherOptions.trace) { 67 | console.log("Obfuscating", props.filename) 68 | } 69 | 70 | const { code, map }: MetroTransformerResult = result.code 71 | ? result 72 | : result.ast 73 | ? generateAndConvert(result.ast, props.filename) 74 | : { code: "", map: "" } 75 | 76 | if (!code) { 77 | return result 78 | } else if (!map) { 79 | return { 80 | code: obfuscateCode(code, obfuscatorOptions), 81 | } 82 | } 83 | 84 | if (otherOptions.emitObfuscatedFiles) { 85 | const emitDir = path.dirname(props.filename) 86 | const filename = extendFileExtension( 87 | path.basename(props.filename), 88 | "obfuscated", 89 | ) 90 | fs.writeFileSync(path.join(emitDir, filename), code) 91 | } 92 | 93 | return maybeTransformMetroResult( 94 | result, 95 | obfuscateCodePreservingSourceMap( 96 | code, 97 | map, 98 | props.filename, 99 | props.src, 100 | obfuscatorOptions, 101 | ), 102 | ) 103 | } 104 | 105 | return result 106 | }, 107 | 108 | getCacheKey() { 109 | return getOwnCacheKey( 110 | upstreamTransformer.getCacheKey 111 | ? upstreamTransformer.getCacheKey() 112 | : "", 113 | callerFilename, 114 | ) 115 | }, 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-obfuscating-transformer 2 | 3 | Obfuscate selected source files when building for React Native. 4 | 5 | ## Installation 6 | 7 | yarn add react-native-obfuscating-transformer --dev 8 | 9 | or 10 | 11 | npm install react-native-obfuscating-transformer --save-dev 12 | 13 | ## Usage 14 | 15 | ### React Native >= 0.59 16 | 17 | #### /metro.config.js 18 | 19 | ```diff 20 | module.exports = { 21 | + transformer: { 22 | + babelTransformerPath: require.resolve("./transformer") 23 | + }, 24 | } 25 | ``` 26 | 27 | #### /transformer.js 28 | 29 | ```js 30 | const obfuscatingTransformer = require("react-native-obfuscating-transformer") 31 | 32 | module.exports = obfuscatingTransformer({ 33 | /* options */ 34 | }) 35 | ``` 36 | 37 | ### React Native < 0.59 38 | 39 | ### /rn-cli.config.js 40 | 41 | ```diff 42 | module.exports = { 43 | + transformer: { 44 | + babelTransformerPath: require.resolve("./transformer") 45 | + }, 46 | } 47 | ``` 48 | 49 | #### /transformer.js 50 | 51 | ```js 52 | const obfuscatingTransformer = require("react-native-obfuscating-transformer") 53 | 54 | module.exports = obfuscatingTransformer({ 55 | /* options */ 56 | }) 57 | ``` 58 | 59 | ## Configuration 60 | 61 | Options are: 62 | 63 | ### `upstreamTransformer: MetroTransformer` 64 | 65 | Defines what the first pass of code transformation is. If you don't use a custom transformer already, 66 | you don't need to set this option. 67 | 68 | TypeScript example: 69 | 70 | ```diff 71 | const obfuscatingTransformer = require('react-native-obfuscating-transformer') 72 | + const typescriptTransformer = require('react-native-typescript-transformer') 73 | 74 | module.exports = obfuscatingTransformer({ 75 | + upstreamTransformer: typescriptTransformer 76 | }) 77 | ``` 78 | 79 | #### Default value: `require('metro/src/transformer')` 80 | 81 | ### `filter: (filename: string, source: string) => boolean` 82 | 83 | Returns true for any files that should be obfuscated and false for any files which should not be obfuscated. 84 | 85 | By default, it obfuscates all files in `src/**/*` 86 | 87 | ### `obfuscatorOptions: ObfuscatorOptions` 88 | 89 | **Warning** — Not all options are guaranteed to produce working code. In particular, `stringArray` definitely breaks builds. 90 | 91 | See the [javascript-obfuscator docs](https://github.com/javascript-obfuscator/javascript-obfuscator) for more info about what each option does. 92 | 93 | ```ts 94 | interface ObfuscatorOptions { 95 | compact?: boolean 96 | controlFlowFlattening?: boolean 97 | controlFlowFlatteningThreshold?: 0.75 98 | deadCodeInjection?: boolean 99 | deadCodeInjectionThreshold?: 0.4 100 | debugProtection?: boolean 101 | debugProtectionInterval?: boolean 102 | disableConsoleOutput?: boolean 103 | domainLock?: string[] 104 | identifierNamesGenerator?: "hexadecimal" | "mangled" 105 | log?: boolean 106 | renameGlobals?: boolean 107 | reservedNames?: string[] 108 | rotateStringArray?: true 109 | seed?: 0 110 | selfDefending?: boolean 111 | sourceMap?: boolean 112 | sourceMapBaseUrl?: string 113 | sourceMapFileName?: string 114 | sourceMapMode?: "separate" | "inline" 115 | stringArray?: boolean 116 | stringArrayEncoding?: string[] 117 | stringArrayThreshold?: 0.75 118 | target?: "browser" | "extension" | "node" 119 | unicodeEscapeSequence?: boolean 120 | } 121 | ``` 122 | 123 | ### `trace: boolean` 124 | 125 | Iff true, prints a list of files being obfuscated 126 | 127 | #### Default value: `false` 128 | 129 | ### `emitObfuscatedFiles: boolean` 130 | 131 | Iff true, emits the obfuscated versions of files alongside their originals, for comparison. 132 | 133 | #### Default value: `false` 134 | 135 | ### `enableInDevelopment: boolean` 136 | 137 | Iff true, enables obfuscation in development mode. 138 | 139 | #### Default value: `false` 140 | 141 | ## License 142 | 143 | MIT 144 | -------------------------------------------------------------------------------- /src/getMetroTransformer.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "@babel/core" 2 | import { RawSourceMap, SourceMapConsumer } from "source-map" 3 | import * as semver from "semver" 4 | import { 5 | MetroRawSourceMap, 6 | convertStandardSourceMapToMetroRawSourceMap, 7 | } from "./composeSourceMaps" 8 | import * as babylon from "@babel/parser" 9 | import traverse from "@babel/traverse" 10 | import generate from "@babel/generator"; 11 | 12 | export interface MetroTransformerResult { 13 | ast?: Node 14 | code?: string 15 | map?: string | RawSourceMap | MetroRawSourceMap 16 | } 17 | 18 | export interface MetroTransformer { 19 | transform(props: { 20 | filename: string 21 | src: string 22 | options: { 23 | dev?: boolean 24 | retainLines?: boolean 25 | // others unused 26 | } 27 | }): MetroTransformerResult 28 | getCacheKey?(): string 29 | } 30 | 31 | function getReactNativeMinorVersion(): number { 32 | const reactNativeVersionString = require("react-native/package.json").version 33 | 34 | const parseResult = semver.parse(reactNativeVersionString) 35 | 36 | if (!parseResult) { 37 | throw new Error( 38 | `Can't parse react-native version string '${reactNativeVersionString}'`, 39 | ) 40 | } 41 | 42 | return parseResult.minor 43 | } 44 | 45 | export function getMetroTransformer( 46 | reactNativeMinorVersion: number = getReactNativeMinorVersion(), 47 | ): MetroTransformer { 48 | if (reactNativeMinorVersion >= 59) { 49 | return require('metro-react-native-babel-transformer/src/index') 50 | } else if (reactNativeMinorVersion >= 56) { 51 | return require("metro/src/reactNativeTransformer") 52 | } else if (reactNativeMinorVersion >= 52) { 53 | return require("metro/src/transformer") 54 | } else if (reactNativeMinorVersion >= 0.47) { 55 | return require("metro-bundler/src/transformer") 56 | } else if (reactNativeMinorVersion === 0.46) { 57 | return require("metro-bundler/build/transformer") 58 | } else { 59 | throw new Error( 60 | `react-native-obfuscating-transformer requires react-native >= 0.46`, 61 | ) 62 | } 63 | } 64 | 65 | export interface ReactNativeObfuscatingTransformerDefaultResult { 66 | code: string 67 | map: string 68 | } 69 | 70 | export function maybeTransformMetroResult( 71 | upstreamResult: MetroTransformerResult, 72 | { code, map }: ReactNativeObfuscatingTransformerDefaultResult, 73 | reactNativeMinorVersion: number = getReactNativeMinorVersion(), 74 | ): MetroTransformerResult { 75 | if (reactNativeMinorVersion >= 52) { 76 | // convert code and map to ast 77 | const ast = babylon.parse(code, { 78 | sourceType: "module", 79 | }) 80 | 81 | const mapConsumer = new SourceMapConsumer(map as any) // upstream types are wrong 82 | ; (traverse as any).cheap(ast, (node: Node) => { 83 | if (node.loc) { 84 | const originalStart = mapConsumer.originalPositionFor(node.loc.start) 85 | if (originalStart.line) { 86 | node.loc.start.line = originalStart.line 87 | node.loc.start.column = originalStart.column 88 | } 89 | const originalEnd = mapConsumer.originalPositionFor(node.loc.end) 90 | if (originalEnd.line) { 91 | node.loc.end.line = originalEnd.line 92 | node.loc.end.column = originalEnd.column 93 | } 94 | } 95 | }) 96 | 97 | return { ast } 98 | } else if (Array.isArray(upstreamResult.map)) { 99 | return { code, map: convertStandardSourceMapToMetroRawSourceMap(map) } 100 | } else { 101 | return { code, map } 102 | } 103 | } 104 | 105 | export function generateAndConvert(ast: Node, filename: string): MetroTransformerResult { 106 | let generatorResult = generate(ast, { 107 | filename: filename, 108 | retainLines: true, 109 | sourceMaps: true, 110 | sourceFileName: filename, 111 | }); 112 | 113 | if (!generatorResult.map) { 114 | return {code: generatorResult.code}; 115 | } 116 | 117 | const map = { 118 | version: generatorResult.map.version + "", 119 | mappings: generatorResult.map.mappings, 120 | names: generatorResult.map.names, 121 | sources: generatorResult.map.sources, 122 | sourcesContent: generatorResult.map.sourcesContent, 123 | file: generatorResult.map.file 124 | } 125 | 126 | return {code: generatorResult.code, map: map}; 127 | } 128 | --------------------------------------------------------------------------------