• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import {
17  createPrinter,
18  createSourceFile, createTextWriter,
19  ScriptTarget,
20  transform,
21  createObfTextSingleLineWriter,
22} from 'typescript';
23
24import type {
25  CompilerOptions,
26  EmitTextWriter,
27  Node,
28  Printer,
29  PrinterOptions,
30  RawSourceMap,
31  SourceFile,
32  SourceMapGenerator,
33  TransformationResult,
34  TransformerFactory,
35} from 'typescript';
36
37import * as fs from 'fs';
38import path from 'path';
39import sourceMap from 'source-map';
40
41import type {IOptions} from './configs/IOptions';
42import {FileUtils} from './utils/FileUtils';
43import {TransformerManager} from './transformers/TransformerManager';
44import {getSourceMapGenerator} from './utils/SourceMapUtil';
45
46import {
47  getMapFromJson,
48  NAME_CACHE_SUFFIX,
49  PROPERTY_CACHE_FILE,
50  readCache, writeCache
51} from './utils/NameCacheUtil';
52import {ListUtil} from './utils/ListUtil';
53import {needReadApiInfo, readProjectProperties, readProjectPropertiesByCollectedPaths} from './common/ApiReader';
54import {ApiExtractor} from './common/ApiExtractor';
55import es6Info from './configs/preset/es6_reserved_properties.json';
56
57export const renameIdentifierModule = require('./transformers/rename/RenameIdentifierTransformer');
58export const renamePropertyModule = require('./transformers/rename/RenamePropertiesTransformer');
59export const renameFileNameModule = require('./transformers/rename/RenameFileNameTransformer');
60
61export {getMapFromJson, readProjectPropertiesByCollectedPaths};
62export let orignalFilePathForSearching: string | undefined;
63
64type ObfuscationResultType = {
65  content: string,
66  sourceMap?: RawSourceMap,
67  nameCache?: { [k: string]: string },
68  filePath?: string
69};
70
71const JSON_TEXT_INDENT_LENGTH: number = 2;
72export class ArkObfuscator {
73  // A text writer of Printer
74  private mTextWriter: EmitTextWriter;
75
76  // A list of source file path
77  private readonly mSourceFiles: string[];
78
79  // Path of obfuscation configuration file.
80  private readonly mConfigPath: string;
81
82  // Compiler Options for typescript,use to parse ast
83  private readonly mCompilerOptions: CompilerOptions;
84
85  // User custom obfuscation profiles.
86  private mCustomProfiles: IOptions;
87
88  private mTransformers: TransformerFactory<Node>[];
89
90  public constructor(sourceFiles?: string[], configPath?: string) {
91    this.mSourceFiles = sourceFiles;
92    this.mConfigPath = configPath;
93    this.mCompilerOptions = {};
94    this.mTransformers = [];
95  }
96
97  public addReservedProperties(newReservedProperties: string[]) {
98    if (newReservedProperties.length === 0) {
99      return;
100    }
101    const nameObfuscationConfig = this.mCustomProfiles.mNameObfuscation;
102    nameObfuscationConfig.mReservedProperties = ListUtil.uniqueMergeList(newReservedProperties,
103      nameObfuscationConfig?.mReservedProperties);
104  }
105
106  public addReservedNames(newReservedNames: string[]) {
107    if (newReservedNames.length === 0) {
108      return;
109    }
110    const nameObfuscationConfig = this.mCustomProfiles.mNameObfuscation;
111    nameObfuscationConfig.mReservedNames = ListUtil.uniqueMergeList(newReservedNames,
112      nameObfuscationConfig?.mReservedNames);
113  }
114  /**
115   * init ArkObfuscator according to user config
116   * should be called after constructor
117   */
118  public init(config?: IOptions): boolean {
119    if (!this.mConfigPath && !config) {
120      console.error('obfuscation config file is not found and no given config.');
121      return false;
122    }
123
124    if (this.mConfigPath) {
125      config = FileUtils.readFileAsJson(this.mConfigPath);
126    }
127
128    this.mCustomProfiles = config;
129
130    if (this.mCustomProfiles.mCompact) {
131      this.mTextWriter = createObfTextSingleLineWriter();
132    } else {
133      this.mTextWriter = createTextWriter('\n');
134    }
135
136    if (this.mCustomProfiles.mEnableSourceMap) {
137      this.mCompilerOptions.sourceMap = true;
138    }
139
140    // load transformers
141    this.mTransformers = TransformerManager.getInstance(this.mCustomProfiles).getTransformers();
142
143    if (needReadApiInfo(this.mCustomProfiles)) {
144      this.mCustomProfiles.mNameObfuscation.mReservedProperties = ListUtil.uniqueMergeList(
145        this.mCustomProfiles.mNameObfuscation.mReservedProperties,
146        this.mCustomProfiles.mNameObfuscation.mReservedNames,
147        es6Info);
148    }
149
150    return true;
151  }
152
153  /**
154   * Obfuscate all the source files.
155   */
156  public async obfuscateFiles(): Promise<void> {
157    if (!path.isAbsolute(this.mCustomProfiles.mOutputDir)) {
158      this.mCustomProfiles.mOutputDir = path.join(path.dirname(this.mConfigPath), this.mCustomProfiles.mOutputDir);
159    }
160    if (this.mCustomProfiles.mOutputDir && !fs.existsSync(this.mCustomProfiles.mOutputDir)) {
161      fs.mkdirSync(this.mCustomProfiles.mOutputDir);
162    }
163
164    readProjectProperties(this.mSourceFiles, this.mCustomProfiles);
165    this.readPropertyCache(this.mCustomProfiles.mOutputDir);
166
167    // support directory and file obfuscate
168    for (const sourcePath of this.mSourceFiles) {
169      if (!fs.existsSync(sourcePath)) {
170        console.error(`File ${FileUtils.getFileName(sourcePath)} is not found.`);
171        return;
172      }
173
174      if (fs.lstatSync(sourcePath).isFile()) {
175        await this.obfuscateFile(sourcePath, this.mCustomProfiles.mOutputDir);
176        continue;
177      }
178
179      const dirPrefix: string = FileUtils.getPrefix(sourcePath);
180      await this.obfuscateDir(sourcePath, dirPrefix);
181    }
182
183    this.producePropertyCache(this.mCustomProfiles.mOutputDir);
184  }
185
186  /**
187   * obfuscate directory
188   * @private
189   */
190  private async obfuscateDir(dirName: string, dirPrefix: string): Promise<void> {
191    const currentDir: string = FileUtils.getPathWithoutPrefix(dirName, dirPrefix);
192    let newDir: string = this.mCustomProfiles.mOutputDir;
193    // there is no need to create directory because the directory names will be obfuscated.
194    if (!this.mCustomProfiles.mRenameFileName?.mEnable) {
195      newDir = path.join(this.mCustomProfiles.mOutputDir, currentDir);
196      if (!fs.existsSync(newDir)) {
197        fs.mkdirSync(newDir);
198      }
199    }
200
201    const fileNames: string[] = fs.readdirSync(dirName);
202    for (let fileName of fileNames) {
203      const filePath: string = path.join(dirName, fileName);
204      if (fs.lstatSync(filePath).isFile()) {
205        await this.obfuscateFile(filePath, newDir);
206        continue;
207      }
208
209      await this.obfuscateDir(filePath, dirPrefix);
210    }
211  }
212
213  private readNameCache(sourceFile: string, outputDir: string): void {
214    if (!this.mCustomProfiles.mNameObfuscation?.mEnable || !this.mCustomProfiles.mEnableNameCache) {
215      return;
216    }
217
218    const nameCachePath: string = path.join(outputDir, FileUtils.getFileName(sourceFile) + NAME_CACHE_SUFFIX);
219    const nameCache: Object = readCache(nameCachePath);
220
221    renameIdentifierModule.historyNameCache = getMapFromJson(nameCache);
222  }
223
224  private readPropertyCache(outputDir: string): void {
225    if (!this.mCustomProfiles.mNameObfuscation?.mRenameProperties || !this.mCustomProfiles.mEnableNameCache) {
226      return;
227    }
228
229    const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE);
230    const propertyCache: Object = readCache(propertyCachePath);
231    if (!propertyCache) {
232      return;
233    }
234
235    renamePropertyModule.historyMangledTable = getMapFromJson(propertyCache);
236  }
237
238  private produceNameCache(namecache: { [k: string]: string }, resultPath: string): void {
239    const nameCachePath: string = resultPath + NAME_CACHE_SUFFIX;
240    fs.writeFileSync(nameCachePath, JSON.stringify(namecache, null, JSON_TEXT_INDENT_LENGTH));
241  }
242
243  private producePropertyCache(outputDir: string): void {
244    if (this.mCustomProfiles.mNameObfuscation &&
245      this.mCustomProfiles.mNameObfuscation.mRenameProperties &&
246      this.mCustomProfiles.mEnableNameCache) {
247      const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE);
248      writeCache(renamePropertyModule.globalMangledTable, propertyCachePath);
249    }
250  }
251
252  async mergeSourceMap(originMap: sourceMap.RawSourceMap, newMap: sourceMap.RawSourceMap): Promise<RawSourceMap> {
253    if (!originMap) {
254      return newMap as RawSourceMap;
255    }
256
257    if (!newMap) {
258      return originMap as RawSourceMap;
259    }
260
261    const originConsumer: sourceMap.SourceMapConsumer = await new sourceMap.SourceMapConsumer(originMap);
262    const newConsumer: sourceMap.SourceMapConsumer = await new sourceMap.SourceMapConsumer(newMap);
263    const newMappingList: sourceMap.MappingItem[] = [];
264    newConsumer.eachMapping((mapping: sourceMap.MappingItem) => {
265      if (mapping.originalLine == null) {
266        return;
267      }
268
269      const originalPos = originConsumer.originalPositionFor({
270        line: mapping.originalLine,
271        column: mapping.originalColumn
272      });
273
274      if (originalPos.source == null) {
275        return;
276      }
277
278      mapping.originalLine = originalPos.line;
279      mapping.originalColumn = originalPos.column;
280      newMappingList.push(mapping);
281    });
282
283    const updatedGenerator: sourceMap.SourceMapGenerator = sourceMap.SourceMapGenerator.fromSourceMap(newConsumer);
284    updatedGenerator['_file'] = originMap.file;
285    updatedGenerator['_mappings']['_array'] = newMappingList;
286    return JSON.parse(updatedGenerator.toString()) as RawSourceMap;
287  }
288
289  /**
290   * A Printer to output obfuscated codes.
291   */
292  public createObfsPrinter(isDeclarationFile: boolean): Printer {
293    // set print options
294    let printerOptions: PrinterOptions = {};
295    let removeOption = this.mCustomProfiles.mRemoveDeclarationComments;
296    let keepDeclarationComments = !removeOption || !removeOption.mEnable || (removeOption.mReservedComments && removeOption.mReservedComments.length > 0);
297
298    if (isDeclarationFile && keepDeclarationComments) {
299      printerOptions.removeComments = false;
300    }
301    if ((!isDeclarationFile && this.mCustomProfiles.mRemoveComments) || (isDeclarationFile && !keepDeclarationComments)) {
302      printerOptions.removeComments = true;
303    }
304
305    return createPrinter(printerOptions);
306  }
307
308  private isObfsIgnoreFile(fileName: string): boolean {
309    let suffix: string = FileUtils.getFileExtension(fileName);
310
311    return (suffix !== 'js' && suffix !== 'ts' && suffix !== 'ets');
312  }
313
314  /**
315   * Obfuscate single source file with path provided
316   *
317   * @param sourceFilePath single source file path
318   * @param outputDir
319   */
320  public async obfuscateFile(sourceFilePath: string, outputDir: string): Promise<void> {
321    const fileName: string = FileUtils.getFileName(sourceFilePath);
322    if (this.isObfsIgnoreFile(fileName)) {
323      fs.copyFileSync(sourceFilePath, path.join(outputDir, fileName));
324      return;
325    }
326
327    // Add the whitelist of file name obfuscation for ut.
328    if (this.mCustomProfiles.mRenameFileName?.mEnable) {
329      this.mCustomProfiles.mRenameFileName.mReservedFileNames.push(this.mConfigPath);
330    }
331    let content: string = FileUtils.readFile(sourceFilePath);
332    this.readNameCache(sourceFilePath, outputDir);
333    const mixedInfo: ObfuscationResultType = await this.obfuscate(content, sourceFilePath);
334
335    if (outputDir && mixedInfo) {
336      // the writing file is for the ut.
337      const testCasesRootPath = path.join(__dirname, '../', 'test/grammar');
338      let relativePath = '';
339      let resultPath = '';
340      if (this.mCustomProfiles.mRenameFileName?.mEnable && mixedInfo.filePath) {
341        relativePath = mixedInfo.filePath.replace(testCasesRootPath, '');
342      } else {
343        relativePath = sourceFilePath.replace(testCasesRootPath, '');
344      }
345      resultPath = path.join(this.mCustomProfiles.mOutputDir, relativePath);
346      fs.mkdirSync(path.dirname(resultPath), {recursive: true});
347      fs.writeFileSync(resultPath, mixedInfo.content);
348
349      if (this.mCustomProfiles.mEnableSourceMap && mixedInfo.sourceMap) {
350        fs.writeFileSync(path.join(resultPath + '.map'),
351          JSON.stringify(mixedInfo.sourceMap, null, JSON_TEXT_INDENT_LENGTH));
352      }
353
354      if (this.mCustomProfiles.mEnableNameCache && this.mCustomProfiles.mEnableNameCache) {
355        this.produceNameCache(mixedInfo.nameCache, resultPath);
356      }
357    }
358  }
359
360  /**
361   * Obfuscate ast of a file.
362   * @param content ast or source code of a source file
363   * @param sourceFilePath
364   * @param previousStageSourceMap
365   * @param historyNameCache
366   * @param originalFilePath When filename obfuscation is enabled, it is used as the source code path.
367   */
368  public async obfuscate(content: SourceFile | string, sourceFilePath: string, previousStageSourceMap?: sourceMap.RawSourceMap,
369    historyNameCache?: Map<string, string>, originalFilePath?: string): Promise<ObfuscationResultType> {
370    let ast: SourceFile;
371    let result: ObfuscationResultType = { content: undefined };
372    if (this.isObfsIgnoreFile(sourceFilePath)) {
373      // need add return value
374      return result;
375    }
376
377    if (typeof content === 'string') {
378      ast = createSourceFile(sourceFilePath, content, ScriptTarget.ES2015, true);
379    } else {
380      ast = content;
381    }
382
383    if (ast.statements.length === 0) {
384      return result;
385    }
386
387    if (historyNameCache && this.mCustomProfiles.mNameObfuscation) {
388      renameIdentifierModule.historyNameCache = historyNameCache;
389    }
390
391    if (this.mCustomProfiles.mRenameFileName?.mEnable ) {
392      orignalFilePathForSearching = originalFilePath ? originalFilePath : ast.fileName;
393    }
394
395    if (!this.mCustomProfiles.mRemoveDeclarationComments || !this.mCustomProfiles.mRemoveDeclarationComments.mEnable) {
396      //@ts-ignore
397      ast.reservedComments = undefined;
398    } else {
399      //@ts-ignore
400      ast.reservedComments ??= this.mCustomProfiles.mRemoveDeclarationComments.mReservedComments ?
401        this.mCustomProfiles.mRemoveDeclarationComments.mReservedComments : [];
402    }
403
404    let transformedResult: TransformationResult<Node> = transform(ast, this.mTransformers, this.mCompilerOptions);
405    ast = transformedResult.transformed[0] as SourceFile;
406
407    // convert ast to output source file and generate sourcemap if needed.
408    let sourceMapGenerator: SourceMapGenerator = undefined;
409    if (this.mCustomProfiles.mEnableSourceMap) {
410      sourceMapGenerator = getSourceMapGenerator(sourceFilePath);
411    }
412
413    this.createObfsPrinter(ast.isDeclarationFile).writeFile(ast, this.mTextWriter, sourceMapGenerator);
414
415    result.filePath = ast.fileName;
416    result.content = this.mTextWriter.getText();
417
418    if (this.mCustomProfiles.mEnableSourceMap && sourceMapGenerator) {
419      let sourceMapJson: RawSourceMap = sourceMapGenerator.toJSON();
420      sourceMapJson.sourceRoot = '';
421      sourceMapJson.file = path.basename(sourceFilePath);
422      if (previousStageSourceMap) {
423        sourceMapJson = await this.mergeSourceMap(previousStageSourceMap, sourceMapJson as sourceMap.RawSourceMap);
424      }
425      result.sourceMap = sourceMapJson;
426    }
427
428    if (this.mCustomProfiles.mEnableNameCache && renameIdentifierModule.nameCache) {
429      result.nameCache = Object.fromEntries(renameIdentifierModule.nameCache);
430    }
431
432    // clear cache of text writer
433    this.mTextWriter.clear();
434    if (renameIdentifierModule.nameCache) {
435      renameIdentifierModule.nameCache.clear();
436    }
437
438    renameIdentifierModule.historyNameCache = undefined;
439    return result;
440  }
441}
442
443export {ApiExtractor};
444