CLI で Authy と共通の MFA Token を生成する


AWS や Google 等で利用可能な MFA の仮想デバイスとしては、複数デバイス間の同期や Chrome Extension を擁する [Authy](https://www.authy.com/) が便利なわけですが、PC 特に CLI を使ってるといちいちデバイス or Chrome Extension を開く必要があり、かなり面倒です。
CLI から token を生成する [OATH Toolkit](http://www.nongnu.org/oath-toolkit/) の oathtool を使えば、この面倒さが少しは解消され、スクリプト化による自動処理も可能になります。
ただし、oathtool 自体は毎回 SecretKey が必要になるため、mfacodegen という名の wrapper script を用意して便利に利用できるようにします。

利用イメージは以下の通り。

![mfacodegen.png](https://qiita-image-store.s3.amazonaws.com/0/15611/fae5560b-064b-a9a3-209e-2bad965ca401.png "mfacodegen.png")


## Authy Desktop から SecretKey を取得する

MFA では

- TOTP (Time-based One Time Password) [RFC 6238](https://tools.ietf.org/html/rfc6238)
- HOTP (HMAC-based One Time Password) [RFC 4226](https://tools.ietf.org/html/rfc4226)

が使われていますが、これらの token を生成するには SecretKey が必要となります。

SecretKey は MFA 設定時の QR code 内に仕込まれているのですが、既に Authy を設定済の場合は アプリ上から SecretKey を見ることが出来ません。
ただし Authy は定期的に token を生成するために内部的には当然 SecretKey を保持しています。
Authy Desktop は JS で token を生成しているため、デバッグポートを有効にした状態でアプリを起動し、Developer Tools で内部を覗けばバッチリ取得できます。

Mac の場合は Terminal を開き、以下のコマンドで Authy Desktop を開きます。

```
open -a "Authy Desktop" --args --remote-debugging-port=5858
```

Windows の場合は Authy Desktop のショートカットを作成し、右クリック -> プロパティ -> ショートカット -> リンク先 の最後に `--remote-debugging-port=5858` を付加して保存。作成したショートカットからアプリを開きましょう。

まず、Authy Chrome Extension を開き、ログインした後のアカウント一覧画面で右クリック、Inspect（検証）を開きます。

Authy Desktop を起動したら、Chrome で http://localhost:5858 を開きます。
デバッグポートに正常に接続できれば以下のような画面が表示されます。

![Content shell remote debugging 2021-08-04 16-20-19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15611/651bfd81-0c9d-ab24-c792-46b531f981f9.png)

Twilio Authy をクリックすると、次のように Developer Tool が使えるようになります。

![main.html 2021-08-04 16-22-55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15611/ebec3586-4d26-03b2-bf10-d2e8f45e7c03.png)

Developer Tools が開いたら、Console タブを開き、以下のコードを入力して実行します。

```javascript
appManager.getAuthenticatorApps().forEach(app=>console.log(app.name,app.originalName,app.decryptedSeed))
```

すると、アカウント名、SecretKey が Console に出力されるはずです。

![main.html 2021-08-04 16-27-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15611/84a041d8-e8d4-31f2-16bb-d48c519c9b2e.png)

得られた SecretKey は、パスワード管理アプリ等を用いて安全に保管しておきましょう。

## oathtool で MFA Token を生成してみる

Mac であれば、brew を使えば簡単にインストールできます。

```command-line
brew install oath-toolkit
```

以下のコマンドで token が生成できることを確認します。

```command-line
stty -echo; echo -n 'Enter SecretKey: '; read skey; echo; stty echo; oathtool -b --totp $skey
```

> `oathtool -b --totp [SecretKey]` だけでも確認できますが、その場合は shell の history に SecretKey が残ってしまうので、後で .bash_history や .zsh_history から削除する必要があります。

## wrapper script (mfacodegen) を作成する

このままだと SecretKey の扱いが面倒で非常に使いにくいので、

- Service ID、SecretKey を書いた key list を用意する
- key list は openssl で encrypt しておく
- wrapper script が key list を参照、decrypt
- wrapper script の引数で渡された Service ID のエントリを元に oathtool で token 生成

のような処理を行います。

### 暗号化した SecretKey リストファイルを作成する

一行が `[ServiceID]\t[SecretKey]` となるようなリスト `mfalist.txt` を適当な場所に生成します。

> ServiceID は CLI のオプションとして無難な文字列にしておいてください。

![mfalist.png](https://qiita-image-store.s3.amazonaws.com/0/15611/75458d2a-68bd-8057-76c1-9339ac8d6ebf.png "mfalist.png")

S/MIME 用の秘密鍵と証明書を生成します。

```command-line
openssl req -x509 -days 3650 -newkey rsa:2048 -keyout /path/to/mfa.key -out /path/to/mfa.crt -subj '/'
```

作成した証明書を用いて、openssl コマンドで encrypt します。

```command-line
openssl smime -encrypt -aes256 -in /path/to/mfalist.txt -out /path/to/mfalist.dat -binary -outform PEM /path/to/mfa.crt
```

念のため、decrypt 可能かどうか、確認しておきましょう。
以下のコマンドで、標準出力に decrypt 結果が表示されれば成功です。

```command-line
openssl smime -decrypt -in /path/to/mfalist.dat -inkey /path/to/mfa.key -binary -inform PEM
```

元の /path/to/mfalist.txt は不要なので削除しておきます。

### mfacodegen を設置する

以下のファイルを path の通った適当な場所に設置します。

> 最初の `LIST_PATH` および `KEY_PATH` は適宜修正してください。
> また、`chmod +x` を忘れずに。

```bash:mfacodegen
#!/bin/bash
set -e

LIST_PATH=/path/to/mfalist.dat
KEY_PATH=~/path/to/mfa.key

usage() {
  echo "Usage: $0 [-clh] [-s service]" 1>&2
  exit 1
}

while getopts cls:h OPT; do
  case $OPT in
    c)  mode='copy'
        ;;
    l)  cmd='show'
        ;;
    s)  cmd='generate'
        service=$OPTARG
        ;;
    h)  usage
        ;;
    \?) usage
        ;;
  esac
done

[ -z "$cmd" ] && usage

generateToken() {
  list=$(loadList)
  seckey=$(echo "${list}" | awk "\$1==\"${service}\"{print \$2}")
  [ -z "$seckey" ] && { echo "Service ${service} not found." >&2; exit 1; }
  if [ "$(uname)" = 'Darwin' -a "$mode" = 'copy' ]; then
    echo -n $(oathtool -b --totp $seckey) | pbcopy
  else
    oathtool -b --totp $seckey
  fi
}

loadList() {
  if [ -p /dev/stdin ]; then
    stdin=$(cat -)
    openssl smime -decrypt -inkey $KEY_PATH -in $LIST_PATH -binary -inform PEM -passin "pass:$stdin"
  else
    openssl smime -decrypt -inkey $KEY_PATH -in $LIST_PATH -binary -inform PEM
  fi
}

showServiceList() {
  list=$(loadList)
  echo "${list}" | cut -f1 | sort
}

case "$cmd" in
  'show' )
    showServiceList
    ;;
  'generate' )
    generateToken
    ;;
esac
```

### 動作確認

`-l` オプションで、サービスの一覧が表示されます。

```command-line
mfacodegen -l
```

`-s [ServiceID]` オプションで、指定のサービス ID の token を生成します。

```command-line
mfacodegen -s github
```
Mac の場合は、`-c -s [ServiceID]` で token がクリップボードにコピーされます。

```command-line
mfacodegen -c -s github
```

これで、CLI で MFA token が取得できるようになりました。