Skip to content

Instantly share code, notes, and snippets.

@wararyo
Last active January 2, 2025 21:32
Show Gist options
  • Save wararyo/ca0ff7b33872a07f1af4d37fad4ba886 to your computer and use it in GitHub Desktop.
Save wararyo/ca0ff7b33872a07f1af4d37fad4ba886 to your computer and use it in GitHub Desktop.
M5PaperS3 Authenticator (TOTP)

M5PaperS3 Authenticator

M5PaperS3で、TOTPによる2段階認証コードを生成できるアプリケーションです!  
一応Google Authenticator等の代わりに使えますが、実用を想定していません🙏
MITライセンスに従い、自己責任でお願いいたします。

Googleアカウント、GitHub、X、Slack、AWSなどがTOTPによる2段階認証に対応しています。

使用方法

  1. WIFI_SSIDおよびWIFI_PASSWORDを入力します。
  2. 「シークレットの取得方法」に従い、シークレットをセットします。
  3. M5PaperS3に書き込みます。
  4. 実機でTOTPが表示されたことを確認します。
  5. #define INITIAL_LAUNCH をコメントアウトして再書き込みすることで、次回以降は高速に起動できます。

シークレットの取得方法

シークレットは5の倍数バイトであることが多いです。

  1. 各種Webサービスから2段階認証の設定に移動し、「認証アプリを使う」などの方法を選択します。
  2. 出現するQRコードを(認証アプリではなく)QRコードスキャンアプリで読み取ります。
  3. otpauth://totp/ から始まるURLが出現するので、secret= から始まる部分をコピーします。
  4. BASE32でデコードするとシークレットが得られます。
MIT License
Copyright (c) 2024 wararyo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
// 2回目以降の起動の場合はここをコメントアウトすることで高速に起動できる
#define INITIAL_LAUNCH
#include <WiFi.h>
#if __has_include(<esp_sntp.h>)
#include <esp_sntp.h>
#define SNTP_ENABLED 1
#elif __has_include(<sntp.h>)
#include <sntp.h>
#define SNTP_ENABLED 1
#endif
#ifndef SNTP_ENABLED
#define SNTP_ENABLED 0
#endif
#include <M5Unified.h>
#include "sha1.h"
#include "TOTP.h"
#include "USB.h"
#include "USBHIDKeyboard.h"
#define WIFI_SSID "YourSSID"
#define WIFI_PASSWORD "your_password"
#define NTP_TIMEZONE "JST-9"
#define NTP_SERVER1 "0.pool.ntp.org"
#define NTP_SERVER2 "1.pool.ntp.org"
#define NTP_SERVER3 "2.pool.ntp.org"
// シークレットの取得方法
// 1. 各種Webサービスから2段階認証の設定に移動し、「認証アプリを使う」などの方法を選択する
// 2. 出現するQRコードを(認証アプリではなく)QRコードスキャンアプリで読み取る
// 3. otpauth://totp/ から始まるURLが出現するので、secret= から始まる部分をコピーする
// 4. BASE32でデコードするとシークレットが得られる
// シークレットは5の倍数バイトであることが多い
uint8_t hmacKey[][10] = {
{0x05, 0x04, 0x03, 0x02, 0x01, 0x00, 0x09, 0x08, 0x07, 0x06},
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09},
{0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00}
};
USBHIDKeyboard Keyboard;
TOTP totp[] = {
TOTP(hmacKey[0], 10),
TOTP(hmacKey[1], 10),
TOTP(hmacKey[2], 10)
};
char code[7]; // 画面をタッチした時に入力されるコード
void fetchRtc();
void updateTotp();
void sleep();
void setup()
{
auto cfg = M5.config();
cfg.clear_display = false;
// clear_display = falseは現バージョンでは機能しないっぽい?ので、下記のようにローカルでM5GFXのソースコードを編集する
// ただし、Arduino IDEだと他のスケッチにも影響が出るため注意する
// 1. Panel_EPDiy.cpp 内の Panel_EPDiy::init の中身を確認する
// 2. startWrite() と endWrite() で囲まれている部分をコメントアウトする
M5.begin(cfg);
M5.Display.setEpdMode(m5gfx::epd_fastest);
// USB初期化
Keyboard.begin();
USB.begin();
if (!M5.Rtc.isEnabled())
{
M5.Display.println("RTC not found.");
for (;;)
{
M5.delay(500);
}
}
#ifdef INITIAL_LAUNCH
// 画面リフレッシュ
M5.Display.setEpdMode(m5gfx::epd_quality);
M5.Display.fillScreen(TFT_BLACK);
M5.Display.fillScreen(TFT_WHITE);
// 現在時刻を取得する
fetchRtc();
// 基本のUI
M5.Display.startWrite();
M5.Display.fillScreen(TFT_WHITE);
// タイトルバー
M5.Display.setFont(&fonts::FreeSans24pt7b);
M5.Display.setTextDatum(TC_DATUM);
M5.Display.drawString("PaperS3 Authenticator", 270, 16);
M5.Display.drawFastHLine(0, 72, 540, TFT_BLACK);
M5.Display.setTextDatum(TL_DATUM);
// 1個目
M5.Display.drawString("Dummy1" , 24, 108);
M5.Display.drawFastHLine(0, 232, 540, TFT_BLACK);
// 2個目
M5.Display.drawString("Dummy2", 24, 268);
M5.Display.drawFastHLine(0, 392, 540, TFT_BLACK);
// 3個目
M5.Display.drawString("Dummy3", 24, 428);
M5.Display.drawFastHLine(0, 552, 540, TFT_BLACK);
updateTotp();
M5.Display.endWrite();
M5.Display.setEpdMode(m5gfx::epd_fastest);
#else
// 電源オフ時の文言のみをリフレッシュする
M5.Display.startWrite();
M5.Display.fillRect(24, 160, 410, 48, TFT_BLACK);
M5.Display.fillRect(24, 320, 410, 48, TFT_BLACK);
M5.Display.fillRect(24, 480, 410, 48, TFT_BLACK);
M5.Display.fillRect(0, 960, 540, -80, TFT_BLACK);
M5.Display.endWrite();
M5.Display.startWrite();
M5.Display.fillRect(24, 160, 410, 48, TFT_WHITE);
M5.Display.fillRect(24, 320, 410, 48, TFT_WHITE);
M5.Display.fillRect(24, 480, 410, 48, TFT_WHITE);
M5.Display.fillRect(0, 960, 540, -80, TFT_WHITE);
updateTotp();
M5.Display.endWrite();
#endif
}
void loop()
{
M5.update();
// 5秒ごとにTOTP更新
static time_t last = millis();
if (millis() - last >= 5000)
{
last = millis();
updateTotp();
}
// 60秒無操作でオフ
static time_t lastInput = millis();
if (millis() - lastInput >= 60000)
{
lastInput = millis();
last = millis() + 5000;
sleep();
}
// 画面タッチでコード送信
auto touch = M5.Touch.getDetail();
if (touch.wasClicked())
{
Keyboard.printf("%s", code);
lastInput = millis();
}
M5.delay(16);
}
void fetchRtc()
{
configTzTime(NTP_TIMEZONE, NTP_SERVER1, NTP_SERVER2, NTP_SERVER3);
M5.Display.setTextSize(2);
M5.Display.print("WiFi:");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
for (int i = 20; i && WiFi.status() != WL_CONNECTED; --i)
{
M5.Display.print(".");
M5.delay(500);
}
if (WiFi.status() == WL_CONNECTED)
{
M5.Display.println("\r\nWiFi Connected.");
M5.Display.print("NTP:");
#if SNTP_ENABLED
while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED)
{
M5.Display.print(".");
M5.delay(1000);
}
#else
M5.delay(1600);
struct tm timeInfo;
while (!getLocalTime(&timeInfo, 1000))
{
M5.Display.print('.');
};
#endif
M5.Display.println("\r\nNTP Connected.");
time_t t = time(nullptr) + 1; // Advance one second.
while (t > time(nullptr))
; /// Synchronization in seconds
M5.Rtc.setDateTime(gmtime(&t));
}
else
{
M5.Display.println("\r\nWiFi none...");
}
M5.Display.setTextSize(1);
}
void updateTotp()
{
// コード算出
auto t = time(nullptr);
char *newCode[3] = {
totp[0].getCode(t),
totp[1].getCode(t),
totp[2].getCode(t)
};
if (strcmp(code, newCode[0]) != 0) {
strcpy(code, newCode[0]);
}
// 描画
long remainedTime = t % 30; // timestep = 30(s)
float arcStart = -90 + (remainedTime / 30.0) * 360;
M5.Display.startWrite();
M5.Display.setFont(&fonts::FreeSans24pt7b);
M5.Display.setTextDatum(TL_DATUM);
M5.Display.setTextSize(1);
M5.Display.drawString(newCode[0], 24, 160);
M5.Display.fillCircle(460, 152, 24, TFT_WHITE);
M5.Display.fillArc(460, 152, 24, 0, arcStart, 270, TFT_DARKGRAY);
M5.Display.drawString(newCode[1], 24, 320);
M5.Display.fillCircle(460, 312, 24, TFT_WHITE);
M5.Display.fillArc(460, 312, 24, 0, arcStart, 270, TFT_DARKGRAY);
M5.Display.drawString(newCode[2], 24, 480);
M5.Display.fillCircle(460, 472, 24, TFT_WHITE);
M5.Display.fillArc(460, 472, 24, 0, arcStart, 270, TFT_DARKGRAY);
// 一応、現在時刻も描画する
static constexpr const char *const wd[7] = {"Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"};
auto tm = localtime(&t); // for local timezone.
M5.Display.setCursor(0, 960);
M5.Display.setTextDatum(BL_DATUM);
M5.Display.setFont(&fonts::Font4);
M5.Display.printf("%04d/%02d/%02d (%s) %02d:%02d:%02d %s\r\n",
tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday,
wd[tm->tm_wday],
tm->tm_hour, tm->tm_min, tm->tm_sec, NTP_TIMEZONE);
M5.Display.setTextSize(1);
M5.Display.endWrite();
}
void sleep()
{
M5.Display.setEpdMode(m5gfx::epd_text);
M5.Display.startWrite();
M5.Display.setFont(&fonts::FreeSans24pt7b);
M5.Display.setTextDatum(TL_DATUM);
M5.Display.setTextColor(TFT_DARKGRAY, TFT_WHITE);
M5.Display.setTextSize(1);
M5.Display.fillCircle(460, 152, 24, TFT_WHITE);
M5.Display.drawString("Power to view code", 24, 160);
M5.Display.fillCircle(460, 312, 24, TFT_WHITE);
M5.Display.drawString("Power to view code", 24, 320);
M5.Display.fillCircle(460, 472, 24, TFT_WHITE);
M5.Display.drawString("Power to view code", 24, 480);
M5.Display.fillRect(0, 960, 540, -80, TFT_WHITE);
M5.Display.endWrite();
// M5.Power.lightSleep(0, true); // タッチで復帰できないかと思ったが不可能だった
M5.Power.powerOff();
}
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:papers3]
platform = espressif32
board = esp32-s3-devkitm-1
framework = arduino
board_build.arduino.memory_type = qio_opi
board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216
build_flags =
-DBOARD_HAS_PSRAM
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
lib_deps =
epdiy=https://github.com/vroland/epdiy.git
m5stack/M5Unified @ ^0.2.2
lucadentella/TOTP library @ ^1.1.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment