1/* 2 * Copyright (c) 2023-2024 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 createPrinter, 18 createTextWriter, 19 transform, 20 createObfTextSingleLineWriter, 21} from 'typescript'; 22 23import type { 24 CompilerOptions, 25 EmitTextWriter, 26 Node, 27 Printer, 28 PrinterOptions, 29 RawSourceMap, 30 SourceFile, 31 SourceMapGenerator, 32 TransformationResult, 33 TransformerFactory, 34} from 'typescript'; 35 36import path from 'path'; 37 38import { LocalVariableCollections, PropCollections } from './utils/CommonCollections'; 39import type { IOptions } from './configs/IOptions'; 40import { FileUtils } from './utils/FileUtils'; 41import { TransformerManager } from './transformers/TransformerManager'; 42import { getSourceMapGenerator } from './utils/SourceMapUtil'; 43import { 44 decodeSourcemap, 45 ExistingDecodedSourceMap, 46 Source, 47 SourceMapLink, 48 SourceMapSegmentObj, 49 mergeSourceMap 50} from './utils/SourceMapMergingUtil'; 51import { 52 deleteLineInfoForNameString, 53 getMapFromJson, 54 IDENTIFIER_CACHE, 55 MEM_METHOD_CACHE 56} from './utils/NameCacheUtil'; 57import { ListUtil } from './utils/ListUtil'; 58import { needReadApiInfo, readProjectPropertiesByCollectedPaths } from './common/ApiReader'; 59import type { ReseverdSetForArkguard } from './common/ApiReader'; 60import { ApiExtractor } from './common/ApiExtractor'; 61import esInfo from './configs/preset/es_reserved_properties.json'; 62import { EventList, TimeSumPrinter, TimeTracker } from './utils/PrinterUtils'; 63import { Extension, type ProjectInfo, type FilePathObj } from './common/type'; 64export { FileUtils } from './utils/FileUtils'; 65export { MemoryUtils } from './utils/MemoryUtils'; 66import { TypeUtils } from './utils/TypeUtils'; 67import { handleReservedConfig } from './utils/TransformUtil'; 68import { UnobfuscationCollections } from './utils/CommonCollections'; 69import { historyAllUnobfuscatedNamesMap } from './initialization/Initializer'; 70export { UnobfuscationCollections } from './utils/CommonCollections'; 71export { separateUniversalReservedItem, containWildcards, wildcardTransformer } from './utils/TransformUtil'; 72export type { ReservedNameInfo } from './utils/TransformUtil'; 73export type { ReseverdSetForArkguard } from './common/ApiReader'; 74 75export { initObfuscationConfig } from './initialization/Initializer'; 76export { nameCacheMap, unobfuscationNamesObj } from './initialization/CommonObject'; 77export { 78 collectResevedFileNameInIDEConfig, // For running unit test. 79 enableObfuscatedFilePathConfig, 80 enableObfuscateFileName, 81 generateConsumerObConfigFile, 82 getRelativeSourcePath, 83 handleObfuscatedFilePath, 84 handleUniversalPathInObf, 85 mangleFilePath, 86 MergedConfig, 87 ObConfigResolver, 88 readNameCache, 89 writeObfuscationNameCache, 90 writeUnobfuscationContent 91} from './initialization/ConfigResolver'; 92export { 93 collectReservedNameForObf 94} from './utils/NodeUtils'; 95 96export const renameIdentifierModule = require('./transformers/rename/RenameIdentifierTransformer'); 97export const renameFileNameModule = require('./transformers/rename/RenameFileNameTransformer'); 98 99export { getMapFromJson, readProjectPropertiesByCollectedPaths, deleteLineInfoForNameString, ApiExtractor, PropCollections }; 100export let orignalFilePathForSearching: string | undefined; 101export let cleanFileMangledNames: boolean = false; 102export interface PerformancePrinter { 103 filesPrinter?: TimeTracker; 104 singleFilePrinter?: TimeTracker; 105 timeSumPrinter?: TimeSumPrinter; 106 iniPrinter: TimeTracker; 107} 108export let performancePrinter: PerformancePrinter = { 109 iniPrinter: new TimeTracker(), 110}; 111 112// When the module is compiled, call this function to clear global collections. 113export function clearGlobalCaches(): void { 114 PropCollections.clearPropsCollections(); 115 UnobfuscationCollections.clear(); 116 LocalVariableCollections.clear(); 117 renameFileNameModule.clearCaches(); 118} 119 120export type ObfuscationResultType = { 121 content: string; 122 sourceMap?: RawSourceMap; 123 nameCache?: { [k: string]: string | {} }; 124 filePath?: string; 125 unobfuscationNameMap?: Map<string, Set<string>>; 126}; 127 128const JSON_TEXT_INDENT_LENGTH: number = 2; 129export class ArkObfuscator { 130 // Used only for testing 131 protected mWriteOriginalFile: boolean = false; 132 133 // A text writer of Printer 134 private mTextWriter: EmitTextWriter; 135 136 // Compiler Options for typescript,use to parse ast 137 private readonly mCompilerOptions: CompilerOptions; 138 139 // User custom obfuscation profiles. 140 protected mCustomProfiles: IOptions; 141 142 private mTransformers: TransformerFactory<Node>[]; 143 144 static mProjectInfo: ProjectInfo | undefined; 145 146 // If isKeptCurrentFile is true, both identifier and property obfuscation are skipped. 147 static mIsKeptCurrentFile: boolean = false; 148 149 public constructor() { 150 this.mCompilerOptions = {}; 151 this.mTransformers = []; 152 } 153 154 public setWriteOriginalFile(flag: boolean): void { 155 this.mWriteOriginalFile = flag; 156 } 157 158 // Pass the collected whitelists related to property obfuscation to Arkguard. 159 public addReservedSetForPropertyObf(properties: ReseverdSetForArkguard): void { 160 if (properties.structPropertySet && properties.structPropertySet.size > 0) { 161 for (let reservedProperty of properties.structPropertySet) { 162 UnobfuscationCollections.reservedStruct.add(reservedProperty); 163 } 164 } 165 166 if (properties.stringPropertySet && properties.stringPropertySet.size > 0) { 167 UnobfuscationCollections.reservedStrProp = properties.stringPropertySet; 168 } 169 170 if (properties.exportNameAndPropSet && properties.exportNameAndPropSet.size > 0) { 171 UnobfuscationCollections.reservedExportNameAndProp = properties.exportNameAndPropSet; 172 } 173 174 if (properties.enumPropertySet && properties.enumPropertySet.size > 0) { 175 for (let reservedEnum of properties.enumPropertySet) { 176 UnobfuscationCollections.reservedEnum.add(reservedEnum); 177 } 178 } 179 } 180 181 public addReservedSetForDefaultObf(properties: ReseverdSetForArkguard): void { 182 if (properties.exportNameSet && properties.exportNameSet.size > 0) { 183 UnobfuscationCollections.reservedExportName = properties.exportNameSet; 184 } 185 } 186 187 public setKeepSourceOfPaths(mKeepSourceOfPaths: Set<string>): void { 188 this.mCustomProfiles.mKeepFileSourceCode.mKeepSourceOfPaths = mKeepSourceOfPaths; 189 } 190 191 public handleTsHarComments(sourceFile: SourceFile, originalPath: string | undefined): void { 192 if (ArkObfuscator.projectInfo?.useTsHar && (originalPath?.endsWith(Extension.ETS) && !originalPath?.endsWith(Extension.DETS))) { 193 // @ts-ignore 194 sourceFile.writeTsHarComments = true; 195 } 196 } 197 198 public get customProfiles(): IOptions { 199 return this.mCustomProfiles; 200 } 201 202 public static get isKeptCurrentFile(): boolean { 203 return ArkObfuscator.mIsKeptCurrentFile; 204 } 205 206 public static set isKeptCurrentFile(isKeptFile: boolean) { 207 ArkObfuscator.mIsKeptCurrentFile = isKeptFile; 208 } 209 210 public static get projectInfo(): ProjectInfo { 211 return ArkObfuscator.mProjectInfo; 212 } 213 214 public static set projectInfo(projectInfo: ProjectInfo) { 215 ArkObfuscator.mProjectInfo = projectInfo; 216 } 217 218 private isCurrentFileInKeepPaths(customProfiles: IOptions, originalFilePath: string): boolean { 219 const keepFileSourceCode = customProfiles.mKeepFileSourceCode; 220 if (keepFileSourceCode === undefined || keepFileSourceCode.mKeepSourceOfPaths.size === 0) { 221 return false; 222 } 223 const keepPaths: Set<string> = keepFileSourceCode.mKeepSourceOfPaths; 224 const originalPath = FileUtils.toUnixPath(originalFilePath); 225 return keepPaths.has(originalPath); 226 } 227 228 /** 229 * init ArkObfuscator according to user config 230 * should be called after constructor 231 */ 232 public init(config: IOptions | undefined): boolean { 233 if (!config) { 234 console.error('obfuscation config file is not found and no given config.'); 235 return false; 236 } 237 238 handleReservedConfig(config, 'mRenameFileName', 'mReservedFileNames', 'mUniversalReservedFileNames'); 239 handleReservedConfig(config, 'mRemoveDeclarationComments', 'mReservedComments', 'mUniversalReservedComments', 'mEnable'); 240 this.mCustomProfiles = config; 241 242 if (this.mCustomProfiles.mCompact) { 243 this.mTextWriter = createObfTextSingleLineWriter(); 244 } else { 245 this.mTextWriter = createTextWriter('\n'); 246 } 247 248 if (this.mCustomProfiles.mEnableSourceMap) { 249 this.mCompilerOptions.sourceMap = true; 250 } 251 252 const enableTopLevel: boolean = this.mCustomProfiles.mNameObfuscation?.mTopLevel; 253 const exportObfuscation: boolean = this.mCustomProfiles.mExportObfuscation; 254 const propertyObfuscation: boolean = this.mCustomProfiles.mNameObfuscation?.mRenameProperties; 255 /** 256 * clean mangledNames in case skip name check when generating names 257 */ 258 cleanFileMangledNames = enableTopLevel && !exportObfuscation && !propertyObfuscation; 259 260 this.initPerformancePrinter(); 261 // load transformers 262 this.mTransformers = new TransformerManager(this.mCustomProfiles).getTransformers(); 263 264 if (needReadApiInfo(this.mCustomProfiles)) { 265 // if -enable-property-obfuscation or -enable-export-obfuscation, collect language reserved keywords. 266 let languageSet: Set<string> = new Set(); 267 for (const key of Object.keys(esInfo)) { 268 const valueArray = esInfo[key]; 269 valueArray.forEach((element: string) => { 270 languageSet.add(element); 271 }); 272 } 273 UnobfuscationCollections.reservedLangForProperty = languageSet; 274 } 275 276 return true; 277 } 278 279 private initPerformancePrinter(): void { 280 if (this.mCustomProfiles.mPerformancePrinter) { 281 const printConfig = this.mCustomProfiles.mPerformancePrinter; 282 const printPath = printConfig.mOutputPath; 283 284 if (printConfig.mFilesPrinter) { 285 performancePrinter.filesPrinter = performancePrinter.iniPrinter; 286 performancePrinter.filesPrinter.setOutputPath(printPath); 287 } else { 288 performancePrinter.iniPrinter = undefined; 289 } 290 291 if (printConfig.mSingleFilePrinter) { 292 performancePrinter.singleFilePrinter = new TimeTracker(printPath); 293 } 294 295 if (printConfig.mSumPrinter) { 296 performancePrinter.timeSumPrinter = new TimeSumPrinter(printPath); 297 } 298 } else { 299 performancePrinter = undefined; 300 } 301 } 302 303 /** 304 * A Printer to output obfuscated codes. 305 */ 306 public createObfsPrinter(isDeclarationFile: boolean): Printer { 307 // set print options 308 let printerOptions: PrinterOptions = {}; 309 let removeOption = this.mCustomProfiles.mRemoveDeclarationComments; 310 let hasReservedList = removeOption?.mReservedComments?.length || removeOption?.mUniversalReservedComments?.length; 311 let keepDeclarationComments = hasReservedList || !removeOption?.mEnable; 312 313 if (isDeclarationFile && keepDeclarationComments) { 314 printerOptions.removeComments = false; 315 } 316 if ((!isDeclarationFile && this.mCustomProfiles.mRemoveComments) || (isDeclarationFile && !keepDeclarationComments)) { 317 printerOptions.removeComments = true; 318 } 319 320 return createPrinter(printerOptions); 321 } 322 323 protected isObfsIgnoreFile(fileName: string): boolean { 324 let suffix: string = FileUtils.getFileExtension(fileName); 325 326 return suffix !== 'js' && suffix !== 'ts' && suffix !== 'ets'; 327 } 328 329 private convertLineBasedOnSourceMap(targetCache: string, sourceMapLink?: SourceMapLink): Map<string, string> { 330 let originalCache: Map<string, string> = renameIdentifierModule.nameCache.get(targetCache); 331 let updatedCache: Map<string, string> = new Map<string, string>(); 332 for (const [key, value] of originalCache) { 333 if (!key.includes(':')) { 334 // No need to save line info for identifier which is not function-like, i.e. key without ':' here. 335 updatedCache[key] = value; 336 continue; 337 } 338 const [scopeName, oldStartLine, oldStartColumn, oldEndLine, oldEndColumn] = key.split(':'); 339 let newKey: string = key; 340 if (!sourceMapLink) { 341 // In Arkguard, we save line info of source code, so do not need to use sourcemap mapping. 342 newKey = `${scopeName}:${oldStartLine}:${oldEndLine}`; 343 updatedCache[newKey] = value; 344 continue; 345 } 346 const startPosition: SourceMapSegmentObj | null = sourceMapLink.traceSegment( 347 // 1: The line number in originalCache starts from 1 while in source map starts from 0. 348 Number(oldStartLine) - 1, Number(oldStartColumn) - 1, ''); // Minus 1 to get the correct original position. 349 if (!startPosition) { 350 // Do not save methods that do not exist in the source code, e.g. 'build' in ArkUI. 351 continue; 352 } 353 const endPosition: SourceMapSegmentObj | null = sourceMapLink.traceSegment( 354 Number(oldEndLine) - 1, Number(oldEndColumn) - 1, ''); // 1: Same as above. 355 if (!endPosition) { 356 // Do not save methods that do not exist in the source code, e.g. 'build' in ArkUI. 357 continue; 358 } 359 const startLine = startPosition.line + 1; // 1: The final line number in updatedCache should starts from 1. 360 const endLine = endPosition.line + 1; // 1: Same as above. 361 newKey = `${scopeName}:${startLine}:${endLine}`; 362 updatedCache[newKey] = value; 363 } 364 return updatedCache; 365 } 366 367 /** 368 * Obfuscate ast of a file. 369 * @param content ast or source code of a source file 370 * @param sourceFilePathObj 371 * @param previousStageSourceMap 372 * @param historyNameCache 373 * @param originalFilePath When filename obfuscation is enabled, it is used as the source code path. 374 */ 375 public async obfuscate( 376 content: SourceFile | string, 377 sourceFilePathObj: FilePathObj, 378 previousStageSourceMap?: RawSourceMap, 379 historyNameCache?: Map<string, string>, 380 originalFilePath?: string, 381 projectInfo?: ProjectInfo, 382 ): Promise<ObfuscationResultType> { 383 ArkObfuscator.projectInfo = projectInfo; 384 let result: ObfuscationResultType = { content: undefined }; 385 if (this.isObfsIgnoreFile(sourceFilePathObj.buildFilePath)) { 386 // need add return value 387 return result; 388 } 389 390 let ast: SourceFile = this.createAst(content, sourceFilePathObj.buildFilePath); 391 if (ast.statements.length === 0) { 392 return result; 393 } 394 395 if (historyNameCache && historyNameCache.size > 0 && this.mCustomProfiles.mNameObfuscation) { 396 renameIdentifierModule.historyNameCache = historyNameCache; 397 } 398 399 if (this.mCustomProfiles.mUnobfuscationOption?.mPrintKeptNames) { 400 let historyUnobfuscatedNames = historyAllUnobfuscatedNamesMap.get(sourceFilePathObj.relativeFilePath); 401 if (historyUnobfuscatedNames) { 402 renameIdentifierModule.historyUnobfuscatedNamesMap = new Map(Object.entries(historyUnobfuscatedNames)); 403 } 404 } 405 406 originalFilePath = originalFilePath ?? ast.fileName; 407 if (this.mCustomProfiles.mRenameFileName?.mEnable) { 408 orignalFilePathForSearching = originalFilePath; 409 } 410 ArkObfuscator.isKeptCurrentFile = this.isCurrentFileInKeepPaths(this.mCustomProfiles, originalFilePath); 411 412 this.handleDeclarationFile(ast); 413 414 ast = this.obfuscateAst(ast); 415 416 this.writeObfuscationResult(ast, sourceFilePathObj.buildFilePath, result, previousStageSourceMap, originalFilePath); 417 418 this.clearCaches(); 419 return result; 420 } 421 422 private createAst(content: SourceFile | string, sourceFilePath: string): SourceFile { 423 performancePrinter?.singleFilePrinter?.startEvent(EventList.CREATE_AST, performancePrinter.timeSumPrinter, sourceFilePath); 424 let ast: SourceFile; 425 if (typeof content === 'string') { 426 ast = TypeUtils.createObfSourceFile(sourceFilePath, content); 427 } else { 428 ast = content; 429 } 430 performancePrinter?.singleFilePrinter?.endEvent(EventList.CREATE_AST, performancePrinter.timeSumPrinter); 431 432 return ast; 433 } 434 435 private obfuscateAst(ast: SourceFile): SourceFile { 436 performancePrinter?.singleFilePrinter?.startEvent(EventList.OBFUSCATE_AST, performancePrinter.timeSumPrinter); 437 let transformedResult: TransformationResult<Node> = transform(ast, this.mTransformers, this.mCompilerOptions); 438 performancePrinter?.singleFilePrinter?.endEvent(EventList.OBFUSCATE_AST, performancePrinter.timeSumPrinter); 439 ast = transformedResult.transformed[0] as SourceFile; 440 return ast; 441 } 442 443 private handleDeclarationFile(ast: SourceFile): void { 444 if (ast.isDeclarationFile) { 445 if (!this.mCustomProfiles.mRemoveDeclarationComments || !this.mCustomProfiles.mRemoveDeclarationComments.mEnable) { 446 //@ts-ignore 447 ast.reservedComments = undefined; 448 //@ts-ignore 449 ast.universalReservedComments = undefined; 450 } else { 451 //@ts-ignore 452 ast.reservedComments ??= this.mCustomProfiles.mRemoveDeclarationComments.mReservedComments ? 453 this.mCustomProfiles.mRemoveDeclarationComments.mReservedComments : []; 454 //@ts-ignore 455 ast.universalReservedComments = this.mCustomProfiles.mRemoveDeclarationComments.mUniversalReservedComments ?? []; 456 } 457 } else { 458 //@ts-ignore 459 ast.reservedComments = this.mCustomProfiles.mRemoveComments ? [] : undefined; 460 //@ts-ignore 461 ast.universalReservedComments = this.mCustomProfiles.mRemoveComments ? [] : undefined; 462 } 463 } 464 465 /** 466 * write obfuscated code, sourcemap and namecache 467 */ 468 private writeObfuscationResult(ast: SourceFile, sourceFilePath: string, result: ObfuscationResultType, 469 previousStageSourceMap?: RawSourceMap, originalFilePath?: string): void { 470 // convert ast to output source file and generate sourcemap if needed. 471 let sourceMapGenerator: SourceMapGenerator = undefined; 472 if (this.mCustomProfiles.mEnableSourceMap) { 473 sourceMapGenerator = getSourceMapGenerator(sourceFilePath); 474 } 475 476 if (sourceFilePath.endsWith('.js')) { 477 TypeUtils.tsToJs(ast); 478 } 479 this.handleTsHarComments(ast, originalFilePath); 480 performancePrinter?.singleFilePrinter?.startEvent(EventList.CREATE_PRINTER, performancePrinter.timeSumPrinter); 481 this.createObfsPrinter(ast.isDeclarationFile).writeFile(ast, this.mTextWriter, sourceMapGenerator); 482 performancePrinter?.singleFilePrinter?.endEvent(EventList.CREATE_PRINTER, performancePrinter.timeSumPrinter); 483 484 result.filePath = ast.fileName; 485 result.content = this.mTextWriter.getText(); 486 487 if (this.mCustomProfiles.mUnobfuscationOption?.mPrintKeptNames) { 488 this.handleUnobfuscationNames(result); 489 } 490 491 if (this.mCustomProfiles.mEnableSourceMap && sourceMapGenerator) { 492 this.handleSourceMapAndNameCache(sourceMapGenerator, sourceFilePath, result, previousStageSourceMap); 493 } 494 } 495 496 private handleUnobfuscationNames(result: ObfuscationResultType): void { 497 result.unobfuscationNameMap = new Map(UnobfuscationCollections.unobfuscatedNamesMap); 498 } 499 500 private handleSourceMapAndNameCache(sourceMapGenerator: SourceMapGenerator, sourceFilePath: string, 501 result: ObfuscationResultType, previousStageSourceMap?: RawSourceMap): void { 502 let sourceMapJson: RawSourceMap = sourceMapGenerator.toJSON(); 503 sourceMapJson.sourceRoot = ''; 504 sourceMapJson.file = path.basename(sourceFilePath); 505 if (previousStageSourceMap) { 506 sourceMapJson = mergeSourceMap(previousStageSourceMap as RawSourceMap, sourceMapJson); 507 } 508 result.sourceMap = sourceMapJson; 509 let nameCache = renameIdentifierModule.nameCache; 510 if (this.mCustomProfiles.mEnableNameCache) { 511 let newIdentifierCache!: Object; 512 let newMemberMethodCache!: Object; 513 if (previousStageSourceMap) { 514 // The process in sdk, need to use sourcemap mapping. 515 // 1: Only one file in the source map; 0: The first and the only one. 516 const sourceFileName = previousStageSourceMap.sources?.length === 1 ? previousStageSourceMap.sources[0] : ''; 517 const source: Source = new Source(sourceFileName, null); 518 const decodedSourceMap: ExistingDecodedSourceMap = decodeSourcemap(previousStageSourceMap); 519 let sourceMapLink: SourceMapLink = new SourceMapLink(decodedSourceMap, [source]); 520 newIdentifierCache = this.convertLineBasedOnSourceMap(IDENTIFIER_CACHE, sourceMapLink); 521 newMemberMethodCache = this.convertLineBasedOnSourceMap(MEM_METHOD_CACHE, sourceMapLink); 522 } else { 523 // The process in Arkguard. 524 newIdentifierCache = this.convertLineBasedOnSourceMap(IDENTIFIER_CACHE); 525 newMemberMethodCache = this.convertLineBasedOnSourceMap(MEM_METHOD_CACHE); 526 } 527 nameCache.set(IDENTIFIER_CACHE, newIdentifierCache); 528 nameCache.set(MEM_METHOD_CACHE, newMemberMethodCache); 529 result.nameCache = { [IDENTIFIER_CACHE]: newIdentifierCache, [MEM_METHOD_CACHE]: newMemberMethodCache }; 530 } 531 } 532 533 private clearCaches(): void { 534 // clear cache of text writer 535 this.mTextWriter.clear(); 536 renameIdentifierModule.clearCaches(); 537 if (cleanFileMangledNames) { 538 PropCollections.globalMangledTable.clear(); 539 PropCollections.newlyOccupiedMangledProps.clear(); 540 } 541 UnobfuscationCollections.unobfuscatedNamesMap.clear(); 542 } 543} 544