Tech Notes

Obfuscate Log Comments for Java

Careful use of logging can greatly assist in the maintenance and debugging of your Java applications. Inspection of a log file can help you understand the flow of the application – great for your own Java applications. But what about other users of your application in a commercial environment. How can you reduce the wealth of information that your logging contains, so that your applications (and its intellectual property), once deployed is not on show for everyone to see?

 

Log Lines

Lets review a typical log statement from an application:

2015-03-26 09:31:57,884 DEBUGĀ  [main] DatabaseUtilities: \
searchingDBTableByName() - Searching internal database, \
class = [Users] name = [jsmith].

This has been generated from the log4j configuration:

log4j.appender.Files.layout.ConversionPattern=%d %-5p [%t] %m\n

and from the Java code:

private static final Logger LOG = Logger.getLogger(DBUtilities.class);

if (LOG.debug().isEnabled())
{
  LOG.debug("DatabaseUtilities: searchingDBTableByName() – Searching internal database, class = [" + c + "] name [" + n + "].");
}

 

The first line helps performance when the log level is set to DEBUG, as time and resources are not used to build the log line unless is is actually going to be used (in a console or file).

The log line is a string built from a mixture of string constants and variable values. Put the two together and you have valuable information, as your string constants give context and meaning to the variables.

 

String Constants and Obfuscation

We can do something about those string constants, by obfuscating them to something that has no meaning.

So in your log file, instead of seeing the text:

searchingDBTableByName() - Searching internal database, class = [

you would see:

_du-1_

Mapping between the obfuscated string and its actual descriptive string is handled by a Java mapping class. In your code, instead of specifying the descriptive string, you request it from a singleton of the mapping class by providing the obfuscated string (key).

Our logging code example then looks like this:

if (LOG.debug().isEnabled())
{
  LOG.debug(SO.g().u("_du-0_") + SO.g().u("_du-1_") + c + SO.g().u("_du-2_") + n + SO.g().u("_du-3_"));
}

If the mapping class is provided with the map of the obfuscated and descriptive strings, it performs the mapping of the key and returns the descriptive string. If it does not have the map it just returns the key.

Without the mapping file, the log line looks like this:

2015-03-26 09:31:57,884 DEBUG [main] _du-0__du-1_Users\
_du-2_jsmith_du-3_

 

Mapping File

Using an Excel spreadsheet to hold the mappings gives far greater flexibility than a text file. The many spreadsheet features available for data presentation and manipulation can supplement the mapping information for other purposes. You could also define multiple worksheets for different applications.

An example spreadsheet is shown in the table below. The first row contains the headings for the columns and the “^” character represents the space character.

Obfuscated String Actual String
_du-0_ DatabaseUtilities:^
_du-1_ searchingDBTableByName() – Searching internal database, class = [
_du-2_ ] name = [
_du-3_ ].
_fu-0_ FileUtilities:^

 

Mapping Class

The mapping class is shown in the code listing below. The first worksheet of the mapping Excel file is read using the Apache POI library in the setSOMappingsFile() method. The first and second columns are queried for the obfuscated string (key) and descriptive string respectively and then populated into a Map. When the mapping class is queried for a key, if that key is found then the associated descriptive string is returned. If the key is not found, then the key is returned. The mapping classes Map will only be populated when the mapping singleton is initialised with the mapping file.

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.TreeMap;

import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;

public class SO
{
  static private SO m_placeholder;
  private TreeMap<String, String> m_mappings;

  protected SO()
  {
    m_placeholder = null;
    m_mappings = new TreeMap<String, String>();
  }

  static public synchronized SO g()
  {
    if (m_placeholder == null)
    {
      m_placeholder = new SO();
    }
    
    return m_placeholder;
  }
    
  public String u(String theObfuscatedString)
  {  
    String u = m_mappings.get(theObfuscatedString);
  
    if (u != null)
    {
      return u;
    }
    else
    {
      return theObfuscatedString;
    }
  }
    
  public void setSOMappingsFile(File theMappingsFile)
    throws InvalidFormatException, IOException
  {
    InputStream inp = new FileInputStream(theMappingsFile);

    Workbook wb = WorkbookFactory.create(inp);
    Sheet sheet = wb.getSheetAt(0);
    Row row = null;
    
    // System.out.println("Setting Mappings start read.");
    
    for (int r = 1; r < sheet.getLastRowNum(); r++)
    {
      // System.out.println("Setting Mappings start read. - in for loop");
      row = sheet.getRow(r);
      // System.out.println("Setting Mappings start read. - got row");
      
      if ((row != null) && (row.getCell(0) != null) && (row.getCell(0).getStringCellValue() != null))
      {
        if (row.getCell(0).getStringCellValue().compareTo("") != 0)
        {
          // System.out.println("Col1 = " + row.getCell(0).getStringCellValue() + "  Col2 = " + row.getCell(1).getStringCellValue());
          m_mappings.put
                     (
                       row.getCell(0).getStringCellValue(),
                       row.getCell(1).getStringCellValue()
                     );
        }
      }
    }
    
    // System.out.println("Setting Mappings all read.");
  }
}

 

Usage

Your application only needs to statically initialise the mapping class as shown in the code listing below. The mapping file, when specified in a system parameter is read by the mapping class. This allows your application to be run in the two different modes of logging, with descriptive strings in your development environment and with obfuscated strings in your customers’ environment.

import au.com.abc.SO;

public final class MyApp
{
  static
  {
    String spdir = System.getProperty("stringmap");
    try
    {
      if (spdir != null)
      {
        System.out.println("-> spdir        : '" + spdir + "'");
        System.out.println("Setting Mappings file: " + spdir);
        SO.g().setSOMappingsFile(new File(spdir));
        System.out.println("Setting Mappings file - done");
      }
    }
    catch (InvalidFormatException e)
    {
      System.err.println("Mappings file: Invalid format.");
    }
    catch (IOException e)
    {
      System.out.println("Mappings file: Not available.");
    }
  }

 

Handling Customer Issues

Hiding logging comes with a price. Unable to read their own log files, your customers must send their log files to you for decoding, as only you have the mapping file to take the obfuscated logs and produce “clean” readable ones.

Realistically, this task can only be performed by a utility. More on this in a later Tech Note.

 

Further Obfuscation

Obfuscating your string constants in logging goes some way in protecting your Java source. You can take it to another level, by using a Java bytecode obfuscator, like YGuard.

YGuard replaces your application’s Java package, class, method, and field names with meaningless characters, to make any reverse engineering of your .class files extremely difficult to understand.

 

Conclusions

Protecting your intellectual property in software engineering is not easy and never complete. You can however, make it as hard as possible for someone to reverse engineer your products. I hope you find this Tech Note useful in helping you to do so.

Back to Tech Notes page