Делаем аппаратный менеджер паролей своими руками

Portret

Опытный
Регистрация
2/1/17
Сообщения
2.327
Репутация
8.135
Реакции
11.449
RUB
0
Сделок через гаранта
3
Где хранить свои суперсекьюрные и регулярно обновляемые пароли от множества интернет-ресурсов? В голове? Скорее всего, не поместятся. На бумажке? Не по-хакерски. Доверять облачному сервису, который спалит пароли если не сегодня, то завтра обязательно? Понадеяться на опенсорсный менеджер паролей? Вяло, товарищи! Скучно!

Сегодня мы рассмотрим настоящий тру-хакерский, тру-гиковский, удобный и безопасный способ хранить пароли, создав свой аппаратный менеджер паролей! Он будет хранить в себе данные разных аккаунтов, логины и зашифрованные пароли, ключ от которых должен держаться отдельно и вводиться непосредственно перед использованием, и при этом устройство будет эмулировать USB-клавиатуру для вывода логина и пароля.



Нет дыма без огня, или немного предыстории
Идея родилась не на пустом месте. Незадолго до ее появления я решил заняться программированием микроконтроллеров. Но так как свободного времени, чтобы серьезно и глубоко посвятить себя этому, было недостаточно для полноценного освоения ни железного С++, ни железобетонного ассемблера, то, чуть не споткнувшись о продолжающую набирать популярность вселенную Arduino, я прямиком угодил в объятия загадочного мира «JavaScript для микроконтроллеров». «Теперь гаджеты программируют на JavaScript» — этот броский заголовок поймал меня на крючок! Разглядывая на сайте «Амперки» изображения красивой отладочной платы, похожей на Arduino Leonardo, но белой и именуемой Iskra JS (не путать с Iskra Neo, которая тоже Iskra, тоже белая, но по сути улучшенная Leonardo), я попал под гипноз описания ее возможностей (и, если что, нахожусь под ним до сих пор).
1.jpg
Отладочная плата Iskra JSСердце платы Iskra JS — прекрасный дуэт микроконтроллера серии STM32F4 и прячущейся в его недрах могучей open source прошивки Espruino, выполняющей функцию интерпретатора языка JavaScript с торчащей наружу консолью а-ля REPL. Те, кто знаком с Node.js, почувствуют ярко выраженное дежавю и смогут вести себя более уверенно в диалоге с Espruino. При всем при этом для Iskra JS подходит весь спектр аксессуаров от Arduino UNO R3. Да и дополнительные библиотеки, представляющие собой JS-модули, имелись в достаточном количестве как от создателей проекта Espruino, так и от разработчиков Iskra JS.





«Зачем все это», или не купить ли нам коммерческий токен
Можно посмотреть и в сторону коммерческих токенов. Но тут скрывается пласт нюансов с дополнительным ПО и универсальностью. Да и за действительно интересные устройства придется выложить немаленькую сумму.

Итак, недолго раздумывая и сразу обзаведясь целым набором «Йодо», где, помимо платы Iskra JS и буклета, были шилды, модули с сенсорами и прочими кнопочками, а также детали необычного конструктора для макетирования корпусов, именуемого структором, я всецело погряз в творчестве.

И вот тогда, наткнувшись в буклете на один из проектов с примером использования эмуляции клавиатуры, я и загорелся идеей сделать «ленивку», набирающую за меня пароли.

Идея зрела долго, ее каждый раз подрезали всякие умные роботы и дома, GSM-сигнализации и прочие радости творчества. Ведь программирование для Iskra JS приносило массу удовольствия, так как не было обременено посредническими процессами — ни предварительным компилированием, ни обязательной прошивкой платы

Процесс прошивки в Espruino-based платах требует некоторых разъяснений. Прошивка в микроконтроллере одна — и это Espruino. Она прошивается единожды и занимает часть флеш-памяти микроконтроллера. В дальнейшем для сохранения вашего JavaScript-кода используется оставшееся место во флеш-памяти. И вот очистку части флеш-памяти от старого кода и запись нового нередко также называют прошивкой, хотя правильнее все же называть это сохранением кода.




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

  • Для хранения логинов и паролей была выбрана карта microSD. Для работы с ней, соответственно, необходим модуль для чтения карт.
  • В роли управления решил задействовать ИК-пульт и модуль с ИК-приемником, которые достались с набором, так как кнопок пульта заведомо было достаточно для возможного будущего расширения, в то время как с размещением новых физических кнопок могли бы возникнуть проблемы, да и дистанционность имеет свои плюсы в использовании.
Далее необходимо было определиться, на чем хранить ключ AES-256 и на чем показывать меню.

  • Так как у «Амперки» не было на тот момент собственных модулей с дисплеем, целостную картинку пришлось нарушить и воспользоваться китайским модулем с OLED-дисплеем с ярким экраном диагональю 0,96 дюйма и с подключением по шине I2C (как показала практика, если собираешься управлять своим менеджером с расстояния более двух метров, то удобней все же будет использовать экран больших размеров).
  • Для хранения ключа после недолгих поисков была выбрана транспортная карта «Единый», работающая по технологии RFID и, как обнаружилось, имеющая небольшую область памяти, свободную для перезаписи. Попадаются карты «Единый» с 80 и 164 байт памяти. Хранится информация страницами по четыре байта. У тех, что со 164 байт на борту, есть 80 байт, свободных для перезаписи (с 16-й по 35-ю страницу при счете с 0). Таких израсходованных карт у меня оказалось приличное количество. Свою роль сыграло и то, что у «Амперки» была полноценная и настроенная на работу с картами «Единый» JS-библиотека для NFC/RFID-модуля на основе микросхемы NXP PN532, что дает стимул покопаться в ней глубже для более детального изучения принципов работы с RFID/NFC-метками.
Определившись со списком, можно было приступать к сборке прототипа и программированию.

Изначально за основу была взята отладочная плата Iskra JS с дополнительной платой расширения. Прототип на ней получился громоздким, чудным и по-своему симпатичным.
2.jpg
Первый прототип без корпуса

Первый прототип в корпусе из структораПозже появилась мини-версия старшей платы — Iskra JS mini c STM32F411CEU6 на борту, и это позволило существенно сократить размеры устройства и сделать его мобильным.
3.png
Отладочная плата Iskra JS miniВот на ее основе мы и соберем наше устройство.





Подготовка
Для начала необходимо установить среду разработки, и если ты собираешься работать с платой из-под винды, то понадобятся драйверы. Подробно об установке среды разработки можно почитать на вики проекта Iskra JS.

Если же не пользуешься браузером Google Chrome, то можно установить нативные приложения для Windows с сайта проекта Espruino либо самостоятельно клонировать текущую версию среды разработки с GitHub и запустить ее локально с помощью фреймворка NW.js, просто скопировав все файлы среды в папку с фреймворком и запустив исполняемый файл nw.

Главное — не забудь поменять в Espruino Web IDE настройки для работы с платами и дополнительными библиотеками от «Амперки»:

  • В разделе SETTINGS → COMMUNICATIONS:
  • в поле Module URL укажи
  • в поле Module Extensions укажи .min.js|.js
  • в поле Save on Send выбери Direct to Flash
  • В разделе SETTINGS → BOARD:
  • в поле Board JSON URL укажи
Окно среды разработки состоит из двух частей: справа — редактор кода, слева — консоль интерпретатора Espruino, доступная при соединении с отладочной платой.
4.jpg
Среда разработки Espruino Web IDE



Сборка прототипа и подключение
Подключим физически все наши модули, разместив на беспаечной макетной плате.
5.jpg
Прототип устройства на макетной платеВыведем питание с пина 3V3 отладочной платы Iskra JS mini на дорожку + макетной платы, а «землю» — с пина GND на –.

OLED-экран с подключением по шине I2C имеет четыре контакта: GND, VDD (VCC), SCK (SCL), SDA. Подключим их к соответствующим пинам на плате:

  • GND — к –,
  • VDD — к +,
  • SCK — к пину B10,
  • SDA — к пину B3.
ИК-приемник имеет три контакта:

  • G — к –,
  • V — к +,
  • S — к пину A1.
RFID/NFC-модуль имеет пять контактов для подключения к отладочной плате (помимо I2C пинов D (SDA) и C (SCL), используется еще пин прерывания Q), а также три контакта для подключения внешней антенны (X, G, X):

  • G — к –,
  • V — к +,
  • Q — к пину B4,
  • D — к пину B9,
  • C — к пину B8,
  • X, G, X — к соответствующим контактам на антенне.
Модуль чтения карт microSD подключается по шине SPI и имеет шесть контактов:

  • G — к –,
  • V — к +,
  • CS — к A4,
  • DI (MOSI) — к B15,
  • DO (MISO) — к B14,
  • SCK — к B13.




Как это должно работать?
Прежде чем начать программировать, неплохо бы подытожить наши представления о том, как у нас все это должно работать. Итак.



Принцип работы
На карте microSD хранятся открытые логины и зашифрованные с помощью AES пароли (при желании можно и логины зашифровать). На ПК/планшете/ТВ выставляем курсор в поле ввода логина, а на «ленивке», с помощью кнопок вверх-вниз на пульте, в меню выбираем аккаунт и, нажав на пульте X и приложив карту «Единый» к RFID-сканеру, начинаем эмуляцию ввода логина. Для ввода пароля нажимаем на пульте Y. Кнопкой пульта Z можно отменить сделанный ранее выбор ввода. Кнопкой + добавляем новые аккаунты.
6.jpg
Пульт для менеджера паролей



Пароли
Длина пароля ограничена 32 символами. Поддерживаемые символы: 0–9, a–z, A–Z, -=[];'`.,/~!@#$^&*()_+<>?{}":|, пробел. Полный перечень поддерживаемых символов и кодов можно посмотреть в исходном коде модуля @amperka/usb-keyboard.js. Знак %используется для дополнения паролей длиной меньше 32 символов и не должен встречаться в самом пароле.





Добавление новых аккаунтов
На карте microSD с файловой системой FAT32 в папке db создаются файлы без расширения, содержащие логин и пароль, разделенные тройным Enter. Имя файла будет названием пункта меню.
7.jpg
Файл аккаунта до кодированияВставляем карту, включаем «ленивку». На экране появится надпись Password Manager, а затем меню с заголовком Select Account (при первом запуске пустое). Нажимаем на пульте кнопку +, по запросу прикладываем карту «Единый» и дожидаемся надписи Encrypted и появления меню с добавленными пунктами (аккаунтами). При этом в файлах на карте пароли будут зашифрованы, а сами файлы получат расширение .enс.
8.jpg
Файл аккаунта после кодирования

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





Программирование
Интерпретатор Espruino по большей части основывается на ECMAScript 5, но за последнее время прошивка обзавелась и приличным набором возможностей из ES6. И стиль написания программ зависит теперь лишь от твоих привычек. Но надо помнить, что точку с запятой интерпретатор за тебя не поставит. Так что будь внимателен.

На Iskra JS, даже не запуская собственной среды разработки Espruino Web IDE, можно просто подключиться к виртуальному последовательному порту через USB-соединение (например, с помощью программы screen в Unix-based ОС) и получить доступ к консоли интерпретатора, где в реальном времени можно как подправить текущий код, так и испробовать новый.
Достаточно использовать встроенную функцию-команду reset(), и микроконтроллер сбросит свое состояние, но не будет запускать код, сохраненный во флеш-памяти, пока мы не скомандуем load(), а до тех пор мы вольны сколько угодно экспериментировать с кодом, не заботясь о «старом» контексте. Вкупе со встроенными в интерпретатор подсказками и автодополнением это экономит уйму времени и добавляет динамики процессу…

Итак, приступим. Запустим Espruino Web IDE, перейдем в окно редактора кода и начнем наше безобразие.

В первую очередь подключим библиотеку, которая сконфигурирует нашу плату как HID-устройство — USB-клавиатуру и добавит функциональность для управления эмуляцией.

var kb = require('@amperka/usb-keyboard');Далее настроим шину SPI для работы с модулем чтения карт:

SPI2.setup({mosi:B15, miso:B14, sck:B13});Подключим сам модуль:

E.connectSDCard(SPI2,A4);и библиотеку для работы с файловой системой FAT32:

var fs=require("fs");Подключим библиотеку для работы с модулем ИК-приемника:

var IRR = require('@amperka/ir-receiver').connect(A1);Настроим I2C1 для работы c RFID/NFC-модулем:

I2C1.setup({sda: B9, scl: B8, bitrate: 400000});Подключим и настроим библиотеку для работы с RFID/NFC-модулем на шине I2C1 с пином прерывания B4. Библиотека настроена на работу с метками Mifare Ultralight.

var nfc = require('@amperka/nfc').connect({i2c: I2C1, irqPin: B4});Создадим массив для временного хранения ключа.

var key = [];Зададим переменную для скорости набора текста в виде задержки в миллисекундах между вводом символов:

var typeSPEED = 100;Зададим переменную busy для защиты от повторного запроса с пульта во время обработки предыдущего:

var busy = 0;Зададим длину пароля и AES-ключа в байтах:

var keylen = 32;Создадим триггеры для активации считывания ключа с карты в key при кодировании и декодировании:

var nfc_on_enc = 0; var nfc_on_dec = 0;Создадим триггер для определения типа вывода: логин (= 1) или пароль (= 0):

var ilogin = 0;Зададим переменную для временного хранения логина:

var ltemp = '';Зададим переменную для временного хранения зашифрованного пароля:

var temp='';Создадим массив для хранения имен новых файлов с логинами/паролями:

var f2enc = [];Зададим разделитель, с помощью которого отделяются логин и пароль в файле. В данном случае разделителем будут три символа новой строки в Unix- и Windows-формате.

var spru = '\n\n\n'; var sprw = '\r\n\r\n\r\n';Создадим объект меню.

var mainmenu = { "" : { "title" : "Select Account", "fontHeight": 15 } };Теперь создадим функцию для чтения папки с файлами логинов/паролей, создания на их основе пунктов меню и присвоения им значений логинов и зашифрованных паролей для передачи на обработку при выборе. Имя файла становится названием пункта меню (без расширения .enc).

function readDB(){ var dbfiles = fs.readdirSync("db"); dbfiles.forEach(function(b){ if(b!=='.' && b!=='..' && b.indexOf(".enc")>0){ var ff = fs.readFile('db/'+b).split(spru); mainmenu[b.slice(0,-4)] = function(){ ltemp = ff[0]; temp = ff[1]; ldraw('INPUT CARD'); if(!nfc_on_enc) nfc_on_dec = 1; }; } }); }Подключим библиотеку графического меню и создадим переменную для управления им. Подробнее о работе с меню можно прочитать здесь.

var menu = require("graphical_menu"); var m;Подключим библиотеку со шрифтом и добавим в стандартную библиотеку Graphics:

require("Font8x16").add(Graphics);Создадим стартовую функцию, выполняющуюся при инициализации экрана. В ней мы установим размер шрифта с помощью метода setFont8x16() и вызовем функцию генерации меню.

function start(){ var err = 0; oled.setFont8x16(); try{ readDB();}catch(e){ err = 1;} if(err){ ldraw('SD CARD ERROR'); } else{ mShow("PASSWORD MANAGER",5000);} }Настроим шину I2C2 для работы с экраном, подключим библиотеку, выполнив стартовую функцию.

I2C2.setup({scl:B10,sda:B3}); var oled = require("SSD1306").connect(I2C2,start);Создадим функцию отрисовки заданного текста на экране на заданное время с последующим возвращением в меню.

function mShow(text,ms){ ldraw(text); setTimeout(()=>{m = menu.list(oled, mainmenu);},ms); }А также функцию отрисовки текста посередине экрана с помощью метода drawString().

function ldraw(text){ oled.clear(); oled.drawString(text,(64-(text.length/2)*8),26); oled.flip(); }Далее создадим функцию эмуляции набора текста на клавиатуре. На текущий момент выставлена скорость десять символов в секунду (typeSPEED = 100 мс), так как с большей скоростью могут возникнуть пропуски символов. Эмуляция клавиатуры на Espruino — штука порой капризная.

function ktype(str){ var cnt = 0; var fcnt = str.length; var int1 = setInterval(()=>{ kb.type(str[cnt++]); if(cnt>=fcnt){ clearInterval(int1); temp=''; busy = 0; mShow('OK',1000); } },typeSPEED); }

Теперь перейдем к функции кодирования пароля при помощи AES средствами встроенной библиотеки crypto. По умолчанию 256 бит — 32 символа пароля максимум и 32 байт ключа. Пароли меньше 32 знаков добиваются до 32 символом %. Затем при декодировании эти символы отбрасываются, поэтому в самом пароле % не должен использоваться. При необходимости выбери другой не используемый в паролях символ на свое усмотрение.

Максимальную длину пароля можно уменьшить до 24 и 16 символов, поменяв значение длины ключа keylen и изменив номера считываемых с карты страниц в p2read. Делай это до того, как будешь добавлять пароли: декодирование паролей другой размерности будет некорректным.

function Enc(text,key){ var fil = '%'; while(text.length<keylen){ text+=fil;} var enc = AES.encrypt(text,key); var enc_t =''; enc = enc.toString().split(','); enc.forEach(function(a){enc_t+=String.fromCharCode(a);}); return enc_t; }Ну и как же без функции декодирования.

function Dec(text,key){ var dec = AES.decrypt(text,key); dec = dec.toString().split(','); var dec_t = ''; dec.forEach(function(a){dec_t+=String.fromCharCode(a);}); return dec_t.split('%')[0]; }Создадим функцию проверки на наличие в папке db новых файлов с логинами/паролями без расширения .enc определенного формата (логин и пароль разделены символами, заданными разделителем spr). Найдя такие файлы, функция создаст массив с именами файлов для кодирования.

function chkf2enc(){ f2enc=[]; var files = fs.readdir("db"); files.forEach(function(a){ if(a!=='.'&&a!=='..'&&a.indexOf(".enc")==-1) f2enc = f2enc.concat(a); }); if(f2enc.length>0){ ldraw('INPUT CARD'); nfc_on_enc = 1; }else{ mShow('NO NEW PASSWORDS',2000);} }Функция считывания значений страниц карты «Единый», заданных в массиве p2read.

function cRead(p,callback){ if(p.length!==0){ nfc.readPage(p[0], function(error, buffer) { if(error){ } else{ key = key.concat(buffer); p.shift(); } if(p.length!==0){ cRead(p,callback); }else{ callback(); } }); } }Создадим функцию, вызываемую при получении ключа, для выполнения одного из следующих действий: кодирования новых паролей, вывода логина, декодирования и вывода пароля. При обработке новых файлов с логинами/паролями в случае неправильного формата файла ему присвоится расширение .err.

function gKey(rkey){ if(nfc_on_enc){ nfc_on_enc = 0; if(rkey.length==keylen){ f2enc.forEach(function(a){ var ft = fs.readFile('db/'+a); var spr = ''; var ferr = 0; if(ft.indexOf(sprw)>=0){ spr = sprw; }else if(ft.indexOf(spru)>=0){ spr = spru; }else{ ferr = 1; } if(!ferr){ var t = ft.split(spr); var tt = t[1].split('\n')[0].split('\r')[0]; var enct = Enc(tt,rkey); fs.writeFile('db/'+a+'.enc',t[0]+spru+enct); fs.unlink('db/'+a); }else{ ldraw('Error format'); fs.writeFile('db/'+a+'.err',ft); fs.unlink('db/'+a); } }); readDB(); busy = 0; mShow('Encrypted',2000); } f2enc=[]; }else if(nfc_on_dec){ nfc_on_dec = 0; if(ilogin){ ldraw('Typing LOGIN'); ktype(ltemp); temp = ''; ltemp = ''; }else{ var fd = Dec(temp,rkey); ldraw('Typing PASSWORD'); ktype(fd); temp = ''; ltemp = ''; } } key=[]; }После создания всех переменных и функций приступим к обработке событий и управлению. Активируем RFID/NFC-модуль, который запустит событие tag при обнаружении RFID-карты.

nfc.wakeUp(function(error) { if (error) { ldraw('NFC ERROR'); } else { nfc.listen(); // Слушаем новые метки } });Теперь зададим обработку события tag при обнаружении RFID-карты. Ключ считывается только при nfc_on_enc=1 или nfc_on_dec=1. Массив p2read содержит номера восьми страниц для считывания с карты метро значений, используемых в роли ключа. Значения страниц можешь выбирать сам из диапазона 16–35. Помним, что одна страница содержит четыре байта и что страниц в массиве должно быть восемь.

nfc.on('tag', function(error, data){ if(error){ mShow('tag read error',1000); } else{ if(nfc_on_enc||nfc_on_dec){ busy = 1; key = []; var p2read = [16,18,20,22,24,26,28,30]; cRead(p2read,()=>gKey(key)); } } setTimeout(function () { nfc.listen(); }, 1000); });Завершает код обработчик кодов ИК-пульта (в данном случае фирменный пульт от «Амперки»). Удержание нажатой кнопки пульта не учитывается. Busy не дает выполнять следующую операцию, пока не завершена предыдущая, защищая от коллизий.

IRR.on('receive', function(code, repeat) { if(repeat){ }else{ if(!busy) { switch(code){ case 378101919: // ВВЕРХ — переход вверх по меню m.move(-1); break; case 378124359: // ВНИЗ — переход вниз по меню m.move(1); break; case 378132519: // ПЛЮС — проверка на наличие новых файлов с логинами/паролями chkf2enc(); break; case 378089679: // X — вывод логина ilogin = 1; m.select(); break; case 378122319: // Y — вывод пароля ilogin = 0; m.select(); break; case 378105999: // Z — отмена ввода ключа nfc_on_enc = 0; nfc_on_dec = 0; mShow('Canceled',1000); break; } } } });Вот и весь код. Понятно, что его можно написать лучше и возможностей прилепить побольше, но это уже на растерзание тебе. Если же кто захочет воплотить его на старшей Iskra JS, то в коде надо будет поменять только пины подключения модулей.

Теперь самое время сохранить код во флеш-памяти микроконтроллера. Для этого нажмем на иконку с изображением микросхемы Send to Espruino. Так как в настройках мы выбрали пункт Direct to Flash, то код с автоматически подгруженными библиотеками сохранится в памяти микроконтроллера.

Теперь остается только переподключить плату к USB-порту для того, чтобы плата воспринялась как HID-устройство.





А как же создать ключ?
Конечно, прежде чем считать ключ, мы должны сначала его создать. Теперь нам нужен код, который понадобится для записи случайных значений в свободную область памяти карты (в те самые 20 свободных страниц, часть которых мы будем использовать в роли 256-битного ключа AES).

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

  • rw() — выполнение этой функции переключит в режим генерации случайных значений и записи на карту «Единый» в страницы с 16-й по 35-ю. Набери в консоли (левой части IDE) rw(), нажми Enter и приложи карту «Единый» — произойдет запись на карту;
  • ro() — выполнение этой функции переключит в режим чтения карты и вывода в консоль значений страниц.
9.jpg
Пример результата работы кода для записи на карту случайных значений I2C1.setup({sda: B9, scl: B8, bitrate: 400000}); var nfc = require('@amperka/nfc').connect({i2c: I2C1, irqPin: B4}); // Подключаем модуль генератора случайных чисел // на основе аппаратного генератора E.hwRand() var hwr = require("@amperka/hw-random"); var rnd = () => hwr.int(0,255); var card = {p:0,err:0}; var m = false; // Триггер режимов false — чтение, true — запись var rw = () => m = true; var ro = () => m = false; // Функция считывания значений страниц (page) с карты «Единый» function cRead(){ nfc.readPage(card.p, function(error, buffer) { if(error){ card.err++; console.log('read page error'); }else{ console.log((card.p + " : " + buffer)); card.p++; card.err = 0; } if(card.err<=3){ setTimeout(cRead,4); } }); } // Функция записи случайных значений в страницы с 16-й по 35-ю карты «Единый» function cWrite(){ if(card.p<36){ nfc.writePage(card.p, [rnd(), rnd(), rnd(), rnd()], function(error) { if (error) { print('write page ERROR'); } else { print('write page OK'); } }); card.p++; setTimeout(cWrite,10); } } // Активируем RFID/NFC-модуль nfc.wakeUp(function(error) { if (error) { console.log('nfc error'); } else { nfc.listen(); // Слушаем новые метки } }); nfc.on('tag', function(error, data){ if(error){ console.log('nfc tag error'); } else{ card.p=0; card.err=0; if(!m) cRead(); if(m) { card.p=16; cWrite();} } setTimeout(function () { nfc.listen(); }, 3000); });Во что можно переделать этот код и как еще можно использовать карты «Единый», будет твоим домашним заданием и тестом на богатую фантазию.

Упаковка

После того как все заработало, можно подумать и об упаковке. Вариантов много: спроектировать корпус и распечатать его на 3D-принтере, найти какой-нибудь бокс в своих закромах, а можно просто купить корпус в магазине.

Лично у меня получился временный и местами корявенький прототип, который дожидается своего апгрейда.
10.jpg
 
Сверху Снизу