RFIDを使用したArduinoでのユーザー認証

Arduino

はじめに



前回の記事で、私はArduinoを使い始めたばかりでした。その結果、気象ステーションは自然に判明しました。 この記事では、さらに先に進みます-InterSystemsCachéアプリケーションで、RFIDカードとArduinoを使用して認証を行います。



認証転送



Cachéには、 認証委任するメカニズムがあります-認証プロセスをユーザーコードに転送します。 有効にするには、次を実行します。



  1. ZAUTHENTICATEルーチンでユーザー認証コードを記述します。 彼女には4つのエントリポイントがあります。ログイン/パスワードの受信、チェックと権限の割り当て、パスワードの変更、トークンの生成です。 詳細については、以下をご覧ください。



  2. Cachéへの認証転送を有効にします(SMP→システム管理→セキュリティ→システムセキュリティ→認証/ CSPセッションオプション、委任認証を許可するフラグを設定し、設定を保存します)。



  3. 必要なサービス(SMP→メニュー→サービスの管理→サービス→許可された認証方法→委任→保存を選択)および/またはアプリケーション(SMP→メニュー→Webアプリケーションの管理→アプリケーション→許可された認証方法→委任→保存を選択)の認証を有効にします。


仕組み



認証転送が有効になっているサービスまたはWebアプリケーションに対してユーザーが認証されると、次のようになります。



  1. ZAUTHENTICATEルーチンが呼び出されます。 このルーチンのコードはユーザー作成であり、 $ ZF呼び出しを含む任意のCachéObjectScriptコードにすることができます。



  2. 次のステップは、ZAUTHENTICATE呼び出しが成功したかどうかによって異なります。



    • ZAUTHENTICATE呼び出しが成功し、このユーザーがZAUTHENTICATEで認証されるのが初めての場合、「委任されたユーザー」タイプのユーザーレコードが彼に対して作成されます。 ZAUTHENTICATEが権利またはその他のプロパティをユーザーに割り当てる場合、それらは対応するユーザープロパティになります。



    • ZAUTHENTICATE呼び出しが成功し、このユーザーがZAUTHENTICATEで認証されるのがこれが初めてではない場合、ユーザーレコードが更新されます。



    • ZAUTHENTICATE呼び出しが成功しない場合、アクセスエラーがユーザーに発行されます。



  3. インスタンスとサービスに対して2要素認証が有効になっている場合、ユーザーとオペレーターの電話番号が検索されます。 指定されている場合、2要素認証が行われ、指定されていない場合、ユーザーは認証されません。



  4. 委任されたユーザーは、ユーザーテーブルに表示されます。


ユーザーはどこから来ましたか?



アプリケーション/サービスに含まれる認証方法に応じて、2つの認証方法があります。





それでは、ZAUTHENTICATEルーチンとそのエントリポイントに移りましょう。



ZAUTHENTICATE



これは、4つのエントリポイントを含む基本的なルーチンです。



CredentialGetCredentials



このエントリポイントは、サービスの認証転送が有効になっているときに呼び出され、ユーザーにログイン/パスワードを要求する代わりに呼び出されます。 このルーチンのコードは、ログインとパスワードを設定します(何らかの方法で)。 その後(このルーチンの外で)、受信したログインとパスワードは、ユーザーが通常どおり入力したかのように認証されます。 ログインとパスワードを取得する方法は、キーボード入力、API、外部デバイスの読み取りのいずれでもかまいません。この記事では、RFIDカードを使用した認証を使用します。



このエントリポイントはステータスを返し、これがエラーの場合、監査に記録され、認証の試みは拒否されます。 例外はエラー$ SYSTEM.Status.Error($$$ GetCredentialsFailed)です。この場合、ユーザーは標準のCachéメソッドを使用してログイン/パスワードの入力を求められます。 署名は次のとおりです。



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





どこで:





このエントリポイントの重要な機能に注意してください。 パスワード認証ペアを使用するサービス/アプリケーションで認証転送と基本認証の両方が有効になっている場合、GetCredentialsを介して受信したログインとパスワードが標準パスワード認証に使用されます。



AUTHZAUTHENTICATE



初期認証が成功した場合、ZAUTHENTICATEはユーザーのロールとその他のプロパティを設定します。 これが最初の認証ではない場合、プロパティを変更できます。 これを行うには、Properties配列のプロパティをルーチンコードで設定します。 署名:



 ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) Public { }
      
      





プロパティの配列:





▍ChangePassword



ユーザーパスワードを変更するためのエントリポイント。 署名は次のとおりです。



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





どこで:





▍SendTwoFactorToken



標準の2要素認証で使用します。 要求形式と認証トークンを定義します。 署名:



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





どこで:







最初に、WindowsのCachéターミナルの最も単純な例を示します。%Service_Consoleサービスは、ユーザーにユーザー名とパスワードを要求します。 このサービスのシステムで認証転送を有効にします。 この後、ZAUTHENTICATEルーチンを(%SYS領域に)記述します。



 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 }
      
      





ターミナルでは、通常のログインに似ています。



 >USER: _SYSTEM >PASS: SYS
      
      





Rfid



RFIDの認証に進みましょう。 アイデアは次のとおりです。Cachéから、暗号化された形式でカードに情報を書き込むことができ、認証中に情報を読み取り、解読し、検証のために返します。



最初に、Arduino UnoとRFID-RC522モジュールから回路を組み立てましょう。







MF522ライブラリを使用したCコードは次のとおりです(同じ場所に他のArduinoモデルのピン配置もあります)。 COMポートで2つのコマンドを受け入れます。





Cコード
 #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 } }
      
      





2つのエントリポイントを持つArduino.Delegateクラス:





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 } }
      
      





Arduino.DelegatedクラスのGetCredentialsメソッドを呼び出すZAUTHENTICATEルーチン:



 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) }
      
      





できた! 組み立てられたデバイスは次のようになります。







端末に暗号化キーを設定します。%SYS領域(Arduino.Delegatedクラスがそこにあるはずです):



 Do ##class(Arduino.Delegated).InitEncryption(Key, IV)
      
      





Keyは暗号化キー、IVは初期化ベクトルです。 ユーザー名とパスワードの暗号化に使用されます。 ArduinoをCachéに接続し、次のコマンドを使用して、認証に関する情報をカードに書き込みます。



 Do ##class(Arduino.Delegated).SetCredentials("_SYSTEM", "SYS")
      
      





必要なサービス/ Webアプリケーションで委任認証とパスワード認証を有効にし、RFIDカードリーダーにカードを持ち込むことで認証できます(たとえば、端末またはシステム管理ポータルで)。



可能な改善





結論



Cachéの柔軟な認証システムにより、任意のユーザー認証ロジックを実装できます。



参照資料



» ドキュメント

» GitHubリポジトリとコード (SAMPLESエリアにはZAUTHENTICATEルーチンの例があります)



All Articles