だるろぐ

明日できることは、今日しない。

Widows Store Apps: WSSE 認証(はてなフォトライフ)

f:id:daruyanagi:20130430181813p:plain

Surface RT(Windows RT)で“はてなブログ”を書くのが微妙にめんどくさいので、“はてなフォトライフ”のクライアントでも作ろうかと思い、@kanaharu ちゃんのブログ(はてなフォトライフに画像をアップロードするプログラムをC#で実装してみた - kanaharu.cpp)を参考にしながら WSSE 認証を実装してみた。

HatenaFotolife クラス

namespace Hatena.Fotolife
{
    public class HatenaFotolife
    {
        const string API_ENDPOINT = "http://f.hatena.ne.jp/atom/";

        public string UserName { get; private set; }
        public string Password { get; private set; }
        public string Title    { get; private set; }
        public string PostUrl  { get; private set; }
        public string FeedUrl  { get; private set; }
        :

みたいなクラスをまず用意。

はてなフォトライフAtomAPIとは - はてなキーワード によると、正しい X-WSSE ヘッダを含んだリクエストを http://f.hatena.ne.jp/atom/ に送ると、

HTTP/1.1 200 OK
Content-Type: application/x.atom+xml

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://purl.org/atom/ns#">
  <link type="application/x.atom+xml" rel="service.post"
        href="http://f.hatena.ne.jp/atom/post" title="fotolife sample">
  <link type="application/x.atom+xml" rel="service.feed"
        href="http://f.hatena.ne.jp/atom/feed" title="fotolife sample">
</feed>

というレスポンスが帰ってくるらしいので、それを PostUrl と FeedUrl に格納してあげる。ついでにタイトルもとっておく。

コンストラクタ

コンストラクタで http://f.hatena.ne.jp/atom/ を叩いて情報を得るようにした。コンストラクタで例外は出さないほうがいいんだっけ? まぁ、いいや。

public HatenaFotolife(string user_name, string password)
{
    if (string.IsNullOrEmpty(user_name))
        throw new ArgumentNullException("Username is null");
    else
        UserName = user_name;

    if (string.IsNullOrEmpty(password))
        throw new ArgumentNullException("Password is null");
    else
        Password = password;

    var request = new HttpRequestMessage(HttpMethod.Get, API_ENDPOINT);
    request.Headers.Add("Accept",
        "application/x.atom+xml, application/xml, text/xml, */*");
    request.Headers.Add("X-WSSE", GenerateWsseHeader());

    var response = new HttpClient().SendAsync(request).Result;
    switch (response.StatusCode)
    {
        case HttpStatusCode.OK:
            var elements = XDocument
                .Load(response.Content.ReadAsStreamAsync().Result)
                .Root.Elements();

            Title = elements
                .First(_ => _.Attribute("rel").Value == "service.post")
                .Attribute("title").Value;
            Title = WebUtility.HtmlDecode(Title);

            PostUrl = elements
               .First(_ => _.Attribute("rel").Value == "service.post")
               .Attribute("href").Value;
            FeedUrl = elements
               .First(_ => _.Attribute("rel").Value == "service.feed")
               .Attribute("href").Value;
            break;

        default:
            Debug.WriteLine(request);
            Debug.WriteLine(response);
            throw new Exception(string.Format("{0}: {1}",
                response.StatusCode, response.ReasonPhrase));
    }
}

オリジナルと違うところは、

を使っているところ。これは .NET 4.5 で追加されたもので、Store Apps で通信を行う場合は基本的にこれを使うことになるらしい。うー、ちゃんと使えるようにならねばね。詳しくは、メイド好きの記事で。

ヘッダーの追加の仕方がよくわからなかったのだけど、HttpRequestMessage を作成してヘッダーを追加し、それを HttpClient で送るという方法でとりあえず動いた。あ、Dispose() してないな、using 使ったほうがよさそうだ。

あと、みんなお馴染み HttpUtility クラスは WebUtility クラス (System.Net) になっている。こういうところでいちいち躓いちゃうのが、今のところのストアアプリ開発の難点。慣れればどうってことないのだろうけど。

WSSE ヘッダーの生成

WSSE認証はHTTPのX-WSSEヘッダを用いて認証用文字列を送信する認証手段です。WSSE認証用文字列にはユーザー名とパスワードが含まれます。このとき、パスワードはSHA1アルゴリズムによって暗号化されたダイジェストとして送信されるため、HTTP基本認証などに比べてセキュアな認証が可能です。

はてなサービスにおけるWSSE認証 - Hatena Developer Center

X-WSSE ヘッダーには以下の4つを含める必要がある。

  • Username:はてなID
  • Nonce:HTTPリクエスト毎に生成したセキュリティ・トークン(ヘッダーに含める際はBase64エンコードが必要)
  • Created:Nonceが作成された日時をISO-8601表記で記述したもの
  • PasswordDigest:Nonce, Created, パスワード(はてなアカウントのパスワード)を文字列連結しSHA1アルゴリズムでダイジェスト化して生成された文字列を、Base64エンコードした文字列

Nonce だけはよくわかんなかったのだけど、適当に 40 バイトぐらいの長さのランダムなデータであればいいみたい。

private string GenerateWsseHeader()
{
    var nonce = GenerateNonce(40);
    var created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
    var password_digest = SHA1ComputeHash(nonce
        .Concat(Encoding.UTF8.GetBytes(created))
        .Concat(Encoding.UTF8.GetBytes(Password))
        .ToArray()
    );

    return string.Format(
        "UsernameToken Username=\"{0}\", PasswordDigest=\"{1}\", " + 
        "Nonce=\"{2}\", Created=\"{3}\"",
        UserName,
        Convert.ToBase64String(password_digest),
        Convert.ToBase64String(nonce),
        created
    );
}
GenerateNonce()

オリジナル(はてなのWSSE認証をC#で実装してみた - kanaharu.cpp)では RNGCryptoServiceProvider を使っていたのだけれど、Store Apps では使えないみたい。

private byte[] GenerateNonce(int length)
{
    var buffer = new byte[length];
    new Random(Environment.TickCount).NextBytes(buffer);
    return buffer;
}

適当に作った。

SHA1 ハッシュの計算

オリジナルでは SHA1Managed を使っていたのだけれど、Store Apps では(以下略

private byte[] SHA1ComputeHash(byte[] input)
{
    return HashAlgorithmProvider
        .OpenAlgorithm("SHA1")
        .HashData(input.AsBuffer())
        .ToArray();
}

酢酸先生(@ch3cooh)の記事を参考に書いてみた。

WindowsRuntimeBufferExtensions.AsBuffer メソッド (System.Runtime.InteropServices.WindowsRuntime)(拡張メソッド)で byte[] から IBuffer へ変換できるよ! これも知ってないとなかなか使いこなせないかもね……。

まとめ

  • WebClient → HttpClient
  • HttpUtility → WebUtility
  • RNGCryptoServiceProvider → Random
  • SHA1Managed → HashAlgorithmProvider.OpenAlgorithm("SHA1")

地道にノウハウを貯めていくしかないな。