jscrambler-metro-plugin 0.0.0-bulbasaur-20250620144942
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +61 -0
- package/lib/constants.js +62 -0
- package/lib/index.js +377 -0
- package/lib/polyfills/globalThis.js +1 -0
- package/lib/sourceMaps.js +179 -0
- package/lib/utils.js +401 -0
- package/package.json +41 -0
package/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jscrambler
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# 
|
2
|
+
# Jscrambler Code Integrity for React-Native (Metro Bundler)
|
3
|
+
|
4
|
+
Jscrambler [Code Integrity](https://jscrambler.com/code-integrity) is a JavaScript protection technology for Web and Mobile Applications. Its main purpose is to enable JavaScript applications to become self-defensive and resilient to tampering and reverse engineering.
|
5
|
+
|
6
|
+
If you're looking to gain control over third-party tags and achieve PCI DSS compliance please refer to Jscrambler [Webpage Integrity](https://jscrambler.com/webpage-integrity).
|
7
|
+
|
8
|
+
Version Compatibility
|
9
|
+
------------------------------------------------------------------------------
|
10
|
+
|
11
|
+
The version's compatibility table match your [Jscrambler Version](https://app.jscrambler.com/settings) with the Jscrambler Metro Plugin.
|
12
|
+
Please make sure you install the right version, otherwise some functionalities might not work properly.
|
13
|
+
|
14
|
+
| _Jscrambler Version_ | _Client and Integrations_ |
|
15
|
+
|:----------:|:-------------:|
|
16
|
+
| _<= 7.1_ | _<= 5.x.x_ |
|
17
|
+
| _\>= 7.2_ | _\>= 6.0.0_ |
|
18
|
+
|
19
|
+
# Usage
|
20
|
+
|
21
|
+
This metro plugin protects your **React Native** bundle using Jscrambler.
|
22
|
+
|
23
|
+
Include the plugin in your `metro.config.js` and add the following code:
|
24
|
+
|
25
|
+
```js
|
26
|
+
const {resolve} = require('path');
|
27
|
+
const jscramblerMetroPlugin = require('jscrambler-metro-plugin')(
|
28
|
+
/* optional */
|
29
|
+
{
|
30
|
+
enable: true,
|
31
|
+
enabledHermes: false, // set if you are using hermes engine
|
32
|
+
ignoreFile: resolve(__dirname, '.jscramblerignore'),
|
33
|
+
params: [
|
34
|
+
{
|
35
|
+
name: 'selfDefending',
|
36
|
+
options: {
|
37
|
+
threshold: 1
|
38
|
+
}
|
39
|
+
}
|
40
|
+
]
|
41
|
+
}
|
42
|
+
);
|
43
|
+
|
44
|
+
module.exports = jscramblerMetroPlugin;
|
45
|
+
```
|
46
|
+
|
47
|
+
You can pass your Jscrambler configuration using the plugin parameter or using
|
48
|
+
the usual `.jscramblerrc` file.
|
49
|
+
|
50
|
+
If you use a different location for the `.jscramblerignore` file, you can use the `ignoreFile` option to tell Jscrambler the path to the file.
|
51
|
+
Otherwise, if a `.jscramblerignore` file is found in a project root folder, it will be considered. You can find more information and examples in Ignoring Files.
|
52
|
+
|
53
|
+
By default, Jscrambler protection is **ignored** when bundle mode is set for **Development**. You can override this behavior by setting env variable `JSCRAMBLER_METRO_DEV=true`
|
54
|
+
|
55
|
+
In order to activate source map generation effectively, you will need to enable source maps both in the Jscrambler configuration file, by adding the following parameter to said file:
|
56
|
+
|
57
|
+
...
|
58
|
+
"sourceMaps": true,
|
59
|
+
...
|
60
|
+
|
61
|
+
and in the [React Native app](https://reactnative.dev/docs/debugging-release-builds?platform=android#enabling-source-maps).
|
package/lib/constants.js
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
const path = require('path');
|
2
|
+
|
3
|
+
const BUNDLE_CMD = 'bundle';
|
4
|
+
const BUNDLE_OUTPUT_CLI_ARG = '--bundle-output';
|
5
|
+
const BUNDLE_SOURCEMAP_OUTPUT_CLI_ARG = '--sourcemap-output';
|
6
|
+
const BUNDLE_DEV_CLI_ARG = '--dev';
|
7
|
+
// path.join so it supports both linux and windows fs
|
8
|
+
const INIT_CORE_MODULE = path.join(process.platform === 'win32' ? '/' : '', 'node_modules', 'react-native', 'Libraries', 'Core', 'InitializeCore.js');
|
9
|
+
const JSCRAMBLER_CLIENT_ID = 6;
|
10
|
+
const JSCRAMBLER_IGNORE = '.jscramblerignore';
|
11
|
+
const JSCRAMBLER_TEMP_FOLDER = '.jscrambler';
|
12
|
+
const JSCRAMBLER_DIST_TEMP_FOLDER = `${JSCRAMBLER_TEMP_FOLDER}/dist`;
|
13
|
+
const JSCRAMBLER_SOURCE_MAPS_TEMP_FOLDER = `${JSCRAMBLER_DIST_TEMP_FOLDER}/jscramblerSourceMaps`;
|
14
|
+
const JSCRAMBLER_PROTECTION_ID_FILE = `${JSCRAMBLER_TEMP_FOLDER}/protectionId`;
|
15
|
+
const JSCRAMBLER_BEG_ANNOTATION = '"JSCRAMBLER-BEG";';
|
16
|
+
const JSCRAMBLER_END_ANNOTATION = '"JSCRAMBLER-END";';
|
17
|
+
const JSCRAMBLER_EXTS = /.(j|t)s(x)?$/i;
|
18
|
+
const JSCRAMBLER_SELF_DEFENDING = 'selfDefending';
|
19
|
+
const JSCRAMBLER_ANTI_TAMPERING = 'antiTampering';
|
20
|
+
const JSCRAMBLER_SELF_HEALING = "selfHealing";
|
21
|
+
const JSCRAMBLER_ANTI_TAMPERING_MODE_RCK = 'RCK';
|
22
|
+
const JSCRAMBLER_ANTI_TAMPERING_MODE_SKL = 'SKL';
|
23
|
+
const JSCRAMBLER_GLOBAL_VARIABLE_INDIRECTION = 'globalVariableIndirection';
|
24
|
+
const JSCRAMBLER_TOLERATE_BENIGN_POISONING = 'tolerateBenignPoisoning';
|
25
|
+
const HERMES_SHOW_SOURCE_DIRECTIVE = '"show source";';
|
26
|
+
const JSCRAMBLER_HERMES_INCOMPATIBILITIES = [
|
27
|
+
{
|
28
|
+
slugName: JSCRAMBLER_SELF_DEFENDING,
|
29
|
+
errorMessage: `Jscrambler ${JSCRAMBLER_SELF_DEFENDING} transformation is not compatible with Hermes engine. Consider using ${JSCRAMBLER_ANTI_TAMPERING} transformation instead`,
|
30
|
+
},
|
31
|
+
];
|
32
|
+
const JSCRAMBLER_HERMES_ADD_SHOW_SOURCE_DIRECTIVE = [
|
33
|
+
JSCRAMBLER_ANTI_TAMPERING,
|
34
|
+
JSCRAMBLER_SELF_HEALING
|
35
|
+
];
|
36
|
+
|
37
|
+
module.exports = {
|
38
|
+
BUNDLE_CMD,
|
39
|
+
BUNDLE_OUTPUT_CLI_ARG,
|
40
|
+
BUNDLE_SOURCEMAP_OUTPUT_CLI_ARG,
|
41
|
+
BUNDLE_DEV_CLI_ARG,
|
42
|
+
INIT_CORE_MODULE,
|
43
|
+
JSCRAMBLER_CLIENT_ID,
|
44
|
+
JSCRAMBLER_IGNORE,
|
45
|
+
JSCRAMBLER_TEMP_FOLDER,
|
46
|
+
JSCRAMBLER_DIST_TEMP_FOLDER,
|
47
|
+
JSCRAMBLER_SOURCE_MAPS_TEMP_FOLDER,
|
48
|
+
JSCRAMBLER_PROTECTION_ID_FILE,
|
49
|
+
JSCRAMBLER_BEG_ANNOTATION,
|
50
|
+
JSCRAMBLER_END_ANNOTATION,
|
51
|
+
JSCRAMBLER_SELF_DEFENDING,
|
52
|
+
JSCRAMBLER_GLOBAL_VARIABLE_INDIRECTION,
|
53
|
+
JSCRAMBLER_TOLERATE_BENIGN_POISONING,
|
54
|
+
JSCRAMBLER_ANTI_TAMPERING,
|
55
|
+
JSCRAMBLER_ANTI_TAMPERING_MODE_RCK,
|
56
|
+
JSCRAMBLER_SELF_HEALING,
|
57
|
+
JSCRAMBLER_HERMES_INCOMPATIBILITIES,
|
58
|
+
JSCRAMBLER_HERMES_ADD_SHOW_SOURCE_DIRECTIVE,
|
59
|
+
JSCRAMBLER_ANTI_TAMPERING_MODE_SKL,
|
60
|
+
HERMES_SHOW_SOURCE_DIRECTIVE,
|
61
|
+
JSCRAMBLER_EXTS
|
62
|
+
}
|
package/lib/index.js
ADDED
@@ -0,0 +1,377 @@
|
|
1
|
+
const {copy, emptyDir, mkdirp, readFile, writeFile} = require('fs-extra');
|
2
|
+
const jscrambler = require('jscrambler').default;
|
3
|
+
const fs = require('fs');
|
4
|
+
const path = require('path');
|
5
|
+
const generateSourceMaps = require('./sourceMaps');
|
6
|
+
const globalThisPolyfill = require('./polyfills/globalThis');
|
7
|
+
|
8
|
+
const {
|
9
|
+
INIT_CORE_MODULE,
|
10
|
+
JSCRAMBLER_CLIENT_ID,
|
11
|
+
JSCRAMBLER_TEMP_FOLDER,
|
12
|
+
JSCRAMBLER_IGNORE,
|
13
|
+
JSCRAMBLER_DIST_TEMP_FOLDER,
|
14
|
+
JSCRAMBLER_PROTECTION_ID_FILE,
|
15
|
+
JSCRAMBLER_BEG_ANNOTATION,
|
16
|
+
JSCRAMBLER_END_ANNOTATION,
|
17
|
+
BUNDLE_SOURCEMAP_OUTPUT_CLI_ARG,
|
18
|
+
HERMES_SHOW_SOURCE_DIRECTIVE,
|
19
|
+
JSCRAMBLER_EXTS
|
20
|
+
} = require('./constants');
|
21
|
+
const {
|
22
|
+
buildModuleSourceMap,
|
23
|
+
buildNormalizePath,
|
24
|
+
extractLocs,
|
25
|
+
getBundlePath,
|
26
|
+
isFileReadable,
|
27
|
+
skipObfuscation,
|
28
|
+
stripEntryPointTags,
|
29
|
+
stripJscramblerTags,
|
30
|
+
addBundleArgsToExcludeList,
|
31
|
+
handleExcludeList,
|
32
|
+
injectTolerateBegninPoisoning,
|
33
|
+
handleAntiTampering,
|
34
|
+
addHermesShowSourceDirective,
|
35
|
+
handleHermesIncompatibilities,
|
36
|
+
wrapCodeWithTags
|
37
|
+
} = require('./utils');
|
38
|
+
|
39
|
+
const debug = !!process.env.DEBUG;
|
40
|
+
|
41
|
+
function logSourceMapsWarning(hasMetroSourceMaps, hasJscramblerSourceMaps) {
|
42
|
+
if (hasMetroSourceMaps) {
|
43
|
+
console.log(`warning: Jscrambler source-maps are DISABLED. Check how to activate them in https://docs.jscrambler.com/code-integrity/documentation/source-maps/api`);
|
44
|
+
} else if (hasJscramblerSourceMaps) {
|
45
|
+
console.log(`warning: Jscrambler source-maps were not generated. Missing metro source-maps (${BUNDLE_SOURCEMAP_OUTPUT_CLI_ARG} is required)`);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
async function obfuscateBundle(
|
50
|
+
{bundlePath, bundleSourceMapPath},
|
51
|
+
{fileNames, entryPointCode},
|
52
|
+
sourceMapFiles,
|
53
|
+
config,
|
54
|
+
projectRoot
|
55
|
+
) {
|
56
|
+
await emptyDir(JSCRAMBLER_TEMP_FOLDER);
|
57
|
+
|
58
|
+
const metroBundle = await readFile(bundlePath, 'utf8');
|
59
|
+
const metroBundleLocs = await extractLocs(metroBundle);
|
60
|
+
let processedMetroBundle = metroBundle;
|
61
|
+
let filteredFileNames = fileNames;
|
62
|
+
const excludeList = [];
|
63
|
+
|
64
|
+
const supportsEntryPoint = await jscrambler.introspectFieldOnMethod.call(
|
65
|
+
jscrambler,
|
66
|
+
config,
|
67
|
+
"mutation",
|
68
|
+
"createApplicationProtection",
|
69
|
+
"entryPoint"
|
70
|
+
);
|
71
|
+
|
72
|
+
// ignore entrypoint obfuscation if its not supported
|
73
|
+
if (!supportsEntryPoint) {
|
74
|
+
delete config.entryPoint;
|
75
|
+
if (typeof entryPointCode === 'string' && entryPointCode.length > 0) {
|
76
|
+
debug && console.log('debug Jscrambler entrypoint option not supported');
|
77
|
+
try {
|
78
|
+
filteredFileNames = fileNames.filter(
|
79
|
+
name => !name.includes(INIT_CORE_MODULE)
|
80
|
+
);
|
81
|
+
processedMetroBundle = stripEntryPointTags(
|
82
|
+
metroBundle,
|
83
|
+
entryPointCode
|
84
|
+
);
|
85
|
+
} catch (err) {
|
86
|
+
console.log("Error processing entry point.");
|
87
|
+
process.exit(-1);
|
88
|
+
}
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
const metroBundleChunks = processedMetroBundle.split(
|
93
|
+
JSCRAMBLER_BEG_ANNOTATION
|
94
|
+
);
|
95
|
+
addBundleArgsToExcludeList(metroBundleChunks[0], excludeList);
|
96
|
+
const metroUserFilesOnly = metroBundleChunks.slice(1).map((c, i) => {
|
97
|
+
const s = c.split(JSCRAMBLER_END_ANNOTATION);
|
98
|
+
// We don't want to extract args from last chunk
|
99
|
+
if (i < metroBundleChunks.length - 2) {
|
100
|
+
addBundleArgsToExcludeList(s[1], excludeList);
|
101
|
+
}
|
102
|
+
return s[0];
|
103
|
+
});
|
104
|
+
|
105
|
+
const sources = [];
|
106
|
+
// .jscramblerignore
|
107
|
+
const defaultJscramblerIgnorePath = path.join(projectRoot, JSCRAMBLER_IGNORE);
|
108
|
+
|
109
|
+
if (typeof config.ignoreFile === 'string') {
|
110
|
+
if (!await isFileReadable(config.ignoreFile)) {
|
111
|
+
console.error(`The *ignoreFile* "${config.ignoreFile}" was not found or is not readable!`);
|
112
|
+
process.exit(-1);
|
113
|
+
}
|
114
|
+
sources.push({ filename: JSCRAMBLER_IGNORE, content: await readFile(config.ignoreFile) })
|
115
|
+
} else if (await isFileReadable(defaultJscramblerIgnorePath)) {
|
116
|
+
sources.push({ filename: JSCRAMBLER_IGNORE, content: await readFile(defaultJscramblerIgnorePath) })
|
117
|
+
}
|
118
|
+
|
119
|
+
// push user files to sources array
|
120
|
+
for (let i = 0; i < metroUserFilesOnly.length; i += 1) {
|
121
|
+
sources.push({
|
122
|
+
filename: filteredFileNames[i], content: metroUserFilesOnly[i]
|
123
|
+
})
|
124
|
+
}
|
125
|
+
|
126
|
+
// Source map files (only for Instrumentation process)
|
127
|
+
for (const { filename, content } of sourceMapFiles) {
|
128
|
+
sources.push({
|
129
|
+
filename, content
|
130
|
+
})
|
131
|
+
}
|
132
|
+
|
133
|
+
// adapt configs for react-native
|
134
|
+
config.sources = sources;
|
135
|
+
config.filesDest = JSCRAMBLER_DIST_TEMP_FOLDER;
|
136
|
+
config.clientId = JSCRAMBLER_CLIENT_ID;
|
137
|
+
|
138
|
+
const supportsExcludeList = await jscrambler.introspectFieldOnMethod.call(
|
139
|
+
jscrambler,
|
140
|
+
config,
|
141
|
+
"mutation",
|
142
|
+
"createApplicationProtection",
|
143
|
+
"excludeList"
|
144
|
+
);
|
145
|
+
|
146
|
+
handleExcludeList(config, {supportsExcludeList, excludeList});
|
147
|
+
|
148
|
+
injectTolerateBegninPoisoning(config);
|
149
|
+
|
150
|
+
if (bundleSourceMapPath && typeof config.sourceMaps === 'undefined') {
|
151
|
+
console.error(`error Metro is generating source maps that won't be useful after Jscrambler protection.
|
152
|
+
If this is not a problem, you can either:
|
153
|
+
1) Disable source maps in metro bundler
|
154
|
+
2) Explicitly disable Jscrambler source maps by adding 'sourceMaps: false' in the Jscrambler config file
|
155
|
+
|
156
|
+
If you want valid source maps, make sure you have access to the feature and enable it in Jscrambler config file by adding 'sourceMaps: true'`
|
157
|
+
);
|
158
|
+
process.exit(-1);
|
159
|
+
}
|
160
|
+
|
161
|
+
const requireStartAtFirstColumn = handleAntiTampering(
|
162
|
+
config,
|
163
|
+
processedMetroBundle,
|
164
|
+
);
|
165
|
+
|
166
|
+
const addShowSource = addHermesShowSourceDirective(config);
|
167
|
+
|
168
|
+
if (addShowSource) {
|
169
|
+
console.log(
|
170
|
+
`info Jscrambler ${HERMES_SHOW_SOURCE_DIRECTIVE} directive added`,
|
171
|
+
);
|
172
|
+
}
|
173
|
+
|
174
|
+
const shouldGenerateSourceMaps = config.sourceMaps && bundleSourceMapPath;
|
175
|
+
|
176
|
+
const jscramblerOp = !!config.instrument
|
177
|
+
? jscrambler.instrumentAndDownload
|
178
|
+
: jscrambler.protectAndDownload;
|
179
|
+
|
180
|
+
// obfuscate or instrument
|
181
|
+
const protectionId = await jscramblerOp.call(jscrambler, config);
|
182
|
+
|
183
|
+
// store protection id
|
184
|
+
await writeFile(JSCRAMBLER_PROTECTION_ID_FILE, protectionId);
|
185
|
+
|
186
|
+
// read obfuscated user files
|
187
|
+
const obfusctedUserFiles = await Promise.all(metroUserFilesOnly.map((c, i) =>
|
188
|
+
readFile(`${JSCRAMBLER_DIST_TEMP_FOLDER}/${filteredFileNames[i]}`, 'utf8')
|
189
|
+
));
|
190
|
+
|
191
|
+
// build final bundle (with JSCRAMBLER TAGS still)
|
192
|
+
const finalBundle = metroBundleChunks.reduce((acc, c, i) => {
|
193
|
+
if (i === 0) {
|
194
|
+
const chunks = c.split('\n');
|
195
|
+
return [`${chunks[0]}${globalThisPolyfill}`, ...chunks.slice(1)].join('\n');
|
196
|
+
}
|
197
|
+
|
198
|
+
let showSource = addShowSource;
|
199
|
+
let startAtFirstColumn = requireStartAtFirstColumn;
|
200
|
+
|
201
|
+
const obfuscatedCode = obfusctedUserFiles[i - 1];
|
202
|
+
const sourceFileIgnored = metroUserFilesOnly[i - 1] === obfuscatedCode;
|
203
|
+
|
204
|
+
if (sourceFileIgnored) {
|
205
|
+
// restore excluded files
|
206
|
+
showSource = false;
|
207
|
+
startAtFirstColumn = false;
|
208
|
+
debug && console.log(`debug Jscrambler File ${fileNames[i - 1]} was excluded`);
|
209
|
+
}
|
210
|
+
|
211
|
+
const tillCodeEnd = c.substr(
|
212
|
+
c.indexOf(JSCRAMBLER_END_ANNOTATION),
|
213
|
+
c.length
|
214
|
+
);
|
215
|
+
return `${acc}${JSCRAMBLER_BEG_ANNOTATION}${
|
216
|
+
showSource ? HERMES_SHOW_SOURCE_DIRECTIVE : ''
|
217
|
+
}${startAtFirstColumn ? '\n' : ''}${obfuscatedCode}${tillCodeEnd}`;
|
218
|
+
}, '');
|
219
|
+
|
220
|
+
await writeFile(bundlePath, stripJscramblerTags(finalBundle));
|
221
|
+
if(!shouldGenerateSourceMaps) {
|
222
|
+
logSourceMapsWarning(bundleSourceMapPath, config.sourceMaps);
|
223
|
+
// nothing more to do
|
224
|
+
return;
|
225
|
+
}
|
226
|
+
|
227
|
+
// process Jscrambler SourceMaps
|
228
|
+
const shouldAddSourceContent = typeof config.sourceMaps === 'object' ? config.sourceMaps.sourceContent : false;
|
229
|
+
console.log(`info Jscrambler Source Maps (${shouldAddSourceContent ? "with" : "no"} source content)`);
|
230
|
+
const finalSourceMap = await generateSourceMaps({
|
231
|
+
jscrambler,
|
232
|
+
config,
|
233
|
+
shouldAddSourceContent,
|
234
|
+
protectionId,
|
235
|
+
metroUserFilesOnly,
|
236
|
+
fileNames: filteredFileNames,
|
237
|
+
bundlePath,
|
238
|
+
bundleSourceMapPath,
|
239
|
+
finalBundle,
|
240
|
+
projectRoot,
|
241
|
+
debug,
|
242
|
+
metroBundleLocs
|
243
|
+
});
|
244
|
+
await writeFile(bundleSourceMapPath, finalSourceMap);
|
245
|
+
}
|
246
|
+
|
247
|
+
function fileExists(modulePath) {
|
248
|
+
return fs.existsSync(modulePath);
|
249
|
+
}
|
250
|
+
|
251
|
+
function isValidExtension(modulePath) {
|
252
|
+
return path.extname(modulePath).match(JSCRAMBLER_EXTS);
|
253
|
+
}
|
254
|
+
|
255
|
+
function validateModule(modulePath, config, projectRoot) {
|
256
|
+
const instrument = !!config.instrument;
|
257
|
+
|
258
|
+
if (
|
259
|
+
!fileExists(modulePath) ||
|
260
|
+
!isValidExtension(modulePath) ||
|
261
|
+
typeof modulePath !== "string"
|
262
|
+
) {
|
263
|
+
return false;
|
264
|
+
} else if (modulePath.includes(INIT_CORE_MODULE) && !instrument) {
|
265
|
+
// This is the entrypoint file
|
266
|
+
config.entryPoint = buildNormalizePath(modulePath, projectRoot);
|
267
|
+
return true;
|
268
|
+
} else if (modulePath.includes("node_modules")) {
|
269
|
+
return false;
|
270
|
+
} else {
|
271
|
+
return true;
|
272
|
+
}
|
273
|
+
}
|
274
|
+
|
275
|
+
/**
|
276
|
+
* Add serialize.processModuleFilter option to metro and attach listener to beforeExit event.
|
277
|
+
* *config.fileSrc* and *config.filesDest* will be ignored.
|
278
|
+
* @param {{enable: boolean, enabledHermes: boolean }} _config
|
279
|
+
* @param {string} [projectRoot=process.cwd()]
|
280
|
+
* @returns {{serializer: {processModuleFilter(*): boolean}}}
|
281
|
+
*/
|
282
|
+
module.exports = function (_config = {}, projectRoot = process.cwd()) {
|
283
|
+
const skipReason = skipObfuscation(_config);
|
284
|
+
if (skipReason) {
|
285
|
+
console.log(`warning: Jscrambler Obfuscation SKIPPED [${skipReason}]`);
|
286
|
+
return {};
|
287
|
+
}
|
288
|
+
|
289
|
+
const bundlePath = getBundlePath();
|
290
|
+
// make sure jscrambler-metro-plugin is properly configure on metro bundler
|
291
|
+
let calledByMetro = false;
|
292
|
+
const fileNames = new Set();
|
293
|
+
const sourceMapFiles = [];
|
294
|
+
const config = Object.assign({}, jscrambler.config, _config);
|
295
|
+
const instrument = !!config.instrument;
|
296
|
+
let entryPointCode;
|
297
|
+
|
298
|
+
if (config.filesDest || config.filesSrc) {
|
299
|
+
console.warn('warning: Jscrambler fields filesDest and fileSrc were ignored. Using input/output values of the metro bundler.')
|
300
|
+
}
|
301
|
+
|
302
|
+
if (!Array.isArray(config.params) || config.params.length === 0) {
|
303
|
+
console.warn('warning: Jscrambler recommends you to declare your transformations list on the configuration file.')
|
304
|
+
}
|
305
|
+
|
306
|
+
process.on('beforeExit', async function (exitCode) {
|
307
|
+
try{
|
308
|
+
if (!calledByMetro) {
|
309
|
+
throw new Error('*jscrambler-metro-plugin* was not properly configured on metro.config.js file. Please verify our documentation in https://docs.jscrambler.com/code-integrity/frameworks-and-libraries/react-native/integration.');
|
310
|
+
}
|
311
|
+
|
312
|
+
console.log(
|
313
|
+
instrument
|
314
|
+
? 'info Jscrambler Instrumenting Code'
|
315
|
+
: `info Jscrambler Obfuscating Code ${
|
316
|
+
config.enabledHermes
|
317
|
+
? "(Using Hermes Engine)"
|
318
|
+
: "(If you are using Hermes Engine set enabledHermes=true)"
|
319
|
+
}`,
|
320
|
+
);
|
321
|
+
|
322
|
+
// check for incompatible transformations and turn off code hardening
|
323
|
+
handleHermesIncompatibilities(config);
|
324
|
+
|
325
|
+
// start obfuscation
|
326
|
+
await obfuscateBundle(bundlePath, {fileNames: Array.from(fileNames), entryPointCode}, sourceMapFiles, config, projectRoot);
|
327
|
+
} catch(err) {
|
328
|
+
console.error(err);
|
329
|
+
process.exit(1);
|
330
|
+
} finally {
|
331
|
+
process.exit(exitCode)
|
332
|
+
}
|
333
|
+
});
|
334
|
+
|
335
|
+
return {
|
336
|
+
serializer: {
|
337
|
+
/**
|
338
|
+
* Select user files ONLY (no vendor) to be obfuscated. That code should be tagged with
|
339
|
+
* {@JSCRAMBLER_BEG_ANNOTATION} and {@JSCRAMBLER_END_ANNOTATION}.
|
340
|
+
* Also gather metro source-maps in case of instrumentation process.
|
341
|
+
* @param {{output: Array<*>, path: string, getSource: function():Buffer}} _module
|
342
|
+
* @returns {boolean}
|
343
|
+
*/
|
344
|
+
processModuleFilter(_module) {
|
345
|
+
calledByMetro = true;
|
346
|
+
|
347
|
+
const modulePath = _module.path;
|
348
|
+
const shouldSkipModule = !validateModule(modulePath, config, projectRoot);
|
349
|
+
|
350
|
+
if (shouldSkipModule) {
|
351
|
+
return true;
|
352
|
+
}
|
353
|
+
|
354
|
+
const normalizePath = buildNormalizePath(modulePath, projectRoot);
|
355
|
+
fileNames.add(normalizePath);
|
356
|
+
|
357
|
+
_module.output.forEach(({data}) => {
|
358
|
+
if (instrument && Array.isArray(data.map)) {
|
359
|
+
sourceMapFiles.push({
|
360
|
+
filename: `${normalizePath}.map`,
|
361
|
+
content: buildModuleSourceMap(
|
362
|
+
data,
|
363
|
+
normalizePath,
|
364
|
+
_module.getSource().toString()
|
365
|
+
)
|
366
|
+
});
|
367
|
+
}
|
368
|
+
if (modulePath.includes(INIT_CORE_MODULE)){
|
369
|
+
entryPointCode = data.code;
|
370
|
+
}
|
371
|
+
data.code = wrapCodeWithTags(data.code);
|
372
|
+
});
|
373
|
+
return true;
|
374
|
+
}
|
375
|
+
}
|
376
|
+
};
|
377
|
+
};
|
@@ -0,0 +1 @@
|
|
1
|
+
module.exports = "!(function(o){o.globalThis=o})('undefined'!=typeof globalThis?globalThis:'undefined'!=typeof global?global:'undefined'!=typeof window?window:this);";
|
@@ -0,0 +1,179 @@
|
|
1
|
+
const {readFile} = require('fs-extra');
|
2
|
+
const sourceMap = require('source-map');
|
3
|
+
const {
|
4
|
+
buildNormalizePath,
|
5
|
+
extractLocs
|
6
|
+
} = require('./utils');
|
7
|
+
const {
|
8
|
+
JSCRAMBLER_SOURCE_MAPS_TEMP_FOLDER
|
9
|
+
} = require('./constants');
|
10
|
+
|
11
|
+
/**
|
12
|
+
* Merge jscrambler source-maps with metro source-map.
|
13
|
+
* Control start/end line of each obfuscated source and calculate amount of lines that needs to
|
14
|
+
* be shifted for none-obfuscated code.
|
15
|
+
* @param {object} payload
|
16
|
+
* @returns {Promise<string>}
|
17
|
+
*/
|
18
|
+
module.exports = async function generateSourceMaps(payload) {
|
19
|
+
const {
|
20
|
+
jscrambler,
|
21
|
+
config,
|
22
|
+
shouldAddSourceContent,
|
23
|
+
protectionId,
|
24
|
+
metroUserFilesOnly,
|
25
|
+
fileNames,
|
26
|
+
debug,
|
27
|
+
bundlePath,
|
28
|
+
bundleSourceMapPath,
|
29
|
+
finalBundle,
|
30
|
+
projectRoot,
|
31
|
+
metroBundleLocs
|
32
|
+
} = payload;
|
33
|
+
|
34
|
+
// download sourcemaps
|
35
|
+
delete config.filesSrc;
|
36
|
+
await jscrambler.downloadSourceMaps(Object.assign({protectionId}, config));
|
37
|
+
|
38
|
+
// read obfuscated source-map from filesystem
|
39
|
+
const obfuscatedSourceMaps = await Promise.all(
|
40
|
+
metroUserFilesOnly.map((c, i) =>
|
41
|
+
readFile(
|
42
|
+
`${JSCRAMBLER_SOURCE_MAPS_TEMP_FOLDER}/${fileNames[i]}.map`,
|
43
|
+
'utf8',
|
44
|
+
).catch((e) => {
|
45
|
+
if (e.code === 'ENOENT') {
|
46
|
+
// when a source file is excluded, the sourcemap is not produced
|
47
|
+
return null;
|
48
|
+
}
|
49
|
+
throw e;
|
50
|
+
}),
|
51
|
+
),
|
52
|
+
);
|
53
|
+
|
54
|
+
// read metro source-map
|
55
|
+
const metroSourceMap = await readFile(bundleSourceMapPath, 'utf8');
|
56
|
+
const finalBundleLocs = await extractLocs(finalBundle);
|
57
|
+
|
58
|
+
const metroSourceMapConsumer = new sourceMap.SourceMapConsumer(
|
59
|
+
metroSourceMap,
|
60
|
+
);
|
61
|
+
// extra source map params that are not processed by the source-map package
|
62
|
+
const metroSourceMapExtraParams = [
|
63
|
+
'x_facebook_sources', // added x_facebook_sources to prevent the third party sources to come back with null values
|
64
|
+
// for when react native debugIds are necessary in the sourcemaps (upload to sentry for example)
|
65
|
+
// needs to be added at the end of this file since the debugIds are not in SourceMapGenerator type
|
66
|
+
'debugId',
|
67
|
+
'debug_id',
|
68
|
+
];
|
69
|
+
// retrieve debug ids and facebook sources after the validation by the sourcemap package
|
70
|
+
const JSONMetroSourceMap = JSON.parse(metroSourceMap);
|
71
|
+
const finalSourceMapGenerator = new sourceMap.SourceMapGenerator({
|
72
|
+
file: bundlePath,
|
73
|
+
});
|
74
|
+
const ofuscatedSourceMapConsumers = obfuscatedSourceMaps.map((map) =>
|
75
|
+
// when a source file is excluded, the sourcemap is not produced
|
76
|
+
map ? new sourceMap.SourceMapConsumer(map) : null,
|
77
|
+
);
|
78
|
+
|
79
|
+
// add all original sources and sourceContents
|
80
|
+
metroSourceMapConsumer.sources.forEach(function (sourceFile) {
|
81
|
+
finalSourceMapGenerator._sources.add(sourceFile)
|
82
|
+
var sourceContent = metroSourceMapConsumer.sourceContentFor(sourceFile);
|
83
|
+
if (shouldAddSourceContent && sourceContent != null) {
|
84
|
+
finalSourceMapGenerator.setSourceContent(sourceFile, sourceContent)
|
85
|
+
}
|
86
|
+
});
|
87
|
+
|
88
|
+
let shiftLines = 0;
|
89
|
+
let tmpShiftLine = 0;
|
90
|
+
let currSource;
|
91
|
+
metroSourceMapConsumer.eachMapping(mapping => {
|
92
|
+
const original = mapping.originalLine ? {line: mapping.originalLine, column: mapping.originalColumn} : null;
|
93
|
+
let newMappings = [{
|
94
|
+
original,
|
95
|
+
source: mapping.source,
|
96
|
+
name: mapping.name,
|
97
|
+
generated: {
|
98
|
+
line: mapping.generatedLine,
|
99
|
+
column: mapping.generatedColumn
|
100
|
+
}
|
101
|
+
}];
|
102
|
+
const normalizePath = buildNormalizePath(mapping.source, projectRoot);
|
103
|
+
const fileNamesIndex = fileNames.indexOf(normalizePath);
|
104
|
+
|
105
|
+
if (currSource !== normalizePath) {
|
106
|
+
// next source
|
107
|
+
currSource = normalizePath;
|
108
|
+
shiftLines = tmpShiftLine;
|
109
|
+
}
|
110
|
+
|
111
|
+
if (
|
112
|
+
fileNamesIndex !== -1 &&
|
113
|
+
/* check if sourceMap was loaded */
|
114
|
+
ofuscatedSourceMapConsumers[fileNamesIndex]
|
115
|
+
) {
|
116
|
+
/* jscrambler obfuscated files */
|
117
|
+
const {lineStart, lineEnd, columnStart} = metroBundleLocs[fileNamesIndex];
|
118
|
+
const {
|
119
|
+
lineStart: finalLineStart,
|
120
|
+
lineEnd: finalLineEnd,
|
121
|
+
columnStart: finalColumnStart,
|
122
|
+
} = finalBundleLocs[fileNamesIndex];
|
123
|
+
const allGeneratedPositionsFor = ofuscatedSourceMapConsumers[fileNamesIndex].allGeneratedPositionsFor({
|
124
|
+
source: normalizePath,
|
125
|
+
line: mapping.generatedLine - lineStart + 1 /* avoid line=0 */,
|
126
|
+
column:
|
127
|
+
mapping.generatedColumn -
|
128
|
+
(mapping.generatedLine === lineStart
|
129
|
+
? columnStart /* column start should be applied only to the first line */
|
130
|
+
: 0),
|
131
|
+
});
|
132
|
+
|
133
|
+
if (allGeneratedPositionsFor.length === 0) {
|
134
|
+
// no match
|
135
|
+
return;
|
136
|
+
}
|
137
|
+
|
138
|
+
newMappings = allGeneratedPositionsFor.map(({line: obfLine, column: obfColumn}) => {
|
139
|
+
const calcFinalLine = finalLineStart + obfLine - 1;
|
140
|
+
// add columnStart only on the first line
|
141
|
+
const calcFinalColumn = obfLine === 1 ? finalColumnStart + obfColumn : obfColumn;
|
142
|
+
|
143
|
+
debug && console.log('original', original, '->', 'final', {line: calcFinalLine, column: calcFinalColumn});
|
144
|
+
|
145
|
+
return Object.assign({}, newMappings[0], {
|
146
|
+
generated: {
|
147
|
+
line: calcFinalLine,
|
148
|
+
column: calcFinalColumn
|
149
|
+
}
|
150
|
+
});
|
151
|
+
});
|
152
|
+
|
153
|
+
// shift lines on next files
|
154
|
+
tmpShiftLine = finalLineEnd - lineEnd;
|
155
|
+
} else {
|
156
|
+
/* vendor code */
|
157
|
+
newMappings[0].generated.line += shiftLines;
|
158
|
+
|
159
|
+
// when the original line/column can't be found, it means that there isn't a real source file associated.
|
160
|
+
// Thus, if source file name exists it must be cleaned (f.e ".") to avoid an invalid mapping error
|
161
|
+
if (newMappings[0].original === null && newMappings[0].source) {
|
162
|
+
newMappings[0].source = null;
|
163
|
+
}
|
164
|
+
}
|
165
|
+
|
166
|
+
newMappings.forEach((newMapping) =>
|
167
|
+
finalSourceMapGenerator.addMapping(newMapping),
|
168
|
+
);
|
169
|
+
})
|
170
|
+
|
171
|
+
const finalSourceMaps = finalSourceMapGenerator.toString();
|
172
|
+
const finalSourceMapsJson = JSON.parse(finalSourceMaps);
|
173
|
+
|
174
|
+
for (const param of metroSourceMapExtraParams) {
|
175
|
+
finalSourceMapsJson[param] = JSONMetroSourceMap[param];
|
176
|
+
}
|
177
|
+
|
178
|
+
return JSON.stringify(finalSourceMapsJson);
|
179
|
+
};
|
package/lib/utils.js
ADDED
@@ -0,0 +1,401 @@
|
|
1
|
+
const fs = require('fs');
|
2
|
+
const readline = require('readline');
|
3
|
+
const {Command} = require('commander');
|
4
|
+
const {Readable} = require('stream');
|
5
|
+
const { sep } = require('path');
|
6
|
+
const metroSourceMap = require('metro-source-map');
|
7
|
+
const {
|
8
|
+
JSCRAMBLER_EXTS,
|
9
|
+
JSCRAMBLER_END_ANNOTATION,
|
10
|
+
JSCRAMBLER_BEG_ANNOTATION,
|
11
|
+
JSCRAMBLER_SELF_DEFENDING,
|
12
|
+
JSCRAMBLER_ANTI_TAMPERING,
|
13
|
+
JSCRAMBLER_HERMES_INCOMPATIBILITIES,
|
14
|
+
JSCRAMBLER_HERMES_ADD_SHOW_SOURCE_DIRECTIVE,
|
15
|
+
JSCRAMBLER_ANTI_TAMPERING_MODE_SKL,
|
16
|
+
JSCRAMBLER_ANTI_TAMPERING_MODE_RCK,
|
17
|
+
JSCRAMBLER_TOLERATE_BENIGN_POISONING,
|
18
|
+
JSCRAMBLER_GLOBAL_VARIABLE_INDIRECTION,
|
19
|
+
BUNDLE_OUTPUT_CLI_ARG,
|
20
|
+
BUNDLE_SOURCEMAP_OUTPUT_CLI_ARG,
|
21
|
+
BUNDLE_DEV_CLI_ARG,
|
22
|
+
HERMES_SHOW_SOURCE_DIRECTIVE,
|
23
|
+
BUNDLE_CMD
|
24
|
+
} = require('./constants');
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Only 'bundle' command triggers obfuscation.
|
28
|
+
* Development bundles will be ignored (--dev true). Use JSCRAMBLER_METRO_DEV to override this behaviour.
|
29
|
+
* @returns {string} skip reason. If falsy value dont skip obfuscation
|
30
|
+
*/
|
31
|
+
function skipObfuscation(config) {
|
32
|
+
if (
|
33
|
+
typeof config === 'object' &&
|
34
|
+
config !== null &&
|
35
|
+
config.enable === false
|
36
|
+
) {
|
37
|
+
return 'Explicitly Disabled';
|
38
|
+
}
|
39
|
+
|
40
|
+
let isBundleCmd = false;
|
41
|
+
const command = new Command();
|
42
|
+
command
|
43
|
+
.command(BUNDLE_CMD)
|
44
|
+
.allowUnknownOption()
|
45
|
+
.action(() => (isBundleCmd = true));
|
46
|
+
command.option(`${BUNDLE_DEV_CLI_ARG} <boolean>`).parse(process.argv);
|
47
|
+
if (!isBundleCmd) {
|
48
|
+
return 'Not a *bundle* command';
|
49
|
+
}
|
50
|
+
if (command.dev === 'true') {
|
51
|
+
return (
|
52
|
+
process.env.JSCRAMBLER_METRO_DEV !== 'true' &&
|
53
|
+
'Development mode. Override with JSCRAMBLER_METRO_DEV=true environment variable'
|
54
|
+
);
|
55
|
+
}
|
56
|
+
return null;
|
57
|
+
}
|
58
|
+
|
59
|
+
/**
|
60
|
+
* Get bundle path based CLI arguments
|
61
|
+
* @returns {{bundlePath: string, bundleSourceMapPath: string}}
|
62
|
+
* @throws {Error} when bundle output was not found
|
63
|
+
*/
|
64
|
+
function getBundlePath() {
|
65
|
+
const command = new Command();
|
66
|
+
command
|
67
|
+
.option(`${BUNDLE_OUTPUT_CLI_ARG} <string>`)
|
68
|
+
.option(`${BUNDLE_SOURCEMAP_OUTPUT_CLI_ARG} <string>`)
|
69
|
+
.parse(process.argv);
|
70
|
+
if (command.bundleOutput) {
|
71
|
+
return {
|
72
|
+
bundlePath: command.bundleOutput,
|
73
|
+
bundleSourceMapPath: command.sourcemapOutput
|
74
|
+
};
|
75
|
+
}
|
76
|
+
console.error('Bundle output path not found.');
|
77
|
+
return process.exit(-1);
|
78
|
+
}
|
79
|
+
|
80
|
+
/**
|
81
|
+
* Extract the lines of code for a given string.
|
82
|
+
* @param {string} inputStr
|
83
|
+
* @returns {Promise<[{lineStart: number, columnStart: number, lineEnd: number}]>}
|
84
|
+
*/
|
85
|
+
function extractLocs(inputStr) {
|
86
|
+
let locs = [];
|
87
|
+
let lines = 0;
|
88
|
+
return new Promise((res, rej) =>
|
89
|
+
readline.createInterface({
|
90
|
+
input: new Readable({
|
91
|
+
read() {
|
92
|
+
this.push(this.sent ? null : inputStr);
|
93
|
+
this.sent = true;
|
94
|
+
}
|
95
|
+
}),
|
96
|
+
crlfDelay: Infinity,
|
97
|
+
terminal: false,
|
98
|
+
historySize: 0
|
99
|
+
})
|
100
|
+
.on('line', line => {
|
101
|
+
lines++;
|
102
|
+
const startTagIndex = line.indexOf(JSCRAMBLER_BEG_ANNOTATION);
|
103
|
+
if (startTagIndex !== -1) {
|
104
|
+
const columnStart = line.includes(
|
105
|
+
`${JSCRAMBLER_BEG_ANNOTATION}${HERMES_SHOW_SOURCE_DIRECTIVE}`,
|
106
|
+
)
|
107
|
+
? HERMES_SHOW_SOURCE_DIRECTIVE.length + startTagIndex
|
108
|
+
: startTagIndex;
|
109
|
+
// occurs with Anti-tampering SKL mode
|
110
|
+
const startAtFirstColumn = line.includes(
|
111
|
+
`${JSCRAMBLER_BEG_ANNOTATION}\n`,
|
112
|
+
);
|
113
|
+
|
114
|
+
locs.push({
|
115
|
+
lineStart: lines,
|
116
|
+
columnStart,
|
117
|
+
startAtFirstColumn,
|
118
|
+
});
|
119
|
+
}
|
120
|
+
|
121
|
+
if (line.indexOf(JSCRAMBLER_END_ANNOTATION) !== -1) {
|
122
|
+
locs[locs.length - 1].lineEnd = lines;
|
123
|
+
}
|
124
|
+
})
|
125
|
+
.on('close', () => res(locs))
|
126
|
+
.on('error', rej)
|
127
|
+
)
|
128
|
+
}
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Strip all Jscrambler tags from code
|
132
|
+
* @param {string} code
|
133
|
+
* @returns {string}
|
134
|
+
*/
|
135
|
+
function stripJscramblerTags(code) {
|
136
|
+
return code.replace(new RegExp(JSCRAMBLER_BEG_ANNOTATION, 'g'), '')
|
137
|
+
.replace(new RegExp(JSCRAMBLER_END_ANNOTATION, 'g'), '')
|
138
|
+
}
|
139
|
+
|
140
|
+
/**
|
141
|
+
* When next character is a new line (\n or \r\n),
|
142
|
+
* we should increment startIndex to avoid user code starting with a new line.
|
143
|
+
* @param {string} startIndex
|
144
|
+
* @param {string} code
|
145
|
+
* @returns {number}
|
146
|
+
* @example
|
147
|
+
* __d(function(g,r,i,a,m,e,d){(detect new line here and start below)
|
148
|
+
* // user code
|
149
|
+
* ...
|
150
|
+
* }
|
151
|
+
*/
|
152
|
+
function shiftStartIndexOnNewLine(startIndex, code) {
|
153
|
+
switch (code[startIndex + 1]) {
|
154
|
+
case '\r':
|
155
|
+
startIndex++;
|
156
|
+
return shiftStartIndexOnNewLine(startIndex, code);
|
157
|
+
case '\n':
|
158
|
+
startIndex++;
|
159
|
+
break;
|
160
|
+
}
|
161
|
+
return startIndex;
|
162
|
+
}
|
163
|
+
|
164
|
+
/**
|
165
|
+
* Wrap code with Jscrambler TAGS {JSCRAMBLER_BEG_ANNOTATION and JSCRAMBLER_END_ANNOTATION}
|
166
|
+
* @param {string} code
|
167
|
+
*/
|
168
|
+
function wrapCodeWithTags(code) {
|
169
|
+
let startIndex = code.indexOf('{');
|
170
|
+
const endIndex = code.lastIndexOf('}');
|
171
|
+
startIndex = shiftStartIndexOnNewLine(startIndex, code);
|
172
|
+
const init = code.substring(0, startIndex + 1);
|
173
|
+
const clientCode = code.substring(startIndex + 1, endIndex);
|
174
|
+
const end = code.substr(endIndex, code.length);
|
175
|
+
const codeWithTags = init + JSCRAMBLER_BEG_ANNOTATION + clientCode + JSCRAMBLER_END_ANNOTATION + end;
|
176
|
+
|
177
|
+
return codeWithTags;
|
178
|
+
}
|
179
|
+
|
180
|
+
/**
|
181
|
+
* Use 'metro-source-map' to build a standard source-map from raw mappings
|
182
|
+
* @param {{code: string, map: Array.<Array<number>>}} output
|
183
|
+
* @param {string} modulePath
|
184
|
+
* @param {string} source
|
185
|
+
* @returns {string}
|
186
|
+
*/
|
187
|
+
function buildModuleSourceMap(output, modulePath, source) {
|
188
|
+
return metroSourceMap
|
189
|
+
.fromRawMappings([
|
190
|
+
{
|
191
|
+
...output,
|
192
|
+
source,
|
193
|
+
path: modulePath
|
194
|
+
}
|
195
|
+
])
|
196
|
+
.toString(modulePath);
|
197
|
+
}
|
198
|
+
|
199
|
+
/**
|
200
|
+
* @param {string} path
|
201
|
+
* @param {string} projectRoot
|
202
|
+
* @returns {string} undefined if path is empty or invalid
|
203
|
+
*
|
204
|
+
* @example
|
205
|
+
* <project_root>/react-native0.59-grocery-list/App/index.js -> App/index.js
|
206
|
+
* <project_root>/react-native0.59-grocery-list/App/index.ts -> App/index.js
|
207
|
+
*/
|
208
|
+
function buildNormalizePath(path, projectRoot) {
|
209
|
+
if (typeof path !== 'string' || path.trim().length === 0) {
|
210
|
+
return;
|
211
|
+
}
|
212
|
+
const relativePath = path.replace(projectRoot, '');
|
213
|
+
let relativePathWithLeadingSlash = relativePath.replace(JSCRAMBLER_EXTS, '.js');
|
214
|
+
if (relativePathWithLeadingSlash.startsWith(sep)) {
|
215
|
+
relativePathWithLeadingSlash = relativePathWithLeadingSlash.substring(1 /* remove leading separator */);
|
216
|
+
}
|
217
|
+
return relativePathWithLeadingSlash.replace(/\\/g, '/'); // replace win32 separator by linux one
|
218
|
+
}
|
219
|
+
|
220
|
+
function getCodeBody(code) {
|
221
|
+
const bodyBegIndex = code.indexOf("{");
|
222
|
+
const bodyEndIndex = code.lastIndexOf("}");
|
223
|
+
// +1 to include last '}'
|
224
|
+
return code.substring(bodyBegIndex, bodyEndIndex + 1);
|
225
|
+
}
|
226
|
+
|
227
|
+
function stripEntryPointTags(metroBundle, entryPointMinified) {
|
228
|
+
const entryPointBody = getCodeBody(entryPointMinified);
|
229
|
+
const entryPointBodyWithTags = wrapCodeWithTags(entryPointBody);
|
230
|
+
const metroChunksByEntrypoint = metroBundle.split(entryPointBodyWithTags);
|
231
|
+
// restore entrypoint original code
|
232
|
+
metroChunksByEntrypoint.splice(1, 0, entryPointBody);
|
233
|
+
return metroChunksByEntrypoint.join('');
|
234
|
+
}
|
235
|
+
|
236
|
+
/**
|
237
|
+
* Check if some file is readable
|
238
|
+
* @param {string} path filename path to be tested
|
239
|
+
* @returns {Promise<boolean>} true if readable, otherwise false
|
240
|
+
*/
|
241
|
+
const isFileReadable = (path) => new Promise((resolve) => {
|
242
|
+
fs.access(path, fs.constants.F_OK | fs.constants.R_OK, error => resolve(!error))
|
243
|
+
})
|
244
|
+
|
245
|
+
const addBundleArgsToExcludeList = (chunk, excludeList) => {
|
246
|
+
const regex = /\(([0-9a-zA-Z_,$ ]+)\)[ ]?{$/gm;
|
247
|
+
const m = regex.exec(chunk);
|
248
|
+
if (Array.isArray(m) && m.length > 1) {
|
249
|
+
for (const arg of m[m.length - 1].split(",")) {
|
250
|
+
if (!excludeList.includes(arg.trim())) {
|
251
|
+
excludeList.push(arg.trim());
|
252
|
+
}
|
253
|
+
}
|
254
|
+
return;
|
255
|
+
}
|
256
|
+
|
257
|
+
console.error(`Unable to add global variables to the exclude list.`);
|
258
|
+
process.exit(1);
|
259
|
+
};
|
260
|
+
|
261
|
+
function handleExcludeList(config, {supportsExcludeList, excludeList}) {
|
262
|
+
if (supportsExcludeList) {
|
263
|
+
config.excludeList = excludeList;
|
264
|
+
} else {
|
265
|
+
// add excludeList to gvi in case the api does not support global excludeList
|
266
|
+
if (Array.isArray(config.params)) {
|
267
|
+
const gvi = config.params.find(
|
268
|
+
(param) => param.name === JSCRAMBLER_GLOBAL_VARIABLE_INDIRECTION
|
269
|
+
);
|
270
|
+
if (gvi) {
|
271
|
+
gvi.options = gvi.options || {};
|
272
|
+
const mixedList = [
|
273
|
+
...new Set(excludeList.concat(gvi.options.excludeList || [])),
|
274
|
+
];
|
275
|
+
gvi.options.excludeList = mixedList;
|
276
|
+
}
|
277
|
+
}
|
278
|
+
}
|
279
|
+
}
|
280
|
+
|
281
|
+
function injectTolerateBegninPoisoning(config) {
|
282
|
+
if (Array.isArray(config.params)) {
|
283
|
+
const sd = config.params.find(
|
284
|
+
(param) => param.name === JSCRAMBLER_SELF_DEFENDING
|
285
|
+
);
|
286
|
+
if (sd) {
|
287
|
+
sd.options = sd.options || {};
|
288
|
+
sd.options.options = sd.options.options || [];
|
289
|
+
if (
|
290
|
+
Array.isArray(sd.options.options) &&
|
291
|
+
!sd.options.options.includes(JSCRAMBLER_TOLERATE_BENIGN_POISONING)
|
292
|
+
) {
|
293
|
+
console.log(`info Jscrambler Tolerate benign poisoning option was automatically added to Self-Defending.`);
|
294
|
+
sd.options.options.push(JSCRAMBLER_TOLERATE_BENIGN_POISONING)
|
295
|
+
}
|
296
|
+
}
|
297
|
+
}
|
298
|
+
}
|
299
|
+
|
300
|
+
/**
|
301
|
+
* @param {object} config
|
302
|
+
* @param {string} processedMetroBundle
|
303
|
+
* @returns {boolean} if true the code must start in the first column
|
304
|
+
*/
|
305
|
+
function handleAntiTampering(config, processedMetroBundle) {
|
306
|
+
let requireStartAtFirstColumn = false
|
307
|
+
if (Array.isArray(config.params)) {
|
308
|
+
const antiTampering = config.params.find(
|
309
|
+
(param) => param.name === JSCRAMBLER_ANTI_TAMPERING
|
310
|
+
);
|
311
|
+
if (antiTampering) {
|
312
|
+
antiTampering.options = antiTampering.options || {};
|
313
|
+
antiTampering.options.mode = antiTampering.options.mode || [JSCRAMBLER_ANTI_TAMPERING_MODE_RCK, JSCRAMBLER_ANTI_TAMPERING_MODE_SKL];
|
314
|
+
if (config.enabledHermes) {
|
315
|
+
if (
|
316
|
+
Array.isArray(antiTampering.options.mode) &&
|
317
|
+
antiTampering.options.mode.includes(JSCRAMBLER_ANTI_TAMPERING_MODE_SKL)
|
318
|
+
) {
|
319
|
+
console.log(`info Jscrambler Anti-Tampering Mode SKL can not be used in hermes engine. RCK mode was SET.`);
|
320
|
+
antiTampering.options.mode = [JSCRAMBLER_ANTI_TAMPERING_MODE_RCK];
|
321
|
+
}
|
322
|
+
}
|
323
|
+
|
324
|
+
if (antiTampering.options.mode.includes(JSCRAMBLER_ANTI_TAMPERING_MODE_SKL)) {
|
325
|
+
const singleLineModule = processedMetroBundle.match(RegExp(`\n\\S+${JSCRAMBLER_BEG_ANNOTATION}`, 'm'));
|
326
|
+
if (singleLineModule !== null) {
|
327
|
+
requireStartAtFirstColumn = true;
|
328
|
+
}
|
329
|
+
}
|
330
|
+
}
|
331
|
+
}
|
332
|
+
return requireStartAtFirstColumn;
|
333
|
+
}
|
334
|
+
|
335
|
+
/**
|
336
|
+
* @param {object} config
|
337
|
+
* @returns {boolean} if true 'show source' directive is added
|
338
|
+
*/
|
339
|
+
function addHermesShowSourceDirective(config) {
|
340
|
+
if (!config.enabledHermes) {
|
341
|
+
return false;
|
342
|
+
}
|
343
|
+
|
344
|
+
for (const slugName of JSCRAMBLER_HERMES_ADD_SHOW_SOURCE_DIRECTIVE) {
|
345
|
+
if (Array.isArray(config.params)) {
|
346
|
+
const showSource = config.params.find((param) => param.name === slugName);
|
347
|
+
if (showSource) {
|
348
|
+
return true;
|
349
|
+
}
|
350
|
+
}
|
351
|
+
}
|
352
|
+
|
353
|
+
return false;
|
354
|
+
}
|
355
|
+
|
356
|
+
/**
|
357
|
+
* @param config
|
358
|
+
* @exception {Error} If an incompatible transformation was selected
|
359
|
+
*/
|
360
|
+
function handleHermesIncompatibilities(config) {
|
361
|
+
if (!config.enabledHermes) {
|
362
|
+
return;
|
363
|
+
}
|
364
|
+
|
365
|
+
if (config.codeHardeningThreshold === undefined) {
|
366
|
+
console.log(`info Jscrambler Code Hardening ignored, as it is incompatible with hermes engine.`);
|
367
|
+
}
|
368
|
+
config.codeHardeningThreshold = 999999999;
|
369
|
+
|
370
|
+
for (const {
|
371
|
+
slugName,
|
372
|
+
errorMessage,
|
373
|
+
} of JSCRAMBLER_HERMES_INCOMPATIBILITIES) {
|
374
|
+
if (Array.isArray(config.params)) {
|
375
|
+
const usingIncompatible = config.params.find(
|
376
|
+
(param) => param.name === slugName,
|
377
|
+
);
|
378
|
+
if (usingIncompatible) {
|
379
|
+
throw new Error(errorMessage);
|
380
|
+
}
|
381
|
+
}
|
382
|
+
}
|
383
|
+
}
|
384
|
+
|
385
|
+
module.exports = {
|
386
|
+
buildModuleSourceMap,
|
387
|
+
buildNormalizePath,
|
388
|
+
extractLocs,
|
389
|
+
getBundlePath,
|
390
|
+
isFileReadable,
|
391
|
+
skipObfuscation,
|
392
|
+
stripEntryPointTags,
|
393
|
+
stripJscramblerTags,
|
394
|
+
addBundleArgsToExcludeList,
|
395
|
+
handleExcludeList,
|
396
|
+
injectTolerateBegninPoisoning,
|
397
|
+
handleAntiTampering,
|
398
|
+
addHermesShowSourceDirective,
|
399
|
+
handleHermesIncompatibilities,
|
400
|
+
wrapCodeWithTags
|
401
|
+
};
|
package/package.json
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
{
|
2
|
+
"name": "jscrambler-metro-plugin",
|
3
|
+
"version": "0.0.0-bulbasaur-20250620144942",
|
4
|
+
"description": "A plugin to use metro with Jscrambler Code Integrity",
|
5
|
+
"exports": "./lib/index.js",
|
6
|
+
"peerDependencies": {
|
7
|
+
"metro-source-map": "0.x"
|
8
|
+
},
|
9
|
+
"dependencies": {
|
10
|
+
"commander": "^2.20.0",
|
11
|
+
"fs-extra": "^8.0.1",
|
12
|
+
"jscrambler": "0.0.0-bulbasaur-20250620144942"
|
13
|
+
},
|
14
|
+
"keywords": [
|
15
|
+
"jscrambler",
|
16
|
+
"javascript",
|
17
|
+
"react-native",
|
18
|
+
"metro",
|
19
|
+
"obfuscate",
|
20
|
+
"protect",
|
21
|
+
"js"
|
22
|
+
],
|
23
|
+
"files": [
|
24
|
+
"lib"
|
25
|
+
],
|
26
|
+
"author": "Jscrambler <support@jscrambler.com>",
|
27
|
+
"license": "MIT",
|
28
|
+
"repository": {
|
29
|
+
"type": "git",
|
30
|
+
"url": "https://github.com/jscrambler/jscrambler.git",
|
31
|
+
"directory": "packages/jscrambler-metro-plugin"
|
32
|
+
},
|
33
|
+
"publishConfig": {
|
34
|
+
"access": "public",
|
35
|
+
"registry": "https://registry.npmjs.org/"
|
36
|
+
},
|
37
|
+
"scripts": {
|
38
|
+
"eslint": "eslint lib/",
|
39
|
+
"eslint:fix": "pnpm run eslint --fix"
|
40
|
+
}
|
41
|
+
}
|