• 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 { NameGeneratorType, getNameGenerator } from '../../generator/NameFactory';
53import type { INameGenerator, NameGeneratorOptions } from '../../generator/INameGenerator';
54import { FileUtils } from '../../utils/FileUtils';
55import { NodeUtils } from '../../utils/NodeUtils';
56import { orignalFilePathForSearching } from '../../ArkObfuscator';
57import type { PathAndExtension } from '../../common/type';
58namespace secharmony {
59
60  // global mangled file name table used by all files in a project
61  export let globalFileNameMangledTable: Map<string, string> = undefined;
62
63  // used for file name cache
64  export let historyFileNameMangledTable: Map<string, string> = undefined;
65
66  let profile: IFileNameObfuscationOption | undefined;
67  let generator: INameGenerator | undefined;
68  let reservedFileNames: Set<string> | undefined;
69  /**
70   * Rename Properties Transformer
71   *
72   * @param option obfuscation options
73   */
74  const createRenameFileNameFactory = function (options: IOptions): TransformerFactory<Node> {
75    profile = options?.mRenameFileName;
76    if (!profile || !profile.mEnable) {
77      return null;
78    }
79
80    return renameFileNameFactory;
81
82    function renameFileNameFactory(context: TransformationContext): Transformer<Node> {
83      let options: NameGeneratorOptions = {};
84      if (profile.mNameGeneratorType === NameGeneratorType.HEX) {
85        options.hexWithPrefixSuffix = true;
86      }
87
88      generator = getNameGenerator(profile.mNameGeneratorType, options);
89      let tempReservedFileNameOrPath: string[] = profile?.mReservedFileNames ?? [];
90      let tempReservedFileName: string[] = ['.', '..', ''];
91      tempReservedFileNameOrPath.map(fileNameOrPath => {
92        if (fileNameOrPath && fileNameOrPath.length > 0) {
93          const directories = FileUtils.splitFilePath(fileNameOrPath);
94          directories.forEach(directory => {
95            tempReservedFileName.push(directory);
96            const pathOrExtension: PathAndExtension = FileUtils.getFileSuffix(directory);
97            if (pathOrExtension.ext) {
98              tempReservedFileName.push(pathOrExtension.ext);
99              tempReservedFileName.push(pathOrExtension.path);
100            }
101          });
102        }
103      });
104      reservedFileNames = new Set<string>(tempReservedFileName);
105
106      return renameFileNameTransformer;
107
108      function renameFileNameTransformer(node: Node): Node {
109        if (globalFileNameMangledTable === undefined) {
110          globalFileNameMangledTable = new Map<string, string>();
111        }
112
113        let ret: Node = updateNodeInfo(node);
114        if (isSourceFile(ret)) {
115          const orignalAbsPath = ret.fileName;
116          const mangledAbsPath: string = getMangleCompletePath(orignalAbsPath);
117          ret.fileName = mangledAbsPath;
118        }
119        return setParentRecursive(ret, true);
120      }
121
122      function updateNodeInfo(node: Node): Node {
123        if (isImportDeclaration(node) || isExportDeclaration(node)) {
124          return updateImportOrExportDeclaration(node);
125        }
126
127        if (isImportCall(node)) {
128          return tryUpdateDynamicImport(node);
129        }
130
131        if (isStructDeclaration(node)) {
132          return tryRemoveVirtualConstructor(node);
133        }
134        return visitEachChild(node, updateNodeInfo, context);
135      }
136    }
137  };
138
139  function updateImportOrExportDeclaration(node: ImportDeclaration | ExportDeclaration): ImportDeclaration | ExportDeclaration {
140    if (!node.moduleSpecifier) {
141      return node;
142    }
143    const mangledModuleSpecifier = renameStringLiteral(node.moduleSpecifier as StringLiteral);
144    if (isImportDeclaration(node)) {
145      return factory.updateImportDeclaration(node, node.modifiers, node.importClause, mangledModuleSpecifier as Expression, node.assertClause);
146    } else {
147      return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, mangledModuleSpecifier as Expression,
148        node.assertClause);
149    }
150  }
151
152  function isImportCall(n: Node): n is ImportCall {
153    return n.kind === SyntaxKind.CallExpression && (<CallExpression>n).expression.kind === SyntaxKind.ImportKeyword;
154  }
155
156  // dynamic import example: let module = import('./a')
157  function tryUpdateDynamicImport(node: CallExpression): CallExpression {
158    if (node.expression && node.arguments.length === 1 && isStringLiteral(node.arguments[0])) {
159      const obfuscatedArgument = [renameStringLiteral(node.arguments[0] as StringLiteral)];
160      if (obfuscatedArgument[0] !== node.arguments[0]) {
161        return factory.updateCallExpression(node, node.expression, node.typeArguments, obfuscatedArgument);
162      }
163    }
164    return node;
165  }
166
167  function renameStringLiteral(node: StringLiteral): Expression {
168    let expr: StringLiteral = renameFileName(node) as StringLiteral;
169    if (expr !== node) {
170      return factory.createStringLiteral(expr.text);
171    }
172    return node;
173  }
174
175  function renameFileName(node: StringLiteral): Node {
176    let original: string = '';
177    original = node.text;
178    original = original.replace(/\\/g, '/');
179
180    if (!canBeObfuscatedFilePath(original)) {
181      return node;
182    }
183
184    let mangledFileName: string = getMangleIncompletePath(original);
185    if (mangledFileName === original) {
186      return node;
187    }
188
189    return factory.createStringLiteral(mangledFileName);
190  }
191
192  export function getMangleCompletePath(originalCompletePath: string): string {
193    originalCompletePath = toUnixPath(originalCompletePath);
194    const { path: filePathWithoutSuffix, ext: extension } = FileUtils.getFileSuffix(originalCompletePath);
195    const mangleFilePath = manglFileName(filePathWithoutSuffix);
196    return mangleFilePath + extension;
197  }
198
199  function getMangleIncompletePath(orignalPath: string): string {
200    // Try to concat the extension for orignalPath.
201    const pathAndExtension : PathAndExtension | undefined = tryValidateFileExisting(orignalPath);
202    if (!pathAndExtension) {
203      return orignalPath;
204    }
205
206    if (pathAndExtension.ext) {
207      const mangleFilePath = manglFileName(pathAndExtension.path);
208      return mangleFilePath;
209    }
210    /**
211     * import * from './filename1.js'. We just need to obfuscate 'filename1' and then concat the extension 'js'.
212     * import * from './direcotry'. For the grammar of importing directory, TSC will look for index.ets/index.ts when parsing.
213     * We obfuscate directory name and do not need to concat extension.
214     */
215    const { path: filePathWithoutSuffix, ext: extension } = FileUtils.getFileSuffix(pathAndExtension.path);
216    const mangleFilePath = manglFileName(filePathWithoutSuffix);
217    return mangleFilePath + extension;
218  }
219
220  function manglFileName(orignalPath: string): string {
221    const originalFileNameSegments: string[] = FileUtils.splitFilePath(orignalPath);
222    const mangledSegments: string[] = originalFileNameSegments.map(originalSegment => mangleFileNamePart(originalSegment));
223    let mangledFileName: string = mangledSegments.join('/');
224    return mangledFileName;
225  }
226
227  function mangleFileNamePart(original: string): string {
228    if (reservedFileNames.has(original)) {
229      return original;
230    }
231
232    const historyName: string = historyFileNameMangledTable?.get(original);
233    let mangledName: string = historyName ? historyName : globalFileNameMangledTable.get(original);
234
235    while (!mangledName) {
236      mangledName = generator.getName();
237      if (mangledName === original || reservedFileNames.has(mangledName)) {
238        mangledName = null;
239        continue;
240      }
241
242      let reserved: string[] = [...globalFileNameMangledTable.values()];
243      if (reserved.includes(mangledName)) {
244        mangledName = null;
245        continue;
246      }
247
248      if (historyFileNameMangledTable && [...historyFileNameMangledTable.values()].includes(mangledName)) {
249        mangledName = null;
250        continue;
251      }
252    }
253    globalFileNameMangledTable.set(original, mangledName);
254    return mangledName;
255  }
256
257  export let transformerPlugin: TransformPlugin = {
258    'name': 'renamePropertiesPlugin',
259    'order': (1 << TransformerOrder.RENAME_FILE_NAME_TRANSFORMER),
260    'createTransformerFactory': createRenameFileNameFactory
261  };
262}
263
264export = secharmony;
265
266function canBeObfuscatedFilePath(filePath: string): boolean {
267  return path.isAbsolute(filePath) || FileUtils.isRelativePath(filePath);
268}
269
270// typescript doesn't add the json extension.
271const extensionOrder: string[] = ['.ets', '.ts', '.d.ets', '.d.ts', '.js'];
272
273function tryValidateFileExisting(importPath: string): PathAndExtension | undefined {
274  let fileAbsPath: string = '';
275  if (path.isAbsolute(importPath)) {
276    fileAbsPath = importPath;
277  } else {
278    fileAbsPath = path.join(path.dirname(orignalFilePathForSearching), importPath);
279  }
280
281  const filePathExtensionLess: string = path.normalize(fileAbsPath);
282  for (let ext of extensionOrder) {
283    const targetPath = filePathExtensionLess + ext;
284    if (fs.existsSync(targetPath)) {
285      return {path: importPath, ext: ext};
286    }
287  }
288
289  // all suffixes are not matched, search this file directly.
290  if (fs.existsSync(filePathExtensionLess)) {
291    return { path: importPath, ext: undefined };
292  }
293  return undefined;
294}
295
296function tryRemoveVirtualConstructor(node: StructDeclaration): StructDeclaration {
297  const sourceFile = NodeUtils.getSourceFileOfNode(node);
298  const tempStructMembers: ClassElement[] = [];
299  if (sourceFile && sourceFile.isDeclarationFile && NodeUtils.isInETSFile(sourceFile)) {
300    for (let member of node.members) {
301      // @ts-ignore
302      if (!isConstructorDeclaration(member) || !member.virtual) {
303        tempStructMembers.push(member);
304      }
305    }
306    const structMembersWithVirtualConstructor = factory.createNodeArray(tempStructMembers);
307    return factory.updateStructDeclaration(node, node.modifiers, node.name, node.typeParameters, node.heritageClauses, structMembersWithVirtualConstructor);
308  }
309  return node;
310}
311
312function toUnixPath(data: string): string {
313  if (/^win/.test(require('os').platform())) {
314    const fileTmps: string[] = data.split(path.sep);
315    const newData: string = path.posix.join(...fileTmps);
316    return newData;
317  }
318  return data;
319}