Введение
В предыдущей статье я только начинал работать с Arduino, в результате чего закономерно получилась метеостанция. В этой статье пойдём дальше — будем делать аутентификацию с помощью RFID карт и Arduino в приложении InterSystems Caché.
Передача аутентификации
В Caché есть механизм делегирования аутентификации — передачи процесса аутентификации пользовательскому коду. Чтобы его включить, надо сделать следующее:
- Написать код аутентификации пользователей в рутине ZAUTHENTICATE. У неё есть 4 точки входа: получения логина/пароля, их проверка и назначение прав, смена пароля, формирование токена. Подробнее об этом ниже.
- Включить передачу аутентификации в Caché (SMP → System Administration → Security → System Security → Authentication/CSP Session Options, установите флаг Allow Delegated authentication и сохраните настройки).
- Включить передачу аутентификации для требуемых сервисов (SMP → Menu → Manage Services → Сервис → Allowed Authentication Methods → выбрать Delegated → Save) и/или приложений (SMP → Menu → Manage Web Applications → Приложение → Allowed Authentication Methods → выбрать Delegated → Save).
Как это работает
Вот что происходит, когда пользователь аутентифицируется в сервисе или веб-приложении, для которого включена передача аутентификации:
- Вызывается рутина ZAUTHENTICATE. Код этой рутины пишется пользователем и может быть любым Caché ObjectScript кодом, в том числе и $ZF вызовы.
- Следующий шаг зависит от того, был ли успешным вызов ZAUTHENTICATE:
- Если вызов ZAUTHENTICATE успешен и это первый раз, когда данный пользователь аутентифицируется с ZAUTHENTICATE, то для него создаётся запись пользователя типа “Delegated user”. Если ZAUTHENTICATE назначает пользователю права или другие свойства, они становятся соответствующими свойствами пользователя.
- Если вызов ZAUTHENTICATE успешен и это не первый раз, когда данный пользователь аутентифицируется с ZAUTHENTICATE, то его запись пользователя обновляется.
- Если вызов ZAUTHENTICATE не успешен, пользователю выдаётся ошибка доступа.
- Если для инстанса и сервиса включена двухфакторная аутентификация, то производится поиск номера телефона пользователя и оператора. Если они заданы, происходит двухфакторная аутентификация, если нет — юзер не аутентифицируется.
- Делегированный пользователь отображается в таблице пользователей.
Откуда пользователи?
Есть два метода аутентификации в зависимости от того какие способы аутентификации включены для приложения/сервиса:
- Delegated — имя/пароль берутся из GetCredentials, проверяются средствами ZAUTHENTICATE (тип пользователя — делегированный).
- Delegated и Password — имя/пароль берутся из GetCredentials, проверяются стандартными механизмами Caché (тип пользователя — Caché).
Теперь перейдём к рассмотрению рутины ZAUTHENTICATE и её точек входа.
ZAUTHENTICATE
Эта основная рутина, содержащая 4 точки входа.
▍GetCredentials
Эта точка входа вызывается в том случае, когда передача аутентификации включена для сервиса, и она вызывается вместо запроса логина/пароля у пользователя. Код этой рутины устанавливает логин и пароль (любым способом). Впоследствии (вне этой рутины) полученные логин и пароль аутентифицируются, будто пользователь их ввёл как обычно. Метод получения логина и пароля может быть любой — ввод с клавиатуры, API, считывание внешнего устройства — в этой статье будем использовать аутентификацию с помощью RFID карты.
Данная точка входа возвращает статус, и если это ошибка, то она будет записана в аудит, а попытка аутентификации будет отклонена. Исключение — ошибка $SYSTEM.Status.Error($$$GetCredentialsFailed), в таком случае пользователю предложат ввести логин/пароль стандартным методом Caché. Сигнатура следующая:
GetCredentials(ServiceName, Namespace, Username, Password, Credentials) Public { }
Где:
- ServiceName — название сервиса, через который идёт подключение
- Namespace — область, если указана при подключении
- Username — имя пользователя
- Password — пароль
- Credentials — в настоящее время не используется
Отмечу важную особенность этой точки входа. Если для сервиса/приложения включена и передача аутентификации и обычная аутентификация по паре логин/пароль (Password Authentication), то логин и пароль, полученные через GetCredentials, будут использованы для стандартной аутентификации по паролю.
▍ZAUTHENTICATE
В случае если первоначальная аутентификация успешна, ZAUTHENTICATE устанавливает роли и другие свойства пользователя. В случае если это не первая аутентификация, свойства могут быть изменены. Для этого в коде рутины устанавливаются свойства массива Properties. Сигнатура:
ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) Public { }
Массив Properties:
- Properties("Comment") — комментарий
- Properties("FullName") — имя и фамилия
- Properties("NameSpace") — стартовая область
- Properties("Roles") — список ролей через запятую
- Properties("Routine") — стартовая рутина
- Properties("Password") — пароль
- Properties("Username") — имя пользователя
- Properties("PhoneNumber") — телефонный номер пользователя
- Properties("PhoneProvider") — оператор телефона
- Properties("AutheEnabled") — включить стандартную двухфакторную аутентификацию (для этого надо установить значение, равное $$$AutheTwoFactorSMS)
▍ChangePassword
Точка входа для смены пароля пользователя. Сигнатура следующая:
ChangePassword(Username, NewPassword, OldPassword, Status) Public { }
Где:
- NewPassword — новый пароль
- OldPassword — старый пароль
- Status — результат операции изменения пароля
▍SendTwoFactorToken
Для использования в стандартной двухфакторной аутентификации. Определяет формат запроса и токена аутентификации. Сигнатура:
SendTwoFactorToken(Username, ServiceName,Namespace,Application,Credentials,SecurityToken,TwoFactorTimeout,UserPhoneNumber) Public { }
Где:
- Application — CSP приложение или рутина, к которой подключается пользователь
- SecurityToken — токен, который будет отправлен пользователю
- TwoFactorTimeout — время действия токена
- UserPhoneNumber — телефонный номер пользователя
Пример
Для начала покажу простейший пример для терминала Caché в Windows — сервиса %Service_Console, который будет спрашивать логин и пароль у пользователя. Включим передачу аутентификации в системе для этого сервиса. После этого напишем рутину ZAUTHENTICATE (в области %SYS):
ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) PUBLIC { #Iinclude %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 (там же есть распиновка для других моделей Arduino). Он по COM порту принимает 2 команды:
- Get — по ней на com порт передаётся содержимое блоков RFID карты 2, 4, 5, 6
- Set@bloc2@bloc4@bloc5@bloc6 — по ней содержимое блоков 2, 4, 5, 6 на карте перезаписывается пришедшими данными
#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, который имеет 2 точки входа:
- SetCredentials — принимает на вход логин и пароль, шифрует их AES шифрованием с помощью ключа хранящегося в системе и записывает на RFID карту.
- GetCredentials — получает шифротекст с карты и расшифровывает его, возвращая логин, пароль и статус операции.
/// 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 (i.e., 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 } }
Рутина ZAUTHENTICATE, которая вызывает класс Arduino.Delegated, метод GetCredentials:
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")
Включаем аутентификацию Delegated и Password в нужных сервисах/веб-приложениях и можно аутентифицироваться (например, в терминале или портале управления системой) поднося карту к считывателю RFID карт.
Возможные улучшения
- Повышение уровня безопасности с помощью использования управляемых ключей шифрования для шифрования логина и пароля.
- Повышение уровня безопасности с помощью использования двухфакторной аутентификации — сначала получать пару логин/пароль, а потом считывать карту, на которой хранится ключ, уникальный для пользователя. Затем нужно сверять полученный ключ с тем, который хранится в системе для данного пользователя. Варианты хранения произвольных данных пользователя обсуждались на Community портале InterSystems.
- Добавить возможность хранения логина и пароля длиннее 15 символов каждый.
Выводы
Гибкая система аутентификации Caché позволяет реализовать произвольную логику аутентификации пользователей.
Ссылки
» Документация
» GitHub репозиторий с кодом (в области SAMPLES есть пример рутины ZAUTHENTICATE)
ссылка на оригинал статьи https://habrahabr.ru/post/279893/
Добавить комментарий