M5StackのM5 DinMeterを使って2段階認証デバイスを作ってみました!サイトにログインするときにパスワードとは別に入力する、アプリで生成した6桁の数字を入力するあれです。本体にQRコードリーダーとRFIDリーダーを内蔵していて、単独で2段階認証デバイスとして機能します。保存したデータは暗号化され、NFCカード内の秘密鍵で複合化することでセキュリティを確保しています。

主な機能
作成したきっかけ
システム構成
ソースプログラム
ケースの製作
セキュリティのこだわり
使い方
設定メニュー
目的別の機能の説明
トラブルQ&A
そのほかの話題
最後に

主な機能
・ワンタイムパスワード(TOTP)の生成
・QRコードをスキャンして認証用のサイトを登録
・Bluetoothキーボードとして機能し、パスワードをPCに送信
・AES 256bitでデータを暗号化して保存
・NFCカードに暗号化を解除するための秘密鍵を保存

このほかにも以下のような機能があります
・バーコードリーダー(PCに読み取ったデータを入力)
・NTPによる時刻設定、および手動設定も可
・時刻表示
・ブラウザを介したファイルの転送(バックアップ用途)
・https対応のWebサーバー
・自動電源オフ機能
・秘密鍵の移動(本体←→NFCカード)
・秘密鍵(NFC)の複製
・認証用のサイトのエクスポートQRコード表示
・内蔵バッテリーによる駆動

目次へ▲

元々2段階認証デバイスを作る予定はなく、QRコードリーダーとNFCリーダーで面白いものが作れないかなぁと考えてました。当初はQRコードから読み取ったURLをNFCに書き込んで、スマホにNFCを近づけるとページが開くものを考えていました。
そこからNFCについて調べてみるととても奥が深く、複雑なメモリ構造があって、パスワードによるプロテクトもできることが判明。NFCのプロテクト機能を使って高セキュリティなデバイスを作ったら面白そうだな!! ということで2段階認証デバイスにしました。

NFCカードにアクセスするためのライブラリとして、M5Stackの製品(WS1850S)で使用できるMFRC522_I2Cというライブラリがあります。まずはこのライブラリを活用するためのライブラリから作りました。完成したライブラリは こちら(NfcEasyWriter) からアクセスできます。

目次へ▲

使用したデバイス

・M5Stack M5 DinMeter
・M5Stack QR Code Scanner Unit
・M5Stack RFID 2 Unit

このほか、QRコードユニットとRFIDユニットの2つを接続するため、二又のGroveケーブルを使用しました。

使用できるNFCカード

Mifare Classic 1K
NTAG213 (144byte)
NTAG215 (504byte)
・NTAG216 (888byte) ※動作未確認

ハードウェア構成図

NFCカードにはAES 256bitで暗号化するための秘密鍵が入っています。本体と分離することで、NFCカードを持っている人だけしか操作できないようになっています。通常2段階認証の設定をするときはQRコードをスマホのアプリ(Google Authenticatorなど)で読み取りますが、本デバイスでも同様にQRコードスキャナーで読み取ります。読み取ったKEYは秘密鍵を使って暗号化され、本体内のストレージに保存します。

2段階認証を行うときはKEYを複合化してワンタイムパスワードを生成し、画面に表示します。本デバイスはBluetoothキーボードとしても動作するため、PCとペアリングすれば、表示されている数字を自動入力できます。

目次へ▲

今回作成したプログラムは GitHub にアップロードしました。

必要なライブラリ

別途以下のライブラリが必要です。Arduino IDEのライブラリマネージャーからインストールしてください。
・M5DinMeter
・M5UnitQRCode
・MFRC522_I2C
・Base32-Decode
・HTTPS_Server_Generic
・TOTP
・BleKeyboard ※これはZIPからインストール

設定が必要な箇所

以下の箇所はご自身の環境に合わせて修正してください。

secret.h

// WiFi接続
#define WIFI_SSID "****"
#define WIFI_PASS "****"

// 暗号化の追加フレーズ
#define IV_PHRASE "hogehoge"   // IV生成時の任意のフレーズ(NFCのパスワードの元になる)
#define BSECRET_PHRASE "hugahuga"   // 秘密鍵の任意のフレーズ

Wi-FiのSSIDとパスワードを設定します。Wi-Fiは時刻設定のために使うので、手動で時刻設定をするなら無くてもかまいません。暗号化の追加フレーズは、データを暗号化するための元となる情報なので、何か適当な文字を入れてください。

コンパイルの方法

コンパイルをする際は、Arduino IDEで以下のように設定します。
・Flash Size = 8MB
・Partition Scheme = custom

アプリのサイズが2MBを超えるので、カスタムパーティションを使っています。partition.csv というファイルがカスタムパーティションの設定ファイルです。またファイルシステムはSPIFFSではなくFATFSを使用します。

動作確認

コンパイルして書き込んだら、RFIDリーダーとQRコードスキャナーをGroveケーブルでDinMeterのPort Aに接続します。

2台ともI2C接続ですので、今回は二又のGroveケーブルを使用します。UNIT HUBのようなGroveポートを分岐するユニットでも代用できます。

目次へ▲

今回はDinMeterとRFIDリーダー、QRコードスキャナーを一体化させたケースを作りました。QRコードスキャナーは頻繁に使うものではないのでオプションにでもしようかと思ったのですが、RFIDリーダーを置くスペースを考えると、RFIDリーダーの下に配置するのが無難でした。

STLファイルは GitHub の stl/ フォルダの中にあります。

ケースと裏蓋とバッテリーのホルダーの3つパーツがあります。DinMeter内蔵のStampS3が結構発熱してバッテリーの温度が上がってしまいそうだったので、バッテリーを浮かせて空間を作るようにしてみました。
バッテリーホルダーは溝にカチッとはまるようになってます。

QRコードスキャナーはスイッチを間に挟んで、電源をオフできるようにしました。なぜかスキャンしていない状態でも発熱が大きく、仕様を見ても電源をオフするような機能はなさそうだったので、物理的に電源を切るようにしました。

ちなみに今回使用したフィラメントはCC3DのPLA MAX スレート グレーというもので、DinMeterの濃いグレーにかなり近い色でした。

目次へ▲

本デバイスは2段階認証というセキュリティに関わるデバイスなため、内部的にもセキュリティの高い設計にしました。メインとなるのは秘密鍵を使用した暗号化で、AES 256bitという強力な暗号化でデータを保護しています。ESP32-S3にはAESのハードウェアアクセラレーターが内蔵されているらしいので、このような処理も高速でできます。

以下は本デバイスでどのように暗号化の処理が行われているかを示す図です。ここで最も保護すべき情報は、2段階認証のワンタイムパスワードを生成するためのキー(SECRET)です。以下の図のOTP情報ファイルの紫色の KEY の内容がそれです。

OTP情報ファイルのKEY (SECRET)の暗号を解くには「秘密鍵A」が必要です。
秘密鍵AはNFCカードに暗号化して保存されています。
その暗号を解くには「秘密鍵B」が必要です。
さらにNFCカードのプロテクトを解くにはパスワードが必要です。

このへんはこだわった…というよりも、面白かったから無駄に複雑にしてみたところです。以下ではそれぞれ順を追って説明していきたいと思います。

秘密鍵BとIV(初期ベクトル)の生成

秘密鍵BとIVは起動時に本体内部で生成し、メモリの中だけに存在します。電源を切っても再び同じ値になるように、Wi-FiのMACアドレスと任意に与えた文字列をSHA256でハッシュ化した値を使用しています。2台のDinMeterで使う場合、MACアドレスが異なり生成される秘密鍵Bが変わるので、バックアップ→リストアで別のデバイスにコピーしても使用できない点に注意してください。

// SHA256でハッシュ化する  output:32byte=256bit
bool sha256(String input, byte* output, size_t outputSize) {
  if (input == nullptr || output == nullptr || outputSize < 32) return false;

  mbedtls_md_context_t ctx;
  mbedtls_md_init(&ctx);
  const mbedtls_md_info_t* mdInfo = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
  if (mdInfo == nullptr) {
    mbedtls_md_free(&ctx);
    return false;
  }
  if (mbedtls_md_setup(&ctx, mdInfo, 0) != 0) {
    mbedtls_md_free(&ctx);
    return false;
  }
  mbedtls_md_starts(&ctx);
  mbedtls_md_update(&ctx, (const unsigned char*)input.c_str(), input.length());
  mbedtls_md_finish(&ctx, output);
  mbedtls_md_free(&ctx);
  return true;
}
// 秘密鍵暗号化用の秘密鍵を生成する
  String text = WiFi.macAddress() + addText;
  uint8_t hash[32];
  if (!sha256(text, hash, sizeof(hash))) return false;
  memcpy(bsecret, hash, 32);

SHA256ハッシュで出てくる値は256bit (32byte)あります。秘密鍵Bは256bit (32byte)なので、32バイト全てを使用します。IVは128bit (16バイト)なので、先頭の16バイトを使用します。

NFCカードのプロテクト解除

NFCカードにはプロテクトがかかていて、パスワードで認証しないと読み書きはできないようになっています。このときに使うパスワードはIVを元に生成されます。NFCカードのパスワードは48bit (6バイト)なので、IVの先頭6バイトをコピーしています。

  // IVを元にNFCのパスワードを生成する
  memcpy(passwdNfc.keyByte, status.iv, sizeof(passwdNfc.keyByte));
  nfc.setAuthKey(&passwdNfc);

暗号化するたびにIVが毎回同じ値というのはおかしいかもしれませんが、今回はストレージに保存されずに電源を投入する都度同じ値を生成するという方針にしているので、とりあえず使いまわすことにします。

秘密鍵Aの複合化

NFCカードに入っている秘密鍵A(暗号化済み)を取り出したら、秘密鍵Bを使って複合化します。秘密鍵Aは電源を切るまでメモリの中に存在します。画面左上の鍵マークが点灯しているのが、メモリ内に秘密鍵Aが読み込まれていることを意味しています。

// AES 256bitで復号化する
size_t decrypt(byte* decryptedData, const byte* encryptedData, size_t encryptedSize, const byte* iv, const byte* key) {
  if (decryptedData == nullptr || encryptedData == nullptr || iv == nullptr || key == nullptr)  return 0;
  byte ivCopy[16];
  size_t decsize = 0;

  // AES初期化
  memcpy(ivCopy, iv, sizeof(ivCopy));
  mbedtls_aes_context aes;
  mbedtls_aes_init(&aes);
  if (mbedtls_aes_setkey_dec(&aes, key, 256) != 0) {
    mbedtls_aes_free(&aes);
    return 0;
  }
  
  // 復号化処理
  for (int i=0; i<encryptedSize; i+=16) {
    byte block[16];
    if (mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, 16, ivCopy, encryptedData+i, block) != 0) {
      mbedtls_aes_free(&aes);
      return 0;
    }
    memcpy(decryptedData+i, block, 16);
    decsize += 16;
  }
  mbedtls_aes_free(&aes);

  return decsize;
}
  // 秘密鍵を複合化するための秘密鍵を生成する
  getBSecret(bsecret, sizeof(bsecret) , BSECRET_PHRASE);

  // 秘密鍵BとIVを使って、秘密鍵Aを複合化する
  size_t declen = decrypt(deced, sdef.secretEnc, sizeof(SecretDef::secretEnc), status.iv, bsecret);

AES 256bitのCBCモードによる暗号化は128bit (16バイト)単位で行われるため、16バイトずつ処理するようになっています。

参考までに暗号化ルーチンはこちらです。専用の関数があるので楽ですね。

// AES 256bitで暗号化する
size_t encrypt(byte* data, size_t dataSize, const byte* iv, const byte* key, byte* encryptedData) {
  if (data == nullptr || iv == nullptr || key == nullptr || encryptedData == nullptr) return 0;
  size_t encsize = 0;
  byte ivCopy[16];

  // AES初期化
  memcpy(ivCopy, iv, 16);
  mbedtls_aes_context aes;
  mbedtls_aes_init(&aes);
  if (mbedtls_aes_setkey_enc(&aes, key, 256) != 0) {
    mbedtls_aes_free(&aes);
    return 0;
  }

  // 暗号化処理
  for (int i=0; i<dataSize; i+=16) {
    size_t blockSize = (dataSize-i >= 16) ? 16 : (dataSize-i);
    byte block[16] = {0};
    memcpy(block, data+i, blockSize);
    if (mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, 16, ivCopy, block, encryptedData+i) != 0) {
      mbedtls_aes_free(&aes);
      return 0;
    }
    encsize += 16;
  }
  mbedtls_aes_free(&aes);
 
  return encsize;
}

ワンタイムパスワードのキー (SECRET)の複合化

2段階認証のワンタイムパスワードを生成するためのキー(SECRET)は、本体のFatFS内にファイルとして存在しています。メタデータは平文のままですが、キーは秘密鍵Aで暗号化されているので、秘密鍵Aを使って複合化します。

  // 秘密鍵AとIVを使って、キーを複合化する
  size_t declen = decrypt(decSecret, reinterpret_cast<byte*>(tp->secret), seclen, status.iv, status.secret);

ワンタイムパスワードの生成

最後にキー(SECRET)をBASE32デコードした値と、現在時刻をTOTPライブラリに与えれば、6桁のワンタイムパスワードが得られます。

// 指定時刻のワンタイムパスワードを取得する
String getTotp(TotpParams* tp, time_t epoch) {
  size_t maxOut = strlen(tp->secret);
  char decodedSecret[maxOut];
  int decLen = base32decode(tp->secret, (unsigned char*) decodedSecret, maxOut);
  if (decLen < 0) return "";
  TOTP totp = TOTP(reinterpret_cast<uint8_t*>(decodedSecret), decLen, tp->period);
  char* code = totp.getCode(epoch);
  return (String)code;
}

ちなみに登録する際のQRコードはURIが otpauth://totp/ で始まる形式になっています。たとえば発行者がCOMPANY、アカウント名がUSER、キー(Base32化後)がHOGEHOGEだった場合、以下のようになります。これをQRコードにして読み込ませているわけです。

otpauth://totp/USER?secret=HOGEHOGE&issuer=COMPANY

Google Authenticatorでは正常に読み込めるのですが、IIJのSmartKeyでは正常に読み取れません。以下のような形式にしないと、発行者やアカウント名が正常に認識してくれないのです。

otpauth://totp/COMPANY%3A%20USER?secret=HOGEHOGE&issuer=COMPANY

本デバイスにある2段階認証のサイトのエクスポート機能は、このような形式のURIをQRコードにして表示しているだけです。

HTTPS対応 Webサーバー

本体内部のストレージのファイルをバックアップするために、Webサーバーを搭載しています。これまで説明してきたように、秘密鍵のような秘匿性の高いデータを安全に保護してきたのに、バックアップ時に平文で転送されてしまったら台無しですよね。そこでWebサーバーもSSLで暗号化したいと思います。調べてみると HTTPS_Server_Generic というHTTPのWebサーバーライブラリがありました。

このライブラリが面白いのは、自己署名証明書(通称オレオレ証明書)をオンデマンドで生成できる点です。通常は別のPC上で生成したバイナリデータをC++のソースコード化してプログラムに埋め込んだりしますが、このライブラリを使えばその必要はありません。

// オレオレ証明書を作成してFatFSに保存する
bool httpsGenerateCertificate() {
  SSLCert* cert = new SSLCert();
  bool res = false;
  // 証明書を作成
  int createCertResult = createSelfSignedCert(
    *cert,
    KEYSIZE_1024,
    "CN=esp32.local,O=OreoreCompany,C=JP",
    "20240101000000",
    "20350101000000"
  );
  if (createCertResult != 0) {
    return false;
  }

  // FatFSに保存
  if (cert->getPKLength() > 0 && cert->getCertLength() > 0) {
    res = saveFile(cert->getPKData(), cert->getPKLength(), FN_SSL_KEY);
    if (!res) return false;
    res = saveFile(cert->getCertData(), cert->getCertLength(), FN_SSL_CERT);
  }

  delete cert;
  return true;
}

これを実行すると完了するまで5~10秒くらいかかります。しかしこれは最初の1回だけ実行すればいいものなので、初期セットアップのときに実行すればOKです。実際にブラウザでアクセスすると、正式な(?)オレオレ証明書が生成できていることがわかります。

さらにBASIC認証を加えることで、操作した人しかアクセスできないようにしました。パスワードはランダムで、実行する度に変わります。

目次へ▲

はじめて設定するときに流れ。
1. 初期セットアップ
2. 画面の見かた
3. 時刻設定
4. 2段階認証のサイトの登録
5. BluetoothでPCとペアリングする
6. 実際にログインしてみる

(1) 初期セットアップ

最初に起動したときは初期セットアップ画面が開きます。ロータリーエンコーダーで動かし、画面の指示に従います。

フォーマットします。アプリ領域を大きく取ってるのでFatFSの領域は小さめですが、それでも856KBあります。

データを暗号化するための秘密鍵の保存先を設定します。後で保存先を変更することもできます。今回は例としてNFCを選択します。空のNFCカード(NTAG213/215を推薦)用意してください。

RFIDリーダーの読み取り部に、NFCカードの中心が来るように置きます。認識するとBEEP音が鳴って書き込みが始まりますので、動かさないようにしてください。

書き込みが完了したらNFCカードを外します。

以上で初期セットアップは完了です!

(2) 画面の見かた

画面のユーザーインターフェースは下図のように、左側が現在の状態を示すステータス表示、右側がメインメニューになっています。

ロータリーエンコーダーを回すとメインメニューのカーソルが移動するので、実行したいメニューのところで押します。設定メニューに入るとさらにサブメニューが表示されます。

(3) 時刻設定

TOTP (Time-based One-Time Password)は時間を基本にパスワードを生成する技術です。そのため現在時刻が正確に設定されている必要があります。M5 DinMeterにはRTCが搭載されているため、バッテリーを接続していればUSBで接続していなくても時刻は保持されます。

メインメニューの歯車アイコンを押して、サブメニューの「NTP時刻同期」を押します。

Wi-Fiに接続して自動的に時刻同期が行われます。しばらく時間がかかります。

もしも自動的に設定された時刻がずれている場合には、「手動時刻設定」にて手動で設定することもできます。

(4) 2段階認証のサイトの登録

新しくサイトを登録して2段階認証を設定するには、設定用のQRコードを読み取ります。本デバイスが正常に動作しなくなった場合に備えて、スマホのアプリの方にも登録しておくことをおすすめします。

サブメニューの「サイトの追加」を押します。

QRコードリーダーのライトが光りますので、QRコードを読み取ります。認識するとBEEP音が鳴り、自動的に次の画面に移動します。ボタンを押す必要はありません。

発行者名とアカウント名、現在のワンタイムパスワードが表示されます。よければ次に進みます。

登録するか聞いてきますので、YESを押します。

秘密鍵をNFCカードに保存している場合は、NFCカードの読み取りを要求されますのでRFIDリーダーに置きます。

以上で登録は完了です。秘密鍵は一度読み取るとメモリ内に保存されるので、電源を切るまで有効です。続けて登録する際は省略されます。

(5) BluetoothでPCとペアリングする

本デバイスはBluetoothキーボードしてPCに接続します。手入力する場合はやらなくてもかまいません。

サブメニューの「BLEペアリング」を押します。

PCで “M5Authenticator” というデバイス名を接続するように表示されます。

PCでBluetoothとデバイス>デバイスの追加>Bluetoothを選択します。

しばらくすると “M5Authenticator” が表示されますので、選択します。

「ペアリングに成功しました」と表示されたら完了です。

左側のステータス表示に、Bluetoothのアイコンが表示されているときは、PCとペアリングしていることを意味しています。

以上でペアリングは完了です。

(6) 実際にログインしてみる

それでは実際にサイトにログインしてみましょう。

メインメニュー右上の鍵アイコン押します。

表示したいサイトを選択します。

するとワンタイムパスワードが表示されます。小さな文字で下に表示されている2つの数字は、前と次のパスワードです。時刻が合ってなくてずれてしまう場合は、この数字を手入力することもできます。

ログインしたいサイトの入力欄にカーソルを合わせたら「送信」ボタンを押すと自動的に入力されます。

送信したあとに再度送信することもできます。パスワードは30秒おきに切り替わるので、切り替わるタイミングが近くなると色が変わって知らせます。

横の電源OFFを選択すれば、メインメニューに戻らなくても電源を切ることができます。いちいち電源を切る手間を省くために、デフォルトでは10秒で自動的に電源が切れるようになっています。時間は設定で変更できます。

目次へ▲

メインメニューの歯車アイコンを押すと表示されるサブメニューの一覧です。後述する「目的別の機能の説明」で説明します。

機能説明
サイトの追加2段階認証のサイトを追加します。
サイトの削除登録した2段階認証のサイトを削除します。
BLEペアリングPCとBluetoothでペアリングします。
バックアップ本デバイス内のファイルをダウンロードしたりアップロードできます。
設定説明
NTP時刻同期インターネットに接続して時刻を設定します。
手動時刻設定手動で時刻を設定します。
自動改行の設定PCにワンタイムパスワードを送信する際に、最後に改行を入れるかどうか。
無操作 自動電源オフの設定操作をしていないときに自動的に電源を切る時間の設定。
PW送信 自動電源オフの設定ワンタイムパスワード送信後に自動的に電源を切る時間の設定。
静音モードBEEP音を鳴らさないようにする設定。
JIS配列モードPCにキー入力する際に、なるべくJIS配列に合わせて送信する設定。
開発者モード開発者用の機能を使えるようにする設定。
セキュリティ説明
秘密鍵の移動秘密鍵を本体からNFC、またはNFCから本体へ移動します。
NFCの秘密鍵を複製予備用にNFCカードの秘密鍵を複製します。
NFCフォーマット秘密鍵として使用したNFCカードを消去します。
サイトのエクスポート登録した2段階認証のサイトをスマホのアプリにエクスポートします。
開発者説明
本体フォーマット本デバイスの全てのデータを消去します。
HEXダンプFatFSのファイルやNFCカードのデータをシリアルにダンプします。
DEBUG BLE全ASCII送信全ASCIIコードをキー入力します。キー刻印との違いを調べるためのテスト用。
SSL証明書再生成WebサーバーのSSL用の証明書を再生成します。

目次へ▲

バーコードやQRコードを読み取ってPCに送信したい

メインメニューのバーコードアイコンを選択すると、QRコードリーダーのライトが点灯します。この状態でQRコードやバーコードを読み取ると、PCに自動的に入力されます。

商品のバーコードを読み取ってAmazonで検索すると便利です!

現在時刻を表示したい

メインメニューの時計アイコンを選択すると、現在時刻が表示されます。もう一度ボタンを押すとメインメニューに戻ります。

登録した2段階認証のサイトを削除したい

サブメニューの「サイトの削除」を選択すると、現在登録されているサイトの一覧が表示されます。削除したいサイトを選択して画面の指示通りに進みます。

故障に備えてバックアップを取りたい

サブメニューの「バックアップ」を選択するとQRコードが表示されるので、そのページにアクセスします。ユーザー名・パスワードは左に書かれている内容を入力します。

はじめてアクセスするときは証明書のエラーが出るので、詳細設定を押します。「~にアクセスする(安全ではありません)」をクリックするとページが表示されます。

ファイル名の部分をクリックするとダウンロードできます。アップロードする場合はファイルを選択してアップロードします。その他、見ての通りです。

ESP32でSSL通信を行う負荷がかかることをやってるので、時々エラーになります。ファイル一覧が出ない場合は「再読込」を押してみてください。

ファイル名用途バックアップ
/config.bin設定ファイル〇必要
/secret_enc.bin秘密鍵〇必要
/ssl_private.derSSL用秘密鍵×しなくていい
/ssl_cert.crtSSL用証明書×しなくていい
/otp-******.binサイト別の2段階認証の情報〇必要

/secret_enc.bin については、NFCカードに秘密鍵を保存している場合は存在しません。バックアップが必要なファイルを保存しておけば、最悪なんとかなるかもしれません。

バックアップファイルからリストアしたい

サイト別の2段階認証の情報ファイル(/otp-******.bin)だけを削除してしまった場合は、そのファイルをアップロードすればOKです。
もし本体を初期化してしまった場合は、まずは仮で初期セットアップを行い、バックアップしたファイルを本体へアップロードします。

PCにワンタイムパスワードを送信する際に、最後に改行を入れたい

サブメニュー「自動改行の設定」で設定できます。
最後にENTERキーを押すとフォームが自動送信されるので、有効にしておくと便利です。

操作をしていないときに自動的に電源を切りたい

サブメニュー「無操作 自動電源オフの設定」で設定できます。
バッテリー給電のみで使用する場合など、省バッテリー運用をしたい場合は自動電源オフを使用すると便利です。ボタンの操作(回転・押す)が一定期間ないと電源が切れます。

ワンタイムパスワード送信後に自動的に電源を切りたい

サブメニュー「PW送信 自動電源オフの設定」で設定できます。
ワンタイムパスワード送信後にいちいち電源オフをするのは面倒なので、自動で電源オフするようにしておくと便利です。カウントダウン中でも操作をすれば残り秒数はリセットされます。

BEEP音を鳴らさないようにしたい

サブメニュー「静音モード」で設定できます。
ただしQRコードユニット内蔵のブザーは電源投入時に鳴ってしまいます。

Bluetoothのキーボードの配列を直したい

サブメニュー「JIS配列モード」で設定できます。
“BleKeyboard”ライブラリはUS配列のキーボードとして認識されるため、バーコードリーダーでURLなどを読み込むと、違う文字が出てしまう場合があります。設定を「JIS配列」にすると、なるべく同じ文字が出るように頑張ります。

どうしても気になる場合は「US配列」に設定して、PC側でキーボードの種類がUS配列になるようにレジストリを編集してみてください。「JIS配列」の設定の方は、USキーボードで「この文字を出すために誤ったキーを押す」という処理をして、結果的に正しい文字が出るようにしているだけです。_(アンダースコア)など一部の文字が出せません。

開発者モードを有効にしたい

「開発者」カテゴリーのサブメニューは、あらかじめ開発者モードをオンにしておく必要があります。
サブメニュー「開発者モード」で設定できます。

秘密鍵をNFCカードに移動したい、本体に移動したい

データを暗号化するために使用する秘密鍵は、NFCカードに保存しておくと安全です。しかし、ログインする度にいちいちNFCカードをかざすのが面倒な場合もあるでしょう。秘密鍵をNFCカードから本体に移動することもできます。同様に本体からNFCカードに移動することもできます。

サブメニューの「秘密鍵の移動」を選択すると、移動先の確認画面が表示されるので、画面の指示通りに進みます。

NFCカードを置く画面になったら、RFIDリーダーにNFCカードを置きます。

予備のNFCカード作りたい

秘密鍵が入ったNFCカードを紛失したり壊れると、2段階認証のワンタイムパスワードを表示することができなくなります。AES 256bit暗号化なので解読はまず不可能。なので予備を作っておくと安心です。

サブメニューの「NFC秘密鍵を複製」を選択します。
秘密鍵の保存先を本体に設定している場合は複製はできません。先に保存先を移動してください。

はじめにオリジナルのNFCカードを読み込みます。順番を間違えないように注意してください。

次に新しいNFCカードに書き込みます。まっさらなNFCカードを用意します。

秘密鍵が入ったNFCカードを消去したい

秘密鍵が入ったNFCカードが1枚だけなら、秘密鍵を本体に移動すれば、NFCカードは空になります。プロテクトも解除されて他で使用できるはずです。もし2枚以上作成している場合は、個別に消去することもできます。

サブメニューの「NFCフォーマット」を選択します。消去したいNFCカードを用意します。

別の段階認証のアプリに登録したサイトエクスポートしたい

2段階認証アプリに登録するためのQRコードは通常最初に1回しか表示されません。スマホのアプリに移行させたい場合は、本デバイスからエクスポートすることができます。

サブメニューの「サイトのエクスポート」を選択し、エクスポートしたいサイトを選択します。

QRコードが表示されるので、Google Authenticatorなどの2段階認証アプリで読み込ませます。

※このQRコードは最初に表示されたものと全く同じではないため、正常にエクスポートできない場合もあります。

以降の「開発者」カテゴリーのサブメニューは、あらかじめ開発者モードをオンにしておく必要があります。

本デバイスを初期化したい

※NFCカードに秘密鍵を保存している場合は、先に本体に移動してから、本体をフォーマットしてください。
サブメニューの「本体フォーマット」を選択して、画面の指示通りに進みます。

ファイルやNFCカードの中身が見たい

ファイルやNFCカードのデータをシリアルポートにHEXダンプします。Arduino IDEやTeratermなどで見ることができます。本デバイスの画面上には表示されません。

サブメニューの「本体フォーマット」を選択して、表示させたいファイルまたはNFCカードを選択します。
秘密鍵が保存されているNFCカードの場合は “暗号化済” の方を選択すると、プロテクト領域も表示することができます。

WebサーバーのSSL証明書を再生成したい

バックアップからのリストアに失敗してWebサーバーにアクセスできなくなってしまった場合は、再生成ができます。これはWebアクセスのみに使用されるものなので、再生成しても2段階認証のデータには影響はありません。

目次へ▲

時には妥協も必要なのです。

ボタンを押しても電源がすぐ切れる

仕様です。もう少しボタンを長く押してみましょう。

バッテリー駆動時にカーソルが逆方向に動く

ロータリーエンコーダーをゆっくり回してみましょう。バッテリー駆動時は処理が遅くなる場合があります。極端に遅くなったり回復したり不安定な動作をすることもあります。

使っているうちに画面がバグった

再起動しましょう。どこかでメモリリークしているのかもしれません。

操作不能で電源が切れない

ボタンを10秒以上長押ししてみてください。強制電源オフ機能を入れてあります。

QRコードスキャナーが温かくなる

通電していると発熱してしまうようです(仕様?)。そこで使用時のみ電源を供給するように、Groveポートの+5Vの間にスイッチを付けました。
QRコードをスキャンするときだけ電源を入れるようにしています。

プログラムを修正して再書き込みしたらデータは消える?

パーティション構成を変えなければ、FatFSのストレージ領域は残るはずです。しかしArduino IDEが勝手にパーティション構成を変えてしまうことがあり、アップロードに失敗するわデータは消えるわという散々な目に合うかもしれません。事前にバックアップしておくことをお忘れなく。

電源をオフにしているのにUSB給電だとなんか温かい

実際のところ、なぜか電源が切れていないようです。M5 DinMeterはちょっと特殊な構造になっていて、電源投入直後にHOLDピン(GPIO 46)をHIGHにする必要があります。逆に電源をオフにするときはLOWにします。しかしUSBから給電されているとオフになりません。公式ドキュメントを見ると「電源オフ: 外部USB電源供給がない場合…」という但し書きがあり、「USB電源供給がある場合…」には触れていない点が引っかかります。

コンパイルして書き込もうとすると途中で止まる

M5 DinMeterの裏側のStampS3のG0ボタンを押しながら、USBケーブルを接続するか、リセットボタンを押してから行うとうまくいきます。公式ドキュメントの動画で解説されています。

2段階認証のサイトの名前やアカウント名を修正したい

変更する機能はありませんが、otp-***.bin というファイルは以下の構造体のデータ形式のまま書き込まれてるので、Binary Editor BZのようなバイナリエディタで該当箇所を編集する方法もあります。secretについては暗号化済みのデータなので変更不可です。

// TOTP URIのパース結果、FatFS保存形式
struct TotpParams {
  uint8_t version = 1;
  char    issuer[32] = {0};
  char    account[32] = {0};
  char    secret[64] = {0};
  uint8_t algorithm = 1;
  uint8_t digit = 6;
  uint8_t period = 30;
  byte    rfui[28] = {0};    // 予約
};

USBから給電せずにシリアルの出力を見るには?

バッテリー駆動時の電圧のログを取りたかったので、D+, D-, GND のみ結線して、+5Vだけ外したケーブルを作ってPCに接続しました。

M5 DinMeter単体で動かせるか?

試してませんが、秘密鍵を本体に保存する設定にして、以下の値をfalseにして再度書き込めば、本体のみでの運用はできるかもしれません。

#define USE_RFID       true // RFIDユニットを使用する
#define USE_QRCODE     true // QR-CODEユニットを使用する

違うNFCカードの秘密鍵読ませたらエラーも出ずにいけちゃったんだけど

でもそれで出てくるワンタイムパスワードはでたらめです。秘密鍵が正しいかチェックする仕組みを作り忘れました。

目次へ▲

USB給電時になぜか電源が切れないからDeep Sleepしてみる

前述の電源切れない問題をなんとかすべく、HOLDピンをLOWにしても電源が切れなかった場合は、ESP32-S3をDeep Sleepするようにしてみました。Deep Sleepに入ったあと、ボタンを押すとDeep Sleepから復帰して再起動するようにしています。

// 【メイン】電源オフ
bool funcPoweroff() {
  // BLE切断
  bleKeyboard.end();  // 実際は何も実装されてない

  // HOLDピンをLOWにして電源を切る(バッテリー駆動時はDC-DC回路がオフになる)
  DinMeter.Rtc.clearIRQ();
  DinMeter.Rtc.disableIRQ();
  DinMeter.Display.fillScreen(TFT_BLACK);
  while (M5.BtnA.isPressed()) {
    M5.update();
    delay(100);
  }
  delay(200);
  digitalWrite(POWER_HOLD_PIN, LOW);  // スリープする。ボタンAで復帰
  delay(1000);

  // USB接続時は電源が切れないのでdeepsleepする
  DinMeter.Display.setBrightness(0);
  while (!M5.BtnA.isPressed()) {
    M5.update();
    delay(100);
  }
  esp_sleep_enable_ext0_wakeup((gpio_num_t)GPIO_BTN_A, LOW);

  // 起きたら再起動
  restart();  // 再起動
  return true;
}

QRコードスキャナーが温かくなるほど電流が流れているのか?

マルチメーターで測定してみました。
QRコードユニット : 118mA
RFIDリーダー : 40mA

いずれもアイドル時の電流値です。結構食いますね。特にQRコードユニットの値が大きいのが気になります。バッテリー駆動時はQRコードユニットの電源スイッチをオンにしていると途中でリセットがかかってしまう場合もあります。回路図を見るとバッテリー(3.7V)から5Vに昇圧してGroveポートに供給しているようです。こうゆう消費電力が大きいデバイスだと能力不足になるんでしょうね。

バッテリー残量、電圧の謎

Powerクラスにはバッテリーの状態を取得する関数があります。USB給電時とバッテリー駆動時でどのように変わるかテストしてみました。

getBatteryLevel()バッテリーの残量(0-100)
getBatteryVoltage()バッテリーの電圧(mV)
getBatteryCurrent()バッテリーの電流
isCharging()充電中か否か
  int32_t lv = DinMeter.Power.getBatteryLevel();
  int16_t v = DinMeter.Power.getBatteryVoltage();
  int32_t ic = DinMeter.Power.getBatteryCurrent();
  int32_t crg = DinMeter.Power.isCharging();
  int r = analogRead(10);
  float v2 = (float)analogReadMilliVolts(10) * 2 / 1000;
項目USB給電
QRオフ
USB給電
QRオン
バッテリー
QRオフ
バッテリー
QRオン
getBatteryLevel()3908451638584674
getBatteryVoltage()8110075100
getBatteryCurrent()0000
isCharging()2222
r2263274823292889
v23.934.543.884.67

この結果を見て釈然としない点があります。getBatteryLevel()は mV での電圧で、計算値v2の電圧(V)とほぼ同じなのですが、QRコードスキャナーがオンのときとオフのときで大きな違いがあります。この電圧値は回路図を見ると、VBAT_INとGND間に1MΩと1MΩの抵抗で分圧したBATADCピンがあり、それがGPIO 10に接続されています。これがどうしてQRコードスキャナーをオンにすると上がってしまうのか、よくわかりません。

テスターで直接バッテリー端子の電圧を測ってみたところ、QRコードスキャナーがオフのときは3.8V、オンのときは3.7Vくらいでした。そもそも3.7Vのバッテリーで出力電圧が4.6Vとかおかしいですから、測定値が間違ってますね。

getBatteryCurrent()の値が0なのは、元々電流測定の機能が付いてないのでしょう。isCharging()が2を返しているのは非対応だからのようですが、これがないとUSB給電中かバッテリー駆動中かわからなくてちょっと困ります。M5 DinMeterはちょっとクセが強いデバイスです。

NFCカードの読み書きに興味がある!

↓がんばって作ったのでよろしければぜひ。

NFCカードを簡単に読み書きするためのライブラリ
https://github.com/kaz-mac/NfcEasyWriter

目次へ▲

気づけばArduinoで作るにはあまりに巨大なプログラムになってしまいました。フォントデータが大きいとはいえ、アプリのサイズは2.3MBもあります。ここまで苦労して時間をかけて作ったものですが、たぶん私はこのデバイスを使うことはないと思います。それは作ることが目的で、使うことが目的ではないからです。

作ってるのが楽しい。特にNFCの仕様を調べているときが一番面白かったですね。実生活でNFCが使われる場面というと、物を識別したり、入退室の認証をしたり、3Dプリンターのフィラメントの情報を取得したり、様々な場所で利用されています。物の認識だけならUIDという、工場出荷時に書き込まれている一意な情報を読み取るだけでできます。データを取得するならデータ領域を読み込むだけです。しかし改竄されて困るようなケースでは書き込みができないうようにしたり、読み込みもプロテクトする必要がでてきます。このようなプロテクト機能を持つ製品を使用すればデータの保護も可能です。どうやったらプロテクトができるのか?? めちゃくちゃワクワクしますね!

MIFARE Classic 1Kのプロテクトについては こちらのブログ の解説がとてもわかりやすく、MFRC522_I2Cライブラリにもアクセスビットを計算する関数があるので、これを利用すれば簡単にプロテクトがかけられます。ところがNTAG 21x(MIFARE Ultralight)のプロテクトには非対応で、対応している他のライブラリもあまり見つかりませんでした。無いなら作るしかないな、ということでデータシートを参考に自分で NfcEasyWriter というライブラリを作りました。認証する仕組み自体はたいしたことはないのですが、設定用のデータの構成がまた興味深くでワクワクが止まりませんでした。笑

これはNTAG21xのメモリ構造で、1ページが4バイトで構成されていて、NTAG213なら44ページまで存在します。4~39ページが自由に書き込めるユーザー領域です。設定用の領域が41と42ページ、パスワードが43ページにあります。この設定用の領域はさらに細かく、以下のように分かれています。

AUTH0 は8ビットのデータで、パスワード認証が必要なページの先頭を指定します。たとえば0x04と指定すれば4ページ以降の全てのページが要認証になります。その際に使用するパスワードがPWDとPACKです。PWDは4バイト(32ビット)あり、認証する際に送信するパスワードになります。PACKは2バイト(16ビット)で、認証が成功した際に戻ってくる値で、PACKを比較することで本当に認証が成功したか確かめられるようになっています。PWDとPACKは書き込み専用のデータ領域になっているので、この部分のデータを読んでもデータは全て 00 になります。

PROT は1ビットのデータで、1にすると書き込みが制限されます。パスワード認証を行えば書き込みができ、読み込みだけなら認証なしで可能です。この辺がMIFARE Classicと大きく違います。CFGLCK は設定を変更できないようにするための1回きりの設定で、1にするともう元には戻せません。ただしPWDとPACKは変更できるそうです。設定は変更されたくないけどパスワードは変更していい、というニーズがどれくらいあるのかちょっと謎ですが。

最後に面白いのが AUTHLIM です。AUTHLIM は3ビットのデータで、認証失敗を許容する回数です。これを設定すると、パスワードを〇回間違えたら二度とアクセスできないようにする自爆機能が作れるんです。試しにどんな風に動作するのか実験してみました。設定できる値は 0~7 で0だと無制限になります。

void developAuthfailLimit() {
  if (! nfc.isUltralight()) return;
  ULConfig ulconf;
  const byte failLimit = 7;  // 0 or 1-7
  bool res;
  if (nfc.readConfigDataUL(&ulconf)) {
    ulconf.ACCESS = (ulconf.ACCESS & ~0x07) | (failLimit & 0x07);   // AUTHLIM
    res = nfc.writeConfigDataUL(&ulconf);   // 一旦AUTHLIMのみ変更
    if (res) {  // モード変更(パスワード、ページ制限)
      res = nfc.writeProtect(PRT_PASSWD_RW, &passwdGood, 16, 0, PRT_NOPASS_RW);
    }
    nfc.setAuthKey(&passwdGood);    // 正しいパスワード
    ULConfigEx ulconfex;
    showConfigInfo(&ulconfex, true);
  }
  res = nfc.unauthUL();
  return;
}

AUTHLIMを7にしてわざとパスワード認証を失敗させてみたところ、以下のような動作をすることがわかりました。
・設定した回数を超えるとロックされ、以降の認証は全て失敗する
・ロックされても保護エリア(AUTH0)より前のページはアクセスできる
・失敗は累計7回ではなく、連続7回で発動。6回失敗後に成功すれば問題ない。
・保護エリアに非認証状態でアクセス(=エラーになる)してもカウントされない
へぇぇ、面白ーい。

おっと、最後にと言いながら脱線しちゃいましたね。

今回いろいろ調べたことで、NFCの仕組みやTOTPの仕組み、データの暗号化やWebサーバーのhttps化など、いろいろな方法を知ることができました。次回も作って終わりの何かを作っていきたいと思います。

目次へ▲

LINEで送る
Pocket