• 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 
16 import fs from "fs";
17 import path from "path";
18 import {
19   ApiExtractor,
20   renamePropertyModule,
21   getMapFromJson
22 } from "arkguard";
23 import { 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 
40 enum 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 
55 function 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 
65 function 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 
83 class 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 
109 export 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 
154 export 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 
503 export 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 
516 export 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 
540 export 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 
561 export 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