В этом проекте я поэтапно расскажу о создании умного сейфа с одноразовым паролем на основе времени (TOTP). Если коротко, то TOTP — это метод генерации 6-циферного пароля на основе текущей даты и времени с использованием предопределённого ключа. То есть, пока сейф будет иметь возможность отслеживать время, я смогу использовать приложение Authenticator для получения нового пароля каждые 30 секунд.

Для более полноценного знакомства с этой технологией рекомендую статью What is a Time-based One-time Password (TOTP)? | Twilio.

Дополнение: я решил пойти ещё дальше и добавил сейфу сканер отпечатка пальца, чему посвятил очередную статью: Сейф с доступом по отпечатку пальца.

Оборудование


  • D1 Mini (ESP8266);
  • модуль реле;
  • пьезодинамик;
  • источник питания (5В);
  • макетная плата.

Шаг 1: предыстория



Дополнительные фото






Началось с того, что я нашёл этот старый сейф, отказавший из-за потёкшей батареи. Для возвращения к жизни ему требовалась лишь небольшая чистка и перепайка разъёмов. Внутри сейфа есть кнопка, которая позволяет делать сброс кода, так что в целом он был работоспособен.

Собравшись поставить его где-нибудь для реального использования, я стал думать, какой бы код в нём установить, чтобы и не забыть его через несколько лет, и чтобы он не был одним из «стандартных», который бы ухитрились подобрать мои детишки. Это натолкнуло меня на мысль об использовании одноразовых паролей через приложение Authenticator (TOTP).

Шаг 2: как устроен сейф



Дополнительные фото






В основе запирания этого сейфа, как и большинства других, лежит выдвижная ригельная пластина. Когда она выдвигается (сейф запирается), пружина выталкивает шток соленоида, который не даёт задвинуть пластину обратно.

Для отпирания можно либо воспользоваться ключом (в середине, на фото видно плохо), либо заставить соленоид вернуться в обратное положение. Для этого, естественно, нужно подать на него ток, за что отвечает плата контроллера. При вводе правильного кода соленоид срабатывает, и замок отпирается.

Вводится код с кнопочной панели на лицевой стороне дверцы. С контроллером эта панель соединяется зеленоватым шлейфом.

Шаг 3: план


Я планирую заменить контроллер на плату D1 Mini, которая будет управлять реле. При вводе верного пароля реле будет на несколько секунд замыкать цепь между источником питания и соленоидом, открывая сейф. Мне также потребуется подключить к D1 Mini оригинальную кнопочную панель и пьезодинамик для звуковой обратной связи.

D1 Mini нужно будет подключить к WiFi, чтобы иметь возможность синхронизировать часы, что необходимо для работы TOTP. Ещё потребуется написать немного простого кода, который будет получать 6-циферный ввод с кнопочной панели и сравнивать его с текущим TOTP. В случае совпадения комбинаций он будет замыкать реле на несколько секунд, отпирая сейф.
На фото показана готовая конфигурация.

  1. Я использовал модуль, но можно взять и голое реле, внеся необходимые корректировки в плату.
  2. Я также задействовал имевшиеся под рукой разъёмы для питания, соленоида и кнопочной панели, которую снял с оригинальной платы.
  3. Питание осуществляет БП на 5В и 2А, но для данного соленоида это, скорее всего, перебор. Вполне сойдёт и что-то попроще, что можно разместить вне ящика. Только не рассчитывайте, что D1 Mini будет управлять соленоидом напрямую.

Шаг 4: декодирование кнопочной панели




Самое сложное – это разобраться в работе кнопочной панели.

Как правило, в панелях 3х4 используется 7 проводов, по одному для каждого ряда и каждого столбца. Ряды подтягиваются к верхнему уровню, а столбцы к земле – при нажатии клавиши цепь замыкается, и контроллер это нажатие регистрирует. Вот неплохая статья, где процесс объясняется более подробно: How to Set Up a Keypad on an Arduino — Circuit Basics.

В итоге мне методом проб и ошибок пришлось выяснять порядок проводов в шлейфе и связывать каждый с правильной строкой/столбцом. Каждую пару я проверял вольтметром в режиме прозвонки, нажимая разные клавиши, пока прибор не регистрировал замыкание цепи.

Вообще, я ожидал, что провода будут идти по порядку (то есть с 1 по 4 для рядов и с 5 по 7 для столбцов), но на деле сочетание оказалось таким:

Провод 1 - столбец 1
Провод 2 - ряд 1
Провод 3 - ряд 2
Провод 4 - столбец 2
Провод 5 - ряд 3
Провод 6 - столбец 3
Провод 7 - ряд 4

Каждый провод подключается к своему контакту на D1 Mini. Ниже показано, как сопоставил их я, но вы, скорее всего, сделаете по-своему. На этот фрагмент потребуется сослаться в коде позже.

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

Обратите внимание, что я использовал TX и RX как обычные контакты ввода-вывода, поскольку изначально хотел также подключить светодиоды (а у D1 Mini контактов не хватает), что, очевидно, лишает нас возможности использовать Serial для логирования и отладки.

Для этого потребуется добавить в Setup() следующие строчки:

void setup() {
  pinMode(TX, FUNCTION_3);
  pinMode(RX, FUNCTION_3);
}

В результате приведённый ниже код будет управлять кнопочной панелью, получать пользовательский ввод, сравнивать его с секретом (реализуем позже) и в случае совпадения открывать сейф. В противном случае и при 5 секундах простоя буфер ввода будет сбрасываться.

#include <Keypad.h>

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;

Keypad customKeypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS);

void setup() {
 pinMode(TX, FUNCTION_3);
 pinMode(RX, FUNCTION_3);
}

void loop() {
 char customKey = customKeypad.getKey();
 if (customKey) {
  code[codeIndex] = customKey;
  codeIndex = codeIndex + 1;

  if (codeIndex == 6) {
   if (strcmp(code, $secret$) == 0) {
    // openSafe();
   } else {
    // incorrectPin();
   }
   codeIndex = 0;
   memset(code, 0, 8);
  }
 }

 if (codeIndex != 0 && (millis() - lastClickMillis > 5000)) {
  // incorrectPin();
  codeIndex = 0;  
  memset(code, 0, 8);
 }
}

Шаг 5: подключение оборудования



Дополнительные фото




Я подключил всё на макетной плате, в которой специально просверлил отверстия для соответствия с оригинальной, что в итоге позволило использовать те же крепёжные винты. Я немного ошибся с измерениями, и в результате получилось чуть криво.

Гребёнки D1 Mini я разместил по центру, получив достаточно места для подключения к различным контактам остальных проводов.

В правом верхнем углу я установил стандартный винтовой зажим (POW) для подачи питания. Он подключается напрямую к ± БП.

Под ним я через такой же разъём, что и на исходной плате, подключил соленоид (SOL). Минус к нему идёт от верхнего разъёма питания (POW), а плюс также от верхнего разъёма, но сначала к ON-контакту реле и уже оттуда возвращается к плюсовому контакту самого соленоида.

Модуль реле подключается к 5В и GND, а его сигнальный контакт к выводу D0 на плате D1 Mini.
Пьезодинамик подключён к GND и контакту D7 на D1 Mini.

Наконец, 7 контактов кнопочной панели подключены к D5, TX, RX и D1-D4.

Шаг 6: код сейфа


На третьем шаге мы написали код для получения ввода с кнопочной панели и генерации из него 6-циферного пароля. Теперь нужно сравнить его с текущим паролем, вычисленным на основе настоящего времени и начального секретного ключа. Для этого я использовал две библиотеки.

ezTime (ropg/ezTime: ezTime (github.com)) является простейшим (из известных мне) решением для синхронизации часов микроконтроллера. Как я уже говорил, это необходимо для вычисления одноразового пароля. Вторая библиотека – это простая реализация TOTP (lucadentella/TOTP-Arduino (github.com)).

Из соображений практичности приведённый код был сокращён.

#include <TOTP.h>
#include <ezTime.h>

uint8_t hmacKey[] = {0x4D, 0x79, 0x20, 0x73, 0x61, 0x66, 0x65};
TOTP totp = TOTP(hmacKey, 7);

void setup() {
 waitForSync();
}

void loop() {
 char customKey = customKeypad.getKey();
 if (customKey) {
  code[codeIndex] = customKey;
  codeIndex = codeIndex + 1;

  if (codeIndex == 6) {
   char* tot = totp.getCode(UTC.now());
   if (strcmp(code, tot) == 0) {
    // openSafe();
   } else {
    // incorrectPin();
   }
   codeIndex = 0;
   memset(code, 0, 8);
  }
 }
}

Здесь определяется ключ для TOTP. Сам этот ключ является строкой My safe в hex-формате. Для подобных преобразований можно использовать любой онлайн-конвертер текста в hex, например: Text to Hex Converter — Online Toolz (online-toolz.com).

uint8_t hmacKey[] = {0x4D, 0x79, 0x20, 0x73, 0x61, 0x66, 0x65};

Имея этот ключ, реализация TOTP сможет вычислять новый 6-циферный код каждые 30 секунд.

Далее нужно создать запись в выбранном приложении Authenticator. Я использую Microsoft Authenticator, но он полностью аналогичен аутентификатору от Google. Любому из этих приложений потребуется представление ключа в формате Base32 (в моём случае это ключ My safe). Для этого можно использовать любой онлайн Base32-энкодер, например, Base32 Encode Online (emn178.github.io).

Строка в формате base32 (в моём случае My safe -> «JV4SA43BMZSQ») выступает ключом для ввода в Authenticator. Можно ввести её в приложении вручную либо использовать QR-код. Вот простой онлайн-инструмент для создания QR-кодов, понятных приложениям Authenticator: Generate QR Codes for Google Authenticator (hersam.com).

В качестве альтернативы чисто для тестирования также сгодится TOTP Generator (danhersam.com). Можете вручную ввести в нём ключ (также строку в формате base32), на что он предоставит вам 6-циферный одноразовый пароль, который сейф будет готов принять в течение 30 секунд.

Полный код доступен в прикреплённом файле, а также на GitHub.

Шаг 7: будьте осторожны


Очевидно, что этот сейф не такой уж “safe”. Помимо риска взлома самого сейфа, здесь присутствуют и другие потенциальные точки сбоя. Если Di Mini умрёт, то вы вообще не сможете с ним взаимодействовать. Кроме того, в случае утраты связи с интернетом программе не удастся синхронизировать время, и код окажется неверен. Так что будьте осторожны.

Спасибо за внимание! Будет здорово, если вы поделитесь своими мыслями в комментариях и расскажете о собственных подобных проектах.

Комментарии (11)


  1. Anthrax_Beta
    25.03.2022 14:12
    +1

    Тут так же как и с обычными OTP, нужны коды, что бы можно было открыть в случае если нет связи с интернетом или сбилось время.


    1. YouHim
      25.03.2022 16:24

      Какой тогда смысл городить этот огород, если можно открыть просто кодом? Плюс еще и ключом.


      1. Iv38
        25.03.2022 18:38

        Коды не простые, а тоже одноразовые, только не привязанные ко времени. Их можно хранить в надёжном месте с неудобным доступом - это аварийный вариант. Но тут да, уже есть ключ, который выполняет эту функцию.


  1. mmMike
    25.03.2022 14:13
    +3

    Соленоид штатно стоял? Странно. Обычно в подобных сейфах ставят примитивный линейный привод (моторчик постоянного тока и винт). Который характерно жужжит, когда вдвигает/выдвигает блокирующий шток.

    В этом случае достаточно батарейного питания (пара AA батареек), которых хватает на годы. Поскольку энергия тратится только на открывание/закрывание.

    А в качестве управляющей схемы что то с током покоя в микроамперы и пробуждением от нажатия на кнопки клавиатуры.

    Как то странно смотрится такое питание как на фото.

    Ну и уже если приспичило TOTP, то можно модуль RTC типа DS3231 использовать. У него уход не большой.

    А лучше HOTP. Чай не Форт Нокс.


  1. CAJAX
    25.03.2022 19:04

    Кроме того, в случае утраты связи с интернетом программе не удастся синхронизировать время, и код окажется неверен. Так что будьте осторожны.

    Если обесточить, часы сбросятся на ноль, так что нет. Собственно без RTC тут TOTP и не нужен


    1. CAJAX
      25.03.2022 19:25
      +2

      А. Сейф без интернета вообще не работает.


      1. Astroscope
        26.03.2022 10:30

        Есть предложение дооснастить сейф приемником GPS для автономного получения точного времени. :)


  1. NicolyaLS
    26.03.2022 00:44
    +1

    Когда увидел соленоид в качестве запирающего элемента ригельной пластины, вспомнил видео про «неодимовый магнит, носок и открытие сейфа за 3 секунды» в ролике на 3 минуты


    1. schetilin
      27.03.2022 14:20

      Соленоид не запирает, а отпирает :) Для закрытия — пружина.


    1. shaposhnikow
      28.03.2022 09:41

      С этим "сейфом" (по факту стальной коробочкой) есть один нюанс:

      он открывается голыми руками, правда нужно немного потренироваться.

      Суть способа: синхронно наносится удар снизу и поворот рукоятки. В момент удара подпружиненный сердечник соленоида кратковременно разблокирует механизм.


  1. klirichek
    26.03.2022 08:48

    Если RTC взять с батарейкой (ну, как раньше на материнках стояли. И служили годами), то можно основную часть схемы держать выключенной (от банального физического выключателя снаружи, до глубокого сна с пробуждением по нажатию на любую кнопку; при этом саму кнопку можно сразу же распознать как первую цифру кода). Тогда не нужно внешнее питание; обычных батареек как в оригинальной схеме хватит за глаза.

    Ну и RTC - при стабильной комнатной температуре вполне норм; с гэпом 30 секунд вполне обеспечат работу на всё время жизни батареи.