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 factory, 18 isStringLiteral, 19 isExportDeclaration, 20 isImportDeclaration, 21 isSourceFile, 22 setParentRecursive, 23 visitEachChild, 24 isStructDeclaration, 25 SyntaxKind, 26 isConstructorDeclaration, 27} from 'typescript'; 28 29import type { 30 CallExpression, 31 Expression, 32 ImportDeclaration, 33 ExportDeclaration, 34 Node, 35 StringLiteral, 36 TransformationContext, 37 Transformer, 38 StructDeclaration, 39 SourceFile, 40 ClassElement, 41 ImportCall, 42 TransformerFactory, 43} from 'typescript'; 44 45import fs from 'fs'; 46import path from 'path'; 47 48import type { IOptions } from '../../configs/IOptions'; 49import type { TransformPlugin } from '../TransformPlugin'; 50import { TransformerOrder } from '../TransformPlugin'; 51import type { IFileNameObfuscationOption } from '../../configs/INameObfuscationOption'; 52import { OhmUrlStatus } from '../../configs/INameObfuscationOption'; 53import { NameGeneratorType, getNameGenerator } from '../../generator/NameFactory'; 54import type { INameGenerator, NameGeneratorOptions } from '../../generator/INameGenerator'; 55import { FileUtils, BUNDLE, NORMALIZE } from '../../utils/FileUtils'; 56import { NodeUtils } from '../../utils/NodeUtils'; 57import { orignalFilePathForSearching, performancePrinter, ArkObfuscator } from '../../ArkObfuscator'; 58import type { PathAndExtension, ProjectInfo } from '../../common/type'; 59import { endFilesEvent, endSingleFileEvent, startFilesEvent, startSingleFileEvent } from '../../utils/PrinterUtils'; 60import { EventList, endSingleFileForMoreTimeEvent, startSingleFileForMoreTimeEvent } from '../../utils/PrinterTimeAndMemUtils'; 61import { needToBeReserved } from '../../utils/TransformUtil'; 62import { MemoryDottingDefine } from '../../utils/MemoryDottingDefine'; 63namespace secharmony { 64 65 // global mangled file name table used by all files in a project 66 export let globalFileNameMangledTable: Map<string, string> = new Map<string, string>(); 67 68 // used for file name cache 69 export let historyFileNameMangledTable: Map<string, string> = undefined; 70 71 // When the module is compiled, call this function to clear global collections related to file name. 72 export function clearCaches(): void { 73 globalFileNameMangledTable.clear(); 74 historyFileNameMangledTable?.clear(); 75 } 76 77 let profile: IFileNameObfuscationOption | undefined; 78 let generator: INameGenerator | undefined; 79 let reservedFileNames: Set<string> | undefined; 80 let localPackageSet: Set<string> | undefined; 81 let useNormalized: boolean = false; 82 let universalReservedFileNames: RegExp[] | undefined; 83 84 /** 85 * Rename Properties Transformer 86 * 87 * @param option obfuscation options 88 */ 89 const createRenameFileNameFactory = function (options: IOptions): TransformerFactory<Node> | null { 90 startFilesEvent(EventList.FILENAME_OBFUSCATION_INITIALIZATION); 91 profile = options?.mRenameFileName; 92 if (!profile || !profile.mEnable) { 93 endFilesEvent(EventList.FILENAME_OBFUSCATION_INITIALIZATION); 94 return null; 95 } 96 97 let nameGeneratorOption: NameGeneratorOptions = {}; 98 99 generator = getNameGenerator(profile.mNameGeneratorType, nameGeneratorOption); 100 let configReservedFileNameOrPath: string[] = profile?.mReservedFileNames ?? []; 101 const tempReservedName: string[] = ['.', '..', '']; 102 configReservedFileNameOrPath.map(fileNameOrPath => { 103 if (!fileNameOrPath || fileNameOrPath.length === 0) { 104 return; 105 } 106 const directories = FileUtils.splitFilePath(fileNameOrPath); 107 directories.forEach(directory => { 108 tempReservedName.push(directory); 109 const pathOrExtension: PathAndExtension = FileUtils.getFileSuffix(directory); 110 if (pathOrExtension.ext) { 111 tempReservedName.push(pathOrExtension.ext); 112 tempReservedName.push(pathOrExtension.path); 113 } 114 }); 115 }); 116 reservedFileNames = new Set<string>(tempReservedName); 117 universalReservedFileNames = profile?.mUniversalReservedFileNames ?? []; 118 endFilesEvent(EventList.FILENAME_OBFUSCATION_INITIALIZATION); 119 return renameFileNameFactory; 120 121 function renameFileNameFactory(context: TransformationContext): Transformer<Node> { 122 let projectInfo: ProjectInfo = ArkObfuscator.mProjectInfo; 123 if (projectInfo && projectInfo.localPackageSet) { 124 localPackageSet = projectInfo.localPackageSet; 125 useNormalized = projectInfo.useNormalized; 126 } 127 128 return renameFileNameTransformer; 129 130 function renameFileNameTransformer(node: Node): Node { 131 if (globalFileNameMangledTable === undefined) { 132 globalFileNameMangledTable = new Map<string, string>(); 133 } 134 135 const recordInfo = ArkObfuscator.recordStage(MemoryDottingDefine.FILENAME_OBFUSCATION); 136 startSingleFileEvent(EventList.FILENAME_OBFUSCATION, performancePrinter.timeSumPrinter); 137 let ret: Node = updateNodeInfo(node); 138 if (!isInOhModules(projectInfo, orignalFilePathForSearching) && isSourceFile(ret)) { 139 const orignalAbsPath = ret.fileName; 140 const mangledAbsPath: string = getMangleCompletePath(orignalAbsPath); 141 ret.fileName = mangledAbsPath; 142 } 143 endSingleFileEvent(EventList.FILENAME_OBFUSCATION, performancePrinter.timeSumPrinter); 144 startSingleFileEvent(EventList.UPDATE_PARENT_NODE); 145 let parentNodes = setParentRecursive(ret, true); 146 endSingleFileEvent(EventList.UPDATE_PARENT_NODE); 147 ArkObfuscator.stopRecordStage(recordInfo); 148 return parentNodes; 149 } 150 151 function updateNodeInfo(node: Node): Node { 152 if (isImportDeclaration(node) || isExportDeclaration(node)) { 153 startSingleFileForMoreTimeEvent(EventList.UPDATE_IMPORT_OR_EXPORT); 154 const updatedNode = updateImportOrExportDeclaration(node); 155 endSingleFileForMoreTimeEvent(EventList.UPDATE_IMPORT_OR_EXPORT); 156 return updatedNode; 157 } 158 if (isImportCall(node)) { 159 startSingleFileForMoreTimeEvent(EventList.UPDATE_DYNAMIC_IMPORT); 160 const tryUpdatedNode = tryUpdateDynamicImport(node); 161 endSingleFileForMoreTimeEvent(EventList.UPDATE_DYNAMIC_IMPORT); 162 return tryUpdatedNode; 163 } 164 165 return visitEachChild(node, updateNodeInfo, context); 166 } 167 } 168 }; 169 170 export function isInOhModules(proInfo: ProjectInfo, originalPath: string): boolean { 171 let ohPackagePath: string = ''; 172 if (proInfo && proInfo.projectRootPath && proInfo.packageDir) { 173 ohPackagePath = FileUtils.toUnixPath(path.resolve(proInfo.projectRootPath, proInfo.packageDir)); 174 } 175 return ohPackagePath && FileUtils.toUnixPath(originalPath).indexOf(ohPackagePath) !== -1; 176 } 177 178 function updateImportOrExportDeclaration(node: ImportDeclaration | ExportDeclaration): ImportDeclaration | ExportDeclaration { 179 if (!node.moduleSpecifier) { 180 return node; 181 } 182 const mangledModuleSpecifier = renameStringLiteral(node.moduleSpecifier as StringLiteral); 183 if (isImportDeclaration(node)) { 184 return factory.updateImportDeclaration(node, node.modifiers, node.importClause, mangledModuleSpecifier as Expression, node.assertClause); 185 } else { 186 return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, mangledModuleSpecifier as Expression, 187 node.assertClause); 188 } 189 } 190 191 export function updateImportOrExportDeclarationForTest(node: ImportDeclaration | ExportDeclaration): ImportDeclaration | ExportDeclaration { 192 return updateImportOrExportDeclaration(node); 193 } 194 195 function isImportCall(n: Node): n is ImportCall { 196 return n.kind === SyntaxKind.CallExpression && (<CallExpression>n).expression.kind === SyntaxKind.ImportKeyword; 197 } 198 199 function canBeObfuscatedFilePath(filePath: string): boolean { 200 return path.isAbsolute(filePath) || FileUtils.isRelativePath(filePath) || isLocalDependencyOhmUrl(filePath); 201 } 202 203 function isLocalDependencyOhmUrl(filePath: string): boolean { 204 // mOhmUrlStatus: for unit test in Arkguard 205 if (profile?.mOhmUrlStatus === OhmUrlStatus.AT_BUNDLE || 206 profile?.mOhmUrlStatus === OhmUrlStatus.NORMALIZED) { 207 return true; 208 } 209 210 let packageName: string; 211 // Only hap and local har need be mangled. 212 if (useNormalized) { 213 if (!filePath.startsWith(NORMALIZE)) { 214 return false; 215 } 216 packageName = handleNormalizedOhmUrl(filePath, true); 217 } else { 218 if (!filePath.startsWith(BUNDLE)) { 219 return false; 220 } 221 packageName = getAtBundlePgkName(filePath); 222 } 223 return localPackageSet && localPackageSet.has(packageName); 224 } 225 226 export function isLocalDependencyOhmUrlForTest(filePath: string): boolean { 227 return isLocalDependencyOhmUrl(filePath); 228 } 229 230 function getAtBundlePgkName(ohmUrl: string): string { 231 /* Unnormalized OhmUrl Format: 232 * hap/hsp: @bundle:${bundleName}/${moduleName}/ 233 * har: @bundle:${bundleName}/${moduleName}@${harName}/ 234 * package name is {moduleName} in hap/hsp or {harName} in har. 235 */ 236 let moduleName: string = ohmUrl.split('/')[1]; // 1: the index of moduleName in array. 237 const indexOfSign: number = moduleName.indexOf('@'); 238 if (indexOfSign !== -1) { 239 moduleName = moduleName.slice(indexOfSign + 1); // 1: the index start from indexOfSign + 1. 240 } 241 return moduleName; 242 } 243 244 // dynamic import example: let module = import('./a') 245 function tryUpdateDynamicImport(node: CallExpression): CallExpression { 246 if (node.expression && node.arguments.length === 1 && isStringLiteral(node.arguments[0])) { 247 const obfuscatedArgument = [renameStringLiteral(node.arguments[0] as StringLiteral)]; 248 if (obfuscatedArgument[0] !== node.arguments[0]) { 249 return factory.updateCallExpression(node, node.expression, node.typeArguments, obfuscatedArgument); 250 } 251 } 252 return node; 253 } 254 255 function renameStringLiteral(node: StringLiteral): Expression { 256 let expr: StringLiteral = renameFileName(node) as StringLiteral; 257 if (expr !== node) { 258 return factory.createStringLiteral(expr.text); 259 } 260 return node; 261 } 262 263 function renameFileName(node: StringLiteral): Node { 264 let original: string = ''; 265 original = node.text; 266 original = original.replace(/\\/g, '/'); 267 268 if (!canBeObfuscatedFilePath(original)) { 269 return node; 270 } 271 272 let mangledFileName: string = getMangleIncompletePath(original); 273 if (mangledFileName === original) { 274 return node; 275 } 276 277 return factory.createStringLiteral(mangledFileName); 278 } 279 280 export function getMangleCompletePath(originalCompletePath: string): string { 281 originalCompletePath = FileUtils.toUnixPath(originalCompletePath); 282 const { path: filePathWithoutSuffix, ext: extension } = FileUtils.getFileSuffix(originalCompletePath); 283 const mangleFilePath = mangleFileName(filePathWithoutSuffix); 284 return mangleFilePath + extension; 285 } 286 287 function getMangleIncompletePath(orignalPath: string): string { 288 // The ohmUrl format does not have file extension 289 if (isLocalDependencyOhmUrl(orignalPath)) { 290 const mangledOhmUrl = mangleOhmUrl(orignalPath); 291 return mangledOhmUrl; 292 } 293 294 // Try to concat the extension for orignalPath. 295 const pathAndExtension : PathAndExtension | undefined = tryValidateFileExisting(orignalPath); 296 if (!pathAndExtension) { 297 return orignalPath; 298 } 299 300 if (pathAndExtension.ext) { 301 const mangleFilePath = mangleFileName(pathAndExtension.path); 302 return mangleFilePath; 303 } 304 /** 305 * import * from './filename1.js'. We just need to obfuscate 'filename1' and then concat the extension 'js'. 306 * import * from './direcotry'. For the grammar of importing directory, TSC will look for index.ets/index.ts when parsing. 307 * We obfuscate directory name and do not need to concat extension. 308 */ 309 const { path: filePathWithoutSuffix, ext: extension } = FileUtils.getFileSuffix(pathAndExtension.path); 310 const mangleFilePath = mangleFileName(filePathWithoutSuffix); 311 return mangleFilePath + extension; 312 } 313 314 export function getMangleIncompletePathForTest(orignalPath: string): string { 315 return getMangleIncompletePath(orignalPath); 316 }; 317 318 export function mangleOhmUrl(ohmUrl: string): string { 319 let mangledOhmUrl: string; 320 // mOhmUrlStatus: for unit test in Arkguard 321 if (useNormalized || profile?.mOhmUrlStatus === OhmUrlStatus.NORMALIZED) { 322 mangledOhmUrl = handleNormalizedOhmUrl(ohmUrl); 323 } else { 324 /** 325 * OhmUrl Format: 326 * fixed parts in hap/hsp: @bundle:${bundleName}/${moduleName}/ 327 * fixed parts in har: @bundle:${bundleName}/${moduleName}@${harName}/ 328 * hsp example: @bundle:com.example.myapplication/entry/index 329 * har example: @bundle:com.example.myapplication/entry@library_test/index 330 * we do not mangle fixed parts. 331 */ 332 const originalOhmUrlSegments: string[] = FileUtils.splitFilePath(ohmUrl); 333 const prefixSegments: string[] = originalOhmUrlSegments.slice(0, 2); // 2: length of fixed parts in array 334 const urlSegments: string[] = originalOhmUrlSegments.slice(2); // 2: index of mangled parts in array 335 const mangledOhmUrlSegments: string[] = urlSegments.map(originalSegment => mangleFileNamePart(originalSegment)); 336 mangledOhmUrl = prefixSegments.join('/') + '/' + mangledOhmUrlSegments.join('/'); 337 } 338 return mangledOhmUrl; 339 } 340 341 /** 342 * Normalized OhmUrl Format: 343 * hap/hsp: @normalized:N&<module name>&<bundle name>&<standard import path>& 344 * har: @normalized:N&&<bundle name>&<standard import path>&<version> 345 * we only mangle <standard import path>. 346 */ 347 export function handleNormalizedOhmUrl(ohmUrl: string, needPkgName?: boolean): string { 348 let originalOhmUrlSegments: string[] = ohmUrl.split('&'); 349 const standardImportPath = originalOhmUrlSegments[3]; // 3: index of standard import path in array. 350 let index = standardImportPath.indexOf('/'); 351 // The format of <module name>: @group/packagename or packagename, 352 // and there should only be one '@' symbol and one path separator '/' if and only if the 'group' exists. 353 if (standardImportPath.startsWith('@')) { 354 index = standardImportPath.indexOf('/', index + 1); 355 } 356 357 const pakName = standardImportPath.substring(0, index); 358 if (needPkgName) { 359 return pakName; 360 } 361 const realImportPath = standardImportPath.substring(index + 1); // 1: index of real import path in array. 362 const originalImportPathSegments: string[] = FileUtils.splitFilePath(realImportPath); 363 const mangledImportPathSegments: string[] = originalImportPathSegments.map(originalSegment => mangleFileNamePart(originalSegment)); 364 const mangledImportPath: string = pakName + '/' + mangledImportPathSegments.join('/'); 365 originalOhmUrlSegments[3] = mangledImportPath; // 3: index of standard import path in array. 366 return originalOhmUrlSegments.join('&'); 367 } 368 369 function mangleFileName(orignalPath: string): string { 370 const originalFileNameSegments: string[] = FileUtils.splitFilePath(orignalPath); 371 const mangledSegments: string[] = originalFileNameSegments.map(originalSegment => mangleFileNamePart(originalSegment)); 372 let mangledFileName: string = mangledSegments.join('/'); 373 return mangledFileName; 374 } 375 376 function mangleFileNamePart(original: string): string { 377 if (needToBeReserved(reservedFileNames, universalReservedFileNames, original)) { 378 return original; 379 } 380 381 const historyName: string = historyFileNameMangledTable?.get(original); 382 let mangledName: string = historyName ? historyName : globalFileNameMangledTable.get(original); 383 384 while (!mangledName) { 385 mangledName = generator.getName(); 386 if (mangledName === original || needToBeReserved(reservedFileNames, universalReservedFileNames, mangledName)) { 387 mangledName = null; 388 continue; 389 } 390 391 let reserved: string[] = [...globalFileNameMangledTable.values()]; 392 if (reserved.includes(mangledName)) { 393 mangledName = null; 394 continue; 395 } 396 397 if (historyFileNameMangledTable && [...historyFileNameMangledTable.values()].includes(mangledName)) { 398 mangledName = null; 399 continue; 400 } 401 } 402 globalFileNameMangledTable.set(original, mangledName); 403 return mangledName; 404 } 405 406 export let transformerPlugin: TransformPlugin = { 407 'name': 'renamePropertiesPlugin', 408 'order': TransformerOrder.RENAME_FILE_NAME_TRANSFORMER, 409 'createTransformerFactory': createRenameFileNameFactory 410 }; 411} 412 413export = secharmony; 414 415// typescript doesn't add the json extension. 416const extensionOrder: string[] = ['.ets', '.ts', '.d.ets', '.d.ts', '.js']; 417 418function tryValidateFileExisting(importPath: string): PathAndExtension | undefined { 419 let fileAbsPath: string = ''; 420 if (path.isAbsolute(importPath)) { 421 fileAbsPath = importPath; 422 } else { 423 fileAbsPath = path.join(path.dirname(orignalFilePathForSearching), importPath); 424 } 425 426 const filePathExtensionLess: string = path.normalize(fileAbsPath); 427 for (let ext of extensionOrder) { 428 const targetPath = filePathExtensionLess + ext; 429 if (fs.existsSync(targetPath)) { 430 return {path: importPath, ext: ext}; 431 } 432 } 433 434 // all suffixes are not matched, search this file directly. 435 if (fs.existsSync(filePathExtensionLess)) { 436 return { path: importPath, ext: undefined }; 437 } 438 return undefined; 439}