ブラウザ上でのパスワードマネージャの作り方
アプリ統合ブラウザ[Biscuit](https://eatbiscuit.com/ja)でパスワードマネージャの要望が多かったので1.2.0で実装しました。なかなかパスワードマネージャを実装しする経験もないと思うので、備忘録も兼ねて実装方法をご紹介します。
パスワードマネージャとは?
Webアプリやサイトのログイン時にパスワードをブラウザに記憶して、再度ログインする際に復元する仕組みのことです。ChromeやFirefoxで見かけるアレです。
パスワードをマネージャの作り方
パスワードマネージャの作り方ですが、最初まったく作り方がわかりませんでした!正直途方にくれたのですが、既存のブラウザのパスワードマネージャの挙動を観察したり、実際に技術検証して、以下の4つの処理が実現できたらパスワードマネージャが作れることがわかってきました。
1. パスワード入力フォームを認識する処理
2. ログインが成功したことを判定する処理
3. パスワードを保存する処理
4. パスワードを復元する処理
1-3がパスワードを保存する処理でこれらは連続した一連の処理として考える必要があります。4に関しては保存されたパスワードをどうログインフォームに反映させるかという部分になります。
以下、それぞれの処理の実装方式について詳しく説明します。
1. パスワードを入力するフォームを認識する
ログイン時にパスワードを保存する前段として、まずはパスワードを入力するフォームを認識する必要があります。いろいろ試した結果、以下の方法でパスワード入力するフォームを認識し、さらに入力内容を監視することにしました。
1. 画面上にパスワードフィールドがあったらログインフォーム
画面上にパスワードフィールドがある箇所はログインフォームである可能性が高いです。正確に言うとログインフォームでない場合もありますが、誤判定してもパスワードを保存するかどうかはユーザーが任意に選択できるので、問題なしと判断しました。
2. パスワードフィールドを含むフォームを監視し、入力内容をElectronのアプリのメモリ上に一時保存する
入力内容をメモリ上に保存するのは、後続のログイン判定でログインが成功した際に、ここで保存した内容をID/パスワードのセットとして保存するためです。
2. ログインが成功したことを判定する
次にログインの成功を判定する必要があります。これはログイン成功をトリガーとして保存ダイアログを出すためです。ログイン失敗した場合は、パスワードの保存は不要なのでその場合は保存ダイアログは出しません。
ログイン処理は仕組みとして次の2パターンが考えられます。
a. フォームのサブミットによるログイン処理の場合:
ElectronのAPIでサブミット処理がフック可能なため、1の入力パスワードと一致するパラメータが送信されたらログイン処理用の通信と判定し、かつ5秒後に画面にパスワードフィールドが存在しなければログイン成功とみなす
b. XHRによるログイン処理の場合:
ElectronのAPIでフックできないため、クライアント側のJSでXHRをフックし、1の入力パスワードと一致するパラメータが送信されたらログイン処理用のXHR通信と判定し、かつ5秒後に画面にパスワードフィールドが存在しなければログイン成功とみなす
「5秒後」ってあたりが泥臭さを感じさせますね。実は最初は「URLが変わったら」という条件にしていたのですが、SPAなどだとURLはログイン後も変わらない場合がありえるため、「5秒後の画面状態にパスワードフィールドが存在するか?」で判断しています。だいたいのアプリはこれで判定できるようになりました。
3. パスワードを保存する
ログイン成功の判定ができたら、保存確認ダイアログを表示してIDやパスワードを保存するだけです。ここでのポイントは暗号化です。パスワードは暗号化して保存しますが、暗号化キーの保存が悩ましいところです。幸い、OSのセキュアな領域を透過的に利用できるkeytarというライブラリが存在したので、暗号化キーはそのライブラリを使用して保存しています。以下のブログが参考になりました。
実装のさいにKeyChainの中をチェックしている際に、同様の実装方式をブラウザのVivaldiがやっていそうなのを見かけて、実装方式として安心感を得ました。
4. パスワードを復元する
最後に保存したパスワードの復元です。1で認識しているログインフォームで復元処理を試みます。その際に、ドメイン名が一致するパスワードを候補としてログインフォームに流し込むようにしています。ログイン画面のURLが常に同じとは限らないため、ある程度ゆるやかな復元方法にしています。同一ドメインに対して複数のパスワードが存在する場合は、フォームにフォーカスがあたったタイミングで一覧を表示して、どちらのパスワードを使用するか選択させるUIを追加しています。
おまけ:実装の試行錯誤のログ
上記の実装方針は「答え」みたいなもので実際は「これでパスワードマネージャが作れる」と確信を持つまではかなり試行錯誤の連続でした。その過程を最後にご紹介します。
基本方針:
「技術検証を目的に仮実装で試し→うまくいったら本実装」というステップを繰り返して実装していきました。(私の開発のモットーは「安く試す」です😊)
作業のログ:
1. パスワードをElectronから復元できるか?をTwitterのログインフォーム+ID/パスワード固定値で実装してみる
-> IDやパスワードがElectronから入力できることを確認
2. ログインフォームを認識するため、「document.querySelector('form input[type=password]')」をいろいろなログインフォームでDeveloper Consoleから実行してみる
-> だいたいこれで判定できそう
3. ログイン成功の判定を実装
-> XHRを考慮していなかったため、半分ぐらいのサイトで判定できない
4. ログイン成功の判定にXHRを考慮
-> OK
5. 保存ダイアログを表示して保存処理
-> ひとまずパスワードは平文で保存し、あとで暗号化は考えることにして先に進む。
6. パスワード復元処理を保存したパスワードからおこなうように本実装
-> 復元できた
7. いろいろなアプリでログイン処理を試しては微調整
-> type=emailもIDである可能性があるので保存するようにしたり
8. 保存パスワードの暗号化処理を実装
-> OK
9. ベータリリースしてベータユーザーに確認してもらう
-> 半分ぐらいのアプリでNGの報告
10. ログイン成功の判定でURLが変更されたらで判定していた処理をパスワードフィールドが存在しなくなったらに変更
-> 9割動作するようになった
11. リリース
->🎉🎉🎉
おわりに
いかがでしたか?楽しいでいただけたでしょうか?すごく大変でしたが、無事便利で使えるものが作れてよかったです。ここで紹介したパスワードマネージャが使えるブラウザBiscuitは以下からダウンロードできます。