Несмотря на огромное количество устройств на базе микроконтроллеров, созданных на волне успеха Arduino, считаные единицы из них имеют форм-фактор обычной флешки, подходящий для непосредственного включения в разъем USB компьютера (USB Type-A). Один из наиболее любопытных их представителей — Espruino Pico.
Вообще говоря, Espruino — это несколько вариантов микроконтроллерных устройств, в которых прошит встроенный интерпретатор JavaScript. Espruino Pico — самое миниатюрное из них. Оригинальный интерпретатор JavaScript, используемый в Espruino, предназначен для быстрой разработки на устройствах с ограниченными процессорными ресурсами. Есть его версии для целого перечня платформ, начиная с ESP8266 и до Raspberry Pi.
Для Espruino существует большое число готовых подгружаемых модулей для самого разнообразного периферийного оборудования, совместимого с экосистемой Arduino (см. раздел модули на сайте Espruino) и инструкций по его подключению (в разделе инструкций и примеров). Есть официальный форум.
РЕКОМЕНДУЕМ: Linux-сервер на микрокомпьютере Omega 2
- История Espruino
- Характеристики Espruino Pico
- Быстрый старт
- Обновление прошивки
- Загрузка первой программы в Espruino
- Отработка нажатий на встроенную кнопку
- Режим HID, эмуляция клавиатуры
- Запись программы во Flash-память
- Запуск Espruino без загрузки пользовательской программы из внутренней Flash-памяти
- Объект E
- Мультифакторная авторизация
- Алгоритм TOTP
- Кодировка Base32
- Вычисление HMAC SHA-1 с помощью библиотеки jsSHA
- Проверка
- Встроенные средства для вычисления HMAC и SHA-1
- Возможные проблемы
- Заключение
История Espruino
Платформа Espruino — не самый молодой проект. Первые публичные упоминания о ней датируются еще 2012-м, и в 2013-м была проведена успешная кампания на Kickstarter по сбору средств на развитие проекта. Ее успех позволил в конце 2014 года провести следующую кампанию, на проект более компактной версии под названием Espruino Pico в форм-факторе размером в половину обычной флешки.
Характеристики Espruino Pico
- Espruino Pico — самая миниатюрная из плат Espruino, ее размеры 3,3 × 1,5 см.
- На контакты платы выведено 22 порта ввода-вывода общего назначения, в том числе девять аналоговых входов, 21 с поддержкой ШИМ (PWM), два последовательных порта, три порта SPI, три порта I2C; все GPIO могут работать с 5 В (что важно для совместимости с модулями, разработанными для Arduino).
- Встроенный разъем USB Type-A позволяет включать устройство непосредственно в USB-порт компьютера без дополнительных кабелей аналогично обычной флешке.
- Два встроенных светодиода и одна кнопка позволяют реализовать минимальное управление устройством без внешних компонентов.
- 32-битный процессор ARM Cortex M4 84 МГц — STM32F401CDU6.
- 384 Кбайт флеш-памяти, 96 Кбайт ОЗУ.
- Встроенный регулятор напряжения 3,3 В 250 мА, работающий в диапазоне от 3,5 до 16 В, позволяет подключать внешний аккумулятор без дополнительных компонентов.
- Потребляемый ток в режиме сна: < 0,05 мА — более двух с половиной лет от батареи 2500 мА ∙ ч.
- Встроенный полевой транзистор для управления цепями с высоким рабочим током.
Дисклеймер! Все примеры кода предназначены исключительно для образовательных целей. Данное решение обладает пониженным уровнем безопасности, секретный ключ хранится на устройстве в открытом виде. Не используй этот код в критически важных системах. Применяй для авторизации промышленно выпускаемые устройства.
Быстрый старт
Для Windows тебе, скорее всего, понадобится установить драйвер виртуального COM-порта.
Под Linux нужно будет сделать следующее: копируем файл 45-espruino.rules в /etc/udev/rules.d, перегружаем правила командой sudo udevadm control —reload-rules и проверяем командой groups, что текущий пользователь входит в группу plugdev. Если это не так, исправляем командой sudo adduser $USER plugdev.
На Mac никаких дополнительных манипуляций потребоваться не должно.
Устанавливаем среду Espruino Web IDE из Crome Web Store.
Рабочая область Espruino Web IDE разделена на две части. В левой расположено окно консоли, в правой — редактор. Нажав на символ </>, редактор можно переключить в графический режим, основанный на среде Blockly, аналогичной Scratch, что может подойти начинающим.
Для подключения к плате необходимо нажать на желтую иконку с разъемом в верхнем левом углу окна. Будет выведен запрос на выбор порта.
При успешном подключении будет выведено приглашение консоли Espruino:
1 2 3 4 |
Connected > > |
Проверим работоспособность вводом 1+2:
1 2 3 |
>1+2 =3 > |
Обновление прошивки
Если доступно обновление прошивки, в правом верхнем углу появится соответствующая иконка. При нажатии на эту иконку запустится мастер обновления, который пошагово проинструктирует, что нужно сделать для обновления прошивки.
Загрузка первой программы в Espruino
Следующий шаг стандартный — «помигать светодиодом». Для этого в правой части среды разработки уже есть готовый код:
1 2 3 4 5 |
var on = false; setInterval(function() { on = !on; LED1.write(on); }, 500); |
Единственное, что можно прокомментировать в этих строках, — это использование встроенного объекта LED1, представляющего собой экземпляр специального класса Pin, который предназначен для управления портами ввода-вывода. В данном случае метод write используется для задания уровня на выходе порта (логические ноль/единица), к которому подключен красный светодиод, установленный на плате.
Нажмем на иконку «Send to Espruino».
Через мгновение в консоль будет выведен лого и замигает красный светодиод:
1 2 3 4 5 6 7 8 9 |
_____ _ | __|___ ___ ___ _ _|_|___ ___ | __|_ -| . | _| | | | | . | |_____|___| _|_| |___|_|_|_|___| |_| http://espruino.com 1v94 Copyright 2016 G.Williams > =undefined > |
Для отключения функции setInterval()можно ввести в консоли clearInterval().
LED, LED1 и B2 (номер контакта микроконтроллера, к которому подключен светодиод) соответствуют красному светодиоду, LED2 и B12(он подключен к контакту B12) — зеленому. Адресовать светодиоды можно и с помощью номеров контактов, к которым они подключены: B2 эквивалентно LED и LED1, B12 — LED2.
Можно воспользоваться и хорошо знакомой ардуинщикам функцией digitalWrite():
1 |
digitalWrite(LED2, 1) |
Есть возможность изменить состояние на заданный период функцией digitalPulse():
digitalPulse(LED1, 1, 50);
Или задать целую последовательность:
1 |
digitalPulse(LED1, 1, [50,200,50]); |
Порты, к которым подключены светодиоды, смонтированные на плате, не поддерживают режим широтно-импульсной модуляции (ШИМ), или Pulse-Width Modulation (PWM), и попытка изменить их яркость, записав что-то в эти порты функцией analogWrite(), приведет к ошибке. Но управление яркостью светодиодов все-таки возможно с помощью встроенного режима программной эмуляции ШИМ:
analogWrite(B2, 0.1, {soft:true});
Более того, можно одновременно с этим задать периодическое включение и отключение:
1 |
analogWrite(LED2, 0.1, { soft: true, freq: 16 }); |
Отработка нажатий на встроенную кнопку
Прочитать состояние кнопки позволяет функция digitalRead():
1 2 |
>digitalRead(BTN) =0 |
Мониторить состояние кнопки можно, периодически считывая ее состояние с помощью setInterval(), однако более корректным будет использование функции watch():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
clearWatch(); // Удалить старые триггеры (если были) setWatch( // Задать новый триггер (watch) function(e) { // Callback при срабатывании триггера // e.state — состояние кнопки digitalPulse(LED1, 1, 50); }, BTN, // Мониторим состояние встроенной кнопки { // Параметры триггера repeat: true, // Мониторить многократно debounce : 50, // Предотвращение дребезга контактов (в миллисекундах) // edge: "rising", // Срабатывать только при нажатии // edge: "falling", // Срабатывать только при отпускании edge: "both", // Срабатывать как при нажатии, так и при отпускании } ); |
Режим HID, эмуляция клавиатуры
Espruino поддерживает работу через USB в режиме HID (Human Interface Device), что позволяет эмулировать клавиатуру, мышь или планшет (подробнее — здесь).
Для эмуляции USB-клавиатуры необходимо подключить внешний модуль USBKeyboard. Введем в правой части окна в текстовом редакторе:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const kb = require("USBKeyboard"); LED1.write(1); kb.type('HELLO, ', () => kb.setModifiers(kb.MODIFY.SHIFT, () => kb.type('WORLD', () => kb.setModifiers(0, () => kb.tap(kb.KEY.ENTER, () => LED1.write(0) ) ) ) ) ); |
Espruino Web IDE определит, что используется внешний модуль, и произведет его поиск в подкаталоге modules каталога проекта (его предварительно необходимо задать в настройках) и в каталоге https://www.espruino.com/modules/ на сайте Espruino, минифицирует его (в соответствии с настройками), затем вместе с текстом программы загрузит в Espruino. Можно указать не только имя модуля, можно указать URL файла с его исходным текстом, в этом случае модуль будет загружен с указанного ресурса.
Запись программы во Flash-память
Для того чтобы режим HID начал работать, плату нужно отключить и подключить снова. Чтобы программа не стерлась при отключении питания, ее нужно сохранить во внутренней Flash-памяти. Для этого предназначена функция save():
1 2 3 4 5 6 7 8 |
>save() =undefined Erasing Flash..... Writing......... Compressed 81600 bytes to 6595 Checking... Done! > |
Запуск Espruino без загрузки пользовательской программы из внутренней Flash-памяти
Если программа, сохраненная во Flash-памяти, работает неправильно и перезаписать ее из-за этого невозможно, необходимо непосредственно после включения нажать встроенную кнопку и подождать около двух секунд. Если при этом поочередно мигают красный и зеленый светодиоды, значит, кнопка была нажата слишком рано и плата перешла в режим загрузчика. Промежуток очень короткий, но, если в него попасть, Espruino запустится без загрузки программы, прошитой во Flash-памяти.
Объект E
Объект E объединяет различные вспомогательные функции, специфичные для Espruino. В числе его методов — обработка сигналов (реализация на C быстрого преобразования Фурье и свертки), интерполяция значений для одномерных и двумерных массивов), низкоуровневые функции для работы с флеш-памятью, сторожевым таймером (watchdog timer), позволяющим автоматически перезагрузить устройство при зависании, управление частотой процессора и другие интересные штуки.
Например, можно считать значение термистора, встроенного в STM32:
1 2 3 |
>E.getTemperature() =33.90860215053 > |
Мультифакторная авторизация
Мультифакторная авторизация (multi-factor authorization, MFA) заслуженно считается эффективным способом (конечно, со своими ограничениями) повышения безопасности доступа к ресурсам.
Алгоритм TOTP
Один из наиболее распространенных методов такой авторизации — одноразовые пароли (one-time passwords) и в частности TOTP (time-base one time passwords), одноразовые пароли, основанные на времени. Их поддержка реализована на большом количестве сайтов. Со стороны клиента их реализация представлена такими популярными приложениями, как Google Authenticator и Authy.
TOTP — это частный случай алгоритма HOTP (HMAC-based one-time password), в котором в качестве счетчика, используемого для генерации хеша, берется порядковый номер временного интервала, начиная с некоторого момента.
HMAC — hash-based message authentication code (код аутентификации, подтверждающий сообщения на основе хеш-функции). Алгоритм HMAC позволяет проверить целостность и подлинность некоторого сообщения с использованием хеш-функции, секретного ключа и самого сообщения. С их помощью генерируется код аутентификации, который может проверить вторая сторона, выполнив такой же алгоритм. В случае HOTP в качестве сообщения берется некий общий для обеих сторон синхронный счетчик.
Алгоритм HOTP описан в RFC 4226:
- вычисляется функция HMAC от значений секретного ключа и счетчика с использованием функции хеширования SHA-1;
- для полученной 20-байтной строки выполняется «динамическое отсечение», для этого берутся ее младшие четыре бита и их значение считается смещением, по которому из этой же строки выделяются четыре
- байта; старший бит полученного значения отбрасывается;
- полученные 31 бит переводятся в десятичный вид, и от них берется нужное количество цифр результирующего пароля, начиная с наименее значимых.
Генерация TOTP-паролей описана в RFC 6238. В качестве общего счетчика в нем используется количество интервалов, начиная с некоторого начального момента, общего для обоих устройств. Как правило, начальным моментом считается начало отсчета времени в Unix-системах, а в качестве интервала принимаются 30-секундные отрезки времени.
Для работы этого алгоритма необходимо, чтобы часы используемых устройств были синхронизированы. Хотя у Espruino Pico есть свои встроенные часы реального времени, они не энергонезависимы, и необходимо либо организовать питание от аккумулятора, либо подключить внешний модуль часов реального времени со встроенным аккумулятором.
Для синхронизации времени с компьютером, на котором ведется разработка, в настройках Espruino Web IDE необходимо включить следующий параметр: Settings → Communications → Set Current Time. Теперь при загрузке кода программы время устройства будет синхронизироваться с основным компьютером.
Секретный ключ на сайтах приводится обычно в виде строки в кодировке Base32 (дополнительно для мобильных устройств может выводиться QR-код, чтобы считать его с помощью встроенной камеры).
Кодировка Base32
Существует несколько вариантов кодировки Base32. В данном случае используется вариант, описанный в стандарте RFC 4648. При этом исходная последовательность байтов рассматривается как непрерывная последовательность битов. Эта битовая последовательность разбивается на блоки по пять бит, и каждому такому блоку ставится в соответствие один из 32 базовых символов. Символы (соответствующие блокам по пять бит) объединяются в группы по пять (40 бит). Если общая длина последовательности не кратна пяти символам (40 битам исходной последовательности), она может дополняться символами =.
При обратном преобразовании каждый из символов строки в кодировке Base32 преобразуется в блоки из пяти бит, затем результирующая последовательность из блоков по пять бит преобразуется в блоки по восемь бит, соответствующие байтам результирующей строки.
Так как мы реализуем только токен, нам необходима лишь функция декодирования строки Base32 (сформированной сервером).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/** * Функция перевода строки в кодировке Base32 в строку шестнадцатеричных цифр * * @param {string} base32 — входная строка в кодировке Base32 * @returns {string} — результат в виде строки шестнадцатеричных цифр */ const base32ToHex = function(base32) { const base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let bits = ""; // Последовательность битов let hex = ""; // Результирующая строка шестнадцатеричных цифр // Преобразуем последовательность символов в строку двоичных цифр for (let i = 0; i < base32.length; i++) { // Текущий символ закодированной строки const ch = base32.charAt(i).toUpperCase(); // Значение 0..31, соответствующее символу Base32 const val = base32chars.indexOf(ch); // Добавляем биты полученного числа в строку двоичных цифр bits += decToBin(val, 5); } // Преобразуем строку двоичных цифр в строку шестнадцатеричных цифр for (let i = 0; i + 4 <= bits.length; i+=4) { // Выделяем четыре бита, соответствующие одной шестнадцатеричной цифре const chunk = bits.substr(i, 4); // Преобразуем четыре бита в одну шестнадцатеричную цифру // и накапливаем в результирующей строке hex = hex + parseInt(chunk, 2).toString(16); } return hex; }; |
Добавим несколько вспомогательных функций для преобразований между системами счислений:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
/** * Вспомогательная функция дополнения строки до заданного количества символов * * @param {string} s — исходная строка * @param {number} l — необходимая длина строки, до которой идет дополнение * @param {string} p — символ-заполнитель * @returns {string} — результирующая строка */ const leftpad = function(s, l, p) { while (s.length < l) s = p + s; // Дополняем слева до достижения нужной длины return s; }; /** * Преобразование числа в строку шестнадцатеричных цифр * * @param {number} num — исходное число * @param {string} [byteLen] — длина результирующего слова в _байтах_ (если результат меньше, * он будет дополнен до этого значения) */ const decToHex = function(num, byteLen) { // Преобразуем десятичное число в строку шестнадцатеричных цифр let res = Math.round(num).toString(16); // Дополняем нулями return leftpad(res, (byteLen || 1)*2, "0"); }; /** * Преобразование строки шестнадцатеричных цифр в число * * @param {string} strHex — строка шестнадцатеричных цифр * @returns {number} — результирующее число */ const hexToDec = function(strHex) { return parseInt(strHex, 16); }; /** * Преобразование числа в строку двоичных цифр * * @param {number} num — исходное число * @param {number} bitLen — количество битов в результате * @returns {string} — строка двоичных цифр */ const decToBin = function(num, bitLen) { // Преобразуем десятичное число в строку шестнадцатеричных цифр // Дополняем нулями return leftpad(num.toString(2), bitLen, '0'); }; |
Загрузим в Espruino и проверим:
1 2 |
>base32ToHex('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'); ="00443214c74254b635cf84653a56d7c675be77df" |
Вычисление HMAC SHA-1 с помощью библиотеки jsSHA
Espruino не содержит встроенной функции для вычисления HMAC SHA-1. Для вычисления OTP можно воспользоваться библиотекой jsSHA, взяв из нее только тот модуль, в котором реализована именно нужная хеш-функция. Функции require() можно напрямую указать URL с кодом необходимого модуля прямо с GitHub.
Добавим в начале файла с исходным кодом:
1 2 3 |
// Подключаем реализацию SHA-1 из библиотеки jsSHA // Код модуля по данной ссылке уже минифицирован const jsSHA = require('https://raw.githubusercontent.com/Caligatio/jsSHA/master/src/sha1.js'); |
Затем объявим новый класс TOTP:
1 2 3 4 |
TOTP = function() { // }; |
И создадим объект этого класса:
1 |
const totpObj = new TOTP(); |
Добавим внутри класса TOTP метод вычисления значения счетчика на основе текущего времени.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/** * Функция вычисления счетчика на основе текущего времени * * @returns {string} — результат в виде строки шестнадцатеричных цифр */ this.calcCounter = function() { // Текущее время в секундах const currentTimeSec = Math.round(new Date().getTime() / 1000.0); // Начало отсчета const startTimeSecT0 = 0; // Длительность интервала const intervalSecTI = 30; // Текущее значение счетчика const counterTC = Math.floor( (currentTimeSec-startTimeSecT0)/intervalSecTI); // Время начала следующего интервала (то есть момент, когда текущий код перестанет быть валиден) const nextIntervalSec = (counterTC+1) * intervalSecTI; // Оставшееся время валидности текущего кода this.validFor = nextIntervalSec - currentTimeSec; // Преобразуем значение счетчика в строку шестнадцатеричных цифр, соответствующую восьми байтам const counterHex = decToHex(counterTC, 8); return counterHex; }; |
Добавим метод вычисления HMAC для SHA-1 с использованием библиотеки jsSHA:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * Функция вычисления HMAC для SHA-1 с использованием библиотеки jsSHA * * @param {string} secretHex — значение secret в виде строки шестнадцатеричных цифр * @param {string} messageHex — значение message в виде строки шестнадцатеричных цифр * @returns {string} — результат в виде строки шестнадцатеричных цифр */ this.hmacSha1 = function (secretHex, messageHex) { // Создаем объект для вычисления HMAC SHA-1 const shaObj = new jsSHA("SHA-1", "HEX"); shaObj.setHMACKey(secretHex, "HEX"); // Задаем secret shaObj.update(messageHex); // Задаем строку return shaObj.getHMAC("HEX"); // Возвращаем полученное значение HMAC }; |
Метод динамического отсечения:
1 2 3 4 5 6 7 8 9 |
this.dynamicTruncate = function(hmacHex) { // В качестве смещения берем последний полубайт (то есть последний символ строки) const offset = hexToDec(hmacHex.substring(hmacHex.length - 1)); // Выделяем четыре байта, начиная с полученного смещения const truncatedHex = hmacHex.substr(offset * 2, 8); // Выделяем младшие 31 бит const truncatedDec = (hexToDec(truncatedHex) & hexToDec("7fffffff")) + ""; return decToHex(truncatedDec); }; |
И главный метод вычисления одноразового пароля, собирающий остальные:
1 2 3 4 5 6 7 8 9 10 11 12 |
this.generateHotp = function(secretHex, counterHex, len) { // По умолчанию длина OTP-пароля — шесть цифр len = len || 6; // Функция вычисления HMAC SHA-1 с использованием библиотеки jsSHA const hmacHex = this.hmacSha1(secretHex, counterHex); // Производим динамическое отсечение const trunc_hex = this.dynamicTruncate(hmacHex); // Преобразуем в строку десятичных цифр const trunc_dec = hexToDec(trunc_hex) + ''; // Возвращаем последние цифры return trunc_dec.substr(trunc_dec.length - len, len); }; |
Основная точка входа с индикацией и логированием:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
this.getOTP = function(secret) { console.log('* Calculating OTP...'); // Гасим красный светодиод digitalWrite(LED1, false); // Включаем мерцание зеленого светодиода analogWrite(LED2, 0.01, { soft: true, freq: 16 }); // Сохраняем текущее время const startTime = new Date(); try { // Вычисляем значение счетчика const counterHex = this.calcCounter(); console.log(`** counterHex: ${counterHex}`); // Переводим секретный ключ из Base32 в строку шестнадцатеричных цифр const secretHex = base32ToHex(secret); console.log(`* secretHex: ${secretHex}`); // Вычисляем одноразовый пароль const otp = this.generateHotp(secretHex, counterHex); console.log(`* DONE, OTP: ${otp} [${ Math.round(new Date() - startTime)/1000} seconds]`); // Индицируем завершение вычислений выключением зеленого светодиода digitalWrite(LED2, false); return otp; // Возвращаем результат } catch (error) { // В случае ошибки // гасим зеленый светодиод digitalWrite(LED2, false); // Индицируем ошибку мерцанием красного светодиода analogWrite(LED1, 1, { soft: true, freq: 5 }); console.log('* ERROR:', error); // Прерываем выполнение с выводом ошибки throw error; } }; |
Подключим модуль клавиатуры в начале исходного текста программы:
1 2 3 |
// Подключаем модуль HID клавиатуры const kb = require("USBKeyboard"); |
Затем вне декларации класса добавим отработку нажатия на кнопку, вызов метода вычисления пароля и его передачу на компьютер эмуляцией набора на клавиатуре.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
setWatch( // Callback при нажатии на кнопку function() { // Гасим все светодиоды digitalWrite(LED1, false); digitalWrite(LED2, false); // Начальные значения: // — пин-код (если есть) const pin = ""; // put_your_PIN_Prefix_here // — секретный постоянный пароль для генерации одноразовых паролей const secretBase32 = "ABCDEFGHIJKLMNOP"; // put_your_google_authenticator_secret_here // !!! // В данной реализации секретный пароль хранится в памяти Espruino в открытом виде // и может быть без проблем считан злоумышленником // !!! // Вычисляем одноразовый пароль const otp = totpObj.getOTP(pin+secretBase32); // Включаем зеленый светодиод на полную яркость digitalWrite(LED2, true); // Имитируем набор на клавиатуре символов пин-кода и одноразового пароля kb.type(pin + otp, function() { // По завершении набора символов имитируем нажатие клавиши `ENTER` kb.tap(kb.KEY.ENTER); // Гасим зеленый светодиод digitalWrite(LED2, false); }); }, // Мониторим изменение порта, к которому подключена встроенная кнопка BTN, // Параметры мониторинга { debounce: 100, // repeat: true, // edge: "rising" // } ); |
Первая версия токена готова.
Проверка
Проверить работоспособность можно, вызвав метод напрямую:
1 2 3 4 5 6 |
>totpObj.getOTP('ABCDEFGHIJKLMNOP'); * Calculating OTP... ** counterHex: 0000000003014aea * secretHex: 00443214c74254b635cf * DONE, OTP: 271471 [1.372 seconds] ="271471" |
Или нажатием на кнопку устройства, поместив указатель мыши в каком-либо текстовом поле.
Проверить корректность вычисления одноразового пароля можно, например, на этом сайте.
Перед тем как пробовать код из примеров на каком-либо реальном сайте:
продублируй код, сохранив его с помощью проверенной программы генерации одноразовых паролей, такой как Google Authenticator или Authy;
распечатай коды восстановления, предлагаемые сайтом;
не забывай, что без подключения аккумулятора после его извлечения из USB-порта время на нем очень быстро сбрасывается, что приведет к генерации некорректных паролей. Необходимо либо установить
аккумулятор, либо подключить модуль часов реального времени со встроенным аккумулятором;
не забывай, что секретный пароль сохранен на устройстве в открытом виде.
Встроенные средства для вычисления HMAC и SHA-1
Генерация пароля устройством занимает более секунды, это не очень комфортно. Библиотека jsSHA реализована на чистом JavaScript, и, хотя время, затрачиваемое на вычисление HMAC SHA-1, практически незаметно на обычном компьютере, на Espruino длительность вычислений становится критичной.
У Espruino есть собственные низкоуровневые нативные реализации вычисления хеш-функций в библиотеках crypto и haslib. Есть и собственная библиотека hmac (хотя она написана на JavaScript, в этом алгоритме сложных вычислений нет и на производительность это не повлияет, в отличие от SHA-1).
Однако использовать напрямую их не получится — в библиотеке haslib нет реализации алгоритма SHA-1, а готовая реализация crypto.SHA1 несовместима с библиотекой hmac. Поэтому для работы со встроенными функциями придется написать «обертку» вокруг функции crypto.SHA1 в класс, совместимый с классом HASH, который используется библиотеками hmac и haslib (исходный код модуля hmac можно посмотреть в каталоге /modules/ непосредственно на сайте espruino.com/, откуда и подгружаются модули функцией require() в Espruino Web IDE). У этого класса всего три метода, причем один из них библиотекой не используется. Еще один момент — объекты типа HASH создаются без ключевого слова new, вызовом конструктора напрямую, поэтому ключевое слово this мы не применяем, создавая новый объект (в переменной self).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// Класс для вычисления SHA-1 с использованием crypto.SHA1() для использования модулем hmac const sha1 = function(m) { const self = {}; // Инициализируем результирующий объект // Сохраняем параметр конструктора в self._message self._message = m || ''; self.block_size = 64; // Функция `update` просто добавляет новую строку к self._message self.update = function(m) { self._message += m; }; // Вычисление дайджеста от self._message self.digest = function() { const digestArrayBuffer = crypto.SHA1(self._message); let s = arrBufToStr( digestArrayBuffer ); return s; }; // Не используется в библиотеке `hashlib` self.hexdigest = function() { throw new Error('Not implemented'); }; return self; }; |
Дополнительно понадобится вспомогательная функция, преобразующая объект типа ArrayBuffer в обычную строку, код каждого символа которой равен значению соответствующего байта исходного объекта:
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * Преобразование объекта ArrayBuffer в строку * * @param {ArrayBuffer} a * @returns {string} */ const arrBufToStr = function(a) { let s = ""; for (let i = 0; i < a.length; i++) s += String.fromCharCode( a[i] ); return s; }; |
И еще пара функций для конверсии различных представлений данных:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/** * Функция перевода символов строки байтов в их шестнадцатеричное представление * в виде строки шестнадцатеричных цифр * * @param {string} str * @returns {string} */ const strToHex = function(str) { let s = ""; for (let i = 0; i < str.length; i++) s += (256+str.charCodeAt(i)).toString(16).substr(-2); return s; }; /** * Функция перевода строки шестнадцатеричных цифр в строку байтов * * @param {string} str * @returns {string} */ const hexToStr = function(str) { let s = ""; for (let i = str.length-1; i>0; i-=2) { const byte = str.substr(i-1,2); s = String.fromCharCode( hexToDec(byte) ) + s; } return s; }; |
Теперь готовы все «кирпичики» для того, чтобы написать новый метод вычисления HMAC SHA-1 класса TOTP:
1 2 3 4 5 6 7 8 9 |
this.hmacSha1_new = function (secretHex, messageHex) { // Преобразуем параметры в нужный вид const secretStr = hexToStr(secretHex); const messageStr = hexToStr(messageHex); // Вычисляем HMAC с помощью встроенной функции, передав ей секретный ключ, сообщение // и наш объект — обертку вокруг `crypto.SHA1` const shaObj = hmac.create(secretStr, messageStr, sha1); return shaObj.hexdigest(); // Возвращаем полученное значение HMAC }; |
И внутри метода generateHotp() заменим вызов функции this.hmacSha1() на this.hmacSha1_new():
1 2 |
//const hmacHex = this.hmacSha1(secretHex, counterHex); const hmacHex = this.hmacSha1_new(secretHex, counterHex); |
Длительность вычислений при этом существенно изменится в лучшую сторону, снизившись до 0,17 с, и перестанет вызывать дискомфорт.
Возможные проблемы
Если плата работает только с подключенным USB, а без него зависает, наиболее вероятная причина — блокировка вывода на консоль, которая по умолчанию настроена на USB и блокирует работу, когда он недоступен. Для устранения проблемы нужно либо убрать все console.log, print(), либо переключить консоль на последовательный порт (только так, чтобы оставить возможность возвращения обратно на USB).
Подробнее об устранении этих и других проблем — на странице Troubleshooting сайта Espruino.
Заключение
Espruino — очень дружелюбная и приятная в работе платформа. Хотя в этой статье мы и не попробовали подключить никакой периферии, поддерживается огромное число стандартных внешних модулей с действительно элементарным подключением. Ее можно смело рекомендовать тем, кто знаком с JavaScript и хочет погрузиться в мир микроконтроллеров.
В качестве дальнейшего развития реализации одноразовых паролей можно вместо эмуляции клавиатуры в режиме HID сделать приложение для компьютера, которое будет обмениваться данными с токеном.
Интересным развитием концепции одноразовых паролей стал стандарт FIDO U2F, поддерживаемый Yubico и Google. Авторизация происходит без ввода кода с клавиатуры, достаточно только нажатия на кнопку токена, поддерживающего этот стандарт. При этом токен выступает в роли HID-устройства, с которым браузер взаимодействует напрямую для обмена ключами с сайтом. К сожалению, ресурсов Espruino Pico недостаточно для его поддержки, по крайней мере с использованием реализаций шифрования на JavaScript.
Поддержка HID позволяет использовать такое устройство и в недобрых целях, аналогично USB Rubber Ducky, эмулируя последовательности нажатий клавиш на целевом компьютере и пытаясь, например, создать командный файл для перезагрузки компьютера и добавить его в папку автостарта текущего пользователя.
РЕКОМЕНДУЕМ: Обзор, установка и программирование на ESP32