All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yworks.yguard.obf.GuardDB Maven / Gradle / Ivy

Go to download

The open-source Java obfuscation tool working with Ant and Gradle by yWorks - the diagramming experts

There is a newer version: 2.10.0
Show newest version
/*
 * YGuard -- an obfuscation library for Java(TM) classfiles.
 *
 * Original Copyright (c) 1999 Mark Welsh ([email protected])
 * Modifications Copyright (c) 2002 yWorks GmbH ([email protected])
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * The author may be contacted at [email protected]
 *
 * Java and all Java-based marks are trademarks or registered
 * trademarks of Sun Microsystems, Inc. in the U.S. and other countries.
 */
package com.yworks.yguard.obf;

import java.io.*;
import java.util.*;
import java.util.zip.*;
import java.util.jar.*;
import java.security.*;

import com.yworks.yguard.*;
import com.yworks.yguard.obf.classfile.*;

import java.util.jar.Attributes;
import java.util.jar.Manifest;

/**
 * Classfile database for obfuscation.
 *
 * @author      Mark Welsh
 */
public class GuardDB implements ClassConstants
{
  // Constants -------------------------------------------------------------
  private static final String STREAM_NAME_MANIFEST = "META-INF/MANIFEST.MF";
  private static final String MANIFEST_NAME_TAG = "Name";
  private static final String MANIFEST_DIGESTALG_TAG = "Digest-Algorithms";
  private static final String CLASS_EXT = ".class";
  private static final String SIGNATURE_PREFIX = "META-INF/";
  private static final String SIGNATURE_EXT = ".SF";
  private static final String LOG_MEMORY_USED = "  Memory in use after class data structure built: ";
  private static final String LOG_MEMORY_TOTAL = "  Total memory available                        : ";
  private static final String LOG_MEMORY_BYTES = " bytes";
  private static final String WARNING_SCRIPT_ENTRY_ABSENT = "");
      }
    }
  }

  /** Remap each class based on the remap database, and remove attributes. */
  public void remapTo(File[] out,
    Filter fileFilter,
    PrintWriter log,
    boolean conserveManifest
    ) throws java.io.IOException, ClassNotFoundException
  {
    // Build database if not already done
    if (classTree == null)
    {
      buildClassTree(log);
    }

    // Generate map table if not already done
    if (!hasMap)
    {
      createMap(log);
    }

    oldManifest = new Manifest[out.length];
    newManifest = new Manifest[out.length];
    parseManifest();

    StringBuffer replaceNameLog = new StringBuffer();
    StringBuffer replaceContentsLog = new StringBuffer();

    JarOutputStream outJar = null;
    // Open the entry and prepare to process it
    DataInputStream inStream = null;
    OutputStream os = null;
    for(int i = 0; i < inJar.length; i++)
    {
      os = null;
      outJar = null;
      //store the whole jar in memory, I known this might be alot, but anyway
      //this is the best option, if you want to create correct jar files...
      List jarEntries = new ArrayList();
      try
      {
        // Go through the input Jar, removing attributes and remapping the Constant Pool
        // for each class file. Other files are copied through unchanged, except for manifest
        // and any signature files - these are deleted and the manifest is regenerated.
        Enumeration entries = inJar[i].entries();
        fireObfuscatingJar(inJar[i].getName(), out[i].getName());
        ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
        while (entries.hasMoreElements())
        {
          // Get the next entry from the input Jar
          JarEntry inEntry = (JarEntry)entries.nextElement();

          // Ignore directories
          if (inEntry.isDirectory())
          {
            continue;
          }

          inStream = new DataInputStream(
            new BufferedInputStream(
            inJar[i].getInputStream(inEntry)));
          String inName = inEntry.getName();
          if (inName.endsWith(CLASS_EXT))
          {
            if (fileFilter == null || fileFilter.accepts(inName)){
              // Write the obfuscated version of the class to the output Jar
              ClassFile cf = ClassFile.create(inStream);
              fireObfuscatingClass(Conversion.toJavaClass(cf.getName()));
              cf.remap(classTree, replaceClassNameStrings, log);
              JarEntry outEntry = new JarEntry(cf.getName() + CLASS_EXT);

              DataOutputStream classOutputStream;
              MessageDigest[] digests;
              if (digestStrings == null){
                digestStrings = new String[]{"SHA-1", "MD5"};
              }
              digests = new MessageDigest[digestStrings.length];
              OutputStream stream = baos;
              // Create an OutputStream piped through a number of digest generators for the manifest

              for (int j = 0; j < digestStrings.length; j++) {
                String digestString = digestStrings[j];
                MessageDigest digest = MessageDigest.getInstance(digestString);
                digests[j] = digest;
                stream = new DigestOutputStream(stream, digest);
              }
              classOutputStream = new DataOutputStream(stream);

              // Dump the classfile, while creating the digests
              cf.write(classOutputStream);
              classOutputStream.flush();
              jarEntries.add(new Object[]{outEntry, baos.toByteArray()});
              baos.reset();
              // Now update the manifest entry for the class with new name and new digests
              updateManifest(i, inName, cf.getName() + CLASS_EXT, digests);
            }
          }
          else if (STREAM_NAME_MANIFEST.equals(inName.toUpperCase()) ||
            (inName.length() > (SIGNATURE_PREFIX.length() + 1 + SIGNATURE_EXT.length()) &&
            inName.indexOf(SIGNATURE_PREFIX) != -1 &&
            inName.substring(inName.length() - SIGNATURE_EXT.length(), inName.length()).equals(SIGNATURE_EXT)))
          {
            // Don't pass through the manifest or signature files
            continue;
          }
          else
          {
            // Copy the non-class entry through unchanged
            long size = inEntry.getSize();
            if (size != -1)
            {

              // Create an OutputStream piped through a number of digest generators for the manifest
              MessageDigest shaDigest = MessageDigest.getInstance("SHA");
              MessageDigest md5Digest = MessageDigest.getInstance("MD5");
              DataOutputStream dataOutputStream =
              new DataOutputStream(new DigestOutputStream(new DigestOutputStream(baos,
              shaDigest),
              md5Digest));

              String outName;

              StringBuffer outNameBuffer = new StringBuffer(80);

              if(resourceHandler != null && resourceHandler.filterName(inName, outNameBuffer))
              {
                outName = outNameBuffer.toString();
                if(!outName.equals(inName))
                {
                  replaceNameLog.append("  \n");
                }
              }
              else
              {
                outName = classTree.getOutName(inName);
              }

              if(resourceHandler == null || !resourceHandler.filterContent(inStream, dataOutputStream, inName))
              {
                byte[] bytes = new byte[(int)size];
                inStream.readFully(bytes);

                // outName = classTree.getOutName(inName);
                // Dump the data, while creating the digests
                dataOutputStream.write(bytes, 0, bytes.length);
              }
              else
              {
                replaceContentsLog.append("  \n");
              }

              dataOutputStream.flush();
              JarEntry outEntry = new JarEntry(outName);


              jarEntries.add(new Object[]{outEntry, baos.toByteArray()});
              baos.reset();
              // Now update the manifest entry for the entry with new name and new digests
              MessageDigest[] digests =
              {shaDigest, md5Digest};
              updateManifest(i , inName, outName, digests);
            }
          }
        }

        os = new FileOutputStream(out[i]);
        if (conserveManifest){
          outJar = new JarOutputStream(new BufferedOutputStream(os),oldManifest[i]);
        } else {
          outJar = new JarOutputStream(new BufferedOutputStream(os),newManifest[i]);
        }
        if (Version.getJarComment() != null) {
          outJar.setComment( Version.getJarComment());
        }

        // sort the entries in ascending order
        Collections.sort(jarEntries, new Comparator(){
          public int compare(Object a, Object b){
                Object[] array1 = (Object[]) a;
                JarEntry entry1 = (JarEntry) array1[0];
                Object[] array2 = (Object[]) b;
                JarEntry entry2 = (JarEntry) array2[0];
                return entry1.getName().compareTo(entry2.getName());
          }
        });
        // Finally, write the big bunch of data
        Set directoriesWritten = new HashSet();
        for (int j = 0; j < jarEntries.size(); j++){
          Object[] array = (Object[]) jarEntries.get(j);
          JarEntry entry = (JarEntry) array[0];
          String name = entry.getName();
          // make sure the directory entries are written to the jar file
          if (!entry.isDirectory()){
                int index = 0;
                while ((index = name.indexOf("/", index + 1))>= 0){
                  String directory = name.substring(0, index+1);
                  if (!directoriesWritten.contains(directory)){
                        directoriesWritten.add(directory);
                        JarEntry directoryEntry = new JarEntry(directory);
                        outJar.putNextEntry(directoryEntry);
                        outJar.closeEntry();
                  }
                }
          }
          // write the entry itself
          byte[] bytes = (byte[]) array[1];
          outJar.putNextEntry(entry);
          outJar.write(bytes);
          outJar.closeEntry();
        }

      }
      catch (Exception e)
      {
        // Log exceptions before exiting
        log.println();
        log.println("");
        throw new IOException("An error ('"+e.getMessage()+"') occured during the remapping! See the log!)");
      }
      finally
      {
        inJar[i].close();
        if (inStream != null)
        {
          inStream.close();
        }
        if (outJar != null)
        {
          outJar.close();
        }
        if (os != null){
          os.close();
        }
      }
    }
    // Write the mapping table to the log file
    classTree.dump(log);
    if(replaceContentsLog.length() > 0 || replaceNameLog.length() > 0)
    {
      log.println("");
    }

  }

  /** Close input JAR file. */
  public void close() throws java.io.IOException
  {
    for(int i = 0; i < inJar.length; i++)
    {
      if (inJar[i] != null)
      {
        inJar[i].close();
        inJar[i] = null;
      }
    }
  }

  // Parse the RFC822-style MANIFEST.MF file
  private void parseManifest()throws java.io.IOException
  {
    for(int i = 0; i < oldManifest.length; i++)
    {
      // The manifest file is the first in the jar and is called
      // (case insensitively) 'MANIFEST.MF'
      oldManifest[i] = inJar[i].getManifest();

      if (oldManifest[i] == null){
        oldManifest[i] = new Manifest();
      }

      // Create a fresh manifest, with a version header
      newManifest[i] = new Manifest();

      // copy all main attributes
      for (Iterator it = oldManifest[i].getMainAttributes().entrySet().iterator(); it.hasNext();) {
        Map.Entry entry = (Map.Entry) it.next();
        Attributes.Name name = (Attributes.Name) entry.getKey();
        String value = (String) entry.getValue();
        if (resourceHandler != null) {
          name = new Attributes.Name(resourceHandler.filterString(name.toString(), "META-INF/MANIFEST.MF"));
          value = resourceHandler.filterString(value, "META-INF/MANIFEST.MF");
        }
        newManifest[i].getMainAttributes().putValue(name.toString(), value);
      }

      newManifest[i].getMainAttributes().putValue("Created-by", "yGuard Bytecode Obfuscator " + Version.getVersion());

      // copy all directory entries
      for (Iterator it = oldManifest[i].getEntries().entrySet().iterator();
            it.hasNext();){
         Map.Entry entry = (Map.Entry) it.next();
         String name = (String) entry.getKey();
         if (name.endsWith("/")){
           newManifest[i].getEntries().put(name, (Attributes) entry.getValue());
         }
      }
    }
  }

  // Update an entry in the manifest file
  private void updateManifest(int manifestIndex, String inName, String outName, MessageDigest[] digests)
  {
    // Create fresh section for entry, and enter "Name" header

    Manifest nm = newManifest[manifestIndex];
    Manifest om = oldManifest[manifestIndex];

    Attributes oldAtts = om.getAttributes(inName);
    Attributes newAtts = new Attributes();
    //newAtts.putValue(MANIFEST_NAME_TAG, outName);

    // copy over non-name and none digest entries
    if (oldAtts != null){
      for(Iterator it = oldAtts.entrySet().iterator(); it.hasNext();){
        Map.Entry entry = (Map.Entry) it.next();
        Object key = entry.getKey();
        String name = key.toString();
        if (!name.equalsIgnoreCase(MANIFEST_NAME_TAG) &&
            name.indexOf("Digest") == -1){
          newAtts.remove(name);
          newAtts.putValue(name, (String)entry.getValue());
        }
      }
    }

    // Create fresh digest entries in the new section
    if (digests != null && digests.length > 0)
    {
      // Digest-Algorithms header
      StringBuffer sb = new StringBuffer();
      for (int i = 0; i < digests.length; i++)
      {
        sb.append(digests[i].getAlgorithm());
        if (i < digests.length -1){
          sb.append(", ");
        }
      }
      newAtts.remove(MANIFEST_DIGESTALG_TAG);
      newAtts.putValue(MANIFEST_DIGESTALG_TAG, sb.toString());

      // *-Digest headers
      for (int i = 0; i < digests.length; i++)
      {
        newAtts.remove(digests[i].getAlgorithm() + "-Digest");
        newAtts.putValue(digests[i].getAlgorithm() + "-Digest", Tools.toBase64(digests[i].digest()));
      }
    }

    if (!newAtts.isEmpty()) {
      // Append the new section to the new manifest
      nm.getEntries().put(outName, newAtts);
    }
  }

  // Create a classfile database.
  private void buildClassTree(PrintWriter log)throws java.io.IOException
  {
    // Go through the input Jar, adding each class file to the database
    classTree = new ClassTree();
    classTree.setPedantic(isPedantic());
    classTree.setReplaceClassNameStrings(replaceClassNameStrings);
    ClassFile.resetDangerHeader();
    
    Map parsedClasses = new HashMap();
    for(int i = 0; i < inJar.length; i++)
    {
      Enumeration entries = inJar[i].entries();
      fireParsingJar(inJar[i].getName());
      while (entries.hasMoreElements())
      {
        // Get the next entry from the input Jar
        ZipEntry inEntry = (ZipEntry)entries.nextElement();
        String name = inEntry.getName();
        if (name.endsWith(CLASS_EXT))
        {
          fireParsingClass(Conversion.toJavaClass(name));
          // Create a full internal representation of the class file
          DataInputStream inStream = new DataInputStream(
          new BufferedInputStream(
          inJar[i].getInputStream(inEntry)));
          ClassFile cf = null;
          try
          {
            cf = ClassFile.create(inStream);
          }
          catch (Exception e)
          {
            log.println(ERROR_CORRUPT_CLASS + createJarName(inJar[i], name) + " -->");
            e.printStackTrace(log);
            throw new ParseException( e );
          }
          finally
          {
            inStream.close();
          }

          if (cf != null){
            final String cfn = cf.getName();
            final String key =
                    "module-info".equals(cfn) ? createModuleKey(cf) : cfn;

            Object[] old = (Object[]) parsedClasses.get(key);
            if (old != null){
              int jarIndex = ((Integer)old[0]).intValue();
              String warning = "yGuard detected a duplicate class definition " +
                "for \n    " + Conversion.toJavaClass(cfn) +
              "\n    [" + createJarName(inJar[jarIndex], old[1].toString()) + "] in \n    [" +
                createJarName(inJar[i], name) + "]";
              log.write("\n");
              if (jarIndex == i){
                throw new IOException(warning + "\nPlease remove inappropriate duplicates first!");
              } else {
                if (pedantic){
                  throw new IOException(warning + "\nMake sure these files are of the same version!");
                } 
              }
            } else {
              parsedClasses.put(key, new Object[]{new Integer(i), name});
            }

            // Check the classfile for references to 'dangerous' methods
            cf.logDangerousMethods(log, replaceClassNameStrings);
            classTree.addClassFile(cf);
          }

        }
      }
    }

    // set the java access modifiers from the containing class (muellese)
    final ClassTree ct = classTree;
    ct.walkTree(new TreeAction()
    {
      public void classAction(Cl cl)
      {
        if (cl.isInnerClass())
        {
          Cl parent = (Cl) cl.getParent();
          cl.access = parent.getInnerClassModifier(cl.getInName());
        }
      }
    });
  }

  private static String createJarName(JarFile jar, String name){
    return "jar:"+jar.getName() + "|" + name;
  }

  private static String createModuleKey( final ClassFile cf ) {
    return "module-info:" + cf.findModuleName();
  }

  // Generate a mapping table for obfuscation.
  private void createMap(PrintWriter log) throws ClassNotFoundException
  {
    // Traverse the class tree, generating obfuscated names within
    // package and class namespaces
    classTree.generateNames();

    // Resolve the polymorphic dependencies of each class, generating
    // non-private method and field names for each namespace
    classTree.resolveClasses();

    // Signal that the namespace maps have been created
    hasMap = true;

    // Write the memory usage at this point to the log file
    Runtime rt = Runtime.getRuntime();
    rt.gc();
    log.println("");

  }

  protected void fireParsingJar(String jar){
    if (listenerList == null) return;
    for (int i = 0, j = listenerList.size(); i < j; i++){
      ((ObfuscationListener)listenerList.get(i)).parsingJar(jar);
    }
  }
  protected void fireParsingClass(String className){
    if (listenerList == null) return;
    for (int i = 0, j = listenerList.size(); i < j; i++){
      ((ObfuscationListener)listenerList.get(i)).parsingClass(className);
    }
  }
  protected void fireObfuscatingJar(String inJar, String outJar){
    if (listenerList == null) return;
    for (int i = 0, j = listenerList.size(); i < j; i++){
      ((ObfuscationListener)listenerList.get(i)).obfuscatingJar(inJar, outJar);
    }
  }
  protected void fireObfuscatingClass(String className){
    if (listenerList == null) return;
    for (int i = 0, j = listenerList.size(); i < j; i++){
      ((ObfuscationListener)listenerList.get(i)).obfuscatingClass(className);
    }
  }

  /** Registers Listener to receive events.
   * @param listener The listener to register.
   */
  public synchronized void addListener(com.yworks.yguard.ObfuscationListener listener)
  {
    if (listenerList == null )
    {
      listenerList = new java.util.ArrayList();
    }
    listenerList.add(listener);
  }

  /** Removes Listener from the list of listeners.
   * @param listener The listener to remove.
   */
  public synchronized void removeListener(com.yworks.yguard.ObfuscationListener listener)
  {
    if (listenerList != null )
    {
      listenerList.remove(listener);
    }
  }

  /** Getter for property replaceClassNameStrings.
   * @return Value of property replaceClassNameStrings.
   *
   */
  public boolean isReplaceClassNameStrings()
  {
    return this.replaceClassNameStrings;
  }

  /** Setter for property replaceClassNameStrings.
   * @param replaceClassNameStrings New value of property replaceClassNameStrings.
   *
   */
  public void setReplaceClassNameStrings(boolean replaceClassNameStrings)
  {
    this.replaceClassNameStrings = replaceClassNameStrings;
  }


  /** Getter for property pedantic.
   * @return Value of property pedantic.
   *
   */
  public boolean isPedantic()
  {
    return this.pedantic;
  }

  /** Setter for property pedantic.
   * @param pedantic New value of property pedantic.
   *
   */
  public void setPedantic(boolean pedantic)
  {
    this.pedantic = pedantic;
    Cl.setPedantic(pedantic);
  }


  /**
   * Returns the obfuscated file name of the java class.
   * The ending ".class" is omitted.
   * @param javaClass the fully qualified name of an unobfuscated class.
   */
  public String translateJavaFile(String javaClass)
  {
    Cl cl = classTree.findClassForName(javaClass.replace('/','.'));
    if(cl != null)
    {
      return cl.getFullOutName();
    }
    else
    {
      return javaClass;
    }
  }


  public String translateJavaClass(String javaClass)
  {
    Cl cl = classTree.findClassForName(javaClass);
    if(cl != null)
    {
      return cl.getFullOutName().replace('/', '.');
    }
    else
    {
      return javaClass;
    }
  }

  public void setDigests(String[] digestStrings) {
    this.digestStrings = digestStrings;
  }

  public void setAnnotationClass(String annotationClass) {
    ObfuscationConfig.annotationClassName = annotationClass;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy