JavaScriptを有効にしてください

【ECDH・aes128gcm】MissCatの通知システムについて

 ·  ☕ 8 min read

こんにちは.論文執筆真っ最中のYuWdです.
最近,研究および論文執筆のタスクが落ち着いてきたのでMissCatの改修をボチボチ行っています.
(MissCatとはiOS向けのMisskeyクライアント)

改修に着手するにあたって,まずは通知システムを直すところに目星をつけたのですが,昔の自堕落(ドキュメントを書かない性分)により通知機能のフローが一切思い出せず,手を付けられない状態でした.普通のフローと違って,MissCatは通知を監視する中継サーバを建てたり,サーバでストリーミングを行っているわけではなく,WebPushを応用した風変わりなフローで機能しているのです.そこで,復習がてらMissCatの通知機能の仕組みについて,ドキュメントに代わる記事を書こうと思った次第であります.

TL;DR

  • MissCatの通知は本来ブラウザが使用する技術であるWebPushを応用して機能している.
  • 通知イベントが発生した際に中継サーバへとイベントを送らせるようMisskeyのエンドポイントから登録.
  • 中継サーバはprime256v1で暗号化されたメッセージを復号し,アプリ用通知メッセージをFCMに投げる.

流れ

MissCatの通知機能について,シーケンス図を以下に示します.



  1. ユーザがMissCatを起動すると,通知に備えるためにFCM(Firebase Cloud Messaging)のtokenを発行します.
  2. tokenが発行できたら,MisskeyのAPIsw/registerを叩きます.このAPIはWebPush用にServiceWorkerが使用するもので,本来MissCatのようなネイティブアプリとは関係がないAPIです.sw/registerにはFCMのtokenが付された中継サーバのURLをendpointとして設定します.
  3. Misskey側で通知イベントが発生すると,Misskeyはsw/registerで登録されたendpoint(ここでは中継サーバ)に通知を投げてきます.このとき,通知メッセージは楕円曲線暗号(prime256v1)で暗号化されているため,中継サーバで復号する必要があります.
  4. 復号された中身はただのJSON形式のデータなので,適切に情報を切り出して,あとは通知用のメッセージを作り,FCMに投げます.

以上が,簡単な通知機能の流れとなります.

WebPush / ServiceWorker

ここから先はフローの理解を深めるべく,よりテクニカルな詳細について書いておこうと思います.

WebPushとはブラウザにおいてプッシュ通知を行う技術です.HTTP/2に準じており,RFC8030にて規格化されています.WebPushはServiceWorkerによって実現され,ブラウザを閉じていてもプッシュ通知を実現することができます.ここで,ServiceWorkerとはブラウザのバックグラウンドで動作するJavaScript環境のことであり,裏で動いてくれているおかげでネイティブアプリのような通知が実現されます.

WebPushでは上のシーケンス図のような中継サーバを用意することは少なく,普通はブラウザベンダが用意したサーバを使うことが多いです.このようなサーバのことを一般にPush Serviceと呼びます.すなわち,WebPushではServiceWorker ⇆ Push Service ⇆ Application Server(上で言うMisskey)の三者が互いに通信しあうことで機能しています.ServiceWorker-Push Service間の通信にはWebSocketが利用されることも多いです.

上にWebPushのシーケンス図を示します.前述の流れと同様,WebPushはpublish-subscribe形式で機能しているため,WebPushが機能するには必ず,初めにServiceWorkerがPush Serverへとsubscribeを要求しないといけません.subscribeが要求されるとPush Serviceはendpointと暗号鍵をServiceWorkerへと送信します.(暗号鍵については次章参照) ServiceWorkerは受け取ったそれらをApplication Serverへ送信して,Application Serverはそれらを記憶しておきます.通知を行うタイミングで,Application Serverは記憶しておいた暗号鍵でメッセージを暗号化したのち,endpointに対してPOSTを発行し,Push Serviceを中継してServiceWorkerへとメッセージが送信されます.最終的にServiceWorkerはメッセージを復号し,通知を発行します.

以上の機能がMisskey上では実際に行われており,MissCatではこれらを流用する形で通知を実現しています.(すなわち「ServiceWorker ⇆ Push Service ⇆ Misskey」⇒「MissCat ⇆ 中継サーバ ⇆ Misskey」)

こうした設計により,

  • 中継サーバが最小限の構成で済む.
  • ストリーミングによる通知の監視を行わずに通知を実現できる.
    • ゆえに中継サーバの負担が少なく,全ユーザに対して監視を行わずに済む.
  • Misskeyで実際に運用されている機能を流用しているため,バージョンアップによる動作不能などの確率が低い.

といったメリットがあります.

Elliptic Curve Diffie-Hellman (ECDH) / aes128gcm

次に暗号化・復号について説明したいと思います.ECDHは楕円曲線を応用した鍵共有プロトコルです.クライアントとサーバがそれぞれ$(A_\mathrm{priv}, A_\mathrm{pub}), (B_\mathrm{priv}, B_\mathrm{pub})$という鍵を持っていた際に,ECDHでは$A_\mathrm{pub} \cdot B_\mathrm{priv}$および$A_\mathrm{priv} \cdot B_\mathrm{pub}$によって全く同一の暗号鍵を生成することができます.したがって,ECDHを用いれば秘密鍵を通信することなく,互いに同じ暗号鍵を用いて暗号化・復号を行うことができるという利点があります.

楕円曲線は離散対数が多項式時間で解けないため(離散対数問題),暗号によく用いられます.ここで,楕円曲線はある有限体$\mathbf{K}$上において$a, b$をパラメタとし,以下の式で定義されます.

$$ y^2 = x^3 + ax + b$$

WebPushではprime256v1と呼ばれる楕円曲線上でECDHが行われます.prime256v1は次のようなパラメタ$a,b$が設定された楕円曲線です.(参考)

$$
\begin{align}
a &= \small {FFFFFFFF\; 00000001\; 00000000\; 00000000\; 00000000\; FFFFFFFF\; FFFFFFFF\; FFFFFFFC} \\
b &= \small {5AC635D8\; AA3A93E7\; B3EBBD55\; 769886BC\; 651D06B0\; CC53B0F6\; 3BCE3C3E\; 27D2604B}
\end{align}
$$

メッセージを復号する上で,まずはペイロードの構造を理解する必要があります.aes128gcmにおけるペイロードは次のようなフォーマットで格納されています.

1
2
3
+-----------+--------+-----------+------------------+
| salt (16) | rs (4) | idlen (1) | contents (idlen) |
+-----------+--------+-----------+------------------+

saltはsaltです.(システムを作ったことがある方ならわかる)
rs, lengthはそれぞれ,メッセージの区切り長,contentsの長さを指します.contentsが実際のメッセージであり,この部分だけが暗号化されています.

aes128gcmでは次のようにペイロードが作成されます.(||はbyteとしてconcatを表す)

  1. $K$ = ECDH($A_\mathrm{pub}, B_\mathrm{priv}$)
  2. PRK_key = HMAC_SHA_256(auth_secret, $K$)
  3. info = “WebPush: info” || 0x00 || $A_\mathrm{pub}$ || $B_\mathrm{priv}$
  4. IKM = HMAC_SHA_256(PRK_key, info || 0x01)
  5. salt = create_salt()
  6. PRK = HMAC_SHA_256(salt, IKM)
  7. cek_info = “Content-Encoding: aes128gcm” || 0x00
  8. CEK = HMAC_SHA_256(PRK, cek_info || 0x01) の先頭16bytes
  9. nonce_info = “Content-Encoding: nonce” || 0x00
  10. nonce = HMAC_SHA_256(PRK, nonce_info || 0x01) の先頭12bytes
  11. contents = encrypt(contents, CEK, nonce)
  12. return salt || rs || idlen || contents

したがって,CEKとnonceがあればcontentsを復号できます.以下に示すのはnode.jsの例です.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  const salt = body.slice(0, 16);
  const rs = body.slice(16, 16 + 4);
  const idlen_hex = body.slice(16 + 4, 16 + 4 + 1).toString("hex");
  const idlen = parseInt(idlen_hex, 16); // keyidの長さ
  const keyid = body.slice(16 + 4 + 1, 16 + 4 + 1 + idlen);
  const content = body.slice(16 + 4 + 1 + idlen, body.length);
  const sender_public = decodeBase64(keyid.toString("base64"));

  // 共有秘密鍵を生成(ECDH)
  receiver_curve = crypto.createECDH("prime256v1");
  receiver_curve.setPrivateKey(receiver_private);
  const sharedSecret = receiver_curve.computeSecret(keyid);

  // key
  const prk_key = sha256(auth_secret, sharedSecret);
  const keyInfo = Buffer.concat([
    Buffer.from("WebPush: info\0"),
    receiver_public,
    sender_public,
    Buffer.from("\1")
  ]);
  const ikm = sha256(prk_key, keyInfo);

  // prk
  const prk = sha256(salt, ikm);

  // cek
  const cekInfo = Buffer.from("Content-Encoding: aes128gcm\0\1");
  const cek = sha256(prk, cekInfo).slice(0, 16);

  // initialization vector
  const nonceInfo = Buffer.from("Content-Encoding: nonce\0\1");
  const nonce = sha256(prk, nonceInfo).slice(0, 12);
  const iv = nonce;

  // aes-128-gcm
  const decipher = crypto.createDecipheriv("aes-128-gcm", cek, iv);
  result = decipher.update(content);

  return result.toString("UTF-8");

FCM / APNs

引用: https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server

ネイティブアプリとしての通知はAPNsを通じて行われます.APNsとはAppleが提供しているPush通知基盤のことであり,上でいうPush Serviceのような存在です.
APNsでは端末一つひとつに固有のtokenとしてdevice-tokenを付与することで一意性を保っており,開発者はAPNsへデバイスの登録を要求する必要があります.
このことを踏まえ,通知の説明すると

  1. APNsへのデバイス登録を行い,device-tokenを受け取る.
  2. FCMにdevice-tokenを送信して,端末を登録する.
  3. FCMは通知の送信を要求されると,device-tokenと通知内容をAPNsに送り通知を依頼する.
  4. APNsが当該端末へと通知を行う.

となります.すなわち,プッシュ通知でいうPush ServiceがAPNsで,Application ServerがFCMに該当するといった次第です.また,3番目の「APNsへの通知依頼」時に,APNsの証明書を切り替えることで,開発環境 / 本番環境を切り替えることができます.

まとめ

  • 未来の自分の助けになると嬉しい
共有

YuWd (Yuiga Wada)
著者
YuWd (Yuiga Wada)
機械学習・競プロ・iOS・Web