• 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} 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');
59
60export {getMapFromJson, readProjectProperties};
61
62type ObfuscationResultType = {
63  content: string,
64  sourceMap?: RawSourceMap,
65  nameCache?: { [k: string]: string }
66};
67
68const JSON_TEXT_INDENT_LENGTH: number = 2;
69export class ArkObfuscator {
70  // A text writer of Printer
71  private mTextWriter: EmitTextWriter;
72
73  // A list of source file path
74  private readonly mSourceFiles: string[];
75
76  // Path of obfuscation configuration file.
77  private readonly mConfigPath: string;
78
79  // Compiler Options for typescript,use to parse ast
80  private readonly mCompilerOptions: CompilerOptions;
81
82  // User custom obfuscation profiles.
83  private mCustomProfiles: IOptions;
84
85  private mTransformers: TransformerFactory<Node>[];
86
87  public constructor(sourceFiles?: string[], configPath?: string) {
88    this.mSourceFiles = sourceFiles;
89    this.mConfigPath = configPath;
90    this.mCompilerOptions = {};
91    this.mTransformers = [];
92  }
93
94  /**
95   * init ArkObfuscator according to user config
96   * should be called after constructor
97   */
98  public init(config?: IOptions): boolean {
99    if (!this.mConfigPath && !config) {
100      return false;
101    }
102
103    if (this.mConfigPath) {
104      config = FileUtils.readFileAsJson(this.mConfigPath);
105    }
106
107    this.mCustomProfiles = config;
108
109    if (this.mCustomProfiles.mCompact) {
110      this.mTextWriter = createObfTextSingleLineWriter();
111    } else {
112      this.mTextWriter = createTextWriter('\n');
113    }
114
115    if (this.mCustomProfiles.mEnableSourceMap) {
116      this.mCompilerOptions.sourceMap = true;
117    }
118
119    // load transformers
120    this.mTransformers = TransformerManager.getInstance().loadTransformers(this.mCustomProfiles);
121
122    if (needReadApiInfo(this.mCustomProfiles)) {
123      this.mCustomProfiles.mNameObfuscation.mReservedProperties = ListUtil.uniqueMergeList(
124        this.mCustomProfiles.mNameObfuscation.mReservedProperties,
125        this.mCustomProfiles.mNameObfuscation.mReservedNames,
126        es6Info);
127    }
128
129    return true;
130  }
131
132  /**
133   * Obfuscate all the source files.
134   */
135  public async obfuscateFiles(): Promise<void> {
136    if (!path.isAbsolute(this.mCustomProfiles.mOutputDir)) {
137      this.mCustomProfiles.mOutputDir = path.join(path.dirname(this.mConfigPath), this.mCustomProfiles.mOutputDir);
138    }
139
140    if (this.mCustomProfiles.mOutputDir && !fs.existsSync(this.mCustomProfiles.mOutputDir)) {
141      fs.mkdirSync(this.mCustomProfiles.mOutputDir);
142    }
143    readProjectProperties(this.mSourceFiles, this.mCustomProfiles);
144    this.readPropertyCache(this.mCustomProfiles.mOutputDir);
145
146    // support directory and file obfuscate
147    for (const sourcePath of this.mSourceFiles) {
148      if (!fs.existsSync(sourcePath)) {
149        console.error(`File ${FileUtils.getFileName(sourcePath)} is not found.`);
150        return;
151      }
152
153      if (fs.lstatSync(sourcePath).isFile()) {
154        await this.obfuscateFile(sourcePath, this.mCustomProfiles.mOutputDir);
155        continue;
156      }
157
158      const dirPrefix: string = FileUtils.getPrefix(sourcePath);
159      await this.obfuscateDir(sourcePath, dirPrefix);
160    }
161
162    this.producePropertyCache(this.mCustomProfiles.mOutputDir);
163  }
164
165  /**
166   * obfuscate directory
167   * @private
168   */
169  private async obfuscateDir(dirName: string, dirPrefix: string): Promise<void> {
170    const currentDir: string = FileUtils.getPathWithoutPrefix(dirName, dirPrefix);
171    const newDir: string = path.join(this.mCustomProfiles.mOutputDir, currentDir);
172    if (!fs.existsSync(newDir)) {
173      fs.mkdirSync(newDir);
174    }
175
176    const fileNames: string[] = fs.readdirSync(dirName);
177    for (let fileName of fileNames) {
178      const filePath: string = path.join(dirName, fileName);
179      if (fs.lstatSync(filePath).isFile()) {
180        await this.obfuscateFile(filePath, newDir);
181        continue;
182      }
183
184      if (fileName === 'node_modules' || fileName === 'oh_modules') {
185        continue;
186      }
187
188      await this.obfuscateDir(filePath, dirPrefix);
189    }
190  }
191
192  private readNameCache(sourceFile: string, outputDir: string): void {
193    if (!this.mCustomProfiles.mNameObfuscation.mEnable || !this.mCustomProfiles.mEnableNameCache) {
194      return;
195    }
196
197    const nameCachePath: string = path.join(outputDir, FileUtils.getFileName(sourceFile) + NAME_CACHE_SUFFIX);
198    const nameCache: Object = readCache(nameCachePath);
199
200    renameIdentifierModule.historyNameCache = getMapFromJson(nameCache);
201  }
202
203  private readPropertyCache(outputDir: string): void {
204    if (!this.mCustomProfiles.mNameObfuscation.mRenameProperties || !this.mCustomProfiles.mEnableNameCache) {
205      return;
206    }
207
208    const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE);
209    const propertyCache: Object = readCache(propertyCachePath);
210    if (!propertyCache) {
211      return;
212    }
213
214    renamePropertyModule.historyMangledTable = getMapFromJson(propertyCache);
215  }
216
217  private produceNameCache(namecache: { [k: string]: string }, sourceFile: string, outputDir: string): void {
218    const nameCachePath: string = path.join(outputDir, FileUtils.getFileName(sourceFile) + NAME_CACHE_SUFFIX);
219    fs.writeFileSync(nameCachePath, JSON.stringify(namecache, null, JSON_TEXT_INDENT_LENGTH));
220  }
221
222  private producePropertyCache(outputDir: string): void {
223    if (this.mCustomProfiles.mNameObfuscation.mRenameProperties && this.mCustomProfiles.mEnableNameCache) {
224      const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE);
225      writeCache(renamePropertyModule.globalMangledTable, propertyCachePath);
226    }
227  }
228
229  async mergeSourceMap(originMap: sourceMap.RawSourceMap, newMap: sourceMap.RawSourceMap): Promise<RawSourceMap> {
230    if (!originMap) {
231      return newMap as RawSourceMap;
232    }
233
234    if (!newMap) {
235      return originMap as RawSourceMap;
236    }
237
238    const originConsumer: sourceMap.SourceMapConsumer = await new sourceMap.SourceMapConsumer(originMap);
239    const newConsumer: sourceMap.SourceMapConsumer = await new sourceMap.SourceMapConsumer(newMap);
240    const newMappingList: sourceMap.MappingItem[] = [];
241    newConsumer.eachMapping((mapping: sourceMap.MappingItem) => {
242      if (mapping.originalLine == null) {
243        return;
244      }
245
246      const originalPos = originConsumer.originalPositionFor({
247        line: mapping.originalLine,
248        column: mapping.originalColumn
249      });
250
251      if (originalPos.source == null) {
252        return;
253      }
254
255      mapping.originalLine = originalPos.line;
256      mapping.originalColumn = originalPos.column;
257      newMappingList.push(mapping);
258    });
259
260    const updatedGenerator: sourceMap.SourceMapGenerator = sourceMap.SourceMapGenerator.fromSourceMap(newConsumer);
261    updatedGenerator['_file'] = originMap.file;
262    updatedGenerator['_mappings']['_array'] = newMappingList;
263    return JSON.parse(updatedGenerator.toString()) as RawSourceMap;
264  }
265
266  /**
267   * A Printer to output obfuscated codes.
268   */
269  public createObfsPrinter(): Printer {
270    // set print options
271    let printerOptions: PrinterOptions = {};
272    if (this.mCustomProfiles.mRemoveComments) {
273      printerOptions.removeComments = true;
274    }
275
276    return createPrinter(printerOptions);
277  }
278
279  private isObfsIgnoreFile(fileName: string): boolean {
280    let suffix: string = FileUtils.getFileExtension(fileName);
281
282    return (suffix !== 'js' && suffix !== 'ts') || fileName.endsWith('.d.ts');
283  }
284
285  /**
286   * Obfuscate single source file with path provided
287   *
288   * @param sourceFilePath single source file path
289   * @param outputDir
290   */
291  public async obfuscateFile(sourceFilePath: string, outputDir: string): Promise<void> {
292    const fileName: string = FileUtils.getFileName(sourceFilePath);
293    if (this.isObfsIgnoreFile(fileName)) {
294      fs.copyFileSync(sourceFilePath, path.join(outputDir, fileName));
295      return;
296    }
297
298    let content: string = FileUtils.readFile(sourceFilePath);
299    this.readNameCache(sourceFilePath, outputDir);
300    const mixedInfo: ObfuscationResultType = await this.obfuscate(content, sourceFilePath);
301
302    if (outputDir && mixedInfo) {
303      fs.writeFileSync(path.join(outputDir, FileUtils.getFileName(sourceFilePath)), mixedInfo.content);
304      if (this.mCustomProfiles.mEnableSourceMap && mixedInfo.sourceMap) {
305        fs.writeFileSync(path.join(outputDir, FileUtils.getFileName(sourceFilePath) + '.map'),
306          JSON.stringify(mixedInfo.sourceMap, null, JSON_TEXT_INDENT_LENGTH));
307      }
308      if (this.mCustomProfiles.mEnableNameCache && this.mCustomProfiles.mEnableNameCache) {
309        this.produceNameCache(mixedInfo.nameCache, sourceFilePath, outputDir);
310      }
311    }
312  }
313
314  /**
315   * Obfuscate ast of a file.
316   * @param content ast or source code of a source file
317   * @param sourceFilePath
318   * @param previousStageSourceMap
319   * @param historyNameCache
320   */
321  public async obfuscate(content: SourceFile | string, sourceFilePath: string, previousStageSourceMap?: sourceMap.RawSourceMap,
322    historyNameCache?: Map<string, string>): Promise<ObfuscationResultType> {
323    let ast: SourceFile;
324    let result: ObfuscationResultType = { content: undefined };
325    if (this.isObfsIgnoreFile(sourceFilePath)) {
326      // need add return value
327      return result;
328    }
329
330    if (typeof content === 'string') {
331      ast = createSourceFile(sourceFilePath, content, ScriptTarget.ES2015, true);
332    } else {
333      ast = content;
334    }
335
336    if (ast.statements.length === 0) {
337      return result;
338    }
339
340    if (historyNameCache && this.mCustomProfiles.mNameObfuscation) {
341      renameIdentifierModule.historyNameCache = historyNameCache;
342    }
343
344    let transformedResult: TransformationResult<Node> = transform(ast, this.mTransformers, this.mCompilerOptions);
345    ast = transformedResult.transformed[0] as SourceFile;
346
347    // convert ast to output source file and generate sourcemap if needed.
348    let sourceMapGenerator: SourceMapGenerator = undefined;
349    if (this.mCustomProfiles.mEnableSourceMap) {
350      sourceMapGenerator = getSourceMapGenerator(sourceFilePath);
351    }
352
353    this.createObfsPrinter().writeFile(ast, this.mTextWriter, sourceMapGenerator);
354
355    result.content = this.mTextWriter.getText();
356
357    if (this.mCustomProfiles.mEnableSourceMap && sourceMapGenerator) {
358      let sourceMapJson: RawSourceMap = sourceMapGenerator.toJSON();
359      sourceMapJson.sourceRoot = '';
360      sourceMapJson.file = path.basename(sourceFilePath);
361      if (previousStageSourceMap) {
362        sourceMapJson = await this.mergeSourceMap(previousStageSourceMap, sourceMapJson as sourceMap.RawSourceMap);
363      }
364      result.sourceMap = sourceMapJson;
365    }
366
367    if (this.mCustomProfiles.mEnableNameCache) {
368      result.nameCache = Object.fromEntries(renameIdentifierModule.nameCache);
369    }
370
371    // clear cache of text writer
372    this.mTextWriter.clear();
373    if (renameIdentifierModule.nameCache) {
374      renameIdentifierModule.nameCache.clear();
375    }
376    return result;
377  }
378}
379export {ApiExtractor};
380