• 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 fs from "fs";
17import path from "path";
18import {
19  ApiExtractor,
20  renamePropertyModule,
21  getMapFromJson
22} from "arkguard";
23import { identifierCaches } from "../../../ark_utils";
24
25/* ObConfig's properties:
26 *   ruleOptions: {
27*     enable: boolean
28 *    rules: string[]
29 *   }
30 *   consumerRules: string[]
31 *
32 * ObfuscationConfig's properties:
33 *   selfConfig: ObConfig
34 *   dependencies: { libraries: ObConfig[], hars: string[] }
35 *   sdkApis: string[]
36 *   obfuscationCacheDir: string
37 *   exportRulePath: string
38 */
39
40enum OptionType {
41  NONE,
42  KEEP_DTS,
43  KEEP_GLOBAL_NAME,
44  KEEP_PROPERTY_NAME,
45  DISABLE_OBFUSCATION,
46  ENABLE_PROPERTY_OBFUSCATION,
47  ENABLE_STRING_PROPERTY_OBFUSCATION,
48  ENABLE_TOPLEVEL_OBFUSCATION,
49  COMPACT,
50  REMOVE_LOG,
51  PRINT_NAMECACHE,
52  APPLY_NAMECACHE,
53}
54
55function isFileExist(filePath: string): boolean {
56  let exist = false;
57  try {
58    fs.accessSync(filePath, fs.constants.F_OK);
59  } catch (err) {
60    exist = !err;
61  }
62  return exist;
63}
64
65function sortAndDeduplicateStringArr(arr: string[]) {
66  if (arr.length == 0) {
67    return arr;
68  }
69
70  arr.sort((a, b) => {
71    return a.localeCompare(b);
72  });
73
74  let tmpArr: string[] = [arr[0]];
75  for (let i = 1; i < arr.length; i++) {
76    if (arr[i] != arr[i - 1]) {
77      tmpArr.push(arr[i]);
78    }
79  }
80  return tmpArr;
81}
82
83class ObOptions {
84  disableObfuscation: boolean = false;
85  enablePropertyObfuscation: boolean = false;
86  enableStringPropertyObfuscation: boolean = false;
87  enableToplevelObfuscation: boolean = false;
88  compact: boolean = false;
89  removeLog: boolean = false;
90  printNameCache: string = '';
91  applyNameCache: string = '';
92
93  merge(other: ObOptions) {
94    this.disableObfuscation = this.disableObfuscation || other.disableObfuscation;
95    this.enablePropertyObfuscation = this.enablePropertyObfuscation || other.enablePropertyObfuscation;
96    this.enableToplevelObfuscation = this.enableToplevelObfuscation || other.enableToplevelObfuscation;
97    this.enableStringPropertyObfuscation = this.enableStringPropertyObfuscation || other.enableStringPropertyObfuscation;
98    this.compact = this.compact || other.compact;
99    this.removeLog = this.removeLog || other.removeLog;
100    if (other.printNameCache.length > 0) {
101      this.printNameCache = other.printNameCache;
102    }
103    if (other.applyNameCache.length > 0) {
104      this.applyNameCache = other.applyNameCache;
105    }
106  }
107}
108
109export class MergedConfig {
110  options: ObOptions = new ObOptions();
111  reservedPropertyNames: string[] = [];
112  reservedNames: string[] = [];
113
114  merge(other: MergedConfig) {
115    this.options.merge(other.options);
116    this.reservedPropertyNames.push(...other.reservedPropertyNames);
117    this.reservedNames.push(...other.reservedNames);
118  }
119
120  sortAndDeduplicate() {
121    this.reservedPropertyNames = sortAndDeduplicateStringArr(
122      this.reservedPropertyNames
123    );
124    this.reservedNames = sortAndDeduplicateStringArr(this.reservedNames);
125  }
126
127  serializeMergedConfig(): string {
128    let resultStr: string = "";
129    const keys = Object.keys(this.options);
130    for (const key of keys) {
131      // skip printNameCache and applyNameCache
132      if (this.options[key] === true && ObConfigResolver.optionsMap.has(String(key))) {
133        resultStr += ObConfigResolver.optionsMap.get(String(key)) + "\n";
134      }
135    }
136
137    if (this.reservedNames.length > 0) {
138      resultStr += ObConfigResolver.KEEP_GLOBAL_NAME + "\n";
139      this.reservedNames.forEach((item) => {
140      resultStr += item + "\n";
141      });
142    }
143    if (this.reservedPropertyNames.length > 0) {
144      resultStr += ObConfigResolver.KEEP_PROPERTY_NAME + "\n";
145      this.reservedPropertyNames.forEach((item) => {
146      resultStr += item + "\n";
147      });
148    }
149    return resultStr;
150  }
151}
152
153
154export class ObConfigResolver {
155  sourceObConfig: any;
156  logger: any;
157  isHarCompiled: boolean | undefined;
158  isTerser: boolean;
159
160  constructor(projectConfig: any, logger: any, isTerser?: boolean) {
161    this.sourceObConfig = projectConfig.obfuscationOptions;
162    this.logger = logger;
163    this.isHarCompiled = projectConfig.compileHar;
164    this.isTerser = isTerser;
165  }
166
167  public resolveObfuscationConfigs(): MergedConfig {
168    let sourceObConfig = this.sourceObConfig;
169    if (!sourceObConfig) {
170      return new MergedConfig();
171    }
172    let enableObfuscation: boolean = sourceObConfig.selfConfig.ruleOptions.enable;
173
174    let selfConfig: MergedConfig = new MergedConfig();
175    if (enableObfuscation) {
176      this.getSelfConfigs(selfConfig);
177      enableObfuscation = !selfConfig.options.disableObfuscation;
178    } else {
179      selfConfig.options.disableObfuscation = true;
180    }
181
182    let needConsumerConfigs: boolean = this.isHarCompiled && sourceObConfig.selfConfig.consumerRules &&
183      sourceObConfig.selfConfig.consumerRules.length > 0;
184    let needDependencyConfigs: boolean = enableObfuscation || needConsumerConfigs;
185
186    let dependencyConfigs: MergedConfig = new MergedConfig();
187    const dependencyMaxLength: number = Math.max(sourceObConfig.dependencies.libraries.length, sourceObConfig.dependencies.hars.length)
188    if (needDependencyConfigs && dependencyMaxLength > 0) {
189      dependencyConfigs = new MergedConfig();
190      this.getDependencyConfigs(sourceObConfig, dependencyConfigs);
191      enableObfuscation = enableObfuscation && !dependencyConfigs.options.disableObfuscation;
192    }
193    const mergedConfigs: MergedConfig = this.getMergedConfigs(selfConfig, dependencyConfigs);
194
195    if (enableObfuscation && mergedConfigs.options.enablePropertyObfuscation) {
196      const systemApiCachePath: string = path.join(sourceObConfig.obfuscationCacheDir, "systemApiCache.json");
197      if (isFileExist(systemApiCachePath)) {
198        this.getSystemApiConfigsByCache(selfConfig, systemApiCachePath);
199      } else {
200        this.getSystemApiCache(selfConfig, systemApiCachePath);
201      }
202    }
203
204    if (needConsumerConfigs) {
205      let selfConsumerConfig = new MergedConfig();
206      this.getSelfConsumerConfig(selfConsumerConfig);
207      this.genConsumerConfigFiles(sourceObConfig, selfConsumerConfig, dependencyConfigs);
208    }
209
210    return mergedConfigs;
211  }
212
213  private getSelfConfigs(selfConfigs: MergedConfig) {
214    if (this.sourceObConfig.selfConfig.ruleOptions.rules) {
215      const configPaths: string[] = this.sourceObConfig.selfConfig.ruleOptions.rules;
216      for (const path of configPaths) {
217        this.getConfigByPath(path, selfConfigs);
218      }
219    }
220  }
221
222  private getConfigByPath(path: string, configs: MergedConfig) {
223    let fileContent = undefined;
224    try {
225      fileContent = fs.readFileSync(path, 'utf-8');
226    } catch (err) {
227      this.logger.error(`Failed to open ${path}. Error message: ${err}`);
228      throw err;
229    }
230    this.handleConfigContent(fileContent, configs, path);
231  }
232
233  // obfuscation options
234  static readonly KEEP_DTS = '-keep-dts';
235  static readonly KEEP_GLOBAL_NAME = '-keep-global-name';
236  static readonly KEEP_PROPERTY_NAME = '-keep-property-name';
237  static readonly DISABLE_OBFUSCATION = '-disable-obfuscation';
238  static readonly ENABLE_PROPERTY_OBFUSCATION = '-enable-property-obfuscation';
239  static readonly ENABLE_STRING_PROPERTY_OBFUSCATION = '-enable-string-property-obfuscation';
240  static readonly ENABLE_TOPLEVEL_OBFUSCATION = '-enable-toplevel-obfuscation';
241  static readonly COMPACT = '-compact';
242  static readonly REMOVE_LOG = '-remove-log';
243  static readonly PRINT_NAMECACHE = '-print-namecache';
244  static readonly APPLY_NAMECACHE = '-apply-namecache';
245
246  static optionsMap: Map<string, string> = new Map([
247    ['disableObfuscation', ObConfigResolver.KEEP_DTS],
248    ['enablePropertyObfuscation', ObConfigResolver.ENABLE_PROPERTY_OBFUSCATION],
249    ['enableStringPropertyObfuscation', ObConfigResolver.ENABLE_STRING_PROPERTY_OBFUSCATION],
250    ['enableToplevelObfuscation', ObConfigResolver.ENABLE_TOPLEVEL_OBFUSCATION],
251    ['compact', ObConfigResolver.COMPACT],
252    ['removeLog', ObConfigResolver.REMOVE_LOG],
253  ]);
254
255  private getTokenType(token: string): OptionType {
256    switch (token) {
257      case ObConfigResolver.KEEP_DTS:
258        return OptionType.KEEP_DTS;
259      case ObConfigResolver.KEEP_GLOBAL_NAME:
260        return OptionType.KEEP_GLOBAL_NAME;
261      case ObConfigResolver.KEEP_PROPERTY_NAME:
262        return OptionType.KEEP_PROPERTY_NAME;
263      case ObConfigResolver.DISABLE_OBFUSCATION:
264        return OptionType.DISABLE_OBFUSCATION;
265      case ObConfigResolver.ENABLE_PROPERTY_OBFUSCATION:
266        return OptionType.ENABLE_PROPERTY_OBFUSCATION;
267      case ObConfigResolver.ENABLE_STRING_PROPERTY_OBFUSCATION:
268        return OptionType.ENABLE_STRING_PROPERTY_OBFUSCATION;
269      case ObConfigResolver.ENABLE_TOPLEVEL_OBFUSCATION:
270        return OptionType.ENABLE_TOPLEVEL_OBFUSCATION;
271      case ObConfigResolver.COMPACT:
272        return OptionType.COMPACT;
273      case ObConfigResolver.REMOVE_LOG:
274        return OptionType.REMOVE_LOG;
275      case ObConfigResolver.PRINT_NAMECACHE:
276        return OptionType.PRINT_NAMECACHE;
277      case ObConfigResolver.APPLY_NAMECACHE:
278        return OptionType.APPLY_NAMECACHE;
279      default:
280        return OptionType.NONE;
281    }
282  }
283
284  private handleConfigContent(data: string, configs: MergedConfig, configPath: string) {
285    data = this.removeComments(data);
286    const tokens = data.split(/[',', '\t', ' ', '\n', '\r\n']/).filter((item) => {
287      if (item != "") {
288        return item;
289      }
290    });
291
292    let type: OptionType = OptionType.NONE;
293    let tokenType: OptionType;
294    let dtsFilePaths: string[] = [];
295    for (let i = 0; i < tokens.length; i++) {
296      const token = tokens[i];
297      tokenType = this.getTokenType(token);
298      // handle switches cases
299      switch (tokenType) {
300        case OptionType.DISABLE_OBFUSCATION: {
301          configs.options.disableObfuscation = true;
302          continue;
303        }
304        case OptionType.ENABLE_PROPERTY_OBFUSCATION: {
305          configs.options.enablePropertyObfuscation = true;
306          continue;
307        }
308        case OptionType.ENABLE_STRING_PROPERTY_OBFUSCATION: {
309          configs.options.enableStringPropertyObfuscation = true;
310        }
311        case OptionType.ENABLE_TOPLEVEL_OBFUSCATION: {
312          configs.options.enableToplevelObfuscation = true;
313          continue;
314        }
315        case OptionType.COMPACT: {
316          configs.options.compact = true;
317          continue;
318        }
319        case OptionType.REMOVE_LOG: {
320          configs.options.removeLog = true;
321          continue;
322        }
323        case OptionType.KEEP_DTS:
324        case OptionType.KEEP_GLOBAL_NAME:
325        case OptionType.KEEP_PROPERTY_NAME:
326        case OptionType.PRINT_NAMECACHE:
327        case OptionType.APPLY_NAMECACHE:
328          type = tokenType;
329          continue;
330        default: {
331          // fall-through
332        }
333      }
334      // handle 'keep' options and 'namecache' options
335      switch (type) {
336        case OptionType.KEEP_DTS: {
337          dtsFilePaths.push(token);
338          continue;
339        }
340        case OptionType.KEEP_GLOBAL_NAME: {
341          configs.reservedNames.push(token);
342          continue;
343        }
344        case OptionType.KEEP_PROPERTY_NAME: {
345          configs.reservedPropertyNames.push(token);
346          continue;
347        }
348        case OptionType.PRINT_NAMECACHE: {
349          configs.options.printNameCache = token;
350          type = OptionType.NONE;
351          continue;
352        }
353        case OptionType.APPLY_NAMECACHE: {
354          configs.options.applyNameCache = token;
355          type = OptionType.NONE;
356          this.determineNameCachePath(token, configPath);
357          continue;
358        }
359        default:
360          continue;
361      }
362    }
363
364    this.resolveDts(dtsFilePaths, configs);
365  }
366
367  // get names in .d.ts files and add them into reserved list
368  private resolveDts(dtsFilePaths: string[], configs: MergedConfig) {
369    ApiExtractor.mPropertySet.clear();
370    dtsFilePaths.forEach((token) => {
371      ApiExtractor.traverseApiFiles(token, ApiExtractor.ApiType.PROJECT);
372    });
373    configs.reservedNames = configs.reservedNames.concat(
374      [...ApiExtractor.mPropertySet]
375    );
376    configs.reservedPropertyNames = configs.reservedPropertyNames.concat(
377      [...ApiExtractor.mPropertySet]
378    );
379    ApiExtractor.mPropertySet.clear();
380  }
381
382  // the content from '#' to '\n' are comments
383  private removeComments(data: string) {
384    const commentStart = "#";
385    const commentEnd = "\n";
386    var tmpStr = "";
387    var isInComments = false;
388    for (let i = 0; i < data.length; i++) {
389      if (isInComments) {
390        isInComments = data[i] != commentEnd;
391      } else if (data[i] != commentStart) {
392        tmpStr += data[i];
393      } else {
394        isInComments = true;
395      }
396    }
397    return tmpStr;
398  }
399
400  /**
401   * systemConfigs includes the API directorys.
402   * component directory and pre_define.js file path needs to be concatenated
403   * @param systemConfigs
404   */
405  private getSystemApiCache(systemConfigs: MergedConfig, systemApiCachePath: string) {
406    ApiExtractor.mPropertySet.clear();
407    const sdkApis: string[] = sortAndDeduplicateStringArr(this.sourceObConfig.sdkApis);
408    for (let apiPath of sdkApis) {
409      this.getSdkApiCache(apiPath);
410      const UIPath: string =  path.join(apiPath,'../build-tools/ets-loader/lib/pre_define.js');
411      if (fs.existsSync(UIPath)) {
412        this.getUIApiCache(UIPath);
413      }
414    }
415    const savedNameAndPropertyList: string[] = sortAndDeduplicateStringArr([...ApiExtractor.mPropertySet])
416    const systemApiContent = {
417      ReservedNames: savedNameAndPropertyList,
418      ReservedPropertyNames: savedNameAndPropertyList,
419    };
420    systemConfigs.reservedPropertyNames.push(...savedNameAndPropertyList);
421    systemConfigs.reservedNames.push(...savedNameAndPropertyList);
422    if (!fs.existsSync(path.dirname(systemApiCachePath))) {
423      fs.mkdirSync(path.dirname(systemApiCachePath), {recursive: true});
424    }
425    fs.writeFileSync(systemApiCachePath, JSON.stringify(systemApiContent, null, 2));
426    ApiExtractor.mPropertySet.clear();
427  }
428
429  private getSdkApiCache(sdkApiPath: string) {
430    ApiExtractor.traverseApiFiles(sdkApiPath, ApiExtractor.ApiType.API);
431    const componentPath: string =  path.join(sdkApiPath,'../component');
432    if (fs.existsSync(componentPath)) {
433      ApiExtractor.traverseApiFiles(componentPath, ApiExtractor.ApiType.COMPONENT);
434    }
435  }
436
437  private getUIApiCache(uiApiPath: string) {
438    ApiExtractor.extractStringsFromFile(uiApiPath);
439  }
440
441  private getDependencyConfigs(sourceObConfig: any, dependencyConfigs: MergedConfig): void {
442    for (const lib of sourceObConfig.dependencies.libraries || []) {
443      if(lib.consumerRules && lib.consumerRules.length > 0) {
444        for (const path of lib.consumerRules) {
445          const thisLibConfigs = new MergedConfig();
446          this.getConfigByPath(path, dependencyConfigs);
447          dependencyConfigs.merge(thisLibConfigs);
448        }
449      }
450    }
451
452    if (sourceObConfig.dependencies && sourceObConfig.dependencies.hars && sourceObConfig.dependencies.hars.length > 0) {
453      for (const path of sourceObConfig.dependencies.hars) {
454        const thisHarConfigs = new MergedConfig();
455        this.getConfigByPath(path, dependencyConfigs);
456        dependencyConfigs.merge(thisHarConfigs);
457      }
458    }
459  }
460
461  private getSystemApiConfigsByCache(systemConfigs: MergedConfig, systemApiCachePath: string) {
462    let systemApiContent = JSON.parse(fs.readFileSync(systemApiCachePath, "utf-8"));
463    if (systemApiContent["ReservedPropertyNames"]) {
464      systemConfigs.reservedPropertyNames = systemApiContent["ReservedPropertyNames"];
465    }
466    if (systemApiContent["ReservedNames"]) {
467      systemConfigs.reservedNames = systemApiContent["ReservedNames"];
468    }
469  }
470
471  private getSelfConsumerConfig(selfConsumerConfig: MergedConfig) {
472    for (const path of this.sourceObConfig.selfConfig.consumerRules) {
473      this.getConfigByPath(path, selfConsumerConfig);
474    }
475  }
476
477  private getMergedConfigs(selfConfigs: MergedConfig, dependencyConfigs: MergedConfig): MergedConfig {
478    if (dependencyConfigs) {
479        selfConfigs.merge(dependencyConfigs);
480    }
481    selfConfigs.sortAndDeduplicate();
482    return selfConfigs;
483  }
484
485  private genConsumerConfigFiles(sourceObConfig: any, selfConsumerConfig: MergedConfig, dependencyConfigs: MergedConfig) {
486    selfConsumerConfig.merge(dependencyConfigs);
487    selfConsumerConfig.sortAndDeduplicate();
488    this.writeConsumerConfigFile(selfConsumerConfig, sourceObConfig.exportRulePath);
489  }
490
491  public writeConsumerConfigFile(selfConsumerConfig: MergedConfig, outpath: string) {
492    const configContent: string = selfConsumerConfig.serializeMergedConfig();
493    fs.writeFileSync(outpath, configContent);
494  }
495
496  private determineNameCachePath(nameCachePath: string, configPath: string): void {
497    if (!fs.existsSync(nameCachePath)) {
498      throw new Error(`The applied namecache file '${nameCachePath}' configured by '${configPath}' does not exist.`);
499    }
500  }
501}
502
503export function readNameCache(nameCachePath: string, logger: any): void {
504  try {
505    const fileContent = fs.readFileSync(nameCachePath, 'utf-8');
506    const nameCache: { IdentifierCache?, PropertyCache? } = JSON.parse(fileContent);
507    if (nameCache.PropertyCache) {
508      renamePropertyModule.historyMangledTable = getMapFromJson(nameCache.PropertyCache);
509    }
510    Object.assign(identifierCaches, nameCache.IdentifierCache);
511  } catch (err) {
512    logger.error(`Failed to open ${nameCachePath}. Error message: ${err}`);
513  }
514}
515
516export function getArkguardNameCache(enablePropertyObfuscation: any) {
517  let writeContent: string = "";
518  const nameCacheCollection: Object = {};
519  nameCacheCollection['IdentifierCache'] = identifierCaches;
520  const mergedNameCache: Map<string, string> = new Map();
521  if (enablePropertyObfuscation) {
522    if (renamePropertyModule.historyMangledTable) {
523      for (const [key, value] of renamePropertyModule.historyMangledTable.entries()) {
524        mergedNameCache.set(key, value);
525      }
526    }
527
528    if (renamePropertyModule.globalMangledTable) {
529      for (const [key, value] of renamePropertyModule.globalMangledTable.entries()) {
530        mergedNameCache.set(key, value);
531      }
532    }
533    nameCacheCollection['PropertyCache'] = Object.fromEntries(mergedNameCache);
534  }
535
536  writeContent += JSON.stringify(nameCacheCollection, null, 2);
537  return writeContent;
538}
539
540export function writeObfuscationNameCache(projectConfig:any, obfuscationCacheDir?: string, printNameCache?: string): void {
541  let writeContent: string = '';
542  if (projectConfig.arkObfuscator) {
543    writeContent = getArkguardNameCache(projectConfig.obfuscationMergedObConfig.options.enablePropertyObfuscation)
544  } else if (projectConfig.terserConfig) {
545    writeContent = JSON.stringify(projectConfig.terserConfig.nameCache, null, 2);
546  } else {
547    return;
548  }
549  if (obfuscationCacheDir && obfuscationCacheDir.length > 0) {
550    const defaultNameCachePath: string = path.join(obfuscationCacheDir,"nameCache.json");
551    if (!fs.existsSync(path.dirname(defaultNameCachePath))) {
552      fs.mkdirSync(path.dirname(defaultNameCachePath), {recursive: true});
553    }
554    fs.writeFileSync(defaultNameCachePath, writeContent);
555  }
556  if (printNameCache && printNameCache.length > 0) {
557    fs.writeFileSync(printNameCache, writeContent);
558  }
559}
560
561export function generateConsumerObConfigFile(obfuscationOptions: any, logger: any) {
562  const projectConfig = { obfuscationOptions, compileHar: true };
563  const obConfig: ObConfigResolver =  new ObConfigResolver(projectConfig, logger);
564  obConfig.resolveObfuscationConfigs();
565}
566