手軽に安全で確実なSafariのフォームへの自動入力を目指して
前回の日記 Safariであらゆるページに自動ログインする方法 では、確かにあらゆるページに自動ログインできるかもしれないが、その方法は結局のところAppleScriptでラップしたJavaScriptなプログラムコードをページごとに書いているに過ぎない...。
- 極めてプログラマー思考な方法で、ユーザーインターフェースも、エクスペリエンスも、へったくれもない。
- 登録したい時には、要素の詳細を表示して、name属性を確認して、set_name("okyakusama...) だなんて たかだか自動ログインのために面倒くさ過ぎ。
- ページの自動操作という意味では、この程度のプログラムコードを書く必要性は許せるかもしれないが...
- そもそもページの自動操作はおまけで、本来の目的は、ユーザーIDとパスワードの自動入力だったのだ。
- 一旦プログラムコードを書けば、あとはスクリプトメニューから選択するだけで良いのだけど、最初の登録がプログラム書く人以外には敷居が高過ぎる。
- この方法を実家のオカンに説明しても、おそらく百万年かかっても理解してもらえないだろう。
もっと多くの人が、手軽に、安全で、確実な自動ログインする環境の必要性を感じた!
- 1Passwordに負けないように、頑張ってみる。
PS
- とは言っても、実家のオカンが金融機関のページにログインして操作する姿など、恐ろしくて想像できない。
- 振り込め詐欺以前に、誤操作・勘違いしまくりの自爆振込の危険性、大いにあり。
手軽にする技
フォームの入力値を収集
- 現状の自分で要素の詳細を表示してフォームの情報を取得する方法はやめて、プログラムが自ら自動でフォームの情報を収集する必要がある。
- フォームの情報を収集するのは、やはりブラウザネイティブな言語であるJavaScriptを利用すべきだと考え、以下のようなコードを考えてみた。
var s=[]; var inputs=document.getElementsByTagName('input'); for(i=0;i<inputs.length;i++){ if(inputs[i].type=='text'||inputs[i].type=='password'){ s.push('{type: \"'+inputs[i].type+ '\", name: \"'+inputs[i].name+ '\", value: \"'+inputs[i].value+'\"}'); } } '{'+s.join(',')+'}'
- 上記のJavaScriptを、AppleScriptでラップして実行してみる。
tell application "Safari"
do JavaScript "var s=[]; var inputs=document.getElementsByTagName('input'); for(i=0;i<inputs.length;i++){if(inputs[i].type=='text'||inputs[i].type=='password'){s.push('{type: \"'+inputs[i].type+'\", name: \"'+inputs[i].name+'\", value: \"'+inputs[i].value+'\"}');}} '{'+s.join(',')+'}';" in document 1
end tell
run script result
--結果:
{{name:"okyakusamaBangou1", type:"text", value:"1234"}, {name:"okyakusamaBangou2", type:"text", value:"5678"}, {name:"okyakusamaBangou3", type:"text", value:"12345"}}
- 例えば、ゆうちょダイレクトのページだと、上記のようなレコード リストが結果として返ってくる。
URLをキーにする
- 上記のレコード リストをURLをキーとして保存することを考える。
- 但し、英数字と「_」以外はAppleScriptではキーとして利用できないので、URLの英数字以外はすべて「_」に置き換えるようにした。
set login_info_record to {https___direct_jp_bank_japanpost_jp_tp1web_U010101SCK_do:{{name:"okyakusamaBangou1", type:"text", value:"1234"}, {name:"okyakusamaBangou2", type:"text", value:"5678"}, {name:"okyakusamaBangou3", type:"text", value:"12345"}}}
レコードを基に自動入力する
- 上記のようなログイン情報のレコードから、URLをキーにフォームの情報を取り出し、再び入力するコードを考えてみた。
set a_record to login_info_record's https___direct_jp_bank_japanpost_jp_tp1web_U010101SCK_do
tell application "Safari"
activate
repeat with r in a_record
do JavaScript "document.getElementsByName(" & quoted form of r's name & ")[0].value = " & quoted form of r's value in document 1
end repeat
end tell
レコードのキーを変数で指定する書き方
- 但し、残念なことにAppleScriptはレコードのキー値を変数で指定する仕組みを持っていない...。
set a_key to "https___direct_jp_bank_japanpost_jp_tp1web_U010101SCK_do"
set a_record to login_info_record's a_key
- 上記のようなコードを書いても、変数とは見なされず「a_key」というキーで探しに行ってしまうのである。
- そこで以下のようにrun script利用して、コード生成することで強引に変数の中身をキー値として利用する。
set login_info_record to {https___direct_jp_bank_japanpost_jp_tp1web_U010101SCK_do:{{name:"okyakusamaBangou1", type:"text", value:"1234"}, {name:"okyakusamaBangou2", type:"text", value:"5678"}, {name:"okyakusamaBangou3", type:"text", value:"12345"}}}
set f to open for access file ((path to scripts folder as text) & "login_info.data") with write permission
write login_info_record to f
close access f
set a_key to "https___direct_jp_bank_japanpost_jp_tp1web_U010101SCK_do"
run script "(read file ((path to scripts folder as text) & \"login_info.data\") as record)'s " & a_key
--結果:
{{name:"okyakusamaBangou1", type:"text", value:"1234"}, {name:"okyakusamaBangou2", type:"text", value:"5678"}, {name:"okyakusamaBangou3", type:"text", value:"12345"}}
安全にする技
暗号化
- ところで、URLをキーとしたlogin_info_recordの内容は、上記のように永続的なデータとしてどこかに保存しておく必要がある。
- しかし、login_info.dataの中身を覗いてみると、以下のような状態。
recousrflistutxtphttps___direct_jp_bank_japanpost_jp_tp1web_U010101SCK_dolistrecopnamutxt"okyakusamaBangou1usrflistutxttypeutxttextutxt valueutxt1234recopnamutxt"okyakusamaBangou2usrflistutxttypeutxttextutxt valueutxt5678recopnamutxt"okyakusamaBangou3usrflistutxttypeutxttextutxt valueutxt 12345
- 1234、5678、12345がはっきりと確認できる。これがパスワードだったらヤバい...。
- そこで、ファイルを覗かれても内容が分からないように暗号化しておく必要がある。
--暗号化(エンコード)
set pw to "abcd"
set in_path to ((path to scripts folder as text) & "login_info.data")'s POSIX path
set out_path to ((path to scripts folder as text) & "encrypted_login_info.data")'s POSIX path
do shell script "openssl enc -e -aes-128-cbc -pass pass:" & pw & " <" & in_path & " >" & out_path
- 暗号化キーpw="abcd"を利用して、out_pathには以下のようなファイルが生成された。
- これなら重要なパスワードも、解読される心配はまずない、と思われる。
- 暗号化されたファイルは、以下のように処理すれば、再び復号化できる。
- オプションにデコードの-dに変更。
- in_pathとout_pathの変更に注意。
--復号化(デコード)
set pw to "abcd"
set in_path to ((path to scripts folder as text) & "encrypted_login_info.data")'s POSIX path
set out_path to ((path to scripts folder as text) & "login_info.data")'s POSIX path
do shell script "openssl enc -d -aes-128-cbc -pass pass:" & pw & " <" & in_path & " >" & out_path
パスワードの保存方法
- ところで、暗号化キーpw="abcd"は、暗号化キーと同時に認証パスワードとしても利用したい。
- ログイン情報の自動入力に先立って、暗号化キー"abcd"をパスワードとして認証を求めるのだ。
- しかし、認証パスワードの入力を求めて"abcd"が正しいと確認するためには、どこかに”abcd”を保存しておく必要がある。
- すると、その"abcd"を隠すためにまた別の暗号化キーが必要になって...という暗号化キーを隠す無限ループに陥ってしまう。
- このような問題は、SHA-1のようなハッシュ関数を利用することで解決できる。
- ”abcd”をSHA-1方式でハッシュ変換すると、以下のような数値が得られる。
set str to "abcd"
do shell script "echo " & str & "|openssl dgst -sha1"
--結果:
"3330b4373640f9e4604991e73c7e86bfd8da2dc3"
- SHA-1変換には、以下のような特徴がある。
- ”abcd” から ”3330b4373640f9e4604991e73c7e86bfd8da2dc3” へ変換はできるが、
- ”3330b4373640f9e4604991e73c7e86bfd8da2dc3” から ”abcd” へ復号はできない。(ほとんど不可能)
つまり、”3330b4373640f9e4604991e73c7e86bfd8da2dc3” から ”abcd” を想像するのは、相当に難しい!
確実にする技
URLの変化に対応する
- 金融機関の嫌らしいところは、URLをキーとした自動ログインを阻止するために、ログインURLをわざと変化させてくることだ。
- 例えば、ゆうちょダイレクトのログイン第二段階では、少なくとURLは以下の3つに変化する。(自分の経験による)
- ?RedirectToken=以降は無視することで解決する。
- direct1・direct2・direct3の変化については、URLのホスト部分の数値を無視することで対応してみた。
tell application "Safari"
set url_info to do JavaScript "document.location" in document 1
end tell
--結果
{origin:"https://direct.jp-bank.japanpost.jp", protocol:"https:", hash:"", pathname:"/tp1web/U010101SCK.do", hostname:"direct.jp-bank.japanpost.jp", |port|:"", href:"https://direct.jp-bank.japanpost.jp/tp1web/U010101SCK.do", |host|:"direct.jp-bank.japanpost.jp", search:""}
""
result & (do shell script "echo " & quoted form of url_info's origin & "|tr -C '[:alpha:]' '_'")
result & (do shell script "echo " & quoted form of url_info's pathname & "|tr -C '[:alnum:]' '_'")
--結果
"https___direct_jp_bank_japanpost_jp__tp1web_U010101SCK_do_"
テキスト以外のフォームにも入力する
- 通常、ユーザーIDとパスワードを入力すれば、ログインは完了する。
- よって、自動ログインには、テキスト入力フォームのみ対応すれば十分である。
- ところが、毎回クレジットカード情報を入力する必要のあるページも稀にある。
- クレジットカード情報の入力には、ラジオボタンやセレクトボタンの操作も必要である。
- そのようなフォーム部品の状態も記録しておき、自動入力してみたい。(欲が出た)
例:
有効期限: 月/年
クレジット
代引き
- 上記フォームを読み取る仕様にJavaScriptを変更してみた。(1行書きなので非常に分かりにくいかも)
tell application "Safari"
do JavaScript "var s=[]; var inputs=document.getElementsByTagName('input'); var options=document.getElementsByTagName('option'); for(i=0;i<inputs.length;i++){if(inputs[i].type!='hidden'&&inputs[i].type!='image'){s.push('{type: \"'+inputs[i].type+'\", name: \"'+inputs[i].name+'\", value: \"'+inputs[i].value+'\", checked: \"'+inputs[i].checked+'\", index: \"'+i+'\"}');}} for(i=0;i<options.length;i++){if(options[i].selected){s.push('{type: \"option\", name: \"'+options[i].name+'\", value: \"'+options[i].value+'\", selected: \"'+options[i].selected+'\", index: \"'+i+'\"}');}} '{'+s.join(',')+'}';" in document 1
end tell
run script result
--結果:
{{name:"payment", index:"0", type:"radio", value:"クレジット", checked:"true"}, {name:"payment", index:"1", type:"radio", value:"代引き", checked:"false"}, {name:"undefined", index:"10", type:"option", value:"10", selected:"true"}, {name:"undefined", index:"18", type:"option", value:"15", selected:"true"}}
- ログイン情報からセレクトボタンやラジオボタンの値も設定する。
set a_record to {{name:"payment", index:"0", type:"radio", value:"クレジット", checked:"true"}, {name:"payment", index:"1", type:"radio", value:"代引き", checked:"false"}, {name:"undefined", index:"10", type:"option", value:"10", selected:"true"}, {name:"undefined", index:"18", type:"option", value:"15", selected:"true"}}
tell application "Safari"
activate
repeat with r in a_record
if r's type = "text" or r's type = "password" then
do JavaScript "document.getElementsByName(" & quoted form of r's name & ")[0].value = " & quoted form of r's value in document 1
else if r's type = "radio" or r's type = "checkbox" then
do JavaScript "document.getElementsByTagName('input')[" & r's index & "].checked = " & r's checked in document 1
else if r's type = "option" then
if (do JavaScript "document.getElementsByTagName('option')[" & r's index & "].value" in document 1) = r's value then
do JavaScript "document.getElementsByTagName('option')[" & r's index & "].selected = " & r's selected in document 1
end if
end if
end repeat
end tell
技の統合
以上の技を使って、手軽に安全で確実なフォームの自動入力スクリプトに仕上げてみた。
ダウンロード&インストール
- auto_loginスクリプトをダウンロードする
- ダウンロードしたauto_login.zipを解凍して、auto_loginフォルダを好みの場所に置いてインストール完了。
- おすすめは、ホームのライブラリのスクリプトフォルダ(~/Library/Scripts/)。
使い方
- auto_loginフォルダ内に自動ログインを支援するアップルスクリプトが入っている。
- 最初に下記スクリプトを実行した時には、マスターパスワードの登録が求められる。
- 次回以降も登録したパスワードによって毎回認証が必要。
- 自動入力したいページを開いて、フォームに必要な情報を入力した状態で、「ログイン情報取得」を実行すると、そのページが保存される。
- 次回そのページを開いたら、フォームに未入力の状態で、「自動ログイン」を実行すると、先ほど保存した情報が自動入力される。
- スクリプトメニューからの実行、AppleScriptエディタからの実行、Quicksilver等のランチャーからの実行に対応している。
- おすすめはQuicksilver等でショートカットを割り当てて起動する方法。
- 自分の場合はQuicksilverで以下のショートカットを割り当てた。
- command-option-shift-Aで自動ログイン。
- command-option-shift-Sでログイン情報取得。
今後の展望
- MacBookでの自動入力については満足できるようになったが、AppleScriptなのでiPhone・iPadでは使えない...。
- iPhone・iPadで自動入力を実現するには、上記AppleScriptでやっていることをすべてJavaScriptで処理すれば良い。
- そのJavaScriptをブックマークレットとして保存して、iPhone・iPadと同期すれば、自動入力については実現できる。
- 実際、1Passwordはそのような仕組みでiPhoen・iPadの自動入力を実現しているらしい。
そんなブックマークレットが生成できるようになって初めて、1Passwordに迫る自動入力環境になるのだと思う。