MaskedPanFormatter.java

/*
 * Copyright (C) 2016 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed 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 com.irurueta.commons;

import com.irurueta.commons.validators.CreditCardNetwork;
import com.irurueta.commons.validators.CreditCardValidator;

/**
 * Generates a masked PAN where only the last 4 digits are displayed and the
 * remaining ones are masked using digit groupings according to the
 * corresponding credit card network.
 * Notice that this class displays a maximum of 4 digits to remain PCI DSS no
 * matter how many visible digits are requested.
 * If less than request digits are available in provided PAN, then only
 * available digits are displayed as the last digits of masked PAN.
 */
public class MaskedPanFormatter {

    /**
     * Default character to use as a masked digit on a credit card PAN.
     */
    public static final char DEFAULT_MASK_CHAR = '*';

    /**
     * Default character to use as a group separator.
     */
    public static final char DEFAULT_SEPARATOR_CHAR = ' ';

    /**
     * Default number of visible digits to be PCI DSS compliant.
     */
    public static final int NUM_VISIBLE_DIGITS = 4;

    /**
     * Constructor.
     */
    private MaskedPanFormatter() {
    }

    /**
     * Formats a credit card PAN (or its last digits) by making visible only the
     * last digits and masking the remaining ones following the digits grouping
     * required for provided credit card network.
     *
     * @param panOrLastDigits credit card number (or its last digits) to be
     *                        masked.
     * @param network         credit card network.
     * @param maskChar        mask character to be used.
     * @param separatorChar   group separator character to be used.
     * @return masked PAN.
     */
    public static String format(final String panOrLastDigits,
                                final CreditCardNetwork network,
                                final char maskChar,
                                final char separatorChar) {
        return format(toDigits(panOrLastDigits), network, maskChar,
                separatorChar, NUM_VISIBLE_DIGITS);
    }

    /**
     * Formats a credit card PAN (or its last digits) by making visible only the
     * last digits and masking the remaining ones following the digits grouping
     * required for provided credit card network.
     * This method uses the space as a group separator.
     *
     * @param panOrLastDigits credit card number (or its last digits) to be
     *                        masked.
     * @param network         credit card network.
     * @param maskChar        mask character to be used.
     * @return masked PAN.
     */
    public static String format(final String panOrLastDigits,
                                final CreditCardNetwork network,
                                final char maskChar) {
        return format(panOrLastDigits, network, maskChar,
                DEFAULT_SEPARATOR_CHAR);
    }

    /**
     * Formats a credit card PAN (or its last digits) by making visible only the
     * last digits and masking the remaining ones following the digits grouping
     * required for provided credit card network.
     * This method uses the space as a group separator and the star character
     * as a masked digit.
     *
     * @param panOrLastDigits credit card number (or its last digits) to be
     *                        masked.
     * @param network         credit card network.
     * @return masked PAN.
     */
    public static String format(final String panOrLastDigits,
                                final CreditCardNetwork network) {
        return format(panOrLastDigits, network, DEFAULT_MASK_CHAR);
    }

    /**
     * Formats a full credit card PAN by making visible only the last digits and
     * masking the remaining ones following the digits grouping required for
     * the detected credit card network.
     * This method needs a full credit card PAN to detect the credit card
     * network using the former digits, and making visible the latter ones.
     * If a credit card network cannot be determined, default grouping is used
     * instead.
     *
     * @param pan           a full credit card number.
     * @param maskChar      mask character to be used.
     * @param separatorChar group separator character to be used.
     * @return masked PAN.
     */
    public static String format(final String pan, final char maskChar, final char separatorChar) {
        return format(pan, CreditCardValidator.detectNetworkFromPAN(pan),
                maskChar, separatorChar);
    }

    /**
     * Formats a full credit card PAN by making visible only the last digits and
     * masking the remaining ones following the digits grouping required for
     * the detected credit card network.
     * This method needs a full credit card PAN to detect the credit card
     * network using the former digits, and making visible the latter ones.
     * If a credit card network cannot be determined, default grouping is used
     * instead.
     * This method uses the space as a group separator.
     *
     * @param pan      a full credit card number.
     * @param maskChar mask character to be used.
     * @return masked PAN.
     */
    public static String format(final String pan, final char maskChar) {
        return format(pan, maskChar, DEFAULT_SEPARATOR_CHAR);
    }

    /**
     * Formats a full credit card PAN by making visible only the last digits and
     * masking the remaining ones following the digits grouping required for
     * the detected credit card network.
     * This method needs a full credit card PAN to detect the credit card
     * network using the former digits, and making visible the latter ones.
     * If a credit card network cannot be determined, default grouping is used
     * instead.
     * This method uses the space as a group separator and the star character
     * as a masked digit.
     *
     * @param pan a full credit card number.
     * @return masked PAN.
     */
    public static String format(final String pan) {
        return format(pan, DEFAULT_MASK_CHAR);
    }

    /**
     * Formats a credit card PAN (or its last digits) by making visible only the
     * last digits and masking the remaining ones following the digits grouping
     * required for provided credit card network.
     *
     * @param digits           array containing the full credit card number (or its last
     *                         digits) to be masked.
     * @param network          credit card network.
     * @param maskChar         mask character to be used.
     * @param separatorChar    group separator character to be used.
     * @param numVisibleDigits number of visible digits. The number of visible
     *                         digits must not exceed 4 to remain PCI DSS compliant. Only the last group
     *                         of digits will be shown unmasked if this value exceed the length of such
     *                         group.
     * @return masked PAN.
     */
    @SuppressWarnings("SameParameterValue")
    private static String format(final byte[] digits, final CreditCardNetwork network,
                                 final char maskChar, final char separatorChar,
                                 int numVisibleDigits) {
        String mask = PanMaskGenerator.generate(network, maskChar,
                separatorChar);

        // limit the number of visible digits to provided value, limited to
        // the length of last group of digits
        final int numGroups = CreditCardValidator.getNumberOfGroupsForNetwork(
                network);
        final int lastGroupLength = CreditCardValidator.
                getMaxDigitsForGroupAndNetwork(numGroups - 1, network);

        numVisibleDigits = Math.min(numVisibleDigits, lastGroupLength);
        final int startPos = Math.max(0, digits.length - numVisibleDigits);
        final int visibleDigits = digits.length - startPos;
        final int endMaskPosition = mask.length() - visibleDigits;

        mask = mask.substring(0, endMaskPosition);
        final StringBuilder builder = new StringBuilder(mask);
        for (int i = startPos; i < digits.length; i++) {
            builder.append(digits[i]);
        }

        return builder.toString();
    }

    /**
     * Converts a PAN into an array of digits. Any non digit character is
     * ignored.
     *
     * @param pan PAN to be converted to digits.
     * @return array containing digits within provided PAN.
     */
    private static byte[] toDigits(final String pan) {
        return CreditCardValidator.toDigits(pan);
    }
}