Edited at

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

More than 1 year has passed since last update.

AWS や Google 等で利用可能な MFA の仮想デバイスとしては、複数デバイス間の同期や Chrome Extension を擁する Authy が便利なわけですが、PC 特に CLI を使ってるといちいちデバイス or Chrome Extension を開く必要があり、かなり面倒です。

CLI から token を生成する OATH Toolkit の oathtool を使えば、この面倒さが少しは解消され、スクリプト化による自動処理も可能になります。

ただし、oathtool 自体は毎回 SecretKey が必要になるため、mfacodegen という名の wrapper script を用意して便利に利用できるようにします。

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

mfacodegen.png


Authy Chrome Extension から SecretKey を取得する

MFA では


  • TOTP (Time-based One Time Password) RFC 6238

  • HOTP (HMAC-based One Time Password) RFC 4226

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

SecretKey は MFA 設定時の QR code 内に仕込まれているのですが、既に Authy を設定済の場合は アプリ上から SecretKey を見ることが出来ません。

ただし Authy は定期的に token を生成するために内部的には当然 SecretKey を保持しています。

Authy Chrome Extension は JS で token を生成しているため、Developer Tools で内部を覗けばバッチリ取得できます。

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

Authy.png

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

requirejs(['models/apps/app_manager'], AppManager => AppManager.get().model.forEach(app => console.log([app.accountType, app.name, app.decryptedSeed].join('\t'))))

すると、Authy の アカウントタイプ(アイコン)、名称、SecretKey が Console に出力されるはずです。

Developer_Tools.png

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


oathtool で MFA Token を生成してみる

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

brew install oath-toolkit

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

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

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

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

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

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

念のため、decrypt 可能かどうか、確認しておきましょう。

以下のコマンドで、標準出力に decrypt 結果が表示されれば成功です。

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 を忘れずに。



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 オプションで、サービスの一覧が表示されます。

mfacodegen -l

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

mfacodegen -s github

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

mfacodegen -c -s github

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

11/21 追記

他ツールとの連携のため、STDIN からの pass phrase 入力に対応。

02/21 修正

rsautl だと大きなファイルを encrypt できないため、smime に変更。