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