Autor projektu otrzymał stary sejf, którego elektronika uległa uszkodzeniu na skutek uszkodzenia baterii. Po wyczyszczeniu rozlanego elektrolitu sejf ponownie działał. Wewnątrz znajduje się przycisk do resetowania kodu PIN. Jak opisuje autor, pomysł wykonania własnego projektu powstał właśnie w trakcie testowania urządzenia. „Kiedy miałem umieścić sejf gdzieś do praktycznego użytku, zacząłem się zastanawiać, jaki byłby dobry kod PIN do użycia, taki, który będę pamiętał przez lata i który nie jest jednym z typowych kodów PIN, których używamy w domu, żeby moje dzieci nie były w stanie go rozgryźć. To sprawiło, że pomyślałem o użyciu haseł jednorazowych, bazujących na algorytmie uwierzytelniającym” pisze autor.
W poniższym artykule przyjrzymy się, w jaki sposób autorowi udało zintegrować mikrokontroler, który można oprogramować w środowisku Arduino, z elektroniką sejfu, a także algorytmowi uwierzytelniania, który używa jednorazowych haseł, generowanych na podstawie czasu systemowego.
Zasada działania
System używa standardu TOTP (Time-based One-Time Passwords), czyli haseł jednorazowych generowanych na podstawie aktualnego czasu. Jest to dosyć szeroko rozpowszechniony mechanizm generowania jednorazowych haseł, stosowany często w systemach uwierzytelniania dwuskładnikowego.
Mówiąc w dużym skrócie – jest to metoda obliczania 6-cyfrowego kodu dostępu z wcześniej określonym kluczem na podstawie bieżącej daty i godziny. Oznacza to, że tak długo, jak sejf może śledzić aktualny czas, będzie można stosować specjalną aplikację, która co 30 sekund generuje nowe hasło pozwalające na otworzenie sejfu.
Algorytm TOTP jest zgodny z otwartym standardem udokumentowanym w RFC 6238. Dane wejściowe obejmują wspólny tajny klucz i czas systemowy. Diagram na rysunku 1 pokazuje, w jaki sposób obie strony mogą osobno obliczyć hasło bez połączenia z Internetem.
Algorytm korzysta z pewnej formy kryptografii klucza symetrycznego – ten sam klucz jest używany przez obie strony do generowania i walidacji tokena. TOTP działa w trybie offline, co oznacza, że jedyne dane wejściowe do algorytmu TOTP to czas systemowy i przechowywany tajny klucz. Ani dane wejściowe, ani obliczenia nie wymagają połączenia z Internetem w celu wygenerowania lub zweryfikowania hasła jednorazowego. Dlatego użytkownik może uzyskać dostęp do TOTP za pośrednictwem aplikacji w trybie offline.
To doskonałe zastosowanie do aplikacji bazującej na mikrokontrolerze, który nie musi mieć połączenia z siecią. Dodatkowo zastosowanie TOTP jest idealne dla użytkowników, którzy mogą potrzebować dostępu do swojego systemu uwierzytelnienia podczas podróży za granicę, samolotem, na odległym obszarze lub w innym miejscu, gdzie połączenie z Internetem nie może być w żaden sposób zrealizowane.
Mikrokontroler, na którym zaimplementowano opisany powyżej algorytm, zastępuje płytkę sterującą, która oryginalnie znajdowała się w sejfie. Za pomocą przekaźnika mikrokontroler steruje elektromagnesem, który otwiera i zamyka sejf po wpisaniu prawidłowego pinu.
Jako wejście, mikrokontroler wykorzystuje oryginalną klawiaturę, dzięki czemu nie było potrzeby wprowadzania zmian w budowie sejfu. Mikrokontroler musi być podłączony do Wi-Fi, aby synchronizować zegar. Jest to wymagane, aby TOTP działał. Prosty kod przyjmuje dane wejściowe z klawiatury i po 6 cyfrach porównuje go z aktualnym TOTP. Jeśli kody są takie same, to układ zamknie przekaźnik na kilka sekund, aby umożliwić otwarcie sejfu.
System uzupełnia zasilacz 5 V, 2 A. Tak duży prąd nie jest wymagany i wynika z wykorzystania modułu, który był akurat pod ręką. Dokładne wymagania co do prądu wejściowego zasilacza zależne są od konkretnego sejfu i zapotrzebowania solenoidu na prąd, gdyż to ten element jest głównym obciążeniem w układzie. Mikrokontroler, mimo zastosowania Wi-Fi, ma istotnie mniejsze zapotrzebowanie na prąd.
Potrzebne elementy
Do zestawienia prezentowanego urządzenia potrzebne będą następujące komponenty:
- moduł D1 Mini z mikrokontrolerem ESP8266,
- moduł z przekaźnikami,
- buzzer piezoelektryczny,
- zasilacz 5 V,
- płytka prototypowa.
Dodatkowo urządzenie korzysta z gotowej klawiatury, która jest częścią oryginalnego systemu zabezpieczenia sejfu. Najtrudniejszą częścią było zrozumienie, jak działa klawiatura. Aby połączyć ją z nowym systemem, wymagane były działania z zakresu inżynierii wstecznej. Klawiatura sejfu to klasyczna klawiatura macierzowa w formacie 3×4. Oznacza to, że do układu podłączone jest siedem przewodów, po jednym na każdy wiersz i na każdą kolumnę. Wiersze są podciągane do zasilania, kolumny łączone z masą. Po naciśnięciu klawisza zamyka się obwód i w ten sposób kontroler może dowiedzieć się, który klawisz został naciśnięty. Konieczne było ustalenie kolejności przewodów na taśmie wychodzącej z klawiatury i powiązanie każdego z odpowiednim wierszem i kolumną. Wymagało to przeprowadzenia kilku prób. Autor zastosował multimetr w trybie pomiaru diody, aby sprawdzić każdą parę wyprowadzeń (fotografia 1).
Naciskając różne klawisze, znalazł zwartą parę dla danego przycisku. Pomiar powtarzany był aż do zidentyfikowania wszystkich kolumn i wierszy. Można było się spodziewać, że wiersze i kolumny będą poukładane w taśmie po kolei. Okazało się, że jest zupełnie inaczej i kolejność poszczególnych linii jest następująca:
- przewód 1 – kolumna 1,
- przewód 2 – rząd 1,
- przewód 3 – rząd 2,
- przewód 4 – kolumna 2,
- przewód 5 – rząd 3,
- przewód 6 – kolumna 3,
- przewód 7 – rząd 4.
Każdy przewód z klawiatury łączy się z innym wyprowadzeniem płytki D1 Mini. Autor zastosował mapowanie pinów tak, jak pokazano na fragmencie kodu z listingu 1. Oczywiście, budując ten system, można zastosować zupełnie inne piny, pamiętając jedynie, aby wprowadzić odpowiednie zmiany w programie.
const byte ROWS = 4;
const byte COLS = 3;
char hexaKeys[ROWS][COLS] = {
{‘1’, ‘2’, ‘3’},
{‘4’, ‘5’, ‘6’},
{‘7’, ‘8’, ‘9’},
{‘A’, ‘0’, ‘B’}
};
byte rowPins[ROWS] = {TX, RX, D2, D4};
byte colPins[COLS] = {D5, D1, D3};
Warto zwrócić uwagę, że linie TX i RX zastosowane zostały jako normalne linie GPIO. Autor podjął taką decyzję, ponieważ do modułu D1 chciał podłączyć także kilka diod LED do sygnalizacji, a nie byłoby to możliwe przy użyciu tylko i wyłącznie samych pinów GPIO. Oznacza to jednak, że nie można zastosować interfejsu szeregowego do komunikacji z komputerem czy debugowania urządzenia. Aby możliwe było użycie linii RX i TX, jako normalnych linii GPIO, należy w bloku Setup() wpisać następujące linie pokazane na listingu 2.
void setup() {
pinMode(TX, FUNCTION_3);
pinMode(RX, FUNCTION_3);
...
}
Kod odpowiedzialny za kontrolę klawiatury i przyjmowanie danych od użytkownika oraz porównywanie ich z sekretnym kluczem (który zostanie omówiony w dalszej części) pokazuje listing 3. Jeśli wprowadzony ciąg jest równy spodziewanemu, to sejf zostanie otwarty, w przeciwnym razie (a także po 5 sekundach bezczynności) blokada ponownie się zamyka i zerowany jest bufor wejściowy.
void loop() {
//...
char customKey = customKeypad.getKey();
if (customKey) {
code[codeIndex] = customKey;
codeIndex = codeIndex + 1;
if (codeIndex == 6) {
if (strcmp(code, $secret$) == 0) {
// Tutaj otwieramy sejf
} else {
// Tutaj informujemy o złym PINie
}
codeIndex = 0;
memset(code, 0, 8);
}
}
if (codeIndex != 0 && (millis() – lastClickMillis > 5000)) {
// Tutaj informujemy o złym PINie
codeIndex = 0;
memset(code, 0, 8);
}
}
Podłączenie elektroniki do sejfu
Wszystkie elementy układu połączone są na płytce uniwersalnej, zgodnie ze schematem pokazanym na rysunku 2. Może być konieczne wywiercenie w płytce uniwersalnej kilku dodatkowych otworów, aby umieścić wszystkie zastosowane moduły. Na środku płytki uniwersalnej umieszczono złącza dla modułu D1 Mini. Dzięki temu pozostaje dużo miejsca na podłączenie przewodów do wszystkich pinów.
W prawym górnym narożniku płytki umieszczono złącze śrubowe do podłączenia zasilacza. Elektromagnes podłączono za pomocą oryginalnego złącza tego elementu. Masa solenoidu podłączona jest bezpośrednio do masy zasilacza, a dodatnie wyprowadzenie podłączono do zasilacza poprzez normalnie otwarte wyprowadzenie przekaźnika, aby możliwe było łatwe sterowanie. Do modułu przekaźników podłączono także zasilanie strony pierwotnej (5 V oraz masę GND). Linia sterująca przekaźnikiem podłączona jest do pinu D0 w module z mikrokontrolerem. Buzzer podłączony jest do masy i do pinu D7 modułu D1 Mini. Finalnie, do mikrokontrolera podłączono siedem linii klawiatury, do pinów TX, RX i D1...D5, zgodnie z wcześniejszym opisem. Na fotografii 2 pokazano gotowy, zmontowany układ zamontowany w sejfie.
Oprogramowanie
Część kodu została już opisana. Kluczowym elementem oprogramowania jest biblioteka TOTP.h, która ma zaimplementowany algorytm generowania tymczasowego jednorazowego kodu PIN. Druga istotna biblioteka, jaką zastosowano, to ezTime.h, która pozwala na wygodne użycie zegara czasu rzeczywistego i jego synchronizację z siecią. Obie biblioteki są dołączanie w pierwszych liniach kodu.
uint8_t hmacKey[] = {
0x4D, 0x79, 0x20, 0x73, 0x61, 0x66, 0x65
};
TOTP totp = TOTP(
hmacKey,
7 // Długość klucza
);
Następnie musimy skonfigurować klucz TOTP – słowo, na podstawie którego generowany jest kod PIN. Podane w zmiennej hmacKey znaki to kody ASCII naszego klucza – listing 4.
TOTP totp = TOTP(hmacKey, 7);
Na tej podstawie generowany jest nowy jednorazowy PIN poprzez obiekt totp co 30 sekund.
Następnie system łączy się z siecią Wi-Fi i synchronizuje czas systemowy z aktualnym czasem, pobranym z sieci. Służy temu funkcja waitForSync(), która znajduje się w bloku Setup() kodu. Aby program działał poprawnie, musimy wcześniej podać także SSID i hasło do sieci bezprzewodowej, w której ma pracować urządzenie.
Kod całego programu został pokazany na listingu 5. W głównej pętli znajduje się fragment, który po wprowadzeniu sześciu znaków z klawiatury (tj. gdy licznik codeIndex osiągnie wartość 6) generuje jednorazowy PIN na podstawie aktualnego czasu – totp.getCode(UTC.now() i porównuje go z wprowadzoną wartością. Jeżeli są takie same, to uruchamia funkcję openSafe(), a jeżeli nie, to funkcję incorrectPin(). Definicje tych funkcji zawierają uruchomienie elektromagnesu sterwanego przekaźnikiem, w momencie, gdy podano poprawny PIN lub załączenie buzzera na określony czas, gdy PIN jest niepoprawny.
#include <ESP8266WiFi.h>
#include <Keypad.h>
#include <TOTP.h>
#include <ezTime.h>
// Konfiguracja Wi-Fi
const char* ssid = "<SSID>";
const char* password = "<PASSWORD>";
WiFiClient WiFiclient;
// Konfiguracja linii GPIO
#define relayLockPin D0
#define buzzer D7
// Konfifguracja klawiatury
const byte ROWS = 4;
const byte COLS = 3;
char hexaKeys[ROWS][COLS] = {
{‘1’, ‘2’, ‘3’},
{‘4’, ‘5’, ‘6’},
{‘7’, ‘8’, ‘9’},
{‘A’, ‘0’, ‘B’}
};
byte rowPins[ROWS] = {TX, RX, D2, D4};
byte colPins[COLS] = {D5, D1, D3};
unsigned long lastClickMillis = 0;
char code[6];
int codeIndex = 0;
uint8_t hmacKey[] = {
// Ten kod tłumaczy się na "My safe"
// Można zastąpić go dowolnym ciągiem znaków – kodów ASCII
0x4D, 0x79, 0x20, 0x73, 0x61, 0x66, 0x65
};
TOTP totp = TOTP(
hmacKey,
7 // Długość klucza
);
Keypad customKeypad =
Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS);
void setup() {
pinMode(TX, FUNCTION_3);
pinMode(RX, FUNCTION_3);
connectWifi(ssid, password);
pinMode(relayLockPin, OUTPUT);
digitalWrite(relayLockPin, HIGH);
pinMode(buzzer, OUTPUT);
digitalWrite(buzzer, LOW);
waitForSync();
}
void connectWifi(const char* ssid, const char* password) {
WiFi.mode(WIFI_STA);
WiFi.hostname("mysafe");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
void loop() {
events(); // Do okazjonalnej aktualizacji czasu
char customKey = customKeypad.getKey();
if (customKey) {
lastClickMillis = millis();
tone(buzzer, 2500, 20);
code[codeIndex] = customKey;
codeIndex = codeIndex + 1;
if (codeIndex == 6) {
char* tot = totp.getCode(UTC.now());
if (strcmp(code, tot) == 0) {
openSafe();
} else {
delay(100);
tone(buzzer, 4500, 150);
delay(200);
tone(buzzer, 4500, 150);
delay(200);
tone(buzzer, 4500, 300);
}
codeIndex = 0;
memset(code, 0, 8);
}
}
// Jeśli nie wpisano całego PIN
// po 5 sekundach układ resetuje wpisywanie go
if (codeIndex != 0 && (millis() – lastClickMillis > 5000)) {
codeIndex = 0;
tone(buzzer, 4500, 120);
}
}
void openSafe() { // Otwarcie sejfu
tone(buzzer, 3000, 1500);
digitalWrite(relayLockPin, LOW);
delay(5000);
digitalWrite(relayLockPin, HIGH);
}
Uwagi końcowe
Ten ciekawy projekt prezentuje alternatywną możliwość zabezpieczenia dostępu do sejfu, jednak analogiczne rozwiązanie można zastosować również np. do zamykania drzwi. Projekt jest tylko prototypem i jest w nim jeszcze wiele miejsca na poprawki i udoskonalenia.
Nie trzeba chyba dodawać, że ten sejf nie jest zbyt bezpieczny. Istnieje wiele potencjalnych punktów awarii, poza samym sejfem. Jeśli mikrokontroler D1 Mini zawiesi się lub uszkodzi, w ogóle nie będzie możliwości otwarcia sejfu. Ponadto, jeśli utraci on połączenie z Internetem, nie będzie w stanie zsynchronizować swojego zegara, a wtedy hasło będzie nieprawidłowo generowane. Podsumowując – należy być ostrożnym, implementując taki system w jakimkolwiek zastosowaniu.
Nikodem Czechowski, EP