Thursday, 5 June 2014

Hashing Passwords in Logback output

Hashing Passwords in Logback output
I have used http://hilite.me/ website to do the java-html conversion.
Lets say we want to encrypt passwords in log files while printing.

Thanks to my Collegue Paul Bentley for achieving this.

Encoder.java

public class Encoder {
 
 private static final int FF = 0xFF;
 private static final char PAD = '0';
 private static final String DEFAULT_REPLACEMENT = "Encoding error";
 
 /**
  * Hex encodes a string.
  * @param digestAlgorithm the encryption algorithm.
  * @param value the value to encode.
  * @param salt the salt for the encryption.
  * @param defaultReplacement 
  * @return the value hex encoded.
  */
 public static String hexEncode(
   final String digestAlgorithm,
   final String value,
   final String salt,
   final String defaultReplacement) {
  //System.out.println("hexEncode " + digestAlgorithm + " " + value + " " + salt + " " + defaultReplacement);
  final StringBuilder hexString = new StringBuilder();
  try {
   final MessageDigest messageDigest = MessageDigest.getInstance(digestAlgorithm);
   for (final byte b : messageDigest.digest((salt + value).getBytes())) {
    final int x = b & FF;
    final String s = Integer.toHexString(x);
    hexString.append(s.length() == 1 ? PAD + s : s);
   }
  } catch (final Exception e) {
   hexString.append(defaultReplacement);
  }
  return hexString.toString();
 }

}

SecureMessageConverter.java

import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Context;

/**
 * Logback converter to encrypt private fields such as passwords.
 */
public class SecureMessageConverter extends ClassicConverter {

 // Properties from logback.xml
 public static final String ALGORITHM = "SecureMessageConverter.ALGORITHM";
 public static final String SALT_FORMAT = "SecureMessageConverter.SALT_FORMAT";
 public static final String DEFAULT_REPLACEMENT = "SecureMessageConverter.DEFAULT_REPLACEMENT";
 public static final String CANDIDATE_PATTERN = "SecureMessageConverter.CANDIDATE_PATTERN";
 public static final String PATTERN = "SecureMessageConverter.PATTERN";

 private static volatile List<Pattern> patterns = new ArrayList<Pattern>();
 private static String algorithm;
 private static String saltFormat;
 private static String defaultReplacement;
 private static Pattern candidatePattern;

 private static final int FIRST_FIELD = 1;
 private static final int ENCRYPTED_FIELD = 2;
 private static final int LAST_FIELD = 3;

 /**
  * Initialises the SecureMessageConverter initialising the resources. Since resource initialisation is relatively expensive
  * this is done only once for all instances of the class. The Context is not available in a constructor so this must be done
  * after construction. Patterns are thread safe so can be shared by instances of this class.
  */
 private void initialise() {
  try {
   synchronized (patterns) {
    if (patterns.isEmpty()) {
     final Context context = getContext();

     algorithm = context.getProperty(ALGORITHM);
     // System.out.println("alogrithm=" + algorithm);

     saltFormat = context.getProperty(SALT_FORMAT);
     // System.out.println("saltFormat=" + saltFormat);

     defaultReplacement = context.getProperty(DEFAULT_REPLACEMENT);
     // System.out.println("defaultReplacement=" + defaultReplacement);

     candidatePattern = Pattern.compile(context.getProperty(CANDIDATE_PATTERN), Pattern.DOTALL);
     // System.out.println("candidatePattern=" + defaultReplacement);

     // Read all the patterns of the form PATTERN1, PATTERN2 .. PATTERNN
     int i = 1;
     String property = context.getProperty(PATTERN + i);
     while (property != null) {
      patterns.add(Pattern.compile(property, Pattern.DOTALL));
      property = context.getProperty(PATTERN + i);
      // System.out.println("pattern" + i + "=" + property);
      i++;
     }
    }
   }
  } catch (final Exception e) {
   e.printStackTrace();
  }
 }

 /**
  * Encrypts the passwords in some text. Since most lines of text are not expected to contain passwords a simple regular
  * expression is used to efficiently find candidates for encryption and the more complex and expensive regular expressions are
  * used only on lines that are identified as candidates.
  * @param event the event to log.
  * @return the text with and matching password fields encrypted.
  */
 @Override
 public String convert(final ILoggingEvent event) {
  initialise();
  String result = new String(event.getFormattedMessage());
  final Matcher preMatcher = candidatePattern.matcher(result);
  if (preMatcher.find()) {
   for (final Pattern pattern : patterns) {
    final Matcher matcher = pattern.matcher(result);
    if (matcher.find() && matcher.groupCount() == LAST_FIELD) {
     result = compose(matcher);
     break;
    }
   }
  }
  return result;
 }

 /**
  * Composes a string from regular expression matching three fields.
  * @param matcher the Matcher from the regular expression.
  * @return the result with the field for encryption hex encoded.
  */
 private String compose(final Matcher matcher) {
  final String salt = new SimpleDateFormat(saltFormat).format(new Date());
  final String hex = Encoder.hexEncode(algorithm, matcher.group(ENCRYPTED_FIELD), salt, defaultReplacement);
  return matcher.group(FIRST_FIELD) + hex + matcher.group(LAST_FIELD);
 }

}

logback.xml

<configuration debug="true" scan="true" scanPeriod="30 seconds">

<property name="SERVER_LOG_LOCATION" value="${com.sun.aas.instanceRoot}/logs" />

<property scope="context" name="SecureMessageConverter.ALGORITHM" value="MD5" />
<property scope="context" name="SecureMessageConverter.SALT_FORMAT" value="yyyy-MM-dd'T'HH:mm:ss" />
<property scope="context" name="SecureMessageConverter.DEFAULT_REPLACEMENT" value="h3ll0" />
<property scope="context" name="SecureMessageConverter.CANDIDATE_PATTERN" value="PASSWORD|credentials" />
<property scope="context" name="SecureMessageConverter.PATTERN1" value="(.*PASSWORD=&quot;)(.*)(&quot; .*)" />
<property scope="context" name="SecureMessageConverter.PATTERN2" value="(.*&lt;.*:credentials&gt;)(.*)(&lt;/.*:credentials&gt;.*)" />
<conversionRule conversionWord="msg" converterClass="uk.co.virginmedia.mw.logback.SecureMessageConverter" />
    <appender name="STDOUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <file>${SERVER_LOG_LOCATION}/server_services.log</file>
  
  <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
    <fileNamePattern>${SERVER_LOG_LOCATION}/server_services_%i.log</fileNamePattern>
    <minIndex>1</minIndex>
    <maxIndex>10</maxIndex>
  </rollingPolicy>

  <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
    <maxFileSize>10MB</maxFileSize>
  </triggeringPolicy>
    
        <encoder>
          <pattern>%d{yyyy-mm-dd'T'HH:mm:ss.SSSZ}|%-5level|_ThreadName=%thread|SERVICE_NAME:%logger|CLASS_NAME:%class|METHOD_NAME:%method|LINE_NUMBER:%file:%line|MESSAGE:%msg%n</pattern>
        </encoder>
    </appender>
 
    <appender name="SIFT" class="ch.qos.logback.classic.sift.SiftingAppender">
  <discriminator class="uk.co.virginmedia.mw.logback.LoggerNameBasedDiscriminator"/>
  <sift>
   <appender name="FILE-${loggerName}" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${SERVER_LOG_LOCATION}/${loggerName}/${loggerName}.log</file>
    
    <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
      <fileNamePattern>${SERVER_LOG_LOCATION}/${loggerName}/${loggerName}_%i.log</fileNamePattern>
      <minIndex>1</minIndex>
      <maxIndex>10</maxIndex>
    </rollingPolicy>

    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>10MB</maxFileSize>
    </triggeringPolicy>
     
    <encoder>
     <pattern>%d{yyyy-mm-dd'T'HH:mm:ss.SSSZ}|%-5level|_ThreadName=%thread|SERVICE_NAME:%logger|CLASS_NAME:%class|METHOD_NAME:%method|LINE_NUMBER:%file:%line|MESSAGE:%msg%n</pattern>
     </encoder>
   </appender>
   </sift>
    </appender>

 <root level="debug">
        <appender-ref ref="STDOUT" />
  <appender-ref ref="SIFT" />
    </root>
</configuration>

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <dependencies>
  <dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-classic</artifactId>
   <scope>provided</scope>
  </dependency>
 </dependencies>

 <build>
  <plugins>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
     <archive>
      <manifest>
       <mainClass>Encoder</mainClass>
      </manifest>
     </archive>
    </configuration>
   </plugin>
  </plugins>
 </build>
</project>

No comments: