
User Authentication on Arduino with RFID

Introduction
In the previous article, I was just starting to work with Arduino, as a result of which the weather station naturally turned out. In this article we will go further - we will do authentication using RFID cards and Arduino in the InterSystems Caché application.
Authentication transfer
Caché has a mechanism for delegating authentication - transferring the authentication process to user code. To enable it, do the following:
- Write user authentication code in ZAUTHENTICATE routine. She has 4 entry points: receiving a login / password, checking them and assigning rights, changing a password, generating a token. More on this below.
- Enable authentication transfer to Caché (SMP → System Administration → Security → System Security → Authentication / CSP Session Options, set the Allow Delegated authentication flag and save the settings).
- Enable authentication for the required services (SMP → Menu → Manage Services → Service → Allowed Authentication Methods → select Delegated → Save) and / or applications (SMP → Menu → Manage Web Applications → Application → Allowed Authentication Methods → select Delegated → Save).
How it works
Here's what happens when a user authenticates with a service or web application for which authentication transfer is enabled:
- The ZAUTHENTICATE routine is invoked. The code for this routine is user-written and can be any Caché ObjectScript code, including $ ZF calls.
- The next step depends on whether the ZAUTHENTICATE call was successful:
- If the ZAUTHENTICATE call is successful and this is the first time that this user is authenticated with ZAUTHENTICATE, a user record of the type “Delegated user” is created for him. If ZAUTHENTICATE assigns rights or other properties to a user, they become the corresponding user properties.
- If the ZAUTHENTICATE call is successful and this is not the first time that this user is authenticated with ZAUTHENTICATE, then his user record is updated.
- If the ZAUTHENTICATE call is not successful, an access error is issued to the user.
- If the ZAUTHENTICATE call is successful and this is the first time that this user is authenticated with ZAUTHENTICATE, a user record of the type “Delegated user” is created for him. If ZAUTHENTICATE assigns rights or other properties to a user, they become the corresponding user properties.
- If two-factor authentication is enabled for the instance and service, then the phone number of the user and operator is searched. If they are specified, two-factor authentication occurs; if not, the user is not authenticated.
- The delegated user is displayed in the user table.
Where are the users from?
There are two authentication methods, depending on which authentication methods are included for the application / service:
- Delegated - the name / password is taken from GetCredentials, checked by ZAUTHENTICATE (user type is delegated).
- Delegated and Password - the name / password are taken from GetCredentials, checked by standard Caché mechanisms (user type is Caché).
Now let's move on to the ZAUTHENTICATE routine and its entry points.
ZAUTHENTICATE
This is a basic routine containing 4 entry points.
▍GetCredentials
This entry point is called when authentication transfer is enabled for the service, and it is called instead of requesting a login / password from the user. The code of this routine sets the login and password (in any way). Subsequently (outside this routine), the received login and password are authenticated, as if the user entered them as usual. The method for obtaining a login and password can be any - keyboard input, API, reading an external device - in this article we will use authentication using an RFID card.
This entry point returns status, and if this is an error, it will be recorded in an audit, and an authentication attempt will be rejected. The exception is the error $ SYSTEM.Status.Error ($$$ GetCredentialsFailed), in which case the user will be prompted to enter the login / password using the standard Caché method. The signature is as follows:
GetCredentials(ServiceName, Namespace, Username, Password, Credentials) Public { }
Where:
- ServiceName - the name of the service through which the connection
- Namespace - the area if specified when connecting
- Username - username
- Password - password
- Credentials - Currently Not Used
I note an important feature of this entry point. If the service / application includes both authentication transfer and basic authentication using the Password Authentication pair, then the login and password obtained through GetCredentials will be used for standard password authentication.
▍ZAUTHENTICATE
In case the initial authentication is successful, ZAUTHENTICATE sets the roles and other properties of the user. If this is not the first authentication, the properties can be changed. To do this, the properties of the Properties array are set in the routine code. Signature:
ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) Public { }
Array of Properties:
- Properties ("Comment") - Comment
- Properties ("FullName") - first and last name
- Properties ("NameSpace") - start area
- Properties ("Roles") - comma-separated list of roles
- Properties ("Routine") - starting routine
- Properties ("Password") - password
- Properties ("Username") - username
- Properties ("PhoneNumber") - user phone number
- Properties ("PhoneProvider") - phone operator
- Properties ("AutheEnabled") - enable standard two-factor authentication (to do this, set the value to $$$ AutheTwoFactorSMS)
▍ChangePassword
Entry point for changing user password. The signature is as follows:
ChangePassword(Username, NewPassword, OldPassword, Status) Public { }
Where:
- NewPassword - new password
- OldPassword - old password
- Status - result of password change operation
▍SendTwoFactorToken
For use in standard two-factor authentication. Defines the format of the request and authentication token. Signature:
SendTwoFactorToken(Username, ServiceName,Namespace,Application,Credentials,SecurityToken,TwoFactorTimeout,UserPhoneNumber) Public { }
Where:
- Application - CSP application or routine to which the user connects
- SecurityToken - the token that will be sent to the user
- TwoFactorTimeout - token expiration time
- UserPhoneNumber - user phone number
Example
To begin with, I will show the simplest example for the Caché terminal on Windows - the% Service_Console service, which will ask the user for the username and password. We will enable authentication in the system for this service. After that, we write the ZAUTHENTICATE routine (in the% SYS area):
ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) PUBLIC {
#Include %occErrors
#Include %occStatus
Quit $$$OK
}
GetCredentials(ServiceName, Namespace, Username, Password, Credentials) Public {
#Include %occErrors
#Include %occStatus
Do ##class(%Prompt).GetString("USER:",.Username)
Do ##class(%Prompt).GetString("PASS:",.Password)
Quit $$$OK
}
In the terminal, it will look similar to a regular login.
>USER: _SYSTEM
>PASS: SYS
Rfid
Let's move on to RFID authentication. The idea is as follows - from Caché we will make it possible to write information on the card in encrypted form, and during authentication we will read it, decrypt it and return it for verification.
To get started, let's collect a circuit from Arduino Uno and the RFID-RC522 module:

Here is the C code using the MF522 library (there is also a pinout for other Arduino models in the same place). It accepts 2 commands on the COM port:
- Get - on it the contents of RFID card blocks 2, 4, 5, 6 are transmitted to the com port
- Set @ bloc2 @ bloc4 @ bloc5 @ bloc6 - on it the contents of blocks 2, 4, 5, 6 on the card are overwritten by the data received
C code
#include //include the SPI bus library
#include //include the RFID reader library
#define SS_PIN 10 //slave select pin
#define RST_PIN 9 //reset pin
#define u1b 2 //Block on a card for user1 byte array
#define u2b 4 //Block on a card for user2 byte array
#define p1b 5 //Block on a card for pass1 byte array
#define p2b 6 //Block on a card for pass2 byte array
MFRC522 mfrc522(SS_PIN, RST_PIN); // instatiate a MFRC522 reader object.
MFRC522::MIFARE_Key key; //create a MIFARE_Key struct named 'key', which will hold the card information
byte readbackblock[18]; //This array is used for reading out a block. The MIFARE_Read method requires a buffer that is at least 18
String inString = ""; // COM port incoming data buffer
void setup() {
Serial.begin(9600); // Initialize serial communications with the PC
SPI.begin(); // Init SPI bus
mfrc522.PCD_Init(); // Init MFRC522 card (in case you wonder what PCD means: proximity coupling device)
// Serial.println("Scan a MIFARE Classic card");
// Prepare the security key for the read and write functions - all six key bytes are set to 0xFF at chip delivery from the factory
// Since the cards in the kit are new and the keys were never defined, they are 0xFF
// if we had a card that was programmed by someone else, we would need to know the key to be able to access it.
// This key would then need to be stored in 'key' instead.
for (byte i = 0; i < 6; i++) {
key.keyByte[i] = 0xFF; // keyByte is defined in the "MIFARE_Key" 'struct' definition in the .h file of the library
}
}
void loop() {
// put your main code here, to run repeatedly:
// Receive data from com port
while (Serial.available() > 0) {
int inChar = Serial.read();
if (inChar != '\n') {
inString += (char)inChar;
} else {
// New line
while (!initCard()); // connect to an RFID card
String Action = inString.substring(0, 3);
if (Action == "Set") {
// Write login and pass into the card
setUserAndPassToCard(inString);
} else if (Action == "Get") {
// Read login and pass from the card
readUserAndPassToCom();
} else {
Serial.println(Action);
}
disconnectCard(); // disconnect RFID card
inString = "";
}
}
}
/// Read blocks with user/pass info and output the to COM port:
/// user1user2@pass1pass2
void readUserAndPassToCom()
{
readBlockToCom(u1b);
readBlockToCom(u2b);
Serial.write("@");
readBlockToCom(p1b);
readBlockToCom(p2b);
Serial.println("");
}
/// Set user/pass info into a card
/// Data: Set@user1@user2@pass1@pass2
/// Data sample: Set@1234567890123456@1234567890123456@1234567890123456@1234567890123456
void setUserAndPassToCard(String Data) {
// Serial.println(Data);
byte user1[16], user2[16], pass1[16], pass2[16];
String user1str = inString.substring(4, 20);
String user2str = inString.substring(21, 37);
String pass1str = inString.substring(38, 54);
String pass2str = inString.substring(55, 71);
stringToArray(user1str, user1, sizeof(user1));
stringToArray(user2str, user2, sizeof(user2));
stringToArray(pass1str, pass1, sizeof(pass1));
stringToArray(pass2str, pass2, sizeof(pass2));
writeBlock(u1b, user1); // u1b is the block number, user1 is the block content
writeBlock(u2b, user2);
writeBlock(p1b, pass1);
writeBlock(p2b, pass2);
Serial.println("Done");
}
void stringToArray(String str, byte array[], int arrlength)
{
for (int j = 0 ; j < arrlength ; j++)
{
array[j] = str.charAt(j);
}
}
bool initCard()
{
// Look for new cards (in case you wonder what PICC means: proximity integrated circuit card)
if ( ! mfrc522.PICC_IsNewCardPresent()) {//if PICC_IsNewCardPresent returns 1, a new card has been found and we continue
return false; //if it did not find a new card is returns a '0' and we return to the start of the loop
}
// Select one of the cards
if ( ! mfrc522.PICC_ReadCardSerial()) {//if PICC_ReadCardSerial returns 1, the "uid" struct (see MFRC522.h lines 238-45)) contains the ID of the read card.
return false; //if it returns a '0' something went wrong and we return to the start of the loop
}
return true;
}
void disconnectCard()
{
// Halt PICC
mfrc522.PICC_HaltA();
// Stop encryption on PCD
mfrc522.PCD_StopCrypto1();
}
void readBlockToCom(int number)
{
readBlock(number, readbackblock);//read the block back
for (int j = 0 ; j < 16 ; j++) //print the block contents
{
Serial.write (readbackblock[j]);//Serial.write() transmits the ASCII numbers as human readable characters to serial monitor
}
}
int writeBlock(int blockNumber, byte arrayAddress[])
{
// this makes sure that we only write into data blocks. Every 4th block is a trailer block for the access/security info.
int largestModulo4Number = blockNumber / 4 * 4;
int trailerBlock = largestModulo4Number + 3; //determine trailer block for the sector
if (blockNumber > 2 && (blockNumber + 1) % 4 == 0) {
Serial.print(blockNumber); //block number is a trailer block (modulo 4); quit and send error code 2
Serial.println(" is a trailer block:");
return 2;
}
//Serial.print(blockNumber);
//Serial.println(" is a data block:");
/*****************************************authentication of the desired block for access***********************************/
byte status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, trailerBlock, &key, &(mfrc522.uid));
// byte PCD_Authenticate(byte command, byte blockAddr, MIFARE_Key *key, Uid *uid);
// this method is used to authenticate a certain block for writing or reading
// command: See enumerations above -> PICC_CMD_MF_AUTH_KEY_A = 0x60 (=1100000),
// this command performs authentication with Key A
// blockAddr is the number of the block from 0 to 15.
// MIFARE_Key *key is a pointer to the MIFARE_Key struct defined above, this struct needs to be defined for each block.
// New cards have all A/B= FF FF FF FF FF FF
// Uid *uid is a pointer to the UID struct that contains the user ID of the card.
if (status != MFRC522::STATUS_OK) {
Serial.print("PCD_Authenticate() failed: ");
Serial.println(mfrc522.GetStatusCodeName(status));
return 3;//return "3" as error message
}
// it appears the authentication needs to be made before every block read/write within a specific sector.
// If a different sector is being authenticated access to the previous one is lost.
/*****************************************writing the block***********************************************************/
status = mfrc522.MIFARE_Write(blockNumber, arrayAddress, 16);
//valueBlockA is the block number, MIFARE_Write(block number (0-15), byte array containing 16 values, number of bytes in block (=16))
// status = mfrc522.MIFARE_Write(9, value1Block, 16);
if (status != MFRC522::STATUS_OK) {
Serial.print("MIFARE_Write() failed: ");
Serial.println(mfrc522.GetStatusCodeName(status));
return 4;//return "4" as error message
}
//Serial.println("block was written");
}
int readBlock(int blockNumber, byte arrayAddress[])
{
int largestModulo4Number = blockNumber / 4 * 4;
int trailerBlock = largestModulo4Number + 3; //determine trailer block for the sector
/*****************************************authentication of the desired block for access********************************************/
byte status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, trailerBlock, &key, &(mfrc522.uid));
// byte PCD_Authenticate(byte command, byte blockAddr, MIFARE_Key *key, Uid *uid);
// this method is used to authenticate a certain block for writing or reading
// command: See enumerations above -> PICC_CMD_MF_AUTH_KEY_A = 0x60 (=1100000),
// this command performs authentication with Key A
// blockAddr is the number of the block from 0 to 15.
// MIFARE_Key *key is a pointer to the MIFARE_Key struct defined above, this struct needs to be defined for each block.
// New cards have all A/B= FF FF FF FF FF FF
// Uid *uid is a pointer to the UID struct that contains the user ID of the card.
if (status != MFRC522::STATUS_OK) {
Serial.print("PCD_Authenticate() failed (read): ");
Serial.println(mfrc522.GetStatusCodeName(status));
return 3;//return "3" as error message
}
// it appears the authentication needs to be made before every block read/write within a specific sector.
// If a different sector is being authenticated access to the previous one is lost.
/*****************************************reading a block***********************************************************/
byte buffersize = 18;//we need to define a variable with the read buffer size, since the MIFARE_Read method below needs a pointer to the variable that contains the size...
status = mfrc522.MIFARE_Read(blockNumber, arrayAddress, &buffersize);//&buffersize is a pointer to the buffersize variable; MIFARE_Read requires a pointer instead of just a number
if (status != MFRC522::STATUS_OK) {
Serial.print("MIFARE_read() failed: ");
Serial.println(mfrc522.GetStatusCodeName(status));
return 4;//return "4" as error message
}
}
The Arduino.Delegate class, which has 2 entry points:
- SetCredentials - accepts a login and password as an input, encrypts them with AES encryption using a key stored in the system and writes it to an RFID card.
- GetCredentials - receives the ciphertext from the card and decrypts it, returning the login, password and operation status.
Arduino.Delegate
/// Delegated Authentication with Arduino.
/// Installation steps:
/// 1. Connect arduino (and upload C code from Delegated.ino there)
/// 2. Make this class visible in %SYS namespace (import there or map pckage)
/// 3. Set SerialPort parameter to a correct value and recompile the class
/// 4. Run Do ##class(Arduino.Delegated).InitEncryption(Key, IV)
/// 5. Write encrypted user credentials to RFID card with SetCredentials
/// 6. Import ZAUTHENTICATE into %SYS
/// 7. Enable Delegated and password auth for relevant services and/or apps
Class Arduino.Delegated [ Abstract ]
{
Parameter SerialPort As %String = "com3";
/// Creates managed encryption key.
/// key - Input key material.
/// Key material 16, 24, or 32 characters long (on Unicode systems, with all character values < 256) is used directly.
/// Otherwise, Password-Based Key Derivation Function #2 (PBKDF2)
/// is used with HMAC-SHA-1,
/// no salt, and one iteration
/// to generate an AES key of the next larger valid size (up to 32 bytes).
/// (See RSA Laboratories Public-Key Cryptography Standards #5 for more information.)
///
/// IV - Initialization vector (optional).
/// If this argument is present it must be 16 characters long (on Unicode systems, with all character values < 256).
/// If this argument is omitted (or is an empty string), a null initialization vector is used.
///
/// Do ##class(Arduino.Delegated).Init("", "")
ClassMethod Init(Key As %String, IV As %String)
{
New $Namespace
Set $Namespace = "%SYS"
Set ^Arduino("Key")= Key
Set ^Arduino("IV")= IV
}
/// Send Arduino the command to set credentials on a card to Username/Password (encrypted)
/// Do ##class(Arduino.Delegated).SetCredentials("_SYSTEM", "SYS")
ClassMethod SetCredentials(Username As %String(MAXLEN=15), Password As %String(MAXLEN=15)) As %Status
{
Set Status = $$$OK
Set CipherUsername = ..EncryptText(Username)
Set CipherPassword = ..EncryptText(Password)
Set User1 = $Extract(CipherUsername, 1, 16)
Set User2 = $Extract(CipherUsername, 17, 32)
Set User2 = ..AppendToString(User2, , 16)
Set Pass1 = $Extract(CipherPassword, 1, 16)
Set Pass2 = $Extract(CipherPassword, 17, 32)
Set Pass2 = ..AppendToString(Pass2, , 16)
Set CommandList = $ListBuild("Set", User1, User2, Pass1, Pass2)
Set Command = $ListToString(CommandList, "@")
Set Status = ..ExecuteCommand(.Command)
If (Status = "Done") {
Set Status = $$$OK
} Else {
Set Status = $$$ERROR($$$GeneralError, "SetCredentials failure, received: " _ Status)
}
Return Status
}
/// Connect to an Arduino device, receive credentials, decode them and set to Username/Password variables.
/// do ##class(Arduino.Delegated).GetCredentials(.Username, .Password)
ClassMethod GetCredentials(Output Username As %String, Output Password As %String) As %Status
{
Kill Username, Password
Set Username = ""
Set Password = ""
Set Status = $$$OK
Set Credentials = ..ExecuteCommand("Get")
If (($L(Credentials) =65) && ($L(Credentials,"@") = 2)) {
Set CipherUsername = $Piece(Credentials, "@", 1)
Set CipherPassword = $Piece(Credentials, "@", 2)
Set CipherUsername = $Extract(CipherUsername, 1, 24) // we need only first 24 characters
Set CipherPassword = $Extract(CipherPassword, 1, 24)
Set Username = ..DecryptText(CipherUsername)
Set Password = ..DecryptText(CipherPassword)
} Else {
Set Status = $$$ERROR($$$GeneralError, "GetCredentials failure, received: " _ Credentials)
}
Return Status
}
/// Send one line at a time, using common terminating characters (i.e., CR) and receive output
/// Possible comands:
/// Get - reads an RFID card and returns information in a format: user@pass
/// Set@user1@user2@pass1@pass2 - sets information on a RFID card
/// in a format: user@pass (where user = user1@user2)
/// Returns output, produced by Arduino
/// w ##class(Arduino.Delegated).ExecuteCommand("Get")
ClassMethod ExecuteCommand(ByRef Command As %String, SerialPort = {..#SerialPort}) As %String
{
set x=""
try {
//Parameters used to open the serial device:
// portstate = " 0801n0" - by byte position:
// 1: space indicates "don't disconnect the port"
// 2: 0 indicates "don't use modem control"
// 3: 8 indicates 8 data bits
// 4: 0 indicates no parity
// 5: 1 indicates one stop bit
// 6: n indicates that flow control is disabled
// 7: 0 indicates disable DTR
// /BAUD=9600 determines the baud rate, of course.
open SerialPort:(:::" 0801n0":/BAUD=9600)
set old = $io //Keep track of the original device
use SerialPort
write $char(10)
hang 1
write Command _ $Char(10)
read x //Read until a termination character is reached
use old
close SerialPort
} catch ex {
close SerialPort
w $System.Status.GetErrorText(ex.AsStatus())
}
return x
}
/// Get key to encode/decode via EncryptText/DecryptText
ClassMethod GetKey() [ CodeMode = expression ]
{
$Get(^Arduino("Key"))
}
/// Get IV to encode/decode via EncryptText/DecryptText
ClassMethod GetIV() [ CodeMode = expression ]
{
$Get(^Arduino("IV"))
}
/// Encrypt PlainText with AESCBCEncrypt
/// Write ##class(Arduino.Delegated).EncryptText("string")
ClassMethod EncryptText(PlainText As %String) As %String
{
Set Text=$ZConvert(PlainText,"O","UTF8")
Set Text=$System.Encryption.AESCBCEncrypt(Text, ..GetKey(), ..GetIV())
Set Ciphertext=$System.Encryption.Base64Encode(Text)
Return Ciphertext
}
/// Decrypt PlainText with AESCBCEncrypt
/// Write ##class(Arduino.Delegated).DecryptText("sFgKzZVle187N4OqhhcXPw==")
ClassMethod DecryptText(CipherText As %String) As %String
{
Set Text=$System.Encryption.Base64Decode(CipherText)
Set Text=$System.Encryption.AESCBCDecrypt(Text, ..GetKey(), ..GetIV())
Set PlainText=$ZConvert(Text,"I","UTF8")
Return PlainText
}
/// Extends right side of a String by Character up to Length chars
/// Write ##class(Arduino.Delegated).AppendToString("")
ClassMethod AppendToString(String As %String, Character As %String(MAXLEN=1) = "_", Length As %Integer = {$Length(String)}) As %String
{
Set Difference = Length - $Length(String)
Return:Difference<=0 String
Set Tail = $Justify("", Difference)
Set Tail = $Translate(Tail, " ", Character)
Return String _ Tail
}
}
The ZAUTHENTICATE routine that calls the Arduino.Delegated class, GetCredentials method:
ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) PUBLIC {
#Include %occStatus
Quit $$$OK
}
GetCredentials(ServiceName, Namespace, Username, Password, Credentials) Public {
#Include %occErrors
#Include %occStatus
Quit ##class(Arduino.Delegated).GetCredentials(.Username, .Password)
}
Done! The assembled device looks like this:

Set the encryption keys in the terminal,% SYS area (the Arduino.Delegated class should be available there):
Do ##class(Arduino.Delegated).InitEncryption(Key, IV)
Where Key is the encryption key, IV is the initialization vector. They will be used to encrypt the username and password. We connect Arduino to Caché and write information for authentication on the card with the command:
Do ##class(Arduino.Delegated).SetCredentials("_SYSTEM", "SYS")
We enable Delegated and Password authentication in the necessary services / web applications and you can authenticate (for example, in the terminal or system management portal) by bringing the card to the RFID card reader.
Possible improvements
- Increase security by using managed encryption keys to encrypt your username and password.
- Increasing security by using two-factor authentication - first get a username / password pair, and then read the card that stores the key that is unique to the user. Then you need to verify the received key with the one that is stored in the system for this user. Options for storing arbitrary user data were discussed on the InterSystems Community portal.
- Add the ability to store login and password longer than 15 characters each.
conclusions
Caché’s flexible authentication system allows you to implement arbitrary user authentication logic.
References
» Documentation
» GitHub repository with code (in the SAMPLES area there is an example of ZAUTHENTICATE routine)