How to authenticate to DESFire with Python

I’ve been trying to get this working with Python for hours and had no luck. I get the same results with an EV1 and EV2. I’m not able to authenticate. I’ve tried sniffing authentication using 2 Proxmarks and the “hf 14a sniff / list” command to get correct APDUs.

Any ideas one what I’m doing wrong?

Edit - Posted code below

I’ve not messed with desfire really but I think authentication has to occur after setting up a secure session. That process uses a kind of Diffie-Hellman key exchange to set up the secure channel, then you with the application. Without a secure session then passing an aes key to auth the application would be sent in the clear, which would be a massive problem.

1 Like

Edit I posted some non working code here earlier. I’m removing it.

Ok looking into it you can do it with a challenge response process without needing a secure channel… but it does not look like your apdu is correct… your code almost looks like it’s jumping to the second part of the challenge response process. The first apdu should look like this;

Also… before we get to deep here…

  • what auth are you using? 3DES or AES? Are you using Authenticate or AuthenticateAES command? The above is the AES command.

  • how did you set your application keys for the AID?

Let me get back to you. I’m tired and I’ve been jumping all over the place on this today.

1 Like

So now is not the time to ask if you think DES Fire EV2 is capable of holding a FIDO2 Passkey?

I have both an APEX and a FlexSecure to do that for me and the setup is easy for both, But I also have a FlexDF2 and I would be interested to know if it can too

Planting the seed
:seedling:

EDITED TO ADD:
This :point_up: might be a completely stupid question, but who knows, I didn’t even consider Authentication could be possible…

Nice work on the Above so far, and thanks for sharing

1 Like

As for a Yubikey, it supports 3DES, AES, and ECC based auth, so it might perform a similar challenge-response mode. Someone that really knows what they are doing might be able to set up a one-time password (OTP) system using a secure counter or secret. But I don’t think your going to be able to do FIDO2, U2F, or anything like that.

I’m guessing you could write some type of custom authentication system or write a password manager around it. My RFID knowledge sucks and I’m not very good at programming. haha

I’m going to keep picking at it throughout the day when I have free time. I really need to read up on this stuff more.

There is a desfire software which uses pc/sc enabled readers.

Its called: Dedicated Desfire Studio , we used it when fixing the support and hunting bugs with the pm3 code base.

4 Likes

digital logic also has something…

never used it but the code is online…

2 Likes

Edit I had an issue, but I resolved it by formatting my card. I’ll erase my logs I previously posted.

I’m using 3DES (3TDEA) since I’m currently testing with an EV1 and EV2 card.

I don’t know if I’m doing this properly, but I created my AID via the Proxmark:

hf mfdes createapp -n 0 --aid 111111 --fid 1111 --dstalgo 3TDEA -t 3TDEA -k 000000000000000000000000000000000000000000000000 --numkeys 01 -v
1 Like

I’m back at it and still trying to authenticate.

I have a couple different EV1, EV2, and EV3 cards with different configurations. Key types, AIDs, files, etc.

My current test card is an EV3 with no AID. It has default settings.

Displaying the 8 byte DES key and auth via Proxmark:

[fpc] pm3 --> hf mfdes detect
[=] Found key num: 0 (0x00)
[=] channel ev1 key des [8]: 00 00 00 00 00 00 00 00 
[fpc] pm3 --> hf mfdes auth  -n 0 -t des -k 0000000000000000
[+] PICC selected and authenticated succesfully
[+] Context: 
[=] Key num: 0 Key algo: des Key[8]: 00 00 00 00 00 00 00 00 
[=] Secure channel: ev1 Command set: niso Communication mode: plain
[=] Session key [8]: 01 02 03 04 61 8C 65 4B  
[=]     IV [8]: 00 00 00 00 00 00 00 00

My first Python authentication script uses:

apdu = [0x90, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]

Response: []
Status Words: 91 7E

Which means:
(0x91, 0x7E): “91 7E: Length of command string invalid”

The complete script is:

from smartcard.System import readers                                                                                                  

# Get available smartcard readers                                   
r = readers()                                                       
if not r:                                                           
    print("No smartcard readers found!")                            
    exit()

# Connect to the first available reader                             
connection = r[0].createConnection()                                
connection.connect()

# Manually send the APDU (e.g., for DES authentication)             
apdu = [0x90, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
# Send the APDU command to the card
response, sw1, sw2 = connection.transmit(apdu)

# Print response
print("Response:", response)      
print(f"Status Words: {sw1:02X} {sw2:02X}")

I’ve also tested the command_des_auth and command_legacy_auth functions at apdu_utils/security_commands.py at master · HotelSierraWhiskey/apdu_utils · GitHub

The output for this is the same:

Sending APDU for authentication: [144, 10, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Response: []
Status Words: 91 7E

Here is the script I built around those functions:

from smartcard.System import readers                                                                                                  
from security_commands import command_aes_auth, command_des_auth, command_legacy_auth

# Get available smartcard readers
r = readers()
if not r:                         
    print("No smartcard readers found!")
    exit()

# Connect to the first available reader
connection = r[0].createConnection()
connection.connect()

# Define the DES key (8 bytes of 0s for your key)
key_id = [0x00] * 8

# Choose the correct authentication function                        
# Use legacy DES authentication if you're using 3DES or legacy DES keys
apdu_auth = command_legacy_auth(key_id)  # Change to command_aes_auth if you're using AES keys

print("Sending APDU for authentication:", apdu_auth)

# Send the APDU for authentication
response, sw1, sw2 = connection.transmit(apdu_auth)

# Print the response
print("Response:", response)      
print(f"Status Words: {sw1:02X} {sw2:02X}")

I’ve had no luck on this.
I’m wondering about the LC field. (length of data). Why is it 0x01? Is it the length of the key? I tried changing it to 0x08.

Is there a way I can view the APDU that is sent when I authenticate via the Proxmark? I have 2 Proxmarks. I tried the sniff command, but couldn’t understand what I was looking at in the .pm3 file it generated.

I don’t know much about DESFire, but your APDU seems to be borked. The response code 917E indeed means that your input APDU has an invalid command length. Your APDU 0x90, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 decodes to (also see Smart card application protocol data unit - Wikipedia):

  • CLA: 0x90 - proprietary, no secure messaging
  • INS: 0x0A - specific to the protocol you are using - DESFire
  • P1, P2: 0x00, 0x00 - Instruction parameters
  • LC: 0x01 - 1 byte of command data expected
  • [ 0x00 ] - The one byte of command data you specified
  • LE: 0x00 - Number of response bytes expected, 0 usually means to receive all available bytes
  • [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ] - Another 7 bytes of unexpected additional data. This throws off the APDU parser of the card.

So, your APDU should look more like this: 0x90, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x00 .

2 Likes

Yup, you are correct. I figured it out last night with the help of the RawNFC app. (I like it!)

Like Amal posted above, there is a challenge and response that I didn’t know about. Right now I’m trying to troubleshoot my reader on Ubuntu 24.04. I’m getting status 90 00 which is successful, but I’m not getting the actual challenge response outputted into my terminal.

I can’t say I know much about DESFire but this is intriguing… You couldn’t get the challenge data?

# 0x0a is legacy auth, 0x1a seems more common *shrugs*
# Because the key is all zero, it is truncated to one byte
auth = [0x90, 0x1A, 0x00, 0x00, 0x01, 0x00, 0x00]
#       key length of one byte    ^ 
#                                      ^ key
data, sw1, sw2 = connection.transmit(auth)        
print(hex(sw1), hex(sw2))                         
>> 0x91 0xaf
# 0xaf "awaiting response" 
print(data)                                     
>> [206, 16, 123, 221, 249, 82, 169, 103]

Edit: I was conflating the key index and key. The key index is provided to the auth request while the actual key is never sent.

I wasn’t earlier. I just tested again and I’m getting a response. Here’s my code.

from smartcard.System import readers                                                                                
from smartcard.util import toHexString
import logging

logging.basicConfig(level=logging.DEBUG)

# Get available smartcard readers
r = readers()
if not r:                     
    print("No smartcard readers found!")
    exit()

# Connect to the first available reader
connection = r[0].createConnection()
connection.connect()

apdu = [0x90, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x00]
print("Sending APDU:", toHexString(apdu))

response, sw1, sw2 = connection.transmit(apdu)

print("Response:", toHexString(response))
print(f"Status Words: {sw1:02X} {sw2:02X}") 

And the response.

Sending APDU: 90 0A 00 00 01 00 00
Response: 81 A6 B8 FD D8 54 E2 06
Status Words: 91 AF

91 AF means “Additional data frame is expected to be sent”

I know once I get a proper challenge response, I’ll have to do some manipulation with that. E.g. Mifare DESFire - An Introduction

Here’s where I’m at working on the challenge and response.

I’m getting this response.

Sending APDU: 90 0A 00 00 01 00 00
Response: 36 B9 B2 25 37 49 2C 94
Status Words: 91 AF
Challenge from card: 36 B9 B2 25 37 49 2C 94
Decrypted rndB: b'7DCC78C339B982B6'
Left Rotated rndB: b'7DCC78C339B982B6'
Concatenated rndA_rndB: b'196148407C1BA1087DCC78C339B982B6'
Challenge Answer: b'EE14720BEBC60B7936B9B22537492C94'
Sending Authentication APDU: 90 AF 00 00 10 EE 14 72 0B EB C6 0B 79 36 B9 B2 25 37 49 2C 94 00
Response: 
Status Words: 91 AE
Authentication failed at response step: 91 AE

91 AE = Authentication status does not allow the requested command

No rotation?

I need to review my code, but I’m pretty sure I’ve got it.

Sending APDU: 90 0A 00 00 01 00 00
Response: 3C 83 0E 91 10 8B 7F C8
Status Words: 91 AF
Challenge from card: 3C 83 0E 91 10 8B 7F C8
Original rndA:  91 95 B1 76 B9 DF 86 53
Decrypted rndB: b'76A9E3CAB91A53E5'
Left Rotated rndB: b'A9E3CAB91A53E576'
Concatenated rndA_rndB: b'9195B176B9DF8653A9E3CAB91A53E576'
Challenge Answer: b'F759C2EE1A3278C3A4877B620AEC4803'
Sending Authentication APDU: 90 AF 00 00 10 F7 59 C2 EE 1A 32 78 C3 A4 87 7B 62 0A EC 48 03 00
Response: 73 51 61 0D B3 61 0E BD
Status Words: 91 00
Encrypted RndA from card: b'7351610DB3610EBD'
Decrypted rndA from card: b'95B176B9DF865391'
Rotated rndA from card: b'9195B176B9DF8653'
Authenticated!!!

Code:

from smartcard.System import readers
from smartcard.util import toHexString
from Cryptodome.Cipher import DES, DES3
from binascii import unhexlify, hexlify
from collections import deque
import secrets
import logging
import os

# pip3 install pyscard pycryptodomex
# Make sure Yubikeys or any other readers are unplugged

logging.basicConfig(level=logging.DEBUG)

def rotate_left(data):
    # Left rotate the bytes by 1 position.
    d = deque(data)
    d.rotate(-1)
    return bytes(d)

def rotate_right(data):
    # Right rotate the bytes by 1 position.
    d = deque(data)
    d.rotate(1)
    return bytes(d)

def decrypt(data, key):
    # Decrypt data with DES in CBC mode with an IV of 0x00.
    cipher = DES.new(key, DES.MODE_CBC, iv=b'\x00' * 8)
    decrypted = cipher.decrypt(data)
    return decrypted

def encrypt(data, key):
    # Encrypt data with DES in CBC mode with an IV of 0x00.
    cipher = DES.new(key, DES.MODE_CBC, iv=b'\x00' * 8)
    encrypted = cipher.encrypt(data)
    return encrypted

def to_bytes(hex_str):
    # Convert a hex string to a byte array.
    return unhexlify(hex_str)

def to_hex(byte_array):
    # Convert a byte array to a hex string.
    return hexlify(byte_array).upper()

r = readers()
if not r:
    print("No smartcard readers found!")
    exit()

# Connect to the first available reader
connection = r[0].createConnection()
connection.connect()

# Send APDU to the card
apdu = [0x90, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x00]
print("Sending APDU:", toHexString(apdu))

# Get the response from the card
response, sw1, sw2 = connection.transmit(apdu)

print("Response:", toHexString(response))
print(f"Status Words: {sw1:02X} {sw2:02X}")

if sw1 == 0x91 and sw2 == 0xAF:
    # 1
    challenge = response
    print(f"Challenge from card: {toHexString(challenge)}")

    # 2 Use random value for rndA
    rndA = list(secrets.token_bytes(8))  # Using 7 bytes for rndA
    print("Original rndA: ", toHexString(rndA))
    # 3 Desfire key
    default_key = to_bytes("0000000000000000")

    # 4 Decrypt the challenge using the default key (this gives rndB)
    rndB = decrypt(bytes(challenge), default_key)
    print("Decrypted rndB:", to_hex(rndB))

    # 5 Rotate rndB left (64-bit left rotation)
    left_rotated_rndB = rotate_left(rndB)
    print("Left Rotated rndB:", to_hex(left_rotated_rndB))

    # 6 Concatenate rndA and left-rotated rndB
    rndA_rndB = bytes(rndA) + left_rotated_rndB
    print("Concatenated rndA_rndB:", to_hex(rndA_rndB))

    # 7 Encrypt the concatenated value to get the challenge answer
    challenge_answer = encrypt(rndA_rndB, default_key)
    print("Challenge Answer:", to_hex(challenge_answer))

    # 8 challenge_answer to the card
    apdu_send_auth = [0x90, 0xAF, 0x00, 0x00, len(challenge_answer)] + list(challenge_answer) + [0x00]
    print("Sending Authentication APDU:", toHexString(apdu_send_auth))

    response, sw1, sw2 = connection.transmit(apdu_send_auth)
    print("Response:", toHexString(response))
    print(f"Status Words: {sw1:02X} {sw2:02X}")

    # 9 Get the actual encrypted rndA from the card
    if sw1 == 0x91 and sw2 == 0x00:  # Check if authentication is working
        encrypted_rndA_from_card = bytes(response)  # This is the actual encrypted RndA
        print("Encrypted RndA from card:", to_hex(encrypted_rndA_from_card))
    else:
        print(f"Authentication failed at response step: {sw1:02X} {sw2:02X}")
        exit()

    # 10 Decrypt the rndA sent by the card
    rotated_rndA_from_card = decrypt(encrypted_rndA_from_card, default_key)
    print("Decrypted rndA from card:", to_hex(rotated_rndA_from_card))

    # 11 Rotate rndA from the card right (64-bit right rotation)
    rndA_from_card = rotate_right(rotated_rndA_from_card)
    print("Rotated rndA from card:", to_hex(rndA_from_card))

    # 12 Verify if the card's rndA matches the original rndA
    if bytes(rndA) == bytes(rndA_from_card):
        print("Authenticated!")
    else:
        print("Authentication failed.")
else:
    print(f"Card responded with an unexpected status: {sw1:02X} {sw2:02X}")
2 Likes

Congrats!

1 Like

Thanks, that took me forever to figure out.

2 Likes