#!/usr/bin/env python3

# Copyright 2023 Sergey Stolyarov <sergei@regolit.com>
#
# Distributed under New BSD License.
# 
# https://opensource.org/license/bsd-3-clause/

import bertlv
import emvutil

from smartcard.System import readers
from smartcard.CardRequest import CardRequest
from smartcard.CardConnection import CardConnection
from smartcard.util import toHexString, toBytes, PACK, HexListToBinString

def main() -> int:
    reader = readers()[0]
    print('Connected reader: {0}'.format(reader))
    cardrequest = CardRequest(timeout=None, readers=[reader])
    print('Waiting for card ...')
    cardservice = cardrequest.waitforcard()
    cardservice.connection.connect()
    print('Card connected.')

    # Select PSE first (for contact cards, "1PAY.SYS.DDF01")
    #       CLA INS P1  P2  Lc  DATA
    apdu = '00  A4  04  00  0E  31 50 41 59 2E 53 59 53 2E 44 44 46 30 31'
    response, sw1, sw2 = transmit_wrapper(cardservice.connection, toBytes(apdu))
    if (sw1,sw2) == (0x90,0x00):
        # take AID from FCI
        pse_data = response
        aid = get_AID_from_PSEFCI(cardservice.connection, pse_data)
        if aid is None:
            aid = guess_AID(cardservice.connection)
    elif (sw1,sw2) == (0x6A,0x82):
        # try with PPSE "2PAY.SYS.DDF01" for contactless cards
        apdu = '00  A4  04  00  0E  32 50 41 59 2E 53 59 53 2E 44 44 46 30 31'
        response, sw1, sw2 = transmit_wrapper(cardservice.connection, toBytes(apdu))
        if (sw1,sw2) == (0x90,0x00):
            # take AID from FCI
            pse_data = response
            aid = get_AID_from_PSEFCI(cardservice.connection, pse_data)
            if aid is None:
                aid = guess_AID(cardservice.connection)
        elif (sw1,sw2) == (0x6A,0x82):
            # guess AID
            print('guessed')
            aid = guess_AID(cardservice.connection)
    else:
        print('Failed to read card.')
        print('{:02X} {:02X}'.format(sw1,sw2))
        return 1

    if aid is None:
        print("Unable to find proper AID")
        return 1

    # Select application with name "{aid}"
    #       CLA INS P1  P2  Lc  DATA
    apdu = '00  A4  04  00  07  ' + toHexString(aid)
    response, sw1, sw2 = transmit_wrapper(cardservice.connection, toBytes(apdu))
    if (sw1,sw2) != (0x90,0x00):
        print('No EMV app found.')
        return 1

    # print(toHexString(response))

    parts = bertlv.parse_bytes(response)
    fciTlv = bertlv.find_tag(0x6F, parts)
    piTlv = bertlv.find_tag(0xA5, fciTlv.value)

    # read all fields from FCI Proprietary Template
    pdolData = None
    for t in piTlv.value:
        if t.tag == 0x50:
            print('Application name: {}'.format(HexListToBinString(t.value)))
        elif t.tag == 0x87:
            print('Application Priority Indicator: priority={}, confirmation required={}'
                .format(t.value[0] & 0x0F, (t.value[0] >> 7 == 1)))
        elif t.tag == 0x9F38:
            print('PDOL is present')
            pdolData = t.value
        elif t.tag == 0x5F2D:
            print('Language preference: {}'.format(HexListToBinString(t.value)))
        elif t.tag == 0x9F11:
            print('Issuer Code Table Index: ISO 8859-{}'.format(t.value[0]))
        elif t.tag == 0x9F12:
            print('Application Preferred Name: {}'.format(HexListToBinString(t.value)))
        elif t.tag == 0xBF0C:
            print('FCI Issuer Discretionary Data is present')
        else:
            print('Unknown tag {:X}: '.format(t.tag, toHexString(t.value)))

    # Start financial transaction
    # prepare dolData
    dolData = [0x83, 0x00]
    if pdolData is not None:
        # parse PDOL data
        lengthByte = False
        totalLength = 0
        for b in pdolData:
            if lengthByte:
                totalLength += b
                lengthByte = False
                continue
            if b & 0x1F != 0x1F:
                # ^^^^^^ last five bits of "b" are not all 1s, so this byte is last one
                # in tag block, so consider next byte as field length
                lengthByte = True
        dolData[1] = totalLength
        dolData.extend([0] * totalLength)

    # Send command "GET PROCESSING OPTIONS"
    #       CLA INS P1  P2  Lc      DATA
    apdu = '80 A8   00  00  {:02X}  {}'.format(len(dolData), toHexString(dolData))
    response, sw1, sw2 = transmit_wrapper(cardservice.connection, toBytes(apdu))
    if (sw1,sw2) != (0x90,0x00):
        print('GET PROCESSING OPTIONS failed')
        return 1

    aipData = None
    aflData = None
    parts = bertlv.parse_bytes(response)
    t = parts[0]
    if t.tag == 0x77:
        x = bertlv.find_tag(0x82, t.value)
        if x is not None:
            aipData = x.value
        x = bertlv.find_tag(0x94, t.value)
        if x is not None:
            aflData = x.value
    elif t.tag == 0x80:
        aipData = t.value[0:2]
        aflData = t.value[2:]

    print('Application Interchange Profile')
    print('  SDA supported: {}'.format('no' if (aipData[0] & 0x40)==0 else 'yes'))
    print('  DDA supported: {}'.format('no' if (aipData[0] & 0x20)==0 else 'yes'))
    print('  Cardholder verification is supported: {}'.format('no' if (aipData[0] & 0x10)==0 else 'yes'))
    print('  Terminal risk management is to be performed: {}'.format('no' if (aipData[0] & 0x8)==0 else 'yes'))
    print('  Issuer authentication is supported: {}'.format('no' if (aipData[0] & 0x4)==0 else 'yes'))
    print('  CDA supported: {}'.format('no' if (aipData[0] & 0x1)==0 else 'yes'))

    # read AFL points
    readObjects = []
    aflPartsCount = len(aflData) // 4
    for i in range(aflPartsCount):
        startByte = i * 4
        sfi = aflData[startByte] >> 3
        firstSfiRec = aflData[startByte + 1]
        lastSfiRec = aflData[startByte + 2]
        offlineAuthRecNumber = aflData[startByte + 3]  # we don't use this value

        #               CLA INS P1  P2  Le
        apdu = toBytes('00  B2  00  00  00')
        for j in range(firstSfiRec, lastSfiRec + 1):
            # set Le=0
            apdu[4] = 0
            # set P1, record number to read
            apdu[2] = j
            # set P2, coding of this parameters defined in ISO 7816-4, section "READ RECORD (S) command"
            p2 = (sfi << 3) | 4
            apdu[3] = p2
            response, sw1, sw2 = transmit_wrapper(cardservice.connection, apdu)
            if sw1 == 0x6C:
                # set new Le and repeat command
                apdu[4] = sw2
                response, sw1, sw2 = transmit_wrapper(cardservice.connection, apdu)
            if (sw1,sw2) != (0x90,0x00):
                print('Failed to read record {} in SFI {}'.format(j, sfi))
                continue
            parts = bertlv.parse_bytes(response)
            rectplTlv = bertlv.find_tag(0x70, parts)
            if rectplTlv is None:
                print('Failed to parse record {} in SFI {}'.format(j, sfi))
                continue
            for t in rectplTlv.value:
                readObjects.append(t)

    print('EMV objects:')
    for t in readObjects:
        print('  {}: {}'.format( emvutil.emv_object_name(t.tag), emvutil.emv_object_repr(t.tag, t.value) ))

    return 0


def get_AID_from_PSEFCI(connection, data):
    parts = bertlv.parse_bytes(data)
    t = bertlv.find_tag(0x6F, parts)
    if t is None:
        # expecting FCI template
        return None
    # pi means "proprietary information"
    piTlv = bertlv.find_tag(0xA5, t.value)
    if piTlv is None:
        # Cannot find EMV block in PSE FCI
        return None
    # piTlv contains data specified in EMV_v4.3 book 1 spec,
    # section "11.3.4 Data Field Returned in the Response Message"
    sfiTlv = bertlv.find_tag(0x88, piTlv.value)
    if sfiTlv is None:
        # Cannot find SFI block in PSE FCI
        return None
    defSfiData = sfiTlv.value
    sfi = defSfiData[0]

    # READ RECORD, see ISO/IEC 7816-4, section "7.3.3 READ RECORD (S) command"
    p2 = (sfi << 3) | 4
    #       CLA INS P1  P2      Le
    apdu = '00  B2  00  {:02X}  00'.format(p2)
    apduTpl = toBytes(apdu)
    foundAIDs = []
    recordNumber = 1
    expectedLength = 0
    while True:
        apdu = apduTpl
        apdu[2] = recordNumber  # P1
        apdu[4] = expectedLength  # Le
        response, sw1, sw2 = transmit_wrapper(connection, apdu)
        if sw1 == 0x6C:
            expectedLength = sw2
            continue
        if (sw1,sw2) != (0x90,0x00):
            break
        if len(response) > 0:
            parts = bertlv.parse_bytes(response)
            psd = bertlv.find_tag(0x70, parts)
            # psd must have tag 0x70
            # see EMV_v4.3 book 1, section "12.2.3 Coding of a Payment System Directory"
            if psd is None:
                return None
            for t in psd.value:
                if t.tag == 0x61:
                    aidTlv = bertlv.find_tag(0x4F, t.value)
                    if aidTlv is not None:
                        foundAIDs.append(aidTlv.value)
        recordNumber += 1
        expectedLength = 0

    if len(foundAIDs) > 0:
        return foundAIDs[0]
    else:
        return None

def guess_AID(connection):
    candidateAIDs = [
        'A0 00 00 00 03 20 10',  # Visa Electron
        'A0 00 00 00 03 10 10',  # Visa Classic
        'A0 00 00 00 04 10 10',  # Mastercard
        'A0 00 00 06 58 10 10',  # MIR Credit
        'A0 00 00 06 58 20 10'   # MIR Debit
    ]
    foundAID = None
    #          CLA INS P1  P2  Le
    apduTpl = '00  A4  04  00  07'
    for aid in candidateAIDs:
        apdu = apduTpl + aid
        response, sw1, sw2 = transmit_wrapper(connection, toBytes(apdu))
        if (sw1,sw2) == (0x90,0x00):
            foundAID = toBytes(aid)
            break

    return foundAID

def transmit_wrapper(connection, apdu):
    response, sw1, sw2 = connection.transmit(apdu)
    if sw1 == 0x61:
        response_data = []
        ne = sw2
        while True:
            gr_apdu = '00 C0 00 00 {:02X}'.format(ne)
            response, sw1, sw2 = connection.transmit(toBytes(gr_apdu))
            if (sw1,sw2) == (0x90,0x00):
                response_data.extend(response)
                break
            elif sw1 == 0x61:
                response_data.extend(response)
                ne = sw2
                continue
            else:
                # error, pass sw1, sw2 back to caller
                response_data = []
                break

        return response_data, 0x90, 0x00
    else:
        return response, sw1, sw2


if __name__ == '__main__':
    main()