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