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} 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'); 59 60export {getMapFromJson, readProjectProperties}; 61 62type ObfuscationResultType = { 63 content: string, 64 sourceMap?: RawSourceMap, 65 nameCache?: { [k: string]: string } 66}; 67 68const JSON_TEXT_INDENT_LENGTH: number = 2; 69export class ArkObfuscator { 70 // A text writer of Printer 71 private mTextWriter: EmitTextWriter; 72 73 // A list of source file path 74 private readonly mSourceFiles: string[]; 75 76 // Path of obfuscation configuration file. 77 private readonly mConfigPath: string; 78 79 // Compiler Options for typescript,use to parse ast 80 private readonly mCompilerOptions: CompilerOptions; 81 82 // User custom obfuscation profiles. 83 private mCustomProfiles: IOptions; 84 85 private mTransformers: TransformerFactory<Node>[]; 86 87 public constructor(sourceFiles?: string[], configPath?: string) { 88 this.mSourceFiles = sourceFiles; 89 this.mConfigPath = configPath; 90 this.mCompilerOptions = {}; 91 this.mTransformers = []; 92 } 93 94 /** 95 * init ArkObfuscator according to user config 96 * should be called after constructor 97 */ 98 public init(config?: IOptions): boolean { 99 if (!this.mConfigPath && !config) { 100 return false; 101 } 102 103 if (this.mConfigPath) { 104 config = FileUtils.readFileAsJson(this.mConfigPath); 105 } 106 107 this.mCustomProfiles = config; 108 109 if (this.mCustomProfiles.mCompact) { 110 this.mTextWriter = createObfTextSingleLineWriter(); 111 } else { 112 this.mTextWriter = createTextWriter('\n'); 113 } 114 115 if (this.mCustomProfiles.mEnableSourceMap) { 116 this.mCompilerOptions.sourceMap = true; 117 } 118 119 // load transformers 120 this.mTransformers = TransformerManager.getInstance().loadTransformers(this.mCustomProfiles); 121 122 if (needReadApiInfo(this.mCustomProfiles)) { 123 this.mCustomProfiles.mNameObfuscation.mReservedProperties = ListUtil.uniqueMergeList( 124 this.mCustomProfiles.mNameObfuscation.mReservedProperties, 125 this.mCustomProfiles.mNameObfuscation.mReservedNames, 126 es6Info); 127 } 128 129 return true; 130 } 131 132 /** 133 * Obfuscate all the source files. 134 */ 135 public async obfuscateFiles(): Promise<void> { 136 if (!path.isAbsolute(this.mCustomProfiles.mOutputDir)) { 137 this.mCustomProfiles.mOutputDir = path.join(path.dirname(this.mConfigPath), this.mCustomProfiles.mOutputDir); 138 } 139 140 if (this.mCustomProfiles.mOutputDir && !fs.existsSync(this.mCustomProfiles.mOutputDir)) { 141 fs.mkdirSync(this.mCustomProfiles.mOutputDir); 142 } 143 readProjectProperties(this.mSourceFiles, this.mCustomProfiles); 144 this.readPropertyCache(this.mCustomProfiles.mOutputDir); 145 146 // support directory and file obfuscate 147 for (const sourcePath of this.mSourceFiles) { 148 if (!fs.existsSync(sourcePath)) { 149 console.error(`File ${FileUtils.getFileName(sourcePath)} is not found.`); 150 return; 151 } 152 153 if (fs.lstatSync(sourcePath).isFile()) { 154 await this.obfuscateFile(sourcePath, this.mCustomProfiles.mOutputDir); 155 continue; 156 } 157 158 const dirPrefix: string = FileUtils.getPrefix(sourcePath); 159 await this.obfuscateDir(sourcePath, dirPrefix); 160 } 161 162 this.producePropertyCache(this.mCustomProfiles.mOutputDir); 163 } 164 165 /** 166 * obfuscate directory 167 * @private 168 */ 169 private async obfuscateDir(dirName: string, dirPrefix: string): Promise<void> { 170 const currentDir: string = FileUtils.getPathWithoutPrefix(dirName, dirPrefix); 171 const newDir: string = path.join(this.mCustomProfiles.mOutputDir, currentDir); 172 if (!fs.existsSync(newDir)) { 173 fs.mkdirSync(newDir); 174 } 175 176 const fileNames: string[] = fs.readdirSync(dirName); 177 for (let fileName of fileNames) { 178 const filePath: string = path.join(dirName, fileName); 179 if (fs.lstatSync(filePath).isFile()) { 180 await this.obfuscateFile(filePath, newDir); 181 continue; 182 } 183 184 if (fileName === 'node_modules' || fileName === 'oh_modules') { 185 continue; 186 } 187 188 await this.obfuscateDir(filePath, dirPrefix); 189 } 190 } 191 192 private readNameCache(sourceFile: string, outputDir: string): void { 193 if (!this.mCustomProfiles.mNameObfuscation.mEnable || !this.mCustomProfiles.mEnableNameCache) { 194 return; 195 } 196 197 const nameCachePath: string = path.join(outputDir, FileUtils.getFileName(sourceFile) + NAME_CACHE_SUFFIX); 198 const nameCache: Object = readCache(nameCachePath); 199 200 renameIdentifierModule.historyNameCache = getMapFromJson(nameCache); 201 } 202 203 private readPropertyCache(outputDir: string): void { 204 if (!this.mCustomProfiles.mNameObfuscation.mRenameProperties || !this.mCustomProfiles.mEnableNameCache) { 205 return; 206 } 207 208 const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE); 209 const propertyCache: Object = readCache(propertyCachePath); 210 if (!propertyCache) { 211 return; 212 } 213 214 renamePropertyModule.historyMangledTable = getMapFromJson(propertyCache); 215 } 216 217 private produceNameCache(namecache: { [k: string]: string }, sourceFile: string, outputDir: string): void { 218 const nameCachePath: string = path.join(outputDir, FileUtils.getFileName(sourceFile) + NAME_CACHE_SUFFIX); 219 fs.writeFileSync(nameCachePath, JSON.stringify(namecache, null, JSON_TEXT_INDENT_LENGTH)); 220 } 221 222 private producePropertyCache(outputDir: string): void { 223 if (this.mCustomProfiles.mNameObfuscation.mRenameProperties && this.mCustomProfiles.mEnableNameCache) { 224 const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE); 225 writeCache(renamePropertyModule.globalMangledTable, propertyCachePath); 226 } 227 } 228 229 async mergeSourceMap(originMap: sourceMap.RawSourceMap, newMap: sourceMap.RawSourceMap): Promise<RawSourceMap> { 230 if (!originMap) { 231 return newMap as RawSourceMap; 232 } 233 234 if (!newMap) { 235 return originMap as RawSourceMap; 236 } 237 238 const originConsumer: sourceMap.SourceMapConsumer = await new sourceMap.SourceMapConsumer(originMap); 239 const newConsumer: sourceMap.SourceMapConsumer = await new sourceMap.SourceMapConsumer(newMap); 240 const newMappingList: sourceMap.MappingItem[] = []; 241 newConsumer.eachMapping((mapping: sourceMap.MappingItem) => { 242 if (mapping.originalLine == null) { 243 return; 244 } 245 246 const originalPos = originConsumer.originalPositionFor({ 247 line: mapping.originalLine, 248 column: mapping.originalColumn 249 }); 250 251 if (originalPos.source == null) { 252 return; 253 } 254 255 mapping.originalLine = originalPos.line; 256 mapping.originalColumn = originalPos.column; 257 newMappingList.push(mapping); 258 }); 259 260 const updatedGenerator: sourceMap.SourceMapGenerator = sourceMap.SourceMapGenerator.fromSourceMap(newConsumer); 261 updatedGenerator['_file'] = originMap.file; 262 updatedGenerator['_mappings']['_array'] = newMappingList; 263 return JSON.parse(updatedGenerator.toString()) as RawSourceMap; 264 } 265 266 /** 267 * A Printer to output obfuscated codes. 268 */ 269 public createObfsPrinter(): Printer { 270 // set print options 271 let printerOptions: PrinterOptions = {}; 272 if (this.mCustomProfiles.mRemoveComments) { 273 printerOptions.removeComments = true; 274 } 275 276 return createPrinter(printerOptions); 277 } 278 279 private isObfsIgnoreFile(fileName: string): boolean { 280 let suffix: string = FileUtils.getFileExtension(fileName); 281 282 return (suffix !== 'js' && suffix !== 'ts') || fileName.endsWith('.d.ts'); 283 } 284 285 /** 286 * Obfuscate single source file with path provided 287 * 288 * @param sourceFilePath single source file path 289 * @param outputDir 290 */ 291 public async obfuscateFile(sourceFilePath: string, outputDir: string): Promise<void> { 292 const fileName: string = FileUtils.getFileName(sourceFilePath); 293 if (this.isObfsIgnoreFile(fileName)) { 294 fs.copyFileSync(sourceFilePath, path.join(outputDir, fileName)); 295 return; 296 } 297 298 let content: string = FileUtils.readFile(sourceFilePath); 299 this.readNameCache(sourceFilePath, outputDir); 300 const mixedInfo: ObfuscationResultType = await this.obfuscate(content, sourceFilePath); 301 302 if (outputDir && mixedInfo) { 303 fs.writeFileSync(path.join(outputDir, FileUtils.getFileName(sourceFilePath)), mixedInfo.content); 304 if (this.mCustomProfiles.mEnableSourceMap && mixedInfo.sourceMap) { 305 fs.writeFileSync(path.join(outputDir, FileUtils.getFileName(sourceFilePath) + '.map'), 306 JSON.stringify(mixedInfo.sourceMap, null, JSON_TEXT_INDENT_LENGTH)); 307 } 308 if (this.mCustomProfiles.mEnableNameCache && this.mCustomProfiles.mEnableNameCache) { 309 this.produceNameCache(mixedInfo.nameCache, sourceFilePath, outputDir); 310 } 311 } 312 } 313 314 /** 315 * Obfuscate ast of a file. 316 * @param content ast or source code of a source file 317 * @param sourceFilePath 318 * @param previousStageSourceMap 319 * @param historyNameCache 320 */ 321 public async obfuscate(content: SourceFile | string, sourceFilePath: string, previousStageSourceMap?: sourceMap.RawSourceMap, 322 historyNameCache?: Map<string, string>): Promise<ObfuscationResultType> { 323 let ast: SourceFile; 324 let result: ObfuscationResultType = { content: undefined }; 325 if (this.isObfsIgnoreFile(sourceFilePath)) { 326 // need add return value 327 return result; 328 } 329 330 if (typeof content === 'string') { 331 ast = createSourceFile(sourceFilePath, content, ScriptTarget.ES2015, true); 332 } else { 333 ast = content; 334 } 335 336 if (ast.statements.length === 0) { 337 return result; 338 } 339 340 if (historyNameCache && this.mCustomProfiles.mNameObfuscation) { 341 renameIdentifierModule.historyNameCache = historyNameCache; 342 } 343 344 let transformedResult: TransformationResult<Node> = transform(ast, this.mTransformers, this.mCompilerOptions); 345 ast = transformedResult.transformed[0] as SourceFile; 346 347 // convert ast to output source file and generate sourcemap if needed. 348 let sourceMapGenerator: SourceMapGenerator = undefined; 349 if (this.mCustomProfiles.mEnableSourceMap) { 350 sourceMapGenerator = getSourceMapGenerator(sourceFilePath); 351 } 352 353 this.createObfsPrinter().writeFile(ast, this.mTextWriter, sourceMapGenerator); 354 355 result.content = this.mTextWriter.getText(); 356 357 if (this.mCustomProfiles.mEnableSourceMap && sourceMapGenerator) { 358 let sourceMapJson: RawSourceMap = sourceMapGenerator.toJSON(); 359 sourceMapJson.sourceRoot = ''; 360 sourceMapJson.file = path.basename(sourceFilePath); 361 if (previousStageSourceMap) { 362 sourceMapJson = await this.mergeSourceMap(previousStageSourceMap, sourceMapJson as sourceMap.RawSourceMap); 363 } 364 result.sourceMap = sourceMapJson; 365 } 366 367 if (this.mCustomProfiles.mEnableNameCache) { 368 result.nameCache = Object.fromEntries(renameIdentifierModule.nameCache); 369 } 370 371 // clear cache of text writer 372 this.mTextWriter.clear(); 373 if (renameIdentifierModule.nameCache) { 374 renameIdentifierModule.nameCache.clear(); 375 } 376 return result; 377 } 378} 379export {ApiExtractor}; 380