本日 Product Hunt に掲載中です! コメントを書く

Authenticator 1.16.0 をリリースしました。 変更点を見る

仕組み

セキュリティを評価する方に向けた技術概要です。ここに書かれていることはすべてソースコードで確認できます。都合の悪い点も省かずに明記しています。

アーキテクチャ

本拡張機能は Manifest V3 の Chrome 拡張機能で、TOTP コードを OTPAuth を使ってローカルで生成します。コードの生成、アカウントの保存と暗号化にバックエンドは関与せず、ユーザー登録の仕組みもありません。当方が運用するサーバーは、無料プランと Pro のための api.authenticator.sh の一つだけで(後述)、このサーバーが秘密鍵、アカウント名、発行元、コードを受け取ることは決してありません。

このサーバーは、誰のアカウントについても鍵素材を保持しません。鍵の預託も、当方が管理する復旧手段もありません。パスワードを忘れ、リカバリーコードも失った場合、当方にできることはありません。これは設計上の選択です。

暗号方式

パスワード保護は任意で、既定では無効です。有効にした場合:

暗号
AES-256-GCM、レコードごと・書き換えごとにランダムな 96 ビット IV
鍵導出
PBKDF2-HMAC-SHA256、600,000 回反復、128 ビットのランダムソルト(OWASP の指針に準拠)
マスターキー
256 ビット。crypto.getRandomValues から一度だけ生成し、パスワードはそれを包むだけです
フィンガープリント
HKDF で導出したサブキーによる HMAC-SHA256。端末間でレコードを統合するために使用します
解錠済みの鍵の保管先
chrome.storage.session — メモリ上のみ。ディスクには一切書き込まず、ブラウザーを閉じると消去され、コンテンツスクリプトからは到達できません
自動ロック
開くたび/操作しないまま 5・15・60 分経過後/ブラウザーを閉じるまで
実装
WebCrypto のみを使用。プリミティブの自作はなく、暗号ライブラリーの同梱もありません。

鍵を二段構えにしている理由

データをパスワード由来の鍵で直接暗号化することはありません。レコードを暗号化するのはランダムなマスターキーで、PBKDF2 の出力はそのマスターキーを包むだけです。ここから二つの結果が生まれ、どちらも重要です:

  • パスワードを変更しても書き換えるのは 32 バイトだけで、全レコード・全バックアップ・全エクスポートを暗号化し直す必要がありません。ストレージがデータを失うのは、まさにその種の一括書き換えの最中です。
  • 160 ビットのリカバリーコードが同じマスターキーを独立に包めるため、パスワードを忘れても復旧できます。コードは一度だけ表示し、確認のために入力し直す必要があり、使用のたびに更新されます。利用者が確認するまでディスクには何も書き込みません。

暗号化されるもの、されないもの

パスワード保護を有効にすると、識別子とフィンガープリントを除くアカウントレコード全体が暗号化されます。サービス名も対象なので、プロファイルを盗まれても、その人がどのサービスにアカウントを持っているかは分かりません。有効化の際は、レコードを暗号化し、復号して突き合わせることで検証したうえで、初めて平文のコピーをすべて削除します。ブラウザーのローカルストレージ、同期、そして 7 世代のローテーションバックアップのすべてが対象です。

評価に関わる点なので、率直に記します:

  • パスワード保護が無効の場合、アカウントは暗号化されずに保存され、Chrome 同期が有効ならば利用者自身の Google アカウントを通じて複製されます。当方に届くことは決してありませんが、Google は保持します。同期は無効にでき、その際すでに同期済みの内容も削除されます。
  • サイトごとの利用履歴(どのアカウントをどのドメインで使ったか)はローカルに保存され、収集されている間は保管庫の保護対象外です。パスワード保護を有効にすると削除されます。単独で無効にすることもできます。

脅威モデル

防げるもの

  • ブラウザーのプロファイルディレクトリーの盗難・複製
  • ブラウザーのデータをまとめて持ち出すインフォスティーラー
  • 放置された端末に物理的に触れられること
  • Chrome 同期を通じてアカウント情報が Google に渡ること
  • バックアップファイルの紛失・盗難(パスワード保護付きエクスポート)

防げないもの

  • 保管庫が解錠されている間、利用者権限で動作するマルウェア
  • 入力中のパスワードを取得するキーロガー
  • 侵害されたブラウザーや OS
  • コードの対象である第三者サービス自体の侵害
  • 自分で書き出した暗号化なしのエクスポート — 素の JSON、otpauth:// リンク、CXF はいずれもファイルを開いた人が読めます

要するに、これが守るのは保存状態のデータです。使用中の時点ですでに攻撃者の制御下にある端末を守ることはできませんし、できるとも言いません。

権限

マニフェストが宣言する権限は五つで、ホスト権限はありません:

storage
アカウントと設定はローカルストレージ、解錠済みの鍵はセッションストレージ、そして任意の同期用コピー
activeTab
該当アカウントを強調表示するためにアクティブタブのホスト名を読み取り、画面から QR コードを読み取る際にそのタブをキャプチャーします
contextMenus
右クリックメニューに「2FAコードを入力」の項目を追加します。http(s) ページの編集可能なフィールド上でのみ表示されます
scripting
コードをフィールドに入れる関数を一つだけ注入します。対象はその一つのタブ、その一回の呼び出しだけで、しかもそのメニュー項目が選ばれた後に限られます
sidePanel
サイドパネルで開く設定のときにアプリをそこに表示します。ページへのアクセス権は一切与えません

コンテンツスクリプトは一切なく、tabs・cookies・webRequest の権限、web-accessible resources のいずれもありません。あなたがそのページでコードを求めるまで、私たちのものは何一つページに存在しません。そのとき動くものは、その一つのタブとその一回の呼び出しのために、あなた自身のクリックが与える許可のもとで注入され、あとには何も残りません。

カメラ

カメラによる QR 読み取りは、ポップアップではなくタブで開いた拡張機能ページ上で動作します。ポップアップはフォーカスを失うと破棄され、権限ダイアログはまさにそれを引き起こすからです。これにはマニフェスト上の権限は不要です。ブラウザー標準のカメラ許可ダイアログを使い、拡張機能のオリジン単位で付与され、サイトの設定から取り消せます。スキャナーを開くまで要求されることはありません。映像はローカルでデコードされ、送信も保存も一切されません。

無料プラン、Pro、1 日 1 回の通信

1.14.0 以降、拡張機能は 1 日 1 回 api.authenticator.sh と通信します。タイミングはブラウザーの起動時か、ポップアップの描画が終わった後で、コードを表示するまでの処理の途中で行うことはありません。リクエストは項目が固定された閉じた一覧で、src/billing/sync.ts 内の一つの関数で組み立てられ、テストによってその一覧に保たれています:

iid
このインストールが作成したランダムな ID。Google アカウント、メールアドレス、ブラウザーのいずれとも結び付いていません。ただし Pro を購入した場合は、購入記録によって、購入元のインストールと、ライセンスキーが有効なすべてのインストールが、支払いに使われたメールアドレスと結び付けられます
ver, ui_lang, browser_lang
拡張機能のバージョン、UI の言語、ブラウザーの言語
installed_at, legacy, legacy_src
インストールされた時期と、それが課金の導入より前かどうか
accounts, groups
保持しているアカウントの件数と、それらが分かれているグループの数。数だけで、どのアカウントか、グループ名は含みません
vault, sync, open_mode, view_mode, style, dark, os
パスワード保護と同期が有効かどうか、拡張機能をどの形で開くか、アカウント一覧の表示(標準・コンパクト・コード非表示)、どのスタイルをライト/ダークのどちらで使っているか、そしてOS(Windows、macOS、Linux、ChromeOS)
pid, lid, has_key, key_hint
インストールが現在保持している署名付きのポリシーとライセンスがどれか、ライセンスキーをすでに保持しているかどうか、そしてそのキーの末尾 4 文字
opens
前回の通信以降、各日に拡張機能が開かれた回数(最大で直近 14 日分)。日ごとの回数だけで、何を見たかは含みません
events
閉じた一覧から選ばれる、送信待ちの課金イベント:上限に達した、ペイウォールが表示された・閉じられた、プランが選ばれた、決済やキーの入力が試された

送信しないもの:秘密鍵、アカウント名、発行元、コード、それらを使うサイト。国はサーバーがエッジでリクエストから取得し、IP アドレスは保存しません。アンインストール時のページは同じインストール ID を ?i= として受け取り、そのインストールが削除されたことをサーバーに伝え、任意のアンケートの回答もそのインストールと結び付けます。

このために chrome.storage.local に保存するのは、インストール ID、最後に受け取った署名付きのポリシーとライセンス、このインストールが保持するライセンスキー(入力されたもの、またはこのインストールで購入したあとに受け取ったもの)、サーバーが受け取るまでの日ごとの開いた回数、送信待ちのイベントで、アカウントのデータは含みません。同期が有効な間は、インストールが課金の導入より前かどうかと、そのライセンスキーも Chrome 同期に保存され、利用者のほかの Chrome プロファイルに引き継がれて Pro が自動で有効になります。同期をオフにすると両方とも Chrome 同期から削除され、オンに戻すと再び保存されます。

返ってくるものと、拡張機能での扱い:

トークン
ポリシー(無料プランのアカウント数の上限)とライセンス(Pro)は、ES256(ECDSA P-256)で署名された JWS です
検証
WebCrypto を使い、ビルドに組み込まれた公開鍵(src/billing/keys.ts)で検証します。Chrome ウェブストア版のビルドは本番用の鍵だけを信頼し、それ以外の鍵を含むビルドはビルド時のチェックで拒否されます
オフライン
上限とライセンスは、最後に受け取った署名付きトークンを使ってローカルで確認します。署名付きのポリシーを一度も受け取っていない間は何も制限されません。サーバーに到達できないことで誰かが締め出されることはありえません
上限にできること
アカウントの追加を止めることだけで、それ以外は何もできません。アカウントを隠す、ロックする、削除する、書き換えることも、コードの表示、エクスポート、バックアップ、復元、同期を妨げることもできません
購入
新しいタブで開く Stripe Checkout で行い、販売者(merchant of record)は Link です。購入者のメールアドレスは、ライセンスキーの発行と再送のために Stripe から当方に届きます。キーはハッシュとして保存し、紛失したキーを再送できるよう暗号化したコピーも保持します
ライセンスキー
キーそのものは、Pro を購入したインストールへの 1 日 1 回の通信の応答にだけ含まれ、そのインストールが key_hint で伝える 4 文字がそのキーと一致するまで送られます。そのため、あとから購入したキーが古いキーに置き換わります。キーが入力されたインストールには一切送られません

この上限は DRM ではありません。コードはオープンソースで、フォークすれば取り除けます。決してあってはならないのは、ある人が他人のライセンスを入手・使用できること、あるいは他人のインストールに制限をかけられることです。それは脆弱性であり、当方のセキュリティポリシーの対象範囲に含まれます。

外部への通信

自分から行う通信は、時刻の確認と課金のための 1 日 1 回の通信の二種類です。以下のそれ以外はすべて利用者がクリックした結果として発生します。Manifest V3 はリモートコードを禁じており、ここに挙げたもののいずれもアカウント・秘密鍵・生成されたコードを運びません。全一覧は次のとおりです:

ホストタイミング利用者データを含むか
time.akamai.com
timeapi.io
cloudflare.com
時刻のずれの確認。キャッシュあり。どの取得先にも届かない場合は設定にその旨が表示されます。時刻がずれると TOTP は成立しません。含まない
authenticator.shインストール時のようこそページ、アンインストール時のフィードバックページ(アドレスにインストール ID を含む)、ポップアップから開いたときのヘルプ・サポートページ。アンインストール時のページに限りインストール ID を含む。それ以外は含まない(通常の Web サーバーログのみ)
api.authenticator.sh1 日 1 回の課金のための通信。加えて、利用者が Pro を購入する、キーを入力する、購入を復元する、サブスクリプションを管理するときの決済・ライセンス・ポータル関連の呼び出し。上に挙げた項目のみで、アカウントのデータは一切含まない。ライセンスキーやメールアドレスは、利用者が入力したときに限る
checkout.stripe.com
billing.stripe.com
Stripe Checkout とカスタマーポータル。利用者が Pro を購入するか、サブスクリプションを管理するときにタブで開きます。購入者がそこで入力した内容。Stripe と Link それぞれの規約が適用されます
chromewebstore.google.comストアの掲載ページ。クリックしたときだけタブで開きます。評価の案内からはレビュー欄。含まない(Google からは通常のストアページ閲覧と同じに見えます)

フォント・スクリプト・スタイル・画像のいずれも第三者ホストからは読み込みません。画面表示に必要なものはすべてパッケージ内に同梱されています。

公開物を検証する

以上のどれも、こちらを信用して受け入れる必要はありません。本拡張機能はオープンソースで、各リリースは再現可能です:

  1. Chrome ウェブストアで公開されている .crx をダウンロードして展開する
  2. 対応する git タグをチェックアウトする
  3. Node 20 LTS で npm ci && npm run build を実行する
  4. 生成された dist/ と展開したパッケージを比較する

生成された dist/ 内の全ファイルの SHA-256 を、各 GitHub リリースに SHA256SUMS-v<version>.txt として sha256sum 形式で添付しています。したがって手順 4 は sha256sum -c を一度実行するだけで済みます。差分はアーカイブ内のファイル順序と、Node のパッチバージョン差による minify 出力の空白に限られるはずです。

実行時の依存は意図的に少なく、OTPAuth・React・jsQR・Zustand・Framer Motion・Lucide のみで、コミット済みのロックファイルで固定しています。CycloneDX 形式の SBOM はご要望に応じて提供します。

ソースコードを見る

当方にないもの

当方は小規模な独立ベンダーです。次のいずれかが貴組織の必須要件であれば、評価を三週間進めた後よりも今知っていただくほうが良いはずです:

  • SOC 2、ISO 27001 などの認証は取得していません
  • 第三者によるペネトレーションテストの実施実績はありません
  • 有償のバグバウンティ制度はありません

その代わりに提供できるのは、ソースコードの完全な公開、検証可能なビルド、最小限の権限、そしてそもそも秘密鍵・アカウント名・コードが一切送られてこないサーバーです。

連絡先

脆弱性の報告およびセキュリティに関するお問い合わせは security@authenticator.sh まで。開示ポリシー、対象範囲、応答時間、セーフハーバー条項はセキュリティポリシーのページに、何が保存され何が端末の外に出るのかはプライバシーポリシーに記載しています。

書面のベンダー評価アンケートへの回答や、セキュリティチームとのコードの読み合わせにも喜んで応じます。

セキュリティ以外の件(インストール、アカウントの復旧、導入に伴う請求関連など)はサポートをご覧ください。脆弱性の報告をそちらに送るのはお控えください。あの窓口は機密として扱っていません。