ChatWork Creator's Note

ビジネスチャット「チャットワーク」のエンジニアとデザイナーのブログです。

チャットワークのWebhookの署名検証を各言語で実装してみた その2

こんにちは。@eielhです。 id:cw-hayashiが投稿した記事である 「チャットワークのWebhookの署名検証を各言語で実装してみた」があります。 しかし、自分が利用しているプログラミング言語が網羅されていなかったので、続きがないのか確認してみたところ「自分でやってください」*1と言われてしまいました。 というわけで、早速挑戦してみました。

準備するもの

必要な準備物は「チャットワークのWebhookの署名検証を各言語で実装してみた」を参照ください。 creators-note.chatwork.com

Elixir

弊社ではAkkaを利用していますが、アクタープログラミングに挑戦してみるにはScalaはなかなかヘビーです。 そんな時でも、Elixirはライトに使えてアクタープログラミングの学習にも最適です。

$ elixir --version
Erlang/OTP 20 [erts-9.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Elixir 1.5.2
token = "A9ne+ygvdV0IZBaPFV2zC1e5Bk+IsI14BPwieRoBQNU="

request_signature = "G7Gtrh5Ee6d8erOVXhWPtUrkNJqqIT5vwLU50KhyLQk="
request_body = ~c[{"webhook_setting_id":"246","webhook_event_type":"message_created","webhook_event_time":1511238729,"webhook_event":{"message_id":"984676321621704704","room_id":36818150,"account_id":1484814,"body":"test","send_time":1511238729,"update_time":0}}]

key = Base.decode64! token
expected_signature = :crypto.hmac(:sha256, key, request_body)
|> Base.encode64
IO.puts expected_signature === request_signature # => true

OTPの関数を利用することになりましたが、簡単に書くことができました。

参考

Rust

弊社でRustは…オープンになっている情報はありませんね。個人的に利用しています。

$ rustc --version
rustc 1.22.1 (05e2e1c41 2017-11-22)
// cargo-deps: base64, hmac, sha2
extern crate base64;
extern crate hmac;
extern crate sha2;

fn main() {
    let token = "A9ne+ygvdV0IZBaPFV2zC1e5Bk+IsI14BPwieRoBQNU=";

    let request_signature = "G7Gtrh5Ee6d8erOVXhWPtUrkNJqqIT5vwLU50KhyLQk=";
    let request_body = r#"{"webhook_setting_id":"246","webhook_event_type":"message_created","webhook_event_time":1511238729,"webhook_event":{"message_id":"984676321621704704","room_id":36818150,"account_id":1484814,"body":"test","send_time":1511238729,"update_time":0}}"#;

    let key = base64::decode(token).unwrap();
    use hmac::Mac;
    let mut mac = hmac::Hmac::<sha2::Sha256>::new(&key).unwrap();
    mac.input(request_body.as_bytes());
    let digest = mac.result();
    let expect_signature = base64::encode(&digest.code().to_vec());
    println!("{}", expect_signature ==  request_signature)  // => true
}

残念ながら標準ライブラリだけで完結していないです。 他の言語と比べると少し複雑ですね。 また、サンプルコードなので、惜しみなくunwrapを利用しています。

実行にはcargo-scriptを利用しました。

参考

Haskell

弊社では、Haskellでプログラミングしてる人が…他にいるのかわかりません。 勉強している人は何人かいます。

#!/usr/bin/env stack
-- stack --resolver=lts-9.14 runghc --package=base64-bytestring --package=cryptohash-cryptoapi
{-# LANGUAGE OverloadedStrings #-}

import Crypto.Hash.CryptoAPI (SHA256)
import Crypto.HMAC (hmac, MacKey(..))
import qualified Data.ByteString as B
import Data.ByteString.Base64 (encode, decodeLenient)
import qualified Data.ByteString.Lazy as L
import qualified Data.Serialize as S

token :: B.ByteString
token = "A9ne+ygvdV0IZBaPFV2zC1e5Bk+IsI14BPwieRoBQNU="

requestSignature :: B.ByteString
requestSignature = "G7Gtrh5Ee6d8erOVXhWPtUrkNJqqIT5vwLU50KhyLQk="
requestBody :: L.ByteString
requestBody = "{\"webhook_setting_id\":\"246\",\"webhook_event_type\":\"message_created\",\"webhook_event_time\":1511238729,\"webhook_event\":{\"message_id\":\"984676321621704704\",\"room_id\":36818150,\"account_id\":1484814,\"body\":\"test\",\"send_time\":1511238729,\"update_time\":0}}"

key :: B.ByteString
key = decodeLenient token
digest :: SHA256
digest = hmac (MacKey key) requestBody

expectBody :: B.ByteString
expectBody = encode . S.encode $ digest

main = print $ expectBody == requestSignature -- => True

lts-9.14を使用しました。 まったく標準ライブラリで完結していません。base64-bytestringcryptohash-cryptoapiを使用しました。

コード自体はとてもシンプルですね。これを見ると、Haskell使ってみたくなりますよね。 hmacは戻り値の型でアルゴリズムを指定できます。

参考

まとめ

どの言語も標準ライブラリをつかって簡単に検証できるわけではないようです。

  • Elixirは関数プログラミング入門としても使いやすそうです。
  • Rustは緻密なプログラミングが必要そうです。
  • Haskellは楽しく学べそうです。*2

みなさんも好きな言語をつかってチャットワークのWebhookをつかったサービスを作ってみてください!

*1:正確には遠回しにしか聞いていません

*2:https://estore.ohmsha.co.jp/titles/978427406885P