/* * Copyright 2014-2015 Groupon, Inc * Copyright 2014-2015 The Billing Project, LLC * * The Billing Project licenses this file to you under the Apache License, version 2.0 * (the "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package org.killbill.billing.server.log.obfuscators; import ch.qos.logback.classic.spi.ILoggingEvent; import com.google.common.annotations.VisibleForTesting; /** * LuhnMaskingObfuscator replaces sequences of digits that pass the Luhn check * with a masking string, leaving only the suffix containing the last four * digits. * <p/> * Inspired from https://github.com/esamson/logback-luhn-mask (licensed under the Apache License, Version 2.0) */ public class LuhnMaskingObfuscator extends Obfuscator { /** * The minimum number of digits a credit card can have. */ private static final int MIN_CC_DIGITS = 13; public LuhnMaskingObfuscator() { super(); } @Override public String obfuscate(final String originalString, final ILoggingEvent event) { return mask(originalString); } private String mask(final String formattedMessage) { if (!hasEnoughDigits(formattedMessage)) { return formattedMessage; } final int length = formattedMessage.length(); int unwrittenStart = 0; int numberStart = -1; int numberEnd; int digitsSeen = 0; final int[] last4pos = {-1, -1, -1, -1}; int pos; char current; final StringBuilder masked = new StringBuilder(formattedMessage.length()); for (pos = 0; pos < length; pos++) { current = formattedMessage.charAt(pos); if (isDigit(current)) { digitsSeen++; if (numberStart == -1) { numberStart = pos; } last4pos[0] = last4pos[1]; last4pos[1] = last4pos[2]; last4pos[2] = last4pos[3]; last4pos[3] = pos; } else if (digitsSeen > 0 && current != ' ' && current != '-') { numberEnd = last4pos[3] + 1; if ((digitsSeen >= MIN_CC_DIGITS) && luhnCheck(stripSeparators(formattedMessage.substring(numberStart, numberEnd)))) { maskCC(formattedMessage, unwrittenStart, numberStart, numberEnd, last4pos[0], masked); unwrittenStart = numberEnd; } numberStart = -1; digitsSeen = 0; } } if (numberStart != -1 && (digitsSeen >= MIN_CC_DIGITS) && luhnCheck(stripSeparators(formattedMessage.substring(numberStart, pos)))) { maskCC(formattedMessage, unwrittenStart, numberStart, pos, last4pos[0], masked); } else { masked.append(formattedMessage, unwrittenStart, pos); } return masked.toString(); } private void maskCC(final String formattedMessage, final int unwrittenStart, final int numberStart, final int numberEnd, final int last4pos, final StringBuilder masked) { masked.append(formattedMessage, unwrittenStart, numberStart); // Don't mask the BIN int binNumbersLeft = 6; int panStartPos = numberStart; char current; while (binNumbersLeft > 0) { current = formattedMessage.charAt(panStartPos); if (isDigit(current)) { masked.append(current); binNumbersLeft--; } panStartPos++; } // Append the mask masked.append(obfuscateConfidentialData(formattedMessage.substring(panStartPos, numberEnd), formattedMessage.substring(last4pos, numberEnd))); // Append last 4 masked.append(formattedMessage, last4pos, numberEnd); } private boolean hasEnoughDigits(final CharSequence formattedMessage) { int digits = 0; final int length = formattedMessage.length(); char current; for (int i = 0; i < length; i++) { current = formattedMessage.charAt(i); if (isDigit(current)) { if (++digits == MIN_CC_DIGITS) { return true; } } else if (digits > 0 && current != ' ' && current != '-') { digits = 0; } } return false; } /** * Implementation of the [Luhn algorithm](http://en.wikipedia.org/wiki/Luhn_algorithm) * to check if the given string is possibly a credit card number. * * @param cardNumber the number to check. It must only contain numeric characters * @return `true` if the given string is a possible credit card number */ @VisibleForTesting boolean luhnCheck(final String cardNumber) { int sum = 0; int digit, addend; boolean doubled = false; for (int i = cardNumber.length() - 1; i >= 0; i--) { digit = Integer.parseInt(cardNumber.substring(i, i + 1)); if (doubled) { addend = digit * 2; if (addend > 9) { addend -= 9; } } else { addend = digit; } sum += addend; doubled = !doubled; } return (sum % 10) == 0; } /** * Remove any ` ` and `-` characters from the given string. * * @param cardNumber the number to clean up * @return if the given string contains no ` ` or `-` characters, the string * itself is returned, otherwise a new string containing no ` ` or `-` * characters is returned */ @VisibleForTesting String stripSeparators(final String cardNumber) { final int length = cardNumber.length(); final char[] result = new char[length]; int count = 0; char cur; for (int i = 0; i < length; i++) { cur = cardNumber.charAt(i); if (!(cur == ' ' || cur == '-')) { result[count++] = cur; } } if (count == length) { return cardNumber; } return new String(result, 0, count); } private static boolean isDigit(final char c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return true; default: return false; } } }