• 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  factory,
18  isStringLiteral,
19  isExportDeclaration,
20  isImportDeclaration,
21  isSourceFile,
22  setParentRecursive,
23  visitEachChild,
24  isStructDeclaration,
25  SyntaxKind,
26  isConstructorDeclaration,
27} from 'typescript';
28
29import type {
30  CallExpression,
31  Expression,
32  ImportDeclaration,
33  ExportDeclaration,
34  Node,
35  StringLiteral,
36  TransformationContext,
37  Transformer,
38  StructDeclaration,
39  SourceFile,
40  ClassElement,
41  ImportCall,
42  TransformerFactory,
43} from 'typescript';
44
45import fs from 'fs';
46import path from 'path';
47
48import type { IOptions } from '../../configs/IOptions';
49import type { TransformPlugin } from '../TransformPlugin';
50import { TransformerOrder } from '../TransformPlugin';
51import type { IFileNameObfuscationOption } from '../../configs/INameObfuscationOption';
52import { OhmUrlStatus } from '../../configs/INameObfuscationOption';
53import { NameGeneratorType, getNameGenerator } from '../../generator/NameFactory';
54import type { INameGenerator, NameGeneratorOptions } from '../../generator/INameGenerator';
55import { FileUtils, BUNDLE, NORMALIZE } from '../../utils/FileUtils';
56import { NodeUtils } from '../../utils/NodeUtils';
57import { orignalFilePathForSearching, performancePrinter, ArkObfuscator } from '../../ArkObfuscator';
58import type { PathAndExtension, ProjectInfo } from '../../common/type';
59import { endFilesEvent, endSingleFileEvent, startFilesEvent, startSingleFileEvent } from '../../utils/PrinterUtils';
60import { EventList, endSingleFileForMoreTimeEvent, startSingleFileForMoreTimeEvent } from '../../utils/PrinterTimeAndMemUtils';
61import { needToBeReserved } from '../../utils/TransformUtil';
62import { MemoryDottingDefine } from '../../utils/MemoryDottingDefine';
63namespace secharmony {
64
65  // global mangled file name table used by all files in a project
66  export let globalFileNameMangledTable: Map<string, string> = new Map<string, string>();
67
68  // used for file name cache
69  export let historyFileNameMangledTable: Map<string, string> = undefined;
70
71  // When the module is compiled, call this function to clear global collections related to file name.
72  export function clearCaches(): void {
73    globalFileNameMangledTable.clear();
74    historyFileNameMangledTable?.clear();
75  }
76
77  let profile: IFileNameObfuscationOption | undefined;
78  let generator: INameGenerator | undefined;
79  let reservedFileNames: Set<string> | undefined;
80  let localPackageSet: Set<string> | undefined;
81  let useNormalized: boolean = false;
82  let universalReservedFileNames: RegExp[] | undefined;
83
84  /**
85   * Rename Properties Transformer
86   *
87   * @param option obfuscation options
88   */
89  const createRenameFileNameFactory = function (options: IOptions): TransformerFactory<Node> | null {
90    startFilesEvent(EventList.FILENAME_OBFUSCATION_INITIALIZATION);
91    profile = options?.mRenameFileName;
92    if (!profile || !profile.mEnable) {
93      endFilesEvent(EventList.FILENAME_OBFUSCATION_INITIALIZATION);
94      return null;
95    }
96
97    let nameGeneratorOption: NameGeneratorOptions = {};
98
99    generator = getNameGenerator(profile.mNameGeneratorType, nameGeneratorOption);
100    let configReservedFileNameOrPath: string[] = profile?.mReservedFileNames ?? [];
101    const tempReservedName: string[] = ['.', '..', ''];
102    configReservedFileNameOrPath.map(fileNameOrPath => {
103      if (!fileNameOrPath || fileNameOrPath.length === 0) {
104        return;
105      }
106      const directories = FileUtils.splitFilePath(fileNameOrPath);
107      directories.forEach(directory => {
108        tempReservedName.push(directory);
109        const pathOrExtension: PathAndExtension = FileUtils.getFileSuffix(directory);
110        if (pathOrExtension.ext) {
111          tempReservedName.push(pathOrExtension.ext);
112          tempReservedName.push(pathOrExtension.path);
113        }
114      });
115    });
116    reservedFileNames = new Set<string>(tempReservedName);
117    universalReservedFileNames = profile?.mUniversalReservedFileNames ?? [];
118    endFilesEvent(EventList.FILENAME_OBFUSCATION_INITIALIZATION);
119    return renameFileNameFactory;
120
121    function renameFileNameFactory(context: TransformationContext): Transformer<Node> {
122      let projectInfo: ProjectInfo = ArkObfuscator.mProjectInfo;
123      if (projectInfo && projectInfo.localPackageSet) {
124        localPackageSet = projectInfo.localPackageSet;
125        useNormalized = projectInfo.useNormalized;
126      }
127
128      return renameFileNameTransformer;
129
130      function renameFileNameTransformer(node: Node): Node {
131        if (globalFileNameMangledTable === undefined) {
132          globalFileNameMangledTable = new Map<string, string>();
133        }
134
135        const recordInfo = ArkObfuscator.recordStage(MemoryDottingDefine.FILENAME_OBFUSCATION);
136        startSingleFileEvent(EventList.FILENAME_OBFUSCATION, performancePrinter.timeSumPrinter);
137        let ret: Node = updateNodeInfo(node);
138        if (!isInOhModules(projectInfo, orignalFilePathForSearching) && isSourceFile(ret)) {
139          const orignalAbsPath = ret.fileName;
140          const mangledAbsPath: string = getMangleCompletePath(orignalAbsPath);
141          ret.fileName = mangledAbsPath;
142        }
143        endSingleFileEvent(EventList.FILENAME_OBFUSCATION, performancePrinter.timeSumPrinter);
144        startSingleFileEvent(EventList.UPDATE_PARENT_NODE);
145        let parentNodes = setParentRecursive(ret, true);
146        endSingleFileEvent(EventList.UPDATE_PARENT_NODE);
147        ArkObfuscator.stopRecordStage(recordInfo);
148        return parentNodes;
149      }
150
151      function updateNodeInfo(node: Node): Node {
152        if (isImportDeclaration(node) || isExportDeclaration(node)) {
153          startSingleFileForMoreTimeEvent(EventList.UPDATE_IMPORT_OR_EXPORT);
154          const updatedNode = updateImportOrExportDeclaration(node);
155          endSingleFileForMoreTimeEvent(EventList.UPDATE_IMPORT_OR_EXPORT);
156          return updatedNode;
157        }
158        if (isImportCall(node)) {
159          startSingleFileForMoreTimeEvent(EventList.UPDATE_DYNAMIC_IMPORT);
160          const tryUpdatedNode = tryUpdateDynamicImport(node);
161          endSingleFileForMoreTimeEvent(EventList.UPDATE_DYNAMIC_IMPORT);
162          return tryUpdatedNode;
163        }
164
165        return visitEachChild(node, updateNodeInfo, context);
166      }
167    }
168  };
169
170  export function isInOhModules(proInfo: ProjectInfo, originalPath: string): boolean {
171    let ohPackagePath: string = '';
172    if (proInfo && proInfo.projectRootPath && proInfo.packageDir) {
173      ohPackagePath = FileUtils.toUnixPath(path.resolve(proInfo.projectRootPath, proInfo.packageDir));
174    }
175    return ohPackagePath && FileUtils.toUnixPath(originalPath).indexOf(ohPackagePath) !== -1;
176  }
177
178  function updateImportOrExportDeclaration(node: ImportDeclaration | ExportDeclaration): ImportDeclaration | ExportDeclaration {
179    if (!node.moduleSpecifier) {
180      return node;
181    }
182    const mangledModuleSpecifier = renameStringLiteral(node.moduleSpecifier as StringLiteral);
183    if (isImportDeclaration(node)) {
184      return factory.updateImportDeclaration(node, node.modifiers, node.importClause, mangledModuleSpecifier as Expression, node.assertClause);
185    } else {
186      return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, mangledModuleSpecifier as Expression,
187        node.assertClause);
188    }
189  }
190
191  export function updateImportOrExportDeclarationForTest(node: ImportDeclaration | ExportDeclaration): ImportDeclaration | ExportDeclaration {
192    return updateImportOrExportDeclaration(node);
193  }
194
195  function isImportCall(n: Node): n is ImportCall {
196    return n.kind === SyntaxKind.CallExpression && (<CallExpression>n).expression.kind === SyntaxKind.ImportKeyword;
197  }
198
199  function canBeObfuscatedFilePath(filePath: string): boolean {
200    return path.isAbsolute(filePath) || FileUtils.isRelativePath(filePath) || isLocalDependencyOhmUrl(filePath);
201  }
202
203  function isLocalDependencyOhmUrl(filePath: string): boolean {
204    // mOhmUrlStatus: for unit test in Arkguard
205    if (profile?.mOhmUrlStatus === OhmUrlStatus.AT_BUNDLE ||
206        profile?.mOhmUrlStatus === OhmUrlStatus.NORMALIZED) {
207      return true;
208    }
209
210    let packageName: string;
211    // Only hap and local har need be mangled.
212    if (useNormalized) {
213      if (!filePath.startsWith(NORMALIZE)) {
214        return false;
215      }
216      packageName = handleNormalizedOhmUrl(filePath, true);
217    } else {
218      if (!filePath.startsWith(BUNDLE)) {
219        return false;
220      }
221      packageName = getAtBundlePgkName(filePath);
222    }
223    return localPackageSet && localPackageSet.has(packageName);
224  }
225
226  export function isLocalDependencyOhmUrlForTest(filePath: string): boolean {
227    return isLocalDependencyOhmUrl(filePath);
228  }
229
230  function getAtBundlePgkName(ohmUrl: string): string {
231    /* Unnormalized OhmUrl Format:
232    * hap/hsp: @bundle:${bundleName}/${moduleName}/
233    * har: @bundle:${bundleName}/${moduleName}@${harName}/
234    * package name is {moduleName} in hap/hsp or {harName} in har.
235    */
236    let moduleName: string = ohmUrl.split('/')[1]; // 1: the index of moduleName in array.
237    const indexOfSign: number = moduleName.indexOf('@');
238    if (indexOfSign !== -1) {
239      moduleName = moduleName.slice(indexOfSign + 1); // 1: the index start from indexOfSign + 1.
240    }
241    return moduleName;
242  }
243
244  // dynamic import example: let module = import('./a')
245  function tryUpdateDynamicImport(node: CallExpression): CallExpression {
246    if (node.expression && node.arguments.length === 1 && isStringLiteral(node.arguments[0])) {
247      const obfuscatedArgument = [renameStringLiteral(node.arguments[0] as StringLiteral)];
248      if (obfuscatedArgument[0] !== node.arguments[0]) {
249        return factory.updateCallExpression(node, node.expression, node.typeArguments, obfuscatedArgument);
250      }
251    }
252    return node;
253  }
254
255  function renameStringLiteral(node: StringLiteral): Expression {
256    let expr: StringLiteral = renameFileName(node) as StringLiteral;
257    if (expr !== node) {
258      return factory.createStringLiteral(expr.text);
259    }
260    return node;
261  }
262
263  function renameFileName(node: StringLiteral): Node {
264    let original: string = '';
265    original = node.text;
266    original = original.replace(/\\/g, '/');
267
268    if (!canBeObfuscatedFilePath(original)) {
269      return node;
270    }
271
272    let mangledFileName: string = getMangleIncompletePath(original);
273    if (mangledFileName === original) {
274      return node;
275    }
276
277    return factory.createStringLiteral(mangledFileName);
278  }
279
280  export function getMangleCompletePath(originalCompletePath: string): string {
281    originalCompletePath = FileUtils.toUnixPath(originalCompletePath);
282    const { path: filePathWithoutSuffix, ext: extension } = FileUtils.getFileSuffix(originalCompletePath);
283    const mangleFilePath = mangleFileName(filePathWithoutSuffix);
284    return mangleFilePath + extension;
285  }
286
287  function getMangleIncompletePath(orignalPath: string): string {
288    // The ohmUrl format does not have file extension
289    if (isLocalDependencyOhmUrl(orignalPath)) {
290      const mangledOhmUrl = mangleOhmUrl(orignalPath);
291      return mangledOhmUrl;
292    }
293
294    // Try to concat the extension for orignalPath.
295    const pathAndExtension : PathAndExtension | undefined = tryValidateFileExisting(orignalPath);
296    if (!pathAndExtension) {
297      return orignalPath;
298    }
299
300    if (pathAndExtension.ext) {
301      const mangleFilePath = mangleFileName(pathAndExtension.path);
302      return mangleFilePath;
303    }
304    /**
305     * import * from './filename1.js'. We just need to obfuscate 'filename1' and then concat the extension 'js'.
306     * import * from './direcotry'. For the grammar of importing directory, TSC will look for index.ets/index.ts when parsing.
307     * We obfuscate directory name and do not need to concat extension.
308     */
309    const { path: filePathWithoutSuffix, ext: extension } = FileUtils.getFileSuffix(pathAndExtension.path);
310    const mangleFilePath = mangleFileName(filePathWithoutSuffix);
311    return mangleFilePath + extension;
312  }
313
314  export function getMangleIncompletePathForTest(orignalPath: string): string {
315    return getMangleIncompletePath(orignalPath);
316  };
317
318  export function mangleOhmUrl(ohmUrl: string): string {
319    let mangledOhmUrl: string;
320    // mOhmUrlStatus: for unit test in Arkguard
321    if (useNormalized || profile?.mOhmUrlStatus === OhmUrlStatus.NORMALIZED) {
322      mangledOhmUrl = handleNormalizedOhmUrl(ohmUrl);
323    } else {
324      /**
325       * OhmUrl Format:
326       * fixed parts in hap/hsp: @bundle:${bundleName}/${moduleName}/
327       * fixed parts in har: @bundle:${bundleName}/${moduleName}@${harName}/
328       * hsp example: @bundle:com.example.myapplication/entry/index
329       * har example: @bundle:com.example.myapplication/entry@library_test/index
330       * we do not mangle fixed parts.
331       */
332      const originalOhmUrlSegments: string[] = FileUtils.splitFilePath(ohmUrl);
333      const prefixSegments: string[] = originalOhmUrlSegments.slice(0, 2); // 2: length of fixed parts in array
334      const urlSegments: string[] = originalOhmUrlSegments.slice(2); // 2: index of mangled parts in array
335      const mangledOhmUrlSegments: string[] = urlSegments.map(originalSegment => mangleFileNamePart(originalSegment));
336      mangledOhmUrl = prefixSegments.join('/') + '/' + mangledOhmUrlSegments.join('/');
337    }
338    return mangledOhmUrl;
339  }
340
341  /**
342   * Normalized OhmUrl Format:
343   * hap/hsp: @normalized:N&<module name>&<bundle name>&<standard import path>&
344   * har: @normalized:N&&<bundle name>&<standard import path>&<version>
345   * we only mangle <standard import path>.
346   */
347  export function handleNormalizedOhmUrl(ohmUrl: string, needPkgName?: boolean): string {
348    let originalOhmUrlSegments: string[] = ohmUrl.split('&');
349    const standardImportPath = originalOhmUrlSegments[3]; // 3: index of standard import path in array.
350    let index = standardImportPath.indexOf('/');
351    // The format of <module name>: @group/packagename or packagename,
352    // and there should only be one '@' symbol and one path separator '/' if and only if the 'group' exists.
353    if (standardImportPath.startsWith('@')) {
354      index = standardImportPath.indexOf('/', index + 1);
355    }
356
357    const pakName = standardImportPath.substring(0, index);
358    if (needPkgName) {
359      return pakName;
360    }
361    const realImportPath = standardImportPath.substring(index + 1); // 1: index of real import path in array.
362    const originalImportPathSegments: string[] = FileUtils.splitFilePath(realImportPath);
363    const mangledImportPathSegments: string[] = originalImportPathSegments.map(originalSegment => mangleFileNamePart(originalSegment));
364    const mangledImportPath: string = pakName + '/' + mangledImportPathSegments.join('/');
365    originalOhmUrlSegments[3] = mangledImportPath; // 3: index of standard import path in array.
366    return originalOhmUrlSegments.join('&');
367  }
368
369  function mangleFileName(orignalPath: string): string {
370    const originalFileNameSegments: string[] = FileUtils.splitFilePath(orignalPath);
371    const mangledSegments: string[] = originalFileNameSegments.map(originalSegment => mangleFileNamePart(originalSegment));
372    let mangledFileName: string = mangledSegments.join('/');
373    return mangledFileName;
374  }
375
376  function mangleFileNamePart(original: string): string {
377    if (needToBeReserved(reservedFileNames, universalReservedFileNames, original)) {
378      return original;
379    }
380
381    const historyName: string = historyFileNameMangledTable?.get(original);
382    let mangledName: string = historyName ? historyName : globalFileNameMangledTable.get(original);
383
384    while (!mangledName) {
385      mangledName = generator.getName();
386      if (mangledName === original || needToBeReserved(reservedFileNames, universalReservedFileNames, mangledName)) {
387        mangledName = null;
388        continue;
389      }
390
391      let reserved: string[] = [...globalFileNameMangledTable.values()];
392      if (reserved.includes(mangledName)) {
393        mangledName = null;
394        continue;
395      }
396
397      if (historyFileNameMangledTable && [...historyFileNameMangledTable.values()].includes(mangledName)) {
398        mangledName = null;
399        continue;
400      }
401    }
402    globalFileNameMangledTable.set(original, mangledName);
403    return mangledName;
404  }
405
406  export let transformerPlugin: TransformPlugin = {
407    'name': 'renamePropertiesPlugin',
408    'order': TransformerOrder.RENAME_FILE_NAME_TRANSFORMER,
409    'createTransformerFactory': createRenameFileNameFactory
410  };
411}
412
413export = secharmony;
414
415// typescript doesn't add the json extension.
416const extensionOrder: string[] = ['.ets', '.ts', '.d.ets', '.d.ts', '.js'];
417
418function tryValidateFileExisting(importPath: string): PathAndExtension | undefined {
419  let fileAbsPath: string = '';
420  if (path.isAbsolute(importPath)) {
421    fileAbsPath = importPath;
422  } else {
423    fileAbsPath = path.join(path.dirname(orignalFilePathForSearching), importPath);
424  }
425
426  const filePathExtensionLess: string = path.normalize(fileAbsPath);
427  for (let ext of extensionOrder) {
428    const targetPath = filePathExtensionLess + ext;
429    if (fs.existsSync(targetPath)) {
430      return {path: importPath, ext: ext};
431    }
432  }
433
434  // all suffixes are not matched, search this file directly.
435  if (fs.existsSync(filePathExtensionLess)) {
436    return { path: importPath, ext: undefined };
437  }
438  return undefined;
439}