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