esphome / feature-requests

ESPHome Feature Request Tracker
https://esphome.io/
415 stars 26 forks source link

Reading ndef data from Yubikeys in PN532 module #2207

Open cfmdmedia opened 1 year ago

cfmdmedia commented 1 year ago

Describe the problem you have/What new integration you would like I have a PN532 component and connected it with IIC to an ESP32 and used the PN532 esphome component. It can read the data written to NFC tags with the Homeassistant App but is not able to read the data from a Yubikey 5 NFC.

YAML code:

text_sensor:
  - platform: template
    name: "NFC Tag Sensor"
    id: nfc_tag_text
    update_interval: 1s

i2c:
  sda: GPIO17
  scl: GPIO16
  scan: true
  id: bus_a

pn532_i2c:
  update_interval: 1s
  i2c_id: bus_a
  on_tag:
    then:
      - logger.log: "NFC TAG Scanned"
      - text_sensor.template.publish:
          id: nfc_tag_text
          state: !lambda |
            if (!tag.has_ndef_message()) {
              return x;
            }
            auto message = tag.get_ndef_message();
            auto records = message->get_records();
            for (auto &record : records) {
              std::string payload = record->get_payload();
              size_t pos = payload.find("https://www.home-assistant.io/tag/");
              if (pos != std::string::npos) {
                return payload.substr(pos + 34);
              } else if (payload.substr(0, 8) == "otpauth:") {
                return payload;
              }
            }
            return x;

Debug logs when scanning a NFC tag with data written to it via the Homeassistant App:

[17:01:14][D][pn532:283]: Mifare ultralight
[17:01:15][D][main:214]: NFC TAG Scanned
[17:01:15][D][text_sensor:067]: 'NFC Tag Sensor': Sending state 'tag_test'
[17:01:15][D][pn532:162]: Found new tag '04-BF-D9-FF-FF-FF-FF'
[17:01:15][D][pn532:166]:   NDEF formatted records:
[17:01:15][D][pn532:168]:     U - https://www.home-assistant.io/tag/tag_test
[17:01:15][D][pn532:168]:     android.com:pkg - io.homeassistant.companion.android
[17:01:15][D][pn532:168]:     android.com:pkg - io.homeassistant.companion.android.minimal
[17:01:15][D][pn532:295]: Waiting to read next tag
[17:01:15][D][text_sensor:067]: 'NFC Tag Sensor': Sending state 'tag_test'

Debug logs when scanning the Yubikey 5 NFC:

[17:01:36][D][pn532:283]: Mifare ultralight
[17:01:36][D][main:214]: NFC TAG Scanned
[17:01:36][D][text_sensor:067]: 'NFC Tag Sensor': Sending state '27-6A-FF-FF-FF-FF-FF'
[17:01:36][D][pn532:162]: Found new tag '27-6A-FF-FF-FF-FF-FF'
[17:01:36][D][pn532:295]: Waiting to read next tag

With the NFC reader of my Smartphone the Yubikey OTP is readable. So the feature is active on the Yubikey.

It would be really nice if the PN532 component of ESPhome would also support reading the NDEF data of other NFC tags like the data from a Yubikey 5 NFC.

Please describe your use case for this integration and alternatives you've tried: I want to read the OTP of the Yubikey with a custom NFC reader to use it a door key. Since the OTP always changes it is more secure than using a static NFC tag that can be cloned easily.

Additional context I found this code where someone already build something similar but it is written in Arduino ADA language and I did not managed to translate this into a custom component. Some info about the yubikey ndef interface

sharkydog commented 1 year ago

Strange, mine does not show in logs at all. And "nfc tools" android app, says it's "Mifare Plus".

NonaSuomy commented 1 year ago

Did you get this working?

sharkydog commented 1 year ago

Nope

sharkydog commented 1 year ago

But, I was able make PN532 respond to a command on UART with a PHP script 😄 , so eventually If I can trigger a known to work tag, I might be able to find out the sequence of commands to active my yubikey.

sharkydog commented 1 year ago

I give up for now, all my other tags (including some credit cards) respond, but not the yubikey.

However, a regular Mifare Classic card/tag could be turned into semi-secure one. In short: set a secure key for Key B for some or all sectors and set access bits to allow read and write only with Key B (C1C2C3 = 011). https://www.nxp.com/docs/en/data-sheet/MF1S50YYX_V1.pdf

But that will turn the tag into NDEF non-compliant, and will require modifications in PN532 component to inject a custom key and I doubt it will be accepted into esphome.

NonaSuomy commented 1 year ago

I feel like I was able to read the yubikey nfc from the arduino PN532 example. I'll have to hook it backup and have a looksee.

NonaSuomy commented 3 months ago

I was able to read the Yubikey NFC OTP code with arduino

These three ADPU commands access the OTP code

  uint8_t apdu1[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00};
  uint8_t apdu2[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04};
  uint8_t apdu3[] = {0x00, 0xB0, 0x00, 0x00, 0x00};
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_PN532.h>

// If using the breakout with SPI, define the pins for SPI communication.
#define PN532_SCK  (13)
#define PN532_MOSI (11)
#define PN532_SS   (10)
#define PN532_MISO (12)

// If using the breakout or shield with I2C, define just the pins connected
// to the IRQ and reset lines.  Use the values below (2, 3) for the shield!
#define PN532_IRQ   (9)
#define PN532_RESET (8)  // Not connected by default on the NFC Shield

// Uncomment just _one_ line below depending on how your breakout or shield
// is connected to the Arduino:

// Use this line for a breakout with a software SPI connection (recommended):
Adafruit_PN532 nfc(PN532_SCK, PN532_MISO, PN532_MOSI, PN532_SS);

// Use this line for a breakout with a hardware SPI connection.  Note that
// the PN532 SCK, MOSI, and MISO pins need to be connected to the Arduino's
// hardware SPI SCK, MOSI, and MISO pins.  On an Arduino Uno these are
// SCK = 13, MOSI = 11, MISO = 12.  The SS line can be any digital IO pin.
//Adafruit_PN532 nfc(PN532_SS);

// Or use this line for a breakout or shield with an I2C connection:
//Adafruit_PN532 nfc(PN532_IRQ, PN532_RESET);

void setup(void) {
  Serial.begin(115200);
  while (!Serial) delay(10); // for Leonardo/Micro/Zero

  Serial.println("Hello!");

  nfc.begin();

  uint32_t versiondata = nfc.getFirmwareVersion();
  if (!versiondata) {
    Serial.print("Didn't find PN53x board");
    while (1); // halt
  }

  // Got ok data, print it out!
  Serial.print("Found chip PN5"); Serial.println((versiondata >> 24) & 0xFF, HEX); 
  Serial.print("Firmware ver. "); Serial.print((versiondata >> 16) & 0xFF, DEC); 
  Serial.print('.'); Serial.println((versiondata >> 8) & 0xFF, DEC);

  // configure board to read RFID tags
  nfc.SAMConfig();

  Serial.println("Waiting for an ISO14443A Card ...");
}

void loop(void) {
  uint8_t success;

  uint8_t apdu1[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00};
  uint8_t apdu2[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04};
  uint8_t apdu3[] = {0x00, 0xB0, 0x00, 0x00, 0x00};

  uint8_t response[255];
  uint8_t responseLength = sizeof(response);

  success = nfc.inListPassiveTarget();

  if (success) {
    Serial.println("NFC tag detected");

    // Send first APDU command
    if (!nfc.inDataExchange(apdu1, sizeof(apdu1), response, &responseLength)) {
      Serial.println("Error in first APDU command");
      return;
    } else {
      Serial.print("Received (SW1=0x"); Serial.print(response[responseLength-2], HEX);
      Serial.print(", SW2=0x"); Serial.print(response[responseLength-1], HEX); Serial.println(")");
    }
    // Send second APDU command
    responseLength = sizeof(response);
    if (!nfc.inDataExchange(apdu2, sizeof(apdu2), response, &responseLength)) {
      Serial.println("Error in second APDU command");
      return;
    } else {
      Serial.print("Received (SW1=0x"); Serial.print(response[responseLength-2], HEX);
      Serial.print(", SW2=0x"); Serial.print(response[responseLength-1], HEX); Serial.println(")");
    }

    // Send third APDU command and read response
    responseLength = sizeof(response);
    if (nfc.inDataExchange(apdu3, sizeof(apdu3), response, &responseLength)) {
      Serial.println("APDU command successful");
      Serial.print("Response length: "); Serial.println(responseLength);
      Serial.println("Response data:");
      nfc.PrintHexChar(response, responseLength);

      // Process the response to extract URL
      if (responseLength > 3) {  // We need at least 4 bytes for a valid NDEF message
        // Assume the payload starts at the 7th byte and continues until the end minus 2 bytes (SW1 and SW2)
        uint8_t payloadStart = 7;
        uint8_t payloadEnd = responseLength - 2;

        String url = "";
        for (int i = payloadStart; i < payloadEnd; i++) {
          if (response[i] >= 32 && response[i] <= 126) {  // Only print printable ASCII characters
            url += (char)response[i];
          }
        }
        Serial.print("Decoded URL: ");
        Serial.println(url);

        // Extract the OTP code from the URL
        int otpStart = url.lastIndexOf('/') + 1;  // OTP code starts after the last '/'
        String otpCode = url.substring(otpStart);
        Serial.print("Extracted OTP Code: ");
        Serial.println(otpCode);
      } else {
        Serial.println("Response too short for NDEF message");
      }
    } else {
      Serial.println("Error in APDU command");
    }
  } else {
    Serial.println("Waiting for an NFC card ...");
  }

  delay(1000);
}
Waiting for an NFC card ...
Tag number: 1
NFC tag detected
Received (SW1=0x90, SW2=0x0)
Received (SW1=0x90, SW2=0x0)
APDU command successful
Response length: 50
Response data:
00 2E D1 01 2A 55 04 68 6F 6D 65 2D 61 73 73 69 73 74 61 6E 74 2E 69 6F 2F 74 61 67 32 2F 30 30 30 30 37 36 37 31 32 32 31 36 33 33 32 30 33 30 90 00  ..�.*U.home-assistant.io/tag2/000076712216332030�.
Decoded URL: home-assistant.io/tag2/000076712216332030
Extracted OTP Code: 000076712216332030
Waiting for an NFC card ...
NonaSuomy commented 3 months ago

Not pretty but I was able to persevere through it with Jesse's help

spi:
  clk_pin: GPIO04
  miso_pin: GPIO05
  mosi_pin: GPIO06

pn532_spi:
  cs_pin: GPIO07
  id: pn532_component
  update_interval: never

text_sensor:
  - platform: yubikey_otp
    name: "Yubikey OTP"
    id: yubikey_nfc_otp
    pn532_id: pn532_component
    update_interval: 5s

https://github.com/NonaSuomy/esphome/tree/yubikey_nfc_otp/esphome/components/yubikey_otp

[05:20:56][D][pn532:469]: APDU commands successful [05:20:56][D][pn532:470]: Response: 00.00.2E.D1.01.2A.55.04.68.6F.6D.65.2D.61.73.73.69.73.74.61.6E.74.2E.69.6F.2F.74.61.67.32.2F.30.30.30.30.37.36.37.31.32.32.31.36.34.39.33.32.38.37.90.00 (51) [05:20:56][D][pn532:484]: Decoded URL: home-assistant.io/tag2/000076712216493287 [05:20:56][D][pn532:488]: Extracted OTP Code: 000076712216493287 [05:20:56][D][yubikey_otp:024]: OTP Code: 000076712216493287 [05:20:56][V][text_sensor:013]: 'Yubikey OTP': Received new state 000076712216493287 [05:20:56][D][text_sensor:064]: 'Yubikey OTP': Sending state '000076712216493287' image

DanielTerletzkiy commented 4 weeks ago

@NonaSuomy first of all, thank you so much for this code example, it is exactly what i needed!

but i have one issue which i cannot solve... my Extracted OTP Code always becomes non-valid towards the end and i cannot figure out exactly why... (could be buffer sue maybe..?)

this is my respone in serial output notice the T1@,Bh

Waiting for an NFC card ...
Tag number: 1
NFC tag detected
Received (SW1=0x90, SW2=0x0)
Received (SW1=0x90, SW2=0x0)
APDU command successful
Response length: 71
Response data:
00 43 D1 01 3F 55 04 6D 79 2E 79 75 62 69 63 6F 2E 63 6F 6D 2F 79 6B 2F 23 63 63 63 63 63 62 6E 74 69 6E 6B 64 66 67 76 6E 67 66 62 74 75 6E 6E 62 6E 72 67 6E 63 67 6E 54 31 81 40 01 00 00 00 2C 09 0C 42 68 BB 0D  .C�.?U.my.yubico.com/yk/#cccccbntinkdfgvngfbtunnbnrgncgnT1�@....,..Bh�.
Decoded URL: my.yubico.com/yk/#cccccbntinkdfgvngfbtunnbnrgncgnT1@,Bh
Extracted OTP Code: #cccccbntinkdfgvngfbtunnbnrgncgnT1@,Bh
Waiting for an NFC card ...

would you know how to solve this issue, it would help me out big-time 😄

EDIT: sometimes i also see the following error: [1110148][E][Wire.cpp:522] requestFrom(): i2cRead returned Error -1

EDIT 2: compared the length of a valid otp token and the corrupted one valid: 44 invalid; 35

NonaSuomy commented 4 weeks ago

Are you using I2C or SPI?

I was having the same issue when using I2C.

If you are using SPI (Allows a larger buffer) if not switch to it. If you are using SPI with that problem, you maybe can try playing with this number: https://github.com/adafruit/Adafruit-PN532/blob/f4e1a91afd0163d8601b243ddcbb2a580da6552a/Adafruit_PN532.cpp#L78

It's a limitation with the adafruit library.

DanielTerletzkiy commented 4 weeks ago

i was doing it with i2c, tried with spi, same issue!

the fix was changing the PN532_PACKBUFFSIZ to a higher number

i then changed back to i2c and it now works perfectly! thank you so much!!!

just a shame that there is no good way to override the PN532_PACKBUFFSIZ definition without maybe "forking" it :/

NonaSuomy commented 4 weeks ago

You're welcome!

May have just been an issue with the esphome implementation of i2c that I was thinking about.

SPI though has better stability overall and can fully control the pn523 IC. I would personally stick with it. It's only 1 extra pin.

Someone said that it used to be set to 261 or something then someone got confused about packet buffer header size and changed it to 64 then it just got left there.

Another person said if you require even more than 255 of buffer that you have to make your own apdu protocol to chunk data properly.