/* * ProGuard -- shrinking, optimization, obfuscation, and preverification * of Java bytecode. * * Copyright (c) 2002-2014 Eric Lafortune (eric@graphics.cornell.edu) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package proguard.obfuscate; import proguard.classfile.*; import proguard.classfile.attribute.*; import proguard.classfile.attribute.visitor.*; import proguard.classfile.constant.ClassConstant; import proguard.classfile.constant.visitor.ConstantVisitor; import proguard.classfile.util.*; import proguard.classfile.visitor.ClassVisitor; import proguard.util.*; import java.util.*; /** * This ClassVisitor comes up with obfuscated names for the * classes it visits, and for their class members. The actual renaming is * done afterward. * * @see ClassRenamer * * @author Eric Lafortune */ public class ClassObfuscator extends SimplifiedVisitor implements ClassVisitor, AttributeVisitor, InnerClassesInfoVisitor, ConstantVisitor { private final DictionaryNameFactory classNameFactory; private final DictionaryNameFactory packageNameFactory; private final boolean useMixedCaseClassNames; private final StringMatcher keepPackageNamesMatcher; private final String flattenPackageHierarchy; private final String repackageClasses; private final boolean allowAccessModification; private final Set classNamesToAvoid = new HashSet(); // Map: [package prefix - new package prefix] private final Map packagePrefixMap = new HashMap(); // Map: [package prefix - package name factory] private final Map packagePrefixPackageNameFactoryMap = new HashMap(); // Map: [package prefix - numeric class name factory] private final Map packagePrefixClassNameFactoryMap = new HashMap(); // Map: [package prefix - numeric class name factory] private final Map packagePrefixNumericClassNameFactoryMap = new HashMap(); // Field acting as temporary variables and as return values for names // of outer classes and types of inner classes. private String newClassName; private boolean numericClassName; /** * Creates a new ClassObfuscator. * @param programClassPool the class pool in which class names * have to be unique. * @param classNameFactory the optional class obfuscation dictionary. * @param packageNameFactory the optional package obfuscation * dictionary. * @param useMixedCaseClassNames specifies whether obfuscated packages and * classes can get mixed-case names. * @param keepPackageNames the optional filter for which matching * package names are kept. * @param flattenPackageHierarchy the base package if the obfuscated package * hierarchy is to be flattened. * @param repackageClasses the base package if the obfuscated classes * are to be repackaged. * @param allowAccessModification specifies whether obfuscated classes can * be freely moved between packages. */ public ClassObfuscator(ClassPool programClassPool, DictionaryNameFactory classNameFactory, DictionaryNameFactory packageNameFactory, boolean useMixedCaseClassNames, List keepPackageNames, String flattenPackageHierarchy, String repackageClasses, boolean allowAccessModification) { this.classNameFactory = classNameFactory; this.packageNameFactory = packageNameFactory; // First append the package separator if necessary. if (flattenPackageHierarchy != null && flattenPackageHierarchy.length() > 0) { flattenPackageHierarchy += ClassConstants.PACKAGE_SEPARATOR; } // First append the package separator if necessary. if (repackageClasses != null && repackageClasses.length() > 0) { repackageClasses += ClassConstants.PACKAGE_SEPARATOR; } this.useMixedCaseClassNames = useMixedCaseClassNames; this.keepPackageNamesMatcher = keepPackageNames == null ? null : new ListParser(new FileNameParser()).parse(keepPackageNames); this.flattenPackageHierarchy = flattenPackageHierarchy; this.repackageClasses = repackageClasses; this.allowAccessModification = allowAccessModification; // Map the root package onto the root package. packagePrefixMap.put("", ""); // Collect all names that have been taken already. programClassPool.classesAccept(new MyKeepCollector()); } // Implementations for ClassVisitor. public void visitProgramClass(ProgramClass programClass) { // Does this class still need a new name? newClassName = newClassName(programClass); if (newClassName == null) { // Make sure the outer class has a name, if it exists. The name will // be stored as the new class name, as a side effect, so we'll be // able to use it as a prefix. programClass.attributesAccept(this); // Figure out a package prefix. The package prefix may actually be // the an outer class prefix, if any, or it may be the fixed base // package, if classes are to be repackaged. String newPackagePrefix = newClassName != null ? newClassName + ClassConstants.INNER_CLASS_SEPARATOR : newPackagePrefix(ClassUtil.internalPackagePrefix(programClass.getName())); // Come up with a new class name, numeric or ordinary. newClassName = newClassName != null && numericClassName ? generateUniqueNumericClassName(newPackagePrefix) : generateUniqueClassName(newPackagePrefix); setNewClassName(programClass, newClassName); } } public void visitLibraryClass(LibraryClass libraryClass) { // This can happen for dubious input, if the outer class of a program // class is a library class, and its name is requested. newClassName = libraryClass.getName(); } // Implementations for AttributeVisitor. public void visitAnyAttribute(Clazz clazz, Attribute attribute) {} public void visitInnerClassesAttribute(Clazz clazz, InnerClassesAttribute innerClassesAttribute) { // Make sure the outer classes have a name, if they exist. innerClassesAttribute.innerClassEntriesAccept(clazz, this); } public void visitEnclosingMethodAttribute(Clazz clazz, EnclosingMethodAttribute enclosingMethodAttribute) { // Make sure the enclosing class has a name. enclosingMethodAttribute.referencedClassAccept(this); String innerClassName = clazz.getName(); String outerClassName = clazz.getClassName(enclosingMethodAttribute.u2classIndex); numericClassName = isNumericClassName(innerClassName, outerClassName); } // Implementations for InnerClassesInfoVisitor. public void visitInnerClassesInfo(Clazz clazz, InnerClassesInfo innerClassesInfo) { // Make sure the outer class has a name, if it exists. int innerClassIndex = innerClassesInfo.u2innerClassIndex; int outerClassIndex = innerClassesInfo.u2outerClassIndex; if (innerClassIndex != 0 && outerClassIndex != 0) { String innerClassName = clazz.getClassName(innerClassIndex); if (innerClassName.equals(clazz.getName())) { clazz.constantPoolEntryAccept(outerClassIndex, this); String outerClassName = clazz.getClassName(outerClassIndex); numericClassName = isNumericClassName(innerClassName, outerClassName); } } } /** * Returns whether the given inner class name is a numeric name. */ private boolean isNumericClassName(String innerClassName, String outerClassName) { int innerClassNameStart = outerClassName.length() + 1; int innerClassNameLength = innerClassName.length(); if (innerClassNameStart >= innerClassNameLength) { return false; } for (int index = innerClassNameStart; index < innerClassNameLength; index++) { if (!Character.isDigit(innerClassName.charAt(index))) { return false; } } return true; } // Implementations for ConstantVisitor. public void visitClassConstant(Clazz clazz, ClassConstant classConstant) { // Make sure the outer class has a name. classConstant.referencedClassAccept(this); } /** * This ClassVisitor collects package names and class names that have to * be kept. */ private class MyKeepCollector implements ClassVisitor { public void visitProgramClass(ProgramClass programClass) { // Does the class already have a new name? String newClassName = newClassName(programClass); if (newClassName != null) { // Remember not to use this name. classNamesToAvoid.add(mixedCaseClassName(newClassName)); // Are we not aggressively repackaging all obfuscated classes? if (repackageClasses == null || !allowAccessModification) { String className = programClass.getName(); // Keep the package name for all other classes in the same // package. Do this recursively if we're not doing any // repackaging. mapPackageName(className, newClassName, repackageClasses == null && flattenPackageHierarchy == null); } } } public void visitLibraryClass(LibraryClass libraryClass) { } /** * Makes sure the package name of the given class will always be mapped * consistently with its new name. */ private void mapPackageName(String className, String newClassName, boolean recursively) { String packagePrefix = ClassUtil.internalPackagePrefix(className); String newPackagePrefix = ClassUtil.internalPackagePrefix(newClassName); // Put the mapping of this package prefix, and possibly of its // entire hierarchy, into the package prefix map. do { packagePrefixMap.put(packagePrefix, newPackagePrefix); if (!recursively) { break; } packagePrefix = ClassUtil.internalPackagePrefix(packagePrefix); newPackagePrefix = ClassUtil.internalPackagePrefix(newPackagePrefix); } while (packagePrefix.length() > 0 && newPackagePrefix.length() > 0); } } // Small utility methods. /** * Finds or creates the new package prefix for the given package. */ private String newPackagePrefix(String packagePrefix) { // Doesn't the package prefix have a new package prefix yet? String newPackagePrefix = (String)packagePrefixMap.get(packagePrefix); if (newPackagePrefix == null) { // Are we keeping the package name? if (keepPackageNamesMatcher != null && keepPackageNamesMatcher.matches(packagePrefix.length() > 0 ? packagePrefix.substring(0, packagePrefix.length()-1) : packagePrefix)) { return packagePrefix; } // Are we forcing a new package prefix? if (repackageClasses != null) { return repackageClasses; } // Are we forcing a new superpackage prefix? // Otherwise figure out the new superpackage prefix, recursively. String newSuperPackagePrefix = flattenPackageHierarchy != null ? flattenPackageHierarchy : newPackagePrefix(ClassUtil.internalPackagePrefix(packagePrefix)); // Come up with a new package prefix. newPackagePrefix = generateUniquePackagePrefix(newSuperPackagePrefix); // Remember to use this mapping in the future. packagePrefixMap.put(packagePrefix, newPackagePrefix); } return newPackagePrefix; } /** * Creates a new package prefix in the given new superpackage. */ private String generateUniquePackagePrefix(String newSuperPackagePrefix) { // Find the right name factory for this package. NameFactory packageNameFactory = (NameFactory)packagePrefixPackageNameFactoryMap.get(newSuperPackagePrefix); if (packageNameFactory == null) { // We haven't seen packages in this superpackage before. Create // a new name factory for them. packageNameFactory = new SimpleNameFactory(useMixedCaseClassNames); if (this.packageNameFactory != null) { packageNameFactory = new DictionaryNameFactory(this.packageNameFactory, packageNameFactory); } packagePrefixPackageNameFactoryMap.put(newSuperPackagePrefix, packageNameFactory); } return generateUniquePackagePrefix(newSuperPackagePrefix, packageNameFactory); } /** * Creates a new package prefix in the given new superpackage, with the * given package name factory. */ private String generateUniquePackagePrefix(String newSuperPackagePrefix, NameFactory packageNameFactory) { // Come up with package names until we get an original one. String newPackagePrefix; do { // Let the factory produce a package name. newPackagePrefix = newSuperPackagePrefix + packageNameFactory.nextName() + ClassConstants.PACKAGE_SEPARATOR; } while (packagePrefixMap.containsValue(newPackagePrefix)); return newPackagePrefix; } /** * Creates a new class name in the given new package. */ private String generateUniqueClassName(String newPackagePrefix) { // Find the right name factory for this package. NameFactory classNameFactory = (NameFactory)packagePrefixClassNameFactoryMap.get(newPackagePrefix); if (classNameFactory == null) { // We haven't seen classes in this package before. // Create a new name factory for them. classNameFactory = new SimpleNameFactory(useMixedCaseClassNames); if (this.classNameFactory != null) { classNameFactory = new DictionaryNameFactory(this.classNameFactory, classNameFactory); } packagePrefixClassNameFactoryMap.put(newPackagePrefix, classNameFactory); } return generateUniqueClassName(newPackagePrefix, classNameFactory); } /** * Creates a new class name in the given new package. */ private String generateUniqueNumericClassName(String newPackagePrefix) { // Find the right name factory for this package. NameFactory classNameFactory = (NameFactory)packagePrefixNumericClassNameFactoryMap.get(newPackagePrefix); if (classNameFactory == null) { // We haven't seen classes in this package before. // Create a new name factory for them. classNameFactory = new NumericNameFactory(); packagePrefixNumericClassNameFactoryMap.put(newPackagePrefix, classNameFactory); } return generateUniqueClassName(newPackagePrefix, classNameFactory); } /** * Creates a new class name in the given new package, with the given * class name factory. */ private String generateUniqueClassName(String newPackagePrefix, NameFactory classNameFactory) { // Come up with class names until we get an original one. String newClassName; String newMixedCaseClassName; do { // Let the factory produce a class name. newClassName = newPackagePrefix + classNameFactory.nextName(); newMixedCaseClassName = mixedCaseClassName(newClassName); } while (classNamesToAvoid.contains(newMixedCaseClassName)); // Explicitly make sure the name isn't used again if we have a // user-specified dictionary and we're not allowed to have mixed case // class names -- just to protect against problematic dictionaries. if (this.classNameFactory != null && !useMixedCaseClassNames) { classNamesToAvoid.add(newMixedCaseClassName); } return newClassName; } /** * Returns the given class name, unchanged if mixed-case class names are * allowed, or the lower-case version otherwise. */ private String mixedCaseClassName(String className) { return useMixedCaseClassNames ? className : className.toLowerCase(); } /** * Assigns a new name to the given class. * @param clazz the given class. * @param name the new name. */ static void setNewClassName(Clazz clazz, String name) { clazz.setVisitorInfo(name); } /** * Retrieves the new name of the given class. * @param clazz the given class. * @return the class's new name, or null if it doesn't * have one yet. */ static String newClassName(Clazz clazz) { Object visitorInfo = clazz.getVisitorInfo(); return visitorInfo instanceof String ? (String)visitorInfo : null; } }