- maksymalna liczba użytkowników: 40 (+ administrator),
- maksymalna liczba zdarzeń: 1000,
- czas automatycznego wylogowania Administratora: 60 s,
- czas automatycznego wygaszenia podświetlenia: 40 s bezczynności użytkownika,
- czas automatycznego wyjścia do menu głównego: 30 s bezczynności użytkownika,
- maksymalny prąd styków przekaźnika wykonawczego: 1 A/220 VAC (szczegóły w dokumentacji elementu),
- napięcie zasilania: 9 VDC,
- maksymalny pobór prądu (przekaźnik wyłączony/załączony): 90/110 mA.
Zasadniczym kompromisem zaimplementowanym w urządzeniu NFC Lock, było zastosowanie wyłącznie numeru seryjnego karty lub urządzenia NFC (UID), jako klucza autoryzacyjnego. Ktoś powie, w czym problem? Otóż problem jest w tym, że na rynku dostępne są „czyste” karty RFID, do których możemy wgrać dowolny numer seryjny i w ten sposób sklonować to z założenia niepowtarzalne peryferium. Co ciekawe, przypomina mi to trochę sytuację z interfejsem 1-wire. Producent i pomysłodawca interfejsu, firma MAXIM INTEGRATED, zapewniał, iż wyposażony w unikalny 64-bitowy numer seryjny układ jest niepowtarzalny, gdyż numer ten wypalany jest w trakcie procesu produkcji. Cóż, dobrych kilka lat temu, bo w wydaniu EP 2/2009 zaprezentowałem projekt „cButton. Emulator pastylek Maxim-Dallas”, za pomocą którego mogliśmy skopiować dowolną pastylkę DS1990 i potem emulować ją z użyciem mikrokontrolera. W ten sposób łatwo dało się oszukać wszelkie czytniki tego typu układów, co sprawdziłem wielokrotnie w praktyce.
Tym razem chciałem zaprojektować „prawdziwy” system kontroli dostępu z pełną rejestracją zdarzeń i użytkowników pozwalający na zastosowanie bardziej wyszukanych sposobów autoryzacji. Z pomocą przyszła mi specyfikacja samych kart Mifare, które są flagowym przykładem technologii RFID. Nie będę w tym miejscu przytaczał wszystkich szczegółów dotyczących samych kart, jak i sposobu ich obsługi, gdyż ten temat drobiazgowo opisałem we wspomnianym wcześniej artykule. Skupię się na elementach nowych, istotnych z punktu widzenia procesu autoryzacji. Mowa o pamięci EEPROM, której nie tak mała ilość, bo 8 kilobitów, znajduje się na każdej karcie tego typu, a której obsługa dostarcza nam zaawansowanych mechanizmów autoryzacji.
Karty Mifare
Jak wspomniałem wcześniej, znaczniki w tym standardzie (1 k) wyposażone są w pamięć EEPROM o rozmiarze 8 kb. Pamięć ta zorganizowana jest w 16 sektorach, zawierających po 4 bloki pamięci, każdy o rozmiarze 16 bajtów. Łatwo policzyć, iż teoretycznie możemy pomieścić 1 kB danych. W praktyce jest to jednak nieco mniej, gdyż każdy sektor posiada zarezerwowany jeden blok (16 bajtów) noszący nazwę Sector Trailer, który przechowuje informacje dotyczące kluczy dostępowych oraz ustawionych reguł dostępu (Access Bits) do bloków tego sektora. Co więcej, pierwszy sektor tejże pamięci EEPROM, oprócz bloku Sector Trailer ma blok producenta Manfacturer Block przechowujący unikatowy identyfikator karty oraz dane producenta. Łatwo więc policzyć, że faktyczna ilość danych jaką możemy zapisać wynosi: (16 sektorów×3 bloki)–1=47 bloków danych co daje 47 bloków danych×16 bajtów=752 bajty. Niby niewiele, ale do całej rzeszy ciekawych zastosowań w zupełności wystarczy, wszak wiele bankowych rozwiązań korzysta z tej funkcjonalności.
Na rysunku 1 pokazano logiczną budowę pamięci EEPROM karty Mifare. Zgodnie z wcześniejszymi ustaleniami, każdy sektor danych zawiera blok o nazwie Sector Trailer, który przechowuje klucze autoryzacyjne (dwa – A i B) umożliwiające dostęp do poszczególnych bloków sektora, jak i reguły zarządzające tym dostępem – oddzielne dla każdego z bloków, w tym bloku Sector Trailer.
Na rysunku 2 pokazano, jak wygląda struktura każdego bloku Sector Trailer:
- pierwsze sześć bajtów (0...5) przechowuje klucz dostępowy A;
- kolejne cztery bajty (6...9) przechowują informacje o ustawionych bitach dostępu do wszystkich czterech bloków pamięci w sektorze z osobna, przy czym bajt czwarty nie jest używany. Co ważne, każdy blok w sektorze może mieć inne reguły dostępu;
- ostatnie sześć bajtów (10...15) przechowuje opcjonalny, drugi klucz B.
Za pomocą kluczy dostępu uzyskujemy prawa odczytu i/lub zapisu do poszczególnych bloków w sektorze, przy czym już teraz warto przyswoić informację, iż wartości klucza A nie da się odczytać, można go tylko zapisać. Skoro nie da się go odczytać (zwraca zawsze wartość 0x000000000000) to jak po raz pierwszy autoryzować odczyt sektora nie znając klucza? To proste, w świeżo zakupionych znacznikach RFID oraz NFC zarówno klucz A jak i klucz B mają wartość 0xFFFFFFFFFFFF, zaś bity dostępu wartość domyślną 0xFF0780, co oznacza następujące ustawienia autoryzacyjne:
- klucz A nie jest możliwy do odczytania (ale wiemy jaką ma fabrycznie ustawioną wartość),
- klucz B jest możliwy do odczytania i jest ustawiony jako 0xFFFFFFFFFFFF,
- klucz B nie jest używany do żadnych operacji na blokach pamięci,
- za pomocą klucza „A” możemy wykonywać dowolne operacje – zapisywać i odczytywać dane, ustawiać własne klucze oraz modyfikować bity dostępu.
Co ważne, klucze znajdują zastosowanie dla całego sektora (każdego bloku danych w ramach sektora), zaś bity dostępu dla każdego z bloków z osobna. Uprzedzam jednak, iż należy zachować szczególną ostrożność podczas ustawiania bitów dostępu, gdyż przez ustawienie niepoprawnych ich wartości możemy sobie zablokować jakikolwiek dostęp do całego sektora i to bezpowrotnie. Dla ułatwienia, w sieci znalazłem kilka kalkulatorów tychże bitów dostępu, ale najlepszy, z jakim się spotkałem znajduje się pod adresem: https://bit.ly/3rw8XET. Tyle po krótce o pamięci EEPROM kart Mifare, przejdźmy zatem do zagadnień implementacyjnych.
Program sterujący
Wybaczcie, ale nie będę w tym miejscu powielał informacji z artykułu o NFClock (EP 4/22) tylko przedstawię nowe, niezbędne z punktu widzenia autoryzacji, funkcje narzędziowe. Pierwsza z nich to funkcja autoryzująca dostęp danych do wybranego sektora karty, której to ciało pokazano na listingu 1.
//Tries to authenticate a block of memory on a MIFARE 1k card
//UID – pointer to a byte array containing the card UID (4 or 7 bytes) and size byte (UID[7])
//Key – pointer to a byte array containing the 6 byte key value
//blockNumber – the block number to authenticate (0..63 for 1KB cards)
uint8_t pn532AuthenticateBlock(uint8_t *UID, uint8_t *Key, uint8_t blockNumber){
uint8_t Result, UIDsize = UID[7];
//Prepare the authentication command
pn532PacketBuffer[0] =PN532_INDATAEXCHANGE; //Data Exchange Command
pn532PacketBuffer[1] = 1; //Max card numbers
pn532PacketBuffer[2] = PN532_AUTH_WITH_KEYA; //Key type A
pn532PacketBuffer[3] = blockNumber; //Authenticated block number (0..63 for 1KB cards)
memcpy(&pn532PacketBuffer[4], Key, 6); //Key, bytes 4...9
memcpy(&pn532PacketBuffer[10], UID, UIDsize); //UID, bytes 10...13 or 10...16
if((Result = pn532SendCommandCheckAck(pn532PacketBuffer, UIDsize == 4? 14: 17)) != NO_ERROR)
return Result;
//Read the response packet
pn532ReadData(pn532PacketBuffer, 12);
//Check if we are authenticated (Status byte == 0)
if(pn532PacketBuffer[7] != 0x00) return AUTHENTICATION_ERROR;
return NO_ERROR;
}
Funkcja jako argument przyjmuje numer seryjny autoryzowanej karty (UID, 4 lub 7 bajtów), który zawiera również pole mówiące o jego rozmiarze (UID[7]), Klucz dostępu A (Key) oraz numer bloku danych, do którego dostęp chcemy uzyskać (blockNumber). Funkcja zwraca 0 (NO_ERROR) w przypadku udanej autoryzacji, lub kod błędu.
//Tries to write an entire 16-byte data block at the specified block address.
uint8_t pn532WriteBlock(uint8_t *Data, uint8_t blockNumber){
uint8_t Result;
//Prepare write command
pn532PacketBuffer[0] =PN532_INDATAEXCHANGE; //Data Exchange Command
pn532PacketBuffer[1] = 1; //Max card numbers
pn532PacketBuffer[2] = PN532_MIFARE_WRITE; //Mifare write command
pn532PacketBuffer[3] = blockNumber; //Written block number (0..63 for 1KB cards)
memcpy(&pn532PacketBuffer[4], Data, 16); //Data
if((Result = pn532SendCommandCheckAck(pn532PacketBuffer, 20)) != NO_ERROR) return Result;
_delay_ms(10);
//Read the response packet
pn532ReadData(pn532PacketBuffer, 26);
return NO_ERROR;
}
Kolejną istotną funkcją jest funkcja pozwalająca na zapis autoryzowanego wcześniej bloku danych (w tym bloku Sector Trailer przechowującego klucze itd). Jej ciało pokazano na listingu 2. Funkcja ta jako argument przyjmuje wskaźnik na dane (16 bajtów) przeznaczone do zapisu oraz numer bloku danych, do którego zapis będziemy realizować. Funkcja zwraca 0 (NO_ERROR) w przypadku powodzenia, lub kod błędu. Na koniec funkcja pozwalająca na odczyt autoryzowanego wcześniej bloku danych (w tym bloku Sector Trailer przechowującego klucze itd). Jej ciało pokazano na listingu 3.
//Tries to read an entire 16-byte data block at the specified block address.
uint8_t pn532ReadBlock(uint8_t *Data, uint8_t blockNumber){
uint8_t Result;
//Prepare read command
pn532PacketBuffer[0] =PN532_INDATAEXCHANGE; //Data Exchange Command
pn532PacketBuffer[1] = 1; //Max card numbers
pn532PacketBuffer[2] = PN532_MIFARE_READ; //Mifare read command
pn532PacketBuffer[3] = blockNumber; //Read block number (0..63 for 1KB cards)
if((Result = pn532SendCommandCheckAck(pn532PacketBuffer, 4)) != NO_ERROR) return Result;
//Read the response packet
pn532ReadData(pn532PacketBuffer, 26);
//Check if there were no errors (Status byte == 0)
if(pn532PacketBuffer[7] != 0x00) return READ_BLOCK_ERROR;
//Copy the 16 data bytes to the output buffer
memcpy(Data, &pn532PacketBuffer[8], 16);
return NO_ERROR;
}
Funkcja ta jako argument przyjmuje wskaźnik na tablicę (16 bajtów), do której zapisane zostaną odczytane dane oraz numer bloku danych, z którego odczyt będziemy realizować. Funkcja zwraca 0 (NO_ERROR) w przypadku powodzenia, lub kod błędu. Na koniec przypomnę specyfikację potencjalnych błędów, których definicje znalazły się w pliku nagłówkowym, a które pokazano na listingu 4.
#define NO_ERROR 0x00
#define ACKNOWLEDGE_FRAME_ERROR 0x01
#define STATUS_TIMEOUT 0x02
#define FIRMWARE_HEADER_ERROR 0x03
#define RFCONFIGURATION_ERROR 0x04
#define SAMCONFIGURATION_ERROR 0x05
#define NO_TAGS_FOUND 0x06
#define AUTHENTICATION_ERROR 0x07
#define READ_BLOCK_ERROR 0x08
Korzystając z powyższych funkcji narzędziowych napisałem 3 inne, bardzo proste funkcje umożliwiające przeprowadzenie podstawowych operacji na karcie RFID/NFC a wyłączną ideą ich powstania była chęć uproszczenia i zwiększenia czytelności kodu źródłowego.
uint8_t cardReadUID(uint8_t *UID){
uint8_t Result;
Result = pn532ReadPassiveTargetID(UID);
return Result;
}
Pierwsza z nich pozwala na odczyt numeru seryjnego karty i pokazano ją na listingu 5, druga pozwala na przeprowadzenie procesu uwierzytelniania karty (listing 6) a trzecia na zapis klucza dostępu A na karcie (listing 7).
uint8_t cardAuthenticate(uint8_t *UID, uint8_t *Key){
uint8_t Result;
Result = pn532AuthenticateBlock(UID, Key, SECTOR1_TRAILER);
return Result;
}
uint8_t defaultKey[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
uint8_t defaultAccessBits[4] = {0xFF, 0x07, 0x80, 0x69};
uint8_t cardWriteKey(uint8_t *Key){
uint8_t Result;
uint8_t trailerBlock[16];
memcpy(trailerBlock, Key, 6); //Key A: bytes 0...5
memcpy(&trailerBlock[6], defaultAccessBits, 4); //Default access bits: bytes 6...9
memcpy(&trailerBlock[10], defaultKey, 6); //Default Key B: bytes 10...15
Result = pn532WriteBlock(trailerBlock, SECTOR1_TRAILER);
return Result;
}
void cardGenerateKey(uint8_t *Key){
for(uint8_t i=0; i<6; i++) Key[i] = rand() & 0xFF;
}
I na sam koniec funkcja pozwalająca na wygenerowanie pseudo-losowego klucza dostępu, której to ciało pokazano na listingu 8. Tyle na tą chwilę w kwestiach implementacyjnych, w związku z czym przejdźmy do schematu naszego urządzenia.
Budowa i działanie
Schemat ideowy urządzenia SAS2 pokazano na rysunku 3. Zbudowano dość prosty system mikroprocesorowy, którego „sercem” jest nowoczesny mikrokontroler ATmega328 taktowany wewnętrznym, wysokostabilnym generatorem RC o częstotliwości 8 MHz, który przy udziale sprzętowego interfejsu TWI (odpowiednik I2C) zapewnia obsługę pamięci EEPROM (U4) oraz obsługę układu MCP7940N-I/P (U5) realizującego funkcjonalność dokładnego zegara czasu rzeczywistego (RTC) wyposażonego w zintegrowany, automatyczny mechanizm podtrzymania zasilania (co zapewnia bateria CR1220).
Mikrokontroler jest odpowiedzialny również za obsługę interfejsu użytkownika zbudowanego z użyciem kilku przycisków typu microswitch, buzzera piezoelektrycznego i niedrogiego, graficznego wyświetlacza LCD COG o rozdzielczości 128×64 piksele, którego obsługa odbywa się z zastosowaniem programowej realizacji interfejsu SPI. Wybór tego, konkretnego typu mikrokontrolera nie był bynajmniej podyktowany niezbędną pojemnością pamięci Flash, gdyż program obsługi aplikacji napisany przy pomocy AVR-GCC zajmuje około 11 kB tej pamięci. Pokaźna część przypada na dwujęzykowe teksty interfejsu użytkownika zapisane w pamięci Flash oraz definicje czcionki ekranowej 5×7 pikseli. Wybór ten wynika wyłącznie z potrzeby dobrania mikrokontrolera o odpowiedniej liczbie wyprowadzeń niezbędnej z uwagi na rodzaj zastosowanego interfejsu użytkownika i liczbę obsługiwanych peryferiów. Nie mniej ważnym elementem przy wyborze zastosowanego typu mikrokontrolera był także wymóg wielkości wbudowanej pamięci EEPROM, która to w naszym wypadku została określona na poziomie 1024 bajtów. Można by, co prawda, zastosować dodatkową pamięć zewnętrzną lecz zastosowane rozwiązanie wydawało mi się optymalne, jeśli chodzi o możliwości urządzenia i cenę.
W implementacji programu obsługi aplikacji nie mogło również zabraknąć miejsca na realizację obsługi modułu RFID/NFC pod postacią peryferium PN532, która to odbywa się z zastosowaniem sprzętowego interfejsu SPI mikrokontrolera. Uważny czytelnik zauważy zapewne, iż zarówno wyświetlacz COG, jak i moduł RFID wymagają interfejsu SPI, zatem dlaczego nie zastosowano do ich obsługi jednego interfejsu sprzętowego mikrokontrolera? To dość proste. Po pierwsze oba peryferia mają inną prędkość, jak i kolejność bitów transmisji (LSB czy MSB), w związku z czym nie chciałem co chwilę zmieniać ustawień sprzętu a po drugie, i dość prozaiczne, łatwiej było mi zaprojektować płytkę drukowaną realizując ww. obsługę za pomocą różnych wyprowadzeń mikrokontrolera. Inną sprawą jest, że dobrze było rozdzielić magistralę krytyczną (PN532) od tej mniej wymagającej (LCD COG). Tyle w kwestiach budowy urządzenia SAS2.
Robert Wołgajew
robert.wolgajew@ep.com.pl
- R1: 47 kΩ
- R2, R5: 1 kΩ
- R3, R4: 2,2 kΩ
- R6: 22 Ω
- C1, C2, C5…C7: ceramiczny MLCC 100 nF (raster 0,1”)
- C3, C4: elektrolityczny 100 μF/16 V (raster 0,1”)
- C8, C9: ceramiczny MLCC 12 pF (raster 0,1”)
- C10: ceramiczny MLCC 100 pF (raster 0,1”)
- C11…C19: ceramiczny MLCC 1 μF (raster 0,2”)
- D1: 1N4148 (DO-34-7)
- D2: BAT85 (DO-34-7)
- T1: BC548 (TO-92)
- U1: LD1117V33 (TO-220)
- U2: ATMEGA328 (DIL28)
- U3: moduł RFID/NFC typu PN532
- U4: 24LC64F-I/P (DIL8)
- U5: MCP7940N-I/P (DIL8)
- LCD: wyświetlacz graficzny graficzny LCD-AG-C128064CF-DIW
- K1: przekaźnik G5LE-14-9
- Q1: rezonator kwarcowy zegarkowy 32768 Hz
- BUZZ: buzzer piezoelektryczny z generatorem 5 V
- PREV, NEXT, UP, DOWN, OK, MENU: microswitch TACT z ośką 16 mm do montażu przewlekanego
- ADMIN: microswitch TACT z ośką 1 mm do montażu przewlekanego
- PWR, LOCK: złącze śrubowe AK500/2 (raster 0,1”)
- BATT: gniazdo baterii CR1220 THT typu DS1092-15-B6P-C CONNFLY
- VCC: bateria CR1220