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