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 { NameGeneratorType, getNameGenerator } from '../../generator/NameFactory'; 53import type { INameGenerator, NameGeneratorOptions } from '../../generator/INameGenerator'; 54import { FileUtils } from '../../utils/FileUtils'; 55import { NodeUtils } from '../../utils/NodeUtils'; 56import { orignalFilePathForSearching } from '../../ArkObfuscator'; 57import type { PathAndExtension } from '../../common/type'; 58namespace secharmony { 59 60 // global mangled file name table used by all files in a project 61 export let globalFileNameMangledTable: Map<string, string> = undefined; 62 63 // used for file name cache 64 export let historyFileNameMangledTable: Map<string, string> = undefined; 65 66 let profile: IFileNameObfuscationOption | undefined; 67 let generator: INameGenerator | undefined; 68 let reservedFileNames: Set<string> | undefined; 69 /** 70 * Rename Properties Transformer 71 * 72 * @param option obfuscation options 73 */ 74 const createRenameFileNameFactory = function (options: IOptions): TransformerFactory<Node> { 75 profile = options?.mRenameFileName; 76 if (!profile || !profile.mEnable) { 77 return null; 78 } 79 80 return renameFileNameFactory; 81 82 function renameFileNameFactory(context: TransformationContext): Transformer<Node> { 83 let options: NameGeneratorOptions = {}; 84 if (profile.mNameGeneratorType === NameGeneratorType.HEX) { 85 options.hexWithPrefixSuffix = true; 86 } 87 88 generator = getNameGenerator(profile.mNameGeneratorType, options); 89 let tempReservedFileNameOrPath: string[] = profile?.mReservedFileNames ?? []; 90 let tempReservedFileName: string[] = ['.', '..', '']; 91 tempReservedFileNameOrPath.map(fileNameOrPath => { 92 if (fileNameOrPath && fileNameOrPath.length > 0) { 93 const directories = FileUtils.splitFilePath(fileNameOrPath); 94 directories.forEach(directory => { 95 tempReservedFileName.push(directory); 96 const pathOrExtension: PathAndExtension = FileUtils.getFileSuffix(directory); 97 if (pathOrExtension.ext) { 98 tempReservedFileName.push(pathOrExtension.ext); 99 tempReservedFileName.push(pathOrExtension.path); 100 } 101 }); 102 } 103 }); 104 reservedFileNames = new Set<string>(tempReservedFileName); 105 106 return renameFileNameTransformer; 107 108 function renameFileNameTransformer(node: Node): Node { 109 if (globalFileNameMangledTable === undefined) { 110 globalFileNameMangledTable = new Map<string, string>(); 111 } 112 113 let ret: Node = updateNodeInfo(node); 114 if (isSourceFile(ret)) { 115 const orignalAbsPath = ret.fileName; 116 const mangledAbsPath: string = getMangleCompletePath(orignalAbsPath); 117 ret.fileName = mangledAbsPath; 118 } 119 return setParentRecursive(ret, true); 120 } 121 122 function updateNodeInfo(node: Node): Node { 123 if (isImportDeclaration(node) || isExportDeclaration(node)) { 124 return updateImportOrExportDeclaration(node); 125 } 126 127 if (isImportCall(node)) { 128 return tryUpdateDynamicImport(node); 129 } 130 131 if (isStructDeclaration(node)) { 132 return tryRemoveVirtualConstructor(node); 133 } 134 return visitEachChild(node, updateNodeInfo, context); 135 } 136 } 137 }; 138 139 function updateImportOrExportDeclaration(node: ImportDeclaration | ExportDeclaration): ImportDeclaration | ExportDeclaration { 140 if (!node.moduleSpecifier) { 141 return node; 142 } 143 const mangledModuleSpecifier = renameStringLiteral(node.moduleSpecifier as StringLiteral); 144 if (isImportDeclaration(node)) { 145 return factory.updateImportDeclaration(node, node.modifiers, node.importClause, mangledModuleSpecifier as Expression, node.assertClause); 146 } else { 147 return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, mangledModuleSpecifier as Expression, 148 node.assertClause); 149 } 150 } 151 152 function isImportCall(n: Node): n is ImportCall { 153 return n.kind === SyntaxKind.CallExpression && (<CallExpression>n).expression.kind === SyntaxKind.ImportKeyword; 154 } 155 156 // dynamic import example: let module = import('./a') 157 function tryUpdateDynamicImport(node: CallExpression): CallExpression { 158 if (node.expression && node.arguments.length === 1 && isStringLiteral(node.arguments[0])) { 159 const obfuscatedArgument = [renameStringLiteral(node.arguments[0] as StringLiteral)]; 160 if (obfuscatedArgument[0] !== node.arguments[0]) { 161 return factory.updateCallExpression(node, node.expression, node.typeArguments, obfuscatedArgument); 162 } 163 } 164 return node; 165 } 166 167 function renameStringLiteral(node: StringLiteral): Expression { 168 let expr: StringLiteral = renameFileName(node) as StringLiteral; 169 if (expr !== node) { 170 return factory.createStringLiteral(expr.text); 171 } 172 return node; 173 } 174 175 function renameFileName(node: StringLiteral): Node { 176 let original: string = ''; 177 original = node.text; 178 original = original.replace(/\\/g, '/'); 179 180 if (!canBeObfuscatedFilePath(original)) { 181 return node; 182 } 183 184 let mangledFileName: string = getMangleIncompletePath(original); 185 if (mangledFileName === original) { 186 return node; 187 } 188 189 return factory.createStringLiteral(mangledFileName); 190 } 191 192 export function getMangleCompletePath(originalCompletePath: string): string { 193 originalCompletePath = toUnixPath(originalCompletePath); 194 const { path: filePathWithoutSuffix, ext: extension } = FileUtils.getFileSuffix(originalCompletePath); 195 const mangleFilePath = manglFileName(filePathWithoutSuffix); 196 return mangleFilePath + extension; 197 } 198 199 function getMangleIncompletePath(orignalPath: string): string { 200 // Try to concat the extension for orignalPath. 201 const pathAndExtension : PathAndExtension | undefined = tryValidateFileExisting(orignalPath); 202 if (!pathAndExtension) { 203 return orignalPath; 204 } 205 206 if (pathAndExtension.ext) { 207 const mangleFilePath = manglFileName(pathAndExtension.path); 208 return mangleFilePath; 209 } 210 /** 211 * import * from './filename1.js'. We just need to obfuscate 'filename1' and then concat the extension 'js'. 212 * import * from './direcotry'. For the grammar of importing directory, TSC will look for index.ets/index.ts when parsing. 213 * We obfuscate directory name and do not need to concat extension. 214 */ 215 const { path: filePathWithoutSuffix, ext: extension } = FileUtils.getFileSuffix(pathAndExtension.path); 216 const mangleFilePath = manglFileName(filePathWithoutSuffix); 217 return mangleFilePath + extension; 218 } 219 220 function manglFileName(orignalPath: string): string { 221 const originalFileNameSegments: string[] = FileUtils.splitFilePath(orignalPath); 222 const mangledSegments: string[] = originalFileNameSegments.map(originalSegment => mangleFileNamePart(originalSegment)); 223 let mangledFileName: string = mangledSegments.join('/'); 224 return mangledFileName; 225 } 226 227 function mangleFileNamePart(original: string): string { 228 if (reservedFileNames.has(original)) { 229 return original; 230 } 231 232 const historyName: string = historyFileNameMangledTable?.get(original); 233 let mangledName: string = historyName ? historyName : globalFileNameMangledTable.get(original); 234 235 while (!mangledName) { 236 mangledName = generator.getName(); 237 if (mangledName === original || reservedFileNames.has(mangledName)) { 238 mangledName = null; 239 continue; 240 } 241 242 let reserved: string[] = [...globalFileNameMangledTable.values()]; 243 if (reserved.includes(mangledName)) { 244 mangledName = null; 245 continue; 246 } 247 248 if (historyFileNameMangledTable && [...historyFileNameMangledTable.values()].includes(mangledName)) { 249 mangledName = null; 250 continue; 251 } 252 } 253 globalFileNameMangledTable.set(original, mangledName); 254 return mangledName; 255 } 256 257 export let transformerPlugin: TransformPlugin = { 258 'name': 'renamePropertiesPlugin', 259 'order': (1 << TransformerOrder.RENAME_FILE_NAME_TRANSFORMER), 260 'createTransformerFactory': createRenameFileNameFactory 261 }; 262} 263 264export = secharmony; 265 266function canBeObfuscatedFilePath(filePath: string): boolean { 267 return path.isAbsolute(filePath) || FileUtils.isRelativePath(filePath); 268} 269 270// typescript doesn't add the json extension. 271const extensionOrder: string[] = ['.ets', '.ts', '.d.ets', '.d.ts', '.js']; 272 273function tryValidateFileExisting(importPath: string): PathAndExtension | undefined { 274 let fileAbsPath: string = ''; 275 if (path.isAbsolute(importPath)) { 276 fileAbsPath = importPath; 277 } else { 278 fileAbsPath = path.join(path.dirname(orignalFilePathForSearching), importPath); 279 } 280 281 const filePathExtensionLess: string = path.normalize(fileAbsPath); 282 for (let ext of extensionOrder) { 283 const targetPath = filePathExtensionLess + ext; 284 if (fs.existsSync(targetPath)) { 285 return {path: importPath, ext: ext}; 286 } 287 } 288 289 // all suffixes are not matched, search this file directly. 290 if (fs.existsSync(filePathExtensionLess)) { 291 return { path: importPath, ext: undefined }; 292 } 293 return undefined; 294} 295 296function tryRemoveVirtualConstructor(node: StructDeclaration): StructDeclaration { 297 const sourceFile = NodeUtils.getSourceFileOfNode(node); 298 const tempStructMembers: ClassElement[] = []; 299 if (sourceFile && sourceFile.isDeclarationFile && NodeUtils.isInETSFile(sourceFile)) { 300 for (let member of node.members) { 301 // @ts-ignore 302 if (!isConstructorDeclaration(member) || !member.virtual) { 303 tempStructMembers.push(member); 304 } 305 } 306 const structMembersWithVirtualConstructor = factory.createNodeArray(tempStructMembers); 307 return factory.updateStructDeclaration(node, node.modifiers, node.name, node.typeParameters, node.heritageClauses, structMembersWithVirtualConstructor); 308 } 309 return node; 310} 311 312function toUnixPath(data: string): string { 313 if (/^win/.test(require('os').platform())) { 314 const fileTmps: string[] = data.split(path.sep); 315 const newData: string = path.posix.join(...fileTmps); 316 return newData; 317 } 318 return data; 319}