スクリプト内のパスワード

数日前に livedoor Reader未読件数取得と自動ログインのスクリプトを紹介しました。そのスクリプトについて Travellers Tale の Hiro さんから掲示板の方に情報をいただきました。以下に引用させていただきます。

「(前略)...property でパスワード設定するのが嫌な場合に do shell script で security コマンドを叩いてやる...(後略)」

この引用における Hiro さんの情報の要点は、

  1. security というコマンドがある。
  2. property でパスワードを設定することの危うさ

です。

security というコマンド、教えていただくまで知りませんでした。Apple の Developer サイトにあるマニュアルで調べてみると、Mac OS X 10.3 の頃からあったようです。ざっと見てみると、キーチェーンと Security.framework に関わる操作が行えるようです。

Security.framework の方は AppleScript から利用する方法は用意されていませんが、キーチェーンの方は Keychain Scripting.app から利用することができます。しかし、以前にも書いたと思うのですが、Keychain Scripting.app はバグがあって容易に利用できない(バグが修正されているかどうかは未確認)。security コマンドは、代替として利用できますね。

そして、スクリプトの中にパスワードをそのまま書き込むことについて。個人的にはそのリスクについて分かっているつもりなのですが、なにしろ設定が簡単という利便性に負けて、よくこのような方法を用いています。

このようにスクリプト中にパスワードを記述することの問題点は、

  1. パスワードが平文
  2. スクリプトを開くと一目瞭然
  3. load script、store script で書き換え可能
  4. コマンドラインの string コマンドでごにょごにょ...

と、まぁ、いろいろあります。個人で使うだけなのだからそれほど神経質になることもない、と言えるかもしれませんが、近頃多発している情報流出は決して他人事ではないですし、Mac ユーザーだから大丈夫と軽々に言えるものでもないので、神経質になるぐらいがちょうどいいのかもしれません。

AppleScript でどこまで神経質になるかケースバイケースですが、スクリプトの中にパスワードを埋め込まない、というようにスクリプトを改変してみましょう。

ところで。最近 Web アプリケーションのセキュリティや脆弱性について調査しています。セキュリティ対策を行うには、どのような攻撃方法があるかを知る必要があります。敵を知り、己を知らば...ですね。知っている攻撃方法もあれば、初めて知るようなものもある。しかし、どの攻撃方法も知恵を絞っているのが分かる。それだけに目的はどうあれ面白い。実際に試してみたくなる(おい)。

Web アプリケーションのサンプルを掲載しているサイトの全てがセキュリティ対策を行っているとも思えない。簡単なサンプルなら、なおさら手を抜いている可能性がある。そういったサイトで手当たり次第試してみる、というのも一興...無論、冗談ですが。

こういうことって書かない方がいいのかな?先導、誘導、煽っているみたいに読める?

自分自身への警告、そういうサイト(があればそういうサイト)への警鐘といったつもりなのですが。わざわざこのような一文を入れなければいけないのが Web で情報を発信する際の面倒臭さと思わないでもない。

閑話休題。

まず、スクリプトを変更する前に行うことがあります。/Applications/Utilities/Keychain Access.app で、インターネットパスワードを作成します。もちろん、この作成も security コマンドや Keychain Scripting.app で行えるのですが、割愛。

Keychain Access.app では、パスワードなどをキーチェーンで管理します。おそらく「ログイン」と「システム」というキーチェーンが既にあると思います。「ログイン」キーチェーンではログインしているユーザーが利用しているパスワードなどを管理しています。「システム」キーチェーンでは管理者や OS が利用するパスワード項目が管理されています。

今から作るインターネットパスワードは、「ログイン」キーチェーンの方に作ります。「ログイン」キーチェーンはログインした時点でロックが解除されています。「ログイン」キーチェーンを選択し、「ファイル」メニューの「新規パスワード項目...」をクリック。パスワードの情報を入力するシートが表示されます。

シートの「キーチェーン項目名:」に URL(この場合は「http://reader.livedoor.com/reader/」)を入力します。その下の「アカウント名:」に livedoor Reader にログインするときの ID を。「パスワード:」の部分に livedoor Reader にログインするときのパスワードを入力し、間違いがないかを確認し「追加」ボタンをクリック。これで livedoor Reader のインターネットパスワードが作成できました。「ログイン」キーチェーンの中に「reader.livedoor.com」という名前のインターネットパスワードが追加されています。

Terminal で security コマンドを使って確認してみましょう。Terminal を起動して次のコマンドを入力します。

security find-internet-password -g -r http -s reader.livedoor.com

-r オプションは、プロトコルの指定です。この場合は、http になります。-s オプションがサーバーの指定になります。今回は、「reader.livedoor.com」です。

-g オプションは、パスワードを出力するかどうかの指定です。このオプションをつけて実行すると、「キーチェーンへのアクセス確認」というダイアログが表示されると思います。これで正常です。拒否すると処理自体が拒否されます。「常に許可」を選択すると Keychain Access.app で作ったインターネットパスワードの「アクセス制御」に security コマンドが追加されます。以降、このダイアログは表示されなくなります。これは、Keychain Access.app で作成したインターネットパスワードをダブルクリックすると表示されるウィンドウで編集できます。もし、「常に許可」をやめたいのであれば、ここで security コマンドを外してください。

先のコマンドを実行するとインターネットパスワードの情報が表示され、最後にパスワードも出力されます。Terminal でなら、この出力を加工すればいいでしょう。が、AppleScript の do shell script 命令で先のコマンドを実行してもパスワードは出力されません。

これは、パスワードが標準エラー出力に出力されているからです。ですので AppleScript からパスワードを取得するなら security コマンドの標準出力と標準エラー出力をリダイレクトして出力するようにします。以下のような感じです。

do shell script "security find-internet-password -g -r http -s reader.livedoor.com 2>&1"

これでパスワードとともに livedoor Reader のログインに関する情報も取得できました。ここで利用した以外にも security コマンドにはいろいろと機能があるので、一度マニュアルを見ておくといいと思います。

出力された結果を見ると分かるように、なかなかごちゃごちゃしています。この結果からパスワードとログイン ID を取り出します。

Script Editor で開く

on run
    set theResult to do shell script "security find-internet-password -g -r http -s reader.livedoor.com 2>&1"

    repeat with thisLine in paragraphs of theResult
        if thisLine contains "password" then
            set livedoorPassword to my getValue(contents of thisLine, ":")
        else if thisLine contains "acct" then
            set livedoorID to my getValue(contents of thisLine, "=")
        end if
    end repeat

    {livedoorID, livedoorPassword}
end run

on getValue(str, offsetStr)
    set n to offset of offsetStr in str
    set str to text (n + 1) thru -1 of str
    set AppleScript's text item delimiters to ASCII character 34
    set theList to text items of str
    set AppleScript's text item delimiters to {""}
    return (theList as Unicode text)
end getValue

これで取り出せますので、後は以前のログインスクリプトと組み合わせて完成。

Script Editor で開く

property readerURL : "http://reader.livedoor.com/reader/"
property notifyAPI : "http://rpc.reader.livedoor.com/notify?user="

on run
    set {livedoorID, livedoorPassword} to my userInfo()
    set itemNum to my unreadedCount(livedoorID)

    try
        tell application (path to frontmost application as Unicode text)
            display dialog "Unreaded Items : " & itemNum with icon 1 buttons {"Cancel", "Login"} default button 2
            my login(livedoorID, livedoorPassword)
        end tell
    on error
        return
    end try
end run

on unreadedCount(userID)
    set theResult to do shell script "curl " & (notifyAPI & userID)
    set the AppleScript's text item delimiters to "|"
    set theResult to every text item of theResult
    set the AppleScript's text item delimiters to {""}
    return (item 2 of theResult)
end unreadedCount

on login(userID, userPassword)
    tell application "Safari"
        activate
        make new document with properties {URL:readerURL}
        set bool to my pageLoaded(10)
        if bool then
            do JavaScript ("document.forms[0].livedoor_id.value = \"" & userID & "\"") in document 1
            do JavaScript ("document.forms[0].password.value = \"" & userPassword & "\"") in document 1
            do JavaScript "document.forms[0].submit()" in document 1
        else
            display dialog "Connection failure." with icon 0 buttons {"OK"} default button 1
        end if
    end tell
end login

on pageLoaded(timeoutValue)
    set num to (time of (current date)) + timeoutValue
    delay 1
    repeat
        tell application "Safari"
            if (do JavaScript "document.readyState" in document 1) is "complete" then
                return true
            end if
        end tell
        if (time of (current date)) is greater than num then exit repeat
    end repeat
    return false
end pageLoaded

on userInfo()
    set theResult to do shell script "security find-internet-password -g -r http -s reader.livedoor.com 2>&1"

    repeat with thisLine in paragraphs of theResult
        if thisLine contains "password" then
            set userPassword to my getValue(contents of thisLine, ":")
        else if thisLine contains "acct" then
            set userID to my getValue(contents of thisLine, "=")
        end if
    end repeat

    return {userID, userPassword}
end userInfo

on getValue(str, offsetStr)
    set n to offset of offsetStr in str
    set str to text (n + 1) thru -1 of str
    set AppleScript's text item delimiters to ASCII character 34
    set theList to text items of str
    set AppleScript's text item delimiters to {""}
    set value to theList as Unicode text
    if value contains space then
        set AppleScript's text item delimiters to space
        set theList to text items of value
        set AppleScript's text item delimiters to {""}
        set value to theList as Unicode text
    end if
    return value
end getValue

いろいろな部分で手を抜いていますが...。getValue() ハンドラを少し変更しています。スペースが含まれていると無条件で排除します。なので ID とパスワードにスペースが含まれていると困ることになるかも(そんな人いないですよね!?)。