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 { 17 createPrinter, 18 createSourceFile, createTextWriter, 19 ScriptTarget, 20 transform, 21 createObfTextSingleLineWriter, 22} from 'typescript'; 23 24import type { 25 CompilerOptions, 26 EmitTextWriter, 27 Node, 28 Printer, 29 PrinterOptions, 30 RawSourceMap, 31 SourceFile, 32 SourceMapGenerator, 33 TransformationResult, 34 TransformerFactory, 35} from 'typescript'; 36 37import * as fs from 'fs'; 38import path from 'path'; 39import sourceMap from 'source-map'; 40 41import type {IOptions} from './configs/IOptions'; 42import {FileUtils} from './utils/FileUtils'; 43import {TransformerManager} from './transformers/TransformerManager'; 44import {getSourceMapGenerator} from './utils/SourceMapUtil'; 45 46import { 47 getMapFromJson, 48 NAME_CACHE_SUFFIX, 49 PROPERTY_CACHE_FILE, 50 readCache, writeCache 51} from './utils/NameCacheUtil'; 52import {ListUtil} from './utils/ListUtil'; 53import {needReadApiInfo, readProjectProperties, readProjectPropertiesByCollectedPaths} from './common/ApiReader'; 54import {ApiExtractor} from './common/ApiExtractor'; 55import es6Info from './configs/preset/es6_reserved_properties.json'; 56 57export const renameIdentifierModule = require('./transformers/rename/RenameIdentifierTransformer'); 58export const renamePropertyModule = require('./transformers/rename/RenamePropertiesTransformer'); 59export const renameFileNameModule = require('./transformers/rename/RenameFileNameTransformer'); 60 61export {getMapFromJson, readProjectPropertiesByCollectedPaths}; 62export let orignalFilePathForSearching: string | undefined; 63 64type ObfuscationResultType = { 65 content: string, 66 sourceMap?: RawSourceMap, 67 nameCache?: { [k: string]: string }, 68 filePath?: string 69}; 70 71const JSON_TEXT_INDENT_LENGTH: number = 2; 72export class ArkObfuscator { 73 // A text writer of Printer 74 private mTextWriter: EmitTextWriter; 75 76 // A list of source file path 77 private readonly mSourceFiles: string[]; 78 79 // Path of obfuscation configuration file. 80 private readonly mConfigPath: string; 81 82 // Compiler Options for typescript,use to parse ast 83 private readonly mCompilerOptions: CompilerOptions; 84 85 // User custom obfuscation profiles. 86 private mCustomProfiles: IOptions; 87 88 private mTransformers: TransformerFactory<Node>[]; 89 90 public constructor(sourceFiles?: string[], configPath?: string) { 91 this.mSourceFiles = sourceFiles; 92 this.mConfigPath = configPath; 93 this.mCompilerOptions = {}; 94 this.mTransformers = []; 95 } 96 97 public addReservedProperties(newReservedProperties: string[]) { 98 if (newReservedProperties.length === 0) { 99 return; 100 } 101 const nameObfuscationConfig = this.mCustomProfiles.mNameObfuscation; 102 nameObfuscationConfig.mReservedProperties = ListUtil.uniqueMergeList(newReservedProperties, 103 nameObfuscationConfig?.mReservedProperties); 104 } 105 106 public addReservedNames(newReservedNames: string[]) { 107 if (newReservedNames.length === 0) { 108 return; 109 } 110 const nameObfuscationConfig = this.mCustomProfiles.mNameObfuscation; 111 nameObfuscationConfig.mReservedNames = ListUtil.uniqueMergeList(newReservedNames, 112 nameObfuscationConfig?.mReservedNames); 113 } 114 /** 115 * init ArkObfuscator according to user config 116 * should be called after constructor 117 */ 118 public init(config?: IOptions): boolean { 119 if (!this.mConfigPath && !config) { 120 console.error('obfuscation config file is not found and no given config.'); 121 return false; 122 } 123 124 if (this.mConfigPath) { 125 config = FileUtils.readFileAsJson(this.mConfigPath); 126 } 127 128 this.mCustomProfiles = config; 129 130 if (this.mCustomProfiles.mCompact) { 131 this.mTextWriter = createObfTextSingleLineWriter(); 132 } else { 133 this.mTextWriter = createTextWriter('\n'); 134 } 135 136 if (this.mCustomProfiles.mEnableSourceMap) { 137 this.mCompilerOptions.sourceMap = true; 138 } 139 140 // load transformers 141 this.mTransformers = TransformerManager.getInstance(this.mCustomProfiles).getTransformers(); 142 143 if (needReadApiInfo(this.mCustomProfiles)) { 144 this.mCustomProfiles.mNameObfuscation.mReservedProperties = ListUtil.uniqueMergeList( 145 this.mCustomProfiles.mNameObfuscation.mReservedProperties, 146 this.mCustomProfiles.mNameObfuscation.mReservedNames, 147 es6Info); 148 } 149 150 return true; 151 } 152 153 /** 154 * Obfuscate all the source files. 155 */ 156 public async obfuscateFiles(): Promise<void> { 157 if (!path.isAbsolute(this.mCustomProfiles.mOutputDir)) { 158 this.mCustomProfiles.mOutputDir = path.join(path.dirname(this.mConfigPath), this.mCustomProfiles.mOutputDir); 159 } 160 if (this.mCustomProfiles.mOutputDir && !fs.existsSync(this.mCustomProfiles.mOutputDir)) { 161 fs.mkdirSync(this.mCustomProfiles.mOutputDir); 162 } 163 164 readProjectProperties(this.mSourceFiles, this.mCustomProfiles); 165 this.readPropertyCache(this.mCustomProfiles.mOutputDir); 166 167 // support directory and file obfuscate 168 for (const sourcePath of this.mSourceFiles) { 169 if (!fs.existsSync(sourcePath)) { 170 console.error(`File ${FileUtils.getFileName(sourcePath)} is not found.`); 171 return; 172 } 173 174 if (fs.lstatSync(sourcePath).isFile()) { 175 await this.obfuscateFile(sourcePath, this.mCustomProfiles.mOutputDir); 176 continue; 177 } 178 179 const dirPrefix: string = FileUtils.getPrefix(sourcePath); 180 await this.obfuscateDir(sourcePath, dirPrefix); 181 } 182 183 this.producePropertyCache(this.mCustomProfiles.mOutputDir); 184 } 185 186 /** 187 * obfuscate directory 188 * @private 189 */ 190 private async obfuscateDir(dirName: string, dirPrefix: string): Promise<void> { 191 const currentDir: string = FileUtils.getPathWithoutPrefix(dirName, dirPrefix); 192 let newDir: string = this.mCustomProfiles.mOutputDir; 193 // there is no need to create directory because the directory names will be obfuscated. 194 if (!this.mCustomProfiles.mRenameFileName?.mEnable) { 195 newDir = path.join(this.mCustomProfiles.mOutputDir, currentDir); 196 if (!fs.existsSync(newDir)) { 197 fs.mkdirSync(newDir); 198 } 199 } 200 201 const fileNames: string[] = fs.readdirSync(dirName); 202 for (let fileName of fileNames) { 203 const filePath: string = path.join(dirName, fileName); 204 if (fs.lstatSync(filePath).isFile()) { 205 await this.obfuscateFile(filePath, newDir); 206 continue; 207 } 208 209 await this.obfuscateDir(filePath, dirPrefix); 210 } 211 } 212 213 private readNameCache(sourceFile: string, outputDir: string): void { 214 if (!this.mCustomProfiles.mNameObfuscation?.mEnable || !this.mCustomProfiles.mEnableNameCache) { 215 return; 216 } 217 218 const nameCachePath: string = path.join(outputDir, FileUtils.getFileName(sourceFile) + NAME_CACHE_SUFFIX); 219 const nameCache: Object = readCache(nameCachePath); 220 221 renameIdentifierModule.historyNameCache = getMapFromJson(nameCache); 222 } 223 224 private readPropertyCache(outputDir: string): void { 225 if (!this.mCustomProfiles.mNameObfuscation?.mRenameProperties || !this.mCustomProfiles.mEnableNameCache) { 226 return; 227 } 228 229 const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE); 230 const propertyCache: Object = readCache(propertyCachePath); 231 if (!propertyCache) { 232 return; 233 } 234 235 renamePropertyModule.historyMangledTable = getMapFromJson(propertyCache); 236 } 237 238 private produceNameCache(namecache: { [k: string]: string }, resultPath: string): void { 239 const nameCachePath: string = resultPath + NAME_CACHE_SUFFIX; 240 fs.writeFileSync(nameCachePath, JSON.stringify(namecache, null, JSON_TEXT_INDENT_LENGTH)); 241 } 242 243 private producePropertyCache(outputDir: string): void { 244 if (this.mCustomProfiles.mNameObfuscation && 245 this.mCustomProfiles.mNameObfuscation.mRenameProperties && 246 this.mCustomProfiles.mEnableNameCache) { 247 const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE); 248 writeCache(renamePropertyModule.globalMangledTable, propertyCachePath); 249 } 250 } 251 252 async mergeSourceMap(originMap: sourceMap.RawSourceMap, newMap: sourceMap.RawSourceMap): Promise<RawSourceMap> { 253 if (!originMap) { 254 return newMap as RawSourceMap; 255 } 256 257 if (!newMap) { 258 return originMap as RawSourceMap; 259 } 260 261 const originConsumer: sourceMap.SourceMapConsumer = await new sourceMap.SourceMapConsumer(originMap); 262 const newConsumer: sourceMap.SourceMapConsumer = await new sourceMap.SourceMapConsumer(newMap); 263 const newMappingList: sourceMap.MappingItem[] = []; 264 newConsumer.eachMapping((mapping: sourceMap.MappingItem) => { 265 if (mapping.originalLine == null) { 266 return; 267 } 268 269 const originalPos = originConsumer.originalPositionFor({ 270 line: mapping.originalLine, 271 column: mapping.originalColumn 272 }); 273 274 if (originalPos.source == null) { 275 return; 276 } 277 278 mapping.originalLine = originalPos.line; 279 mapping.originalColumn = originalPos.column; 280 newMappingList.push(mapping); 281 }); 282 283 const updatedGenerator: sourceMap.SourceMapGenerator = sourceMap.SourceMapGenerator.fromSourceMap(newConsumer); 284 updatedGenerator['_file'] = originMap.file; 285 updatedGenerator['_mappings']['_array'] = newMappingList; 286 return JSON.parse(updatedGenerator.toString()) as RawSourceMap; 287 } 288 289 /** 290 * A Printer to output obfuscated codes. 291 */ 292 public createObfsPrinter(isDeclarationFile: boolean): Printer { 293 // set print options 294 let printerOptions: PrinterOptions = {}; 295 let removeOption = this.mCustomProfiles.mRemoveDeclarationComments; 296 let keepDeclarationComments = !removeOption || !removeOption.mEnable || (removeOption.mReservedComments && removeOption.mReservedComments.length > 0); 297 298 if (isDeclarationFile && keepDeclarationComments) { 299 printerOptions.removeComments = false; 300 } 301 if ((!isDeclarationFile && this.mCustomProfiles.mRemoveComments) || (isDeclarationFile && !keepDeclarationComments)) { 302 printerOptions.removeComments = true; 303 } 304 305 return createPrinter(printerOptions); 306 } 307 308 private isObfsIgnoreFile(fileName: string): boolean { 309 let suffix: string = FileUtils.getFileExtension(fileName); 310 311 return (suffix !== 'js' && suffix !== 'ts' && suffix !== 'ets'); 312 } 313 314 /** 315 * Obfuscate single source file with path provided 316 * 317 * @param sourceFilePath single source file path 318 * @param outputDir 319 */ 320 public async obfuscateFile(sourceFilePath: string, outputDir: string): Promise<void> { 321 const fileName: string = FileUtils.getFileName(sourceFilePath); 322 if (this.isObfsIgnoreFile(fileName)) { 323 fs.copyFileSync(sourceFilePath, path.join(outputDir, fileName)); 324 return; 325 } 326 327 // Add the whitelist of file name obfuscation for ut. 328 if (this.mCustomProfiles.mRenameFileName?.mEnable) { 329 this.mCustomProfiles.mRenameFileName.mReservedFileNames.push(this.mConfigPath); 330 } 331 let content: string = FileUtils.readFile(sourceFilePath); 332 this.readNameCache(sourceFilePath, outputDir); 333 const mixedInfo: ObfuscationResultType = await this.obfuscate(content, sourceFilePath); 334 335 if (outputDir && mixedInfo) { 336 // the writing file is for the ut. 337 const testCasesRootPath = path.join(__dirname, '../', 'test/grammar'); 338 let relativePath = ''; 339 let resultPath = ''; 340 if (this.mCustomProfiles.mRenameFileName?.mEnable && mixedInfo.filePath) { 341 relativePath = mixedInfo.filePath.replace(testCasesRootPath, ''); 342 } else { 343 relativePath = sourceFilePath.replace(testCasesRootPath, ''); 344 } 345 resultPath = path.join(this.mCustomProfiles.mOutputDir, relativePath); 346 fs.mkdirSync(path.dirname(resultPath), {recursive: true}); 347 fs.writeFileSync(resultPath, mixedInfo.content); 348 349 if (this.mCustomProfiles.mEnableSourceMap && mixedInfo.sourceMap) { 350 fs.writeFileSync(path.join(resultPath + '.map'), 351 JSON.stringify(mixedInfo.sourceMap, null, JSON_TEXT_INDENT_LENGTH)); 352 } 353 354 if (this.mCustomProfiles.mEnableNameCache && this.mCustomProfiles.mEnableNameCache) { 355 this.produceNameCache(mixedInfo.nameCache, resultPath); 356 } 357 } 358 } 359 360 /** 361 * Obfuscate ast of a file. 362 * @param content ast or source code of a source file 363 * @param sourceFilePath 364 * @param previousStageSourceMap 365 * @param historyNameCache 366 * @param originalFilePath When filename obfuscation is enabled, it is used as the source code path. 367 */ 368 public async obfuscate(content: SourceFile | string, sourceFilePath: string, previousStageSourceMap?: sourceMap.RawSourceMap, 369 historyNameCache?: Map<string, string>, originalFilePath?: string): Promise<ObfuscationResultType> { 370 let ast: SourceFile; 371 let result: ObfuscationResultType = { content: undefined }; 372 if (this.isObfsIgnoreFile(sourceFilePath)) { 373 // need add return value 374 return result; 375 } 376 377 if (typeof content === 'string') { 378 ast = createSourceFile(sourceFilePath, content, ScriptTarget.ES2015, true); 379 } else { 380 ast = content; 381 } 382 383 if (ast.statements.length === 0) { 384 return result; 385 } 386 387 if (historyNameCache && this.mCustomProfiles.mNameObfuscation) { 388 renameIdentifierModule.historyNameCache = historyNameCache; 389 } 390 391 if (this.mCustomProfiles.mRenameFileName?.mEnable ) { 392 orignalFilePathForSearching = originalFilePath ? originalFilePath : ast.fileName; 393 } 394 395 if (!this.mCustomProfiles.mRemoveDeclarationComments || !this.mCustomProfiles.mRemoveDeclarationComments.mEnable) { 396 //@ts-ignore 397 ast.reservedComments = undefined; 398 } else { 399 //@ts-ignore 400 ast.reservedComments ??= this.mCustomProfiles.mRemoveDeclarationComments.mReservedComments ? 401 this.mCustomProfiles.mRemoveDeclarationComments.mReservedComments : []; 402 } 403 404 let transformedResult: TransformationResult<Node> = transform(ast, this.mTransformers, this.mCompilerOptions); 405 ast = transformedResult.transformed[0] as SourceFile; 406 407 // convert ast to output source file and generate sourcemap if needed. 408 let sourceMapGenerator: SourceMapGenerator = undefined; 409 if (this.mCustomProfiles.mEnableSourceMap) { 410 sourceMapGenerator = getSourceMapGenerator(sourceFilePath); 411 } 412 413 this.createObfsPrinter(ast.isDeclarationFile).writeFile(ast, this.mTextWriter, sourceMapGenerator); 414 415 result.filePath = ast.fileName; 416 result.content = this.mTextWriter.getText(); 417 418 if (this.mCustomProfiles.mEnableSourceMap && sourceMapGenerator) { 419 let sourceMapJson: RawSourceMap = sourceMapGenerator.toJSON(); 420 sourceMapJson.sourceRoot = ''; 421 sourceMapJson.file = path.basename(sourceFilePath); 422 if (previousStageSourceMap) { 423 sourceMapJson = await this.mergeSourceMap(previousStageSourceMap, sourceMapJson as sourceMap.RawSourceMap); 424 } 425 result.sourceMap = sourceMapJson; 426 } 427 428 if (this.mCustomProfiles.mEnableNameCache && renameIdentifierModule.nameCache) { 429 result.nameCache = Object.fromEntries(renameIdentifierModule.nameCache); 430 } 431 432 // clear cache of text writer 433 this.mTextWriter.clear(); 434 if (renameIdentifierModule.nameCache) { 435 renameIdentifierModule.nameCache.clear(); 436 } 437 438 renameIdentifierModule.historyNameCache = undefined; 439 return result; 440 } 441} 442 443export {ApiExtractor}; 444