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