FlutterでGoogle Authenticatorを自作した👨‍💻

## サマリ 📖

年末年始にまとまった時間が取れたのでFlutterの勉強を兼ねて**自作ワンタイムパスワードアプリ**[^1]（Google Authenticatorもどき）を作りました。
下記GIFのように、2段階認証用のQRコードを読み取って、ワンタイムパスワードを取得することができます。
現状Googleにしか対応していませんが、QRコードを読み取り正しいワンタイムパスワードを発行し、Googleログイン時の2段階認証を通過できます。

https://github.com/kawamou/toy-authenticator

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287774/c0c9d629-e9d2-33c1-7364-f93d9d3ccbe9.gif)
（GIF中で使っているQRコードはダミーです。[`qrencode`](https://fukuchi.org/works/qrencode/index.html.ja)で適当に作りました）

## 概要編 👨‍🏫
### ワンタイムパスワードの仕組み

基本的にGoogle Authenticator等のワンタイムパスワードを発行するサービスは、**TOTP（Time-based One-time Password）アルゴリズム**に従っています。

>TOTPアルゴリズムはRFC6238で定義されたアルゴリズムで、サーバとクライアントが共有する秘密鍵および現在時刻から確認用のコードを生成するものです。

[TOTPを実装する](https://blog.web-apps.tech/implementing_totp/)

仕組みは非常にシンプルで分かりやすいです。

```
- QRコードには秘密鍵が含まれている
- クライアントはQRコードを読み取り秘密鍵を入手
- 秘密鍵 + 現在時刻をもとにパスコードを算出。サーバに送信
- サーバも同様の秘密鍵を持っているのでパスコードを算出し一致を確認する
```

つまり、TOTPはざっくり言うと**現在時刻（unix時間）と秘密鍵で決まる関数**[^2]です。
そのため、秘密鍵（と各種設定値）さえサーバとクライアントで共有できれば全く同じコードを生成できます。
下記の図が非常に分かりやすかったです。

![image.png](https://cdn-ssl-devio-img.classmethod.jp/wp-content/uploads/2019/07/totp-implementation-pure-python01.png)

[AWSのMFAの仕組みを実装して読み解いてみた](https://dev.classmethod.jp/articles/totp-implementation-pure-python/)

例えばGoogleサービスの場合だとQRコードを読み取ると下記のような文字列が取得できます[^3]。

```
otpauth://totp/Google:{user_name}@{service_name}?secret={secret_base32}&issuer=Google
```

`secret`部分が`base32`でエンコードされた秘密鍵を表しています。
こちらと現在時刻を組み合わせてパスコードを算出します。

正確なアルゴリズムの内容については前述のリンク先等が詳しいです。

## 実装編 👨‍💻
### TOTPアルゴリズム

自身で実装しても良かったのですが、便利なパッケージ（`dart-otp`）があったので利用しました。
興味ある方は自前実装するのもアリだと思います。ロジック自体はDartコードで100-200行くらいで実装できます。

https://github.com/Daegalus/dart-otp/blob/master/lib/otp.dart

### QRコード読み取り

下記を参考に実装しました。

https://zuma-lab.com/posts/flutter-qr-code-scanner-camera

QRコード関連パッケージには`barcode_scan`と`qr_code_scanner`等があります。
前者は開発が止まっているようですが、monoさんが[`barcode_scan2`](https://pub.dev/packages/barcode_scan2)として復活させている模様。
今回は実装した後に`barcode_scan2`の存在を知ったため`qr_code_scanner`を使っています。

実装する上で特に手詰まりはありませんでしたが、強いて言うなら、QRコードが表示されている間、無限に読み取りのコールバックが呼ばれる問題がありました。
公式のIssueの通り、一度読み取った時点で`firstDetection`フラグを立て、後続のコールバックを止めるようにしました。

https://github.com/juliuscanute/qr_code_scanner/issues/11

### アニメーション

アニメーションはトップ画面のカウントダウンタイマー的な部分で使っています。
当初Flutterアニメーションの理解が非常に弱かったのですが、itomeさんの記事を読んでかなり理解が深まりました。
メチャクチャ分かりやすかったです。

https://itome.team/blog/2019/12/flutter-advent-calendar-day15/

https://itome.team/blog/2019/12/flutter-advent-calendar-day16/

また、Flutterを書き進めるとなるべく`StatefulWidget`を廃したい力学が働いてきます（？）が、アニメーションについては素直に`StatefulWidget`で実装しても良い気がしました（今回も`StatefulWiget`で実装しています）。

https://twitter.com/_mono/status/1164289764079521792

サークルの描画自体の実装はこちらを参考にしました。

https://medium.com/@ankittale4/architecture-a-radial-progress-bar-with-flutter-bf824d19ea30

### 余談：Flutterのキャッチアップ方法

完全にFlutter初見だったので、まずは下記、社の先輩が書いた記事を参考にさせていただき、ざっくりとFlutterの全体感を掴みました。

https://zenn.dev/tsuruo/articles/6cc7b5bf24bb65

contextやWidgetツリー等の各種ツリー、Riverpodの裏側の理解はこちらの記事（有料ですが）が非常に分かりやすかったです。

https://zenn.dev/chooyan/books/934f823764db62

## その他参考

GIFを作るのに参考にさせていただきました。

https://qiita.com/yusuga/items/ba7b5c2cac3f2928f040

[^1]: Googleのワンタイムパスワードが発行できることは確認していますが、飽くまで勉強を兼ねて作ったオモチャプロダクトです。実用には不向きです
[^2]: 他にも必要な定数はありますが
[^3]: どのアプリもotpauth URIの仕様は大枠では共通ですが、細かい箇所が異なっています