📜 ⬆️ ⬇️

User Authentication for Arduino with RFID

Arduino

Introduction


In the previous article, I was just starting to work with the Arduino, as a result of which the weather station naturally turned out. In this article let's 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 — passing the authentication process to a custom code. To enable it, do the following:

  1. Write the user authentication code in the ZAUTHENTICATE routine. It has 4 entry points: getting a login / password, checking them and assigning rights, changing the password, creating a token. More on this below.

  2. Enable authentication to Caché (SMP → System Administration → Security → System Security → Authentication / CSP Session Options, set the Allow Delegated authentication flag and save the settings).
    ')
  3. Enable authentication for 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 is what happens when a user is authenticated in a service or web application for which authentication transfer is enabled:

  1. Called the routine ZAUTHENTICATE. The code for this routine is written by the user and can be any Caché ObjectScript code, including $ ZF calls.

  2. The next step depends on whether the ZAUTHENTICATE call was successful:

    • If the call to ZAUTHENTICATE is successful and this is the first time this user is authenticated with ZAUTHENTICATE, then a user record of the type “Delegated user” is created for him. If ZAUTHENTICATE assigns rights or other properties to the user, they become the corresponding user properties.

    • If the call to ZAUTHENTICATE is successful and it is not the first time this user is authenticated with ZAUTHENTICATE, then his user record is updated.

    • If the call to ZAUTHENTICATE is not successful, the user is given an access error.

  3. If two-factor authentication is enabled for the instance and service, the user and operator phone numbers are searched. If they are set, two-factor authentication occurs; if not, the user is not authenticated.

  4. 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 enabled for the application / service:


We now turn to the consideration of the ZAUTHENTICATE routine and its entry points.

ZAUTHENTICATE


This main routine contains 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 username / password from the user. The code of this routine sets the login and password (in any way). Subsequently (outside of this routine), the received login and password are authenticated, as if the user entered them as usual. The method of obtaining a login and password can be any - input from the keyboard, API, reading external device - in this article we will use authentication using an RFID card.

This entry point returns the status, and if this is an error, it will be recorded in the audit, and the authentication attempt will be rejected. The exception is the error $ SYSTEM.Status.Error ($$$ GetCredentialsFailed), in which case the user will be prompted to enter a login / password using the standard Caché method. The signature is as follows:

GetCredentials(ServiceName, Namespace, Username, Password, Credentials) Public { } 

Where:


I will note an important feature of this entry point. If the service / application includes both authentication and password authentication (Password Authentication), the login and password obtained through GetCredentials will be used for standard password authentication.

â–ŤZAUTHENTICATE


In case the initial authentication succeeds, ZAUTHENTICATE sets the roles and other properties of the user. In case 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 { } 

Properties array:


â–ŤChangePassword


Entry point to change user password. The signature is as follows:

 ChangePassword(Username, NewPassword, OldPassword, Status) Public { } 

Where:


â–ŤSendTwoFactorToken


For use in standard two-factor authentication. Defines the request format and authentication token. Signature:

 SendTwoFactorToken(Username, ServiceName,Namespace,Application,Credentials,SecurityToken,TwoFactorTimeout,UserPhoneNumber) Public { } 

Where:


Example


To begin with, I'll show you the simplest example for a Caché terminal on Windows — the% Service_Console service, which will ask the user for the username and password. Enable the transfer of authentication in the system for this service. After that we will 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 like a normal login.

 >USER: _SYSTEM >PASS: SYS 

RFID


Let's go to RFID authentication. The idea is as follows - from Caché we will make it possible to write information in an encrypted form onto a card, and during authentication we will read it, decrypt it and return it for verification.

To begin with, we will assemble a circuit from Arduino Uno and the RFID-RC522 module:



Here is the C code that uses the MF522 library (there is also a pinout for other Arduino models). It accepts 2 commands via COM port:


C code
 #include <SPI.h> //include the SPI bus library #include <MFRC522.h> //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 } } 

Arduino.Delegate class, which has 2 entry points:


Arduino.Delegate
 /// Delegated Authentication with Arduino. /// Installation steps:<br> /// 1. Connect arduino (and upload C code from Delegated.ino there)<br> /// 2. Make this class visible in %SYS namespace (import there or map pckage)<br> /// 3. Set SerialPort parameter to a correct value and recompile the class<br> /// 4. Run <example>Do ##class(Arduino.Delegated).InitEncryption(Key, IV)</example> /// 5. Write encrypted user credentials to RFID card with SetCredentials<br> /// 6. Import ZAUTHENTICATE into %SYS<br> /// 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.<br> /// 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.) /// <br><br> /// 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. /// <br> /// <example>Do ##class(Arduino.Delegated).Init("", "")</example> 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) /// <example>Do ##class(Arduino.Delegated).SetCredentials("_SYSTEM", "SYS")</example> 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. /// <example>do ##class(Arduino.Delegated).GetCredentials(.Username, .Password)</example> 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 (ie, CR) and receive output /// Possible comands:<br> /// <b>Get</b> - reads an RFID card and returns information in a format: user@pass<br> /// <b>Set@user1@user2@pass1@pass2</b> - sets information on a RFID card /// in a format: user@pass (where user = user1@user2)<br> /// Returns output, produced by Arduino /// <example>w ##class(Arduino.Delegated).ExecuteCommand("Get")</example> 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 /// <example>Write ##class(Arduino.Delegated).EncryptText("string")</example> 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 /// <example>Write ##class(Arduino.Delegated).DecryptText("sFgKzZVle187N4OqhhcXPw==")</example> 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 /// <example>Write ##class(Arduino.Delegated).AppendToString("")</example> 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, the 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:



Install 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 username and password. We connect Arduino to Caché and write the authentication information to 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 a terminal or a system management portal) by bringing the card to the RFID card reader.

Possible improvements



findings


Caché's flexible authentication system allows the implementation of arbitrary user authentication logic.

Links


» Documentation
» GitHub repository with code (in the SAMPLES area there is an example of the ZAUTHENTICATE routine)

Source: https://habr.com/ru/post/279893/


All Articles