Toy RFID authentication system with esp8266 and mfrc522

Preface

A while back, I had an idea to create a device that would allow me to authenticate myself in my room and automate the process of turning my workstation on and log my account in without the password. It turned out to be an interesting project, so here’s an article about it :)

You can find the source code for this project here.


Overview of the device

close up of mounted device from the front

This is the device in it’s current form. It has a NodeMCU with an ESP8266 on board as the main processing unit, an MFRC522 RFID/NFC sensor for short-range radio authentication, and a little PC speaker just for fun.

The whole assembly is powered by a single USB cable, that’s plugged into my workstation, which is configured to power the USB ports, even when in standby.


How it works

Now, the fun part!

To wake the workstation, I settled on using WakeOnLan. The esp8266 supports it and there is a convenient library for Arduino IDE. For the authentication process I wanted to use my phone’s builtin NFC, but it changes it’s ID periodically, which defeats it’s usefullness as an authenticator. So instead, I went with a cheap NFC marker, placed inside my phone’s case.

cheap nfc marker from aliexpress

The one remaining problem was automatic login. I use Gnome 3 with GDM on my pc, so I could just enable automatic login feature globally. But I wanted it to trigger only when authentication was performed by the device.

For this problem, the solution was a custom PAM module (and just a bit of dirty hackery).


Building the device (hardware)

The hardware side of things is rather simple. I connected the rfid reader to the esp, with SPI communication. Info on interfacing mfrc522 with arduino can be found here. I also connected the speaker to pin D8 on the nodemcu.

Pinout in my case:


MFRC522     NodeMCU     ESP8266

SDA         D2          4/SDA
SCK         D5          14/HSPI SCK
MOSI        D7          13/HSPI MOSI
MISO        D6          12/HSPI MISO          
IRQ         None        None
GND         G           _
RST         D3          0
3.3V        3V          _


SPEAKER     NodeMCU     ESP8266

Black       G           _
Red         D8          15

view from the side

My board has a modified WiFi antenna for better signal reception, but this is not necessary in any way.


Building the device (software)

Now, the software. I’m not going to describe everything here. You can find the commented source code on the github, linked in the head of this post.

For WOL functionality, I used WakeOnLan library by a7md0.

I also include SPI.h for communicating with the reader, MFRC522.h for controlling the reader, ESP8266WiFi.h to implement an authentication server, WiFiUdp.h as a dependency for WakeOnLan.h.

The main loop just tells the reader to scan for nearby devices every second. If it finds a device, I retrieve it’s ID to match against my marker’s ID. If the IDs are matching, the authentication is successfull and that fact will be remembered for 2 minutes.

Upon a successfull authentication, a WOL magic packet, containing my workstation MAC address is broadcasted.

successfull authentication

if (cardID.equals(allowedID)) {
    signal = allowed;
    //Serial.println("allowed");
    WOL.sendMagicPacket(targetMAC);
    lastAuthTime = millis(); //remember authentication for some time
  }

There are also cute audio and visual signals for every successfull of unsuccessfull physical authentication attempt.

signals used for feedback

void emitSignal(signal_t signal) {
  pinMode(LED_PIN, OUTPUT);
  pinMode(SPK_PIN, OUTPUT);
  if (signal == detected) {
    for (byte i = 0; i < 2; i++) {
      delay(50);
      digitalWrite(LED_PIN, HIGH);
      delay(50);
      digitalWrite(LED_PIN, LOW);
    }
  }
  else if (signal == allowed) {
    tone(SPK_PIN, 1000);
    delay(100);
    noTone(SPK_PIN);
  }
  else if (signal == denied) {
    for (byte i = 0; i < 2; i++) {
      delay(100);
      tone(SPK_PIN, 700);
      delay(100);
      noTone(SPK_PIN);
    }
  }

  pinMode(LED_PIN, INPUT);
  pinMode(SPK_PIN, INPUT);
}

Also located in the main loop is the authentication server. It’s a listening TCP socket on port 80. Now, this protocol is extremely insecure, as it was created for convenience, and not in any way security. You can make it much safer using additional computation steps, but I didn’t bother.

If you send AUTHENTICATE in plain text to the authentication server, you will receive either an OK if the authentication was performed in last 2 minutes, or a FAIL if it wasn’t. A packet, containing UNKNOWN will be sent if the server doesn’t understand your request.

authentication server processing block

if (WiFiClient con = socket.available()) {
    while (con.connected()) {
      char incoming[64];
      memset(incoming, 0x0, sizeof(incoming));
      con.read(incoming, sizeof(incoming));
      String inStr = String(incoming);
      inStr.trim();
      //Serial.print("Received: ");
      //Serial.println(inStr);
      if (inStr.equals("AUTHENTICATE")) {
        if (canAuthenticate()) {
          con.write("OK\n", 3);
        }
        else {
          con.write("FAIL\n", 5);
        }
      }
      else {
        con.write("UNKNOWN\n", 8);
      }
      con.stop();
    }
}

Using the device (software)

WOL feature is straightforward to use. Just configure your machine for WOL and configure the device to use the machine’s MAC address for WOL packets.

The automatic login functionality is a bit harder. There might be multiple options for unix-based systems, but I went with creating a custom PAM module. Info on writing PAM modules can be found here and here.

My module implements PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv). The implementation contacts the address, passed as argv[1] on the port, passed as argv[2] and asks for authentication. It returns PAM_SUCCESS upon receiving an OK from the server and returns PAM_PERM_DENIED otherwise.

authentication routine

PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
 
    if (argc < 2) {
        return PAM_PERM_DENIED;
    }
 
    struct sockaddr_in server;
 
    int sockfd;
 
    server.sin_family = AF_INET;
    if (!inet_pton(AF_INET, argv[0], &server.sin_addr)) {
        return PAM_PERM_DENIED;
    }
    int port = 0;
    port = atoi(argv[1]);
    if (port < 1 || port > 65535) {
        return PAM_PERM_DENIED;       
    }                                 
    server.sin_port = htons(port);    
                                      
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bool retVal = authAllowed(sockfd, &server); //try to contact the server and check authentication status

    close(sockfd);

    if (retVal == true) {
        return PAM_SUCCESS;
    }
    else {
        return PAM_PERM_DENIED;
    }
}

To build the module I use make build && sudo make install. This will compile and install the module to /lib/security, where pam modules are located. Very simple module, but it works well for my purposes.

To configure my display manager to use this module I edited /etc/pam.d/gdm-password. I added auth sufficient pam_rfbs.so 192.168.1.115 80 to the top of the config file. Obviously, substitute 192.168.1.115 with your device’s address.

my /etc/pam.d/gdm-password

#%PAM-1.0

auth       sufficient              pam_rfbs.so 192.168.1.115 80
auth       include                     system-local-login
auth       optional                    pam_gnome_keyring.so

account    include                     system-local-login

password   include                     system-local-login
password   optional                    pam_gnome_keyring.so use_authtok

session    include                     system-local-login
session    optional                    pam_gnome_keyring.so auto_start

GDM quirk

Now, Gnome Display Manager doesn’t have a user auto-select feature. Therefore I created a little xdotool script, that runs as a systemd service. It simulates the user pressing RETURN key to select the user automatically. This will only work if GDM process runs on xorg, and not on wayland. For this I edited /etc/gdm/custom.conf to include WaylandEnable=false under the [daemon] section.


Epilogue

I had a lot of fun building the device and playing with it when it was ready :)

It was inspired by a similar idea I saw in Serial Experiments Lain, episode 6. Later I added another MCU to my setup to automatically control the power supply of my audio hardware and control a CCFL tube above my desk. I will describe this project in another article.