#!/usr/bin/env python
import re
import argparse
import time

"""
Utility Python3 script to recover the full PRIMARY ACCOUNT NUMBER (PAN) of a Credit Card number truncated.

Dependencies:
    None

References:
    https://www.pcidssguide.com/what-do-the-credit-card-numbers-mean/
    https://www.investopedia.com/terms/p/primary-account-number-pan.asp
    https://ikajo.com/glossary/primary-account-number
    https://en.wikipedia.org/wiki/Payment_card_number
    https://en.wikipedia.org/wiki/PAN_truncation
    https://stripe.com/docs/testing
    https://pypi.org/project/credit-card-checker/
"""


def is_valid(card_num):
    """
    CREDITS: https://pypi.org/project/credit-card-checker/
    Code was imported because link to the GitHub repository was returning HTTP 404.
    """
    number = card_num.replace(' ', '')
    if len(number) < 2 or re.findall(r"\D", number):
        return False
    total = 0
    reversed_num = number[::-1]
    for i, char in enumerate(reversed_num):
        digit = int(reversed_num[i])
        if i % 2 == 1:
            digit *= 2
            if digit > 9:
                digit -= 9
        total += digit
    return total % 10 == 0


if __name__ == "__main__":

    parser = argparse.ArgumentParser(description="Recover the full PRIMARY ACCOUNT NUMBER (PAN) of a Credit Card number truncated.")
    required_params = parser.add_argument_group("required named arguments")
    required_params.add_argument("-i", action="store", dest="pan_file", help="Path to file containing the list of PAN to process.", required=True)
    args = parser.parse_args()

    # Load the list of PAN number truncated
    # One PAN by line
    # PAN format expected is '40000566****5556' where '*' represent hidden digits
    with open(args.pan_file, mode="r", encoding="utf-8") as f:
        pan_list = f.read().splitlines()
    print(f"[+] PAN loaded: {len(pan_list)}")

    # Identify the number of hidden digits via the first PAN
    hidden_digits_count = pan_list[0].count("*")

    # Process the list of PAN
    print(f"[+] PAN recovery...")
    start = time.time()
    digits_to_guess_range = "9" * hidden_digits_count
    pan_recovered = 0
    for pan in pan_list:
        for digits_to_guess in range(0, int(digits_to_guess_range)+1):
            digits = str(digits_to_guess).zfill(hidden_digits_count)
            pan_try = pan.replace("*" * hidden_digits_count, digits)
            if is_valid(pan_try):
                print(f"{pan} ==> {digits} ==> {pan_try}")
                pan_recovered += 1
    end = time.time() - start
    print(f"[+] PAN recovered in {round(end,6)} seconds: {pan_recovered}")
