salt)を計算する。(サーバ公開鍵のバイナリデータ)を取得する。16)を計算し、得られる128ビットのバイナリデータをAES-GCMでの暗号鍵とする。12)を計算し、得られる96ビットのバイナリデータをAES-GCMでのnonceとする。10.の暗号鍵と11.のnonceを使って128ビットAES-GCMでメッセージを暗号化する。50+)でメッセージを区切って暗号化する。49の場合は4079バイト単位。49の場合は0x00の1バイト)を付けて、実際には4080バイト単位で暗号化する。暗号化されたペイロードは16バイトのヘッダ込みで4096バイト単位となる。暗号鍵は11.で生成したものを利用する。先頭ブロックを0としてカウンタの値をブロック単位で1ずつ増加させる。AES暗号化で使うIVの値は、12.のnonceの下位6バイトについてカウンタの値とのXORを取って置き換えたものとする(上位6バイトはそのまま)。POSTで暗号化したメッセージを送信する。このとき、HTTPリクエストヘッダに次のパラメータを指定する。APIから取得できるGCMサーバのURLを次のように書き換える必要があります。(FCM)のものとなります(上記のような書き換えが不要になります)。Workerでプッシュ通知を受信アプリケーションサーバ側で正しくブラウザとの鍵共有とメッセージの暗号化ができていれば、プッシュサーバ経由でブラウザにメッセージをプッシュ通知し、ブラウザ側で暗号を解読して通知を表示することが出来ます。Pushに対応している場合はevt.dataにメッセージ(ペイロード)の内容が格納されます。ペイロードが空であれば、evt.dataがnullとなります。APIに似た感じで、evt.data.text()で文字列、evt.data.json()でJSONオブジェクト、evt.data.arrayBuffer()でArrayBuffer、evt.data.blob()でBlobとしてメッセージを受け取ることが可能です。APIと違い、Promiseではなくメソッドの戻り値として直接受け取ります。Capitan)で試してみた限りでは、ウィンドウが全て閉じられていると、通知をクリックしてもウィンドウが開かず、中身がなくても良いので何かウィンドウを開いていれば、通知クリックでそのWebアプリを開くことができるようになる、といった挙動でした。今後のバージョンアップで改善されることを願います。*IETFより新しい仕様ドラフト[draft-ietf-webpush-encryption-06](https://tools.ietf.org/html/draft-ietf-webpush-encryption-06)が公開されました。**WebPushの暗号化手順に変更が加えられた模様ですので、ブラウザ及びプッシュサーバの対応状況が確認出来次第、追って本記事もアップデートします。Push暗号化にサーバ認証を組み合わせるVAPIDの使い方を別記事にまとめました](http://qiita.com/tomoyukilabs/items/9346eb44b5a48b294762)。Pushが利用できるようになります。Pushのサンプルページ](https://labs.othersight.jp/webpushtest/)を用意しました。参考までにコードを[GitHub](https://github.com/tomoyukilabs/WebPushTest)に置きました。APIを使ってみる](http://qiita.com/tomoyukilabs/items/c7268aa29447a1d0a3fb)」で書きましたように、ブラウザでのプッシュ通知の対応が徐々に形になってきています。48以降**で対応していますので、本記事ではその方法について説明します。APIの実装が進んでいるようです](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/pushapi/)。)今回は暗号化が絡みますので、ちょっと難しい内容になりますが、ご了承下さい。8030)](https://tools.ietf.org/html/rfc8030)が標準化されており、ChromeとFirefoxの両方がWebPushに対応しています。WebPushはHTTP/2の新機能であるサーバプッシュ等を活用したことが特徴となっていますが、これは主にブラウザとプッシュ通知サービス側の仕組みであり、開発者は特にそれを意識せず、従来のHTTPのノウハウがあれば使えるようになっています。アプリケーションサーバがあります。ブラウザはプッシュ通知の購読をプッシュサーバに要求すると、プッシュサーバからUAに、アプリケーションサーバからUAにプッシュ通知を送る際のプッシュサーバ側のURLが送られます。APIではこのURLを**endpoint**と呼んでいます。ちなみに、現時点(2016/3/1)ではFirefoxはオープンソースのプッシュ通知サーバ[autopush](http://autopush.readthedocs.org/)をプッシュサーバとして利用しており、HTTP/2ではなくWebSocketでautopushサーバと接続しています。Androidでも同様のプッシュサーバを利用していますが、プッシュ通知自体は現時点ではGCMを間接的に利用しているようです。Pushでは、さらに、中間者攻撃などのリスクを軽減してプライバシーを高めるために、アプリケーションサーバとUAの2者だけがメッセージを解読できるようにするために、鍵共有プロトコルを用いたメッセージの暗号化を用います[[draft-ietf-webpush-encryption-02](https://tools.ietf.org/html/draft-ietf-webpush-encryption-02)]。Mode)**を利用することになっています。メッセージを送る側も受ける側も、まず公開鍵と秘密鍵の鍵ペアを作成する。「送る側の公開鍵と受ける側の秘密鍵」「受ける側の公開鍵と送る側の秘密鍵」のどちらの組み合わせでも同じ共有鍵を生成できる。送る側と受ける側の両方が秘密鍵を外部に漏らさない限りは暗号の解読が困難になり、それでいて共有鍵を直接送らずに同じ鍵を共有できる、というわけです。UAが鍵ペアを生成したら、その公開鍵だけをアプリケーションサーバに渡す。アプリケーションサーバも鍵ペアを生成し、その秘密鍵と、UAから受け取った公開鍵で、共有鍵を生成し、後述するsaltと併せて暗号化に使う。アプリケーションサーバは自身の公開鍵とsalt、暗号文をプッシュサーバ経由でUAに渡す。UAはアプリケーションサーバの公開鍵と自身の秘密鍵で共有鍵を生成し、saltを組み合わせてアプリケーションサーバと同じ手順で暗号文を解読して平文に戻す。暗号化に使う初期化ベクタ)は、鍵共有から生成できる共有鍵や、saltと呼ばれる乱数、等から、HMAC等の処理を施して生成するため、より一層解読を困難にできるよう工夫されています。プッシュ通知の具体的な手順では、実際のWebアプリとWebサーバの両方における実装の仕方を大まかに紹介します。APIを使ってみた](http://qiita.com/tomoyukilabs/items/8fffb4280c1914b6aa3d)で示しているような手順で、`pushManager.subscribe(options)`や`pushManager.getSubscription()`によって、subscriptionを取得します。Pushに対応しているブラウザの場合は、`subscription.endpoint`でプッシュサーバのendpointに加えて、`subscription.getKey()`によって必要な鍵を取得することが出来ます。鍵共有プロトコルECDHで利用する**ブラウザの公開鍵**を取得します。ここで、`p256dh`という名称は、楕円曲線の種類である、P-256あるいはprime256v1を指します。Secret)を取得します。[ECDHの鍵共有だけではセキュリティ上の懸念が残るため仕様に追加されたもののようです](https://github.com/webpush-wg/webpush-encryption/pull/4)。以下、authと呼びます。URLエンコードによって文字列に変換してからアプリケーションサーバに送信しています。Worker側で処理できるようになります。ややこしい処理になりますので、説明するよりはサンプルコードを示したほうが良いのではと思います。Encryptionに対応したNode.js用のサンプルコードがGitHubで公開されていますので、こちらを試しに使ったり、アプリケーションサーバの実装の参考にしたりするとよいでしょう。関連するリンクを次に示します。https://github.com/martinthomson/encrypted-content-encoding下の2つについては、npmでもインストール可能です。https://github.com/martinthomson/encrypted-content-encoding/blob/master/nodejs/ece.js辺りになると思います。これらのコードで鍵ペアの生成やメッセージの暗号化を行っています。Jetty等)上記のNode.jsのコードを参考にして、こちらでJavaのコードを書いてみました。サンプルページも用意しておりますので、ChromeあるいはFirefoxでお試し下さい。https://labs.othersight.jp/webpushtestJavaでの実装を試してみて注意が必要だった点は次の通りです。Castleの暗号ライブラリ](http://www.bouncycastle.org/wiki/display/JA1/Elliptic+Curve+Key+Pair+Generation+and+Key+Factories)を利用する必要があります。BouncyCastleProvider());`でプロバイダを追加し、その後の各メソッドで`BC`を暗号プロバイダとして指定する必要があります。上記のNode.jsでも同じですが、楕円曲線の名前としては`prime256v1`を指定します。アプリケーションサーバの公開鍵`localPublicKey`をプッシュ通知に付与する際には、`localPublicKey.getEncoded()`ではなく、`localPublicKey.getQ().getEncoded(false)`を送るようにします。ブラウザの公開鍵が65バイトなので、同じ65バイトのデータになっていれば正しい公開鍵と考えられます。具体的には何の計算をしているのかざっとですが、実際のアルゴリズムを書き下します。以下、`||`は、バイナリデータの連結を表します。上記のライブラリを活用する場合は下記の詳細なアルゴリズムは参照不要ですが、ご自身で暗号化等を実装する場合等に参考になれば幸いです。EncryptionではHKDFによって鍵やNonceと呼ばれる暗号化パラメータを生成しますので、HKDFの手順を簡単に説明します。具体的にはextractとexpandの2つの関数で構成されます。Material)をハッシュする。Key)と呼び、32バイトのバイナリデータとなる。SHA-256によってハッシュし、そのうち先頭の`L`バイトを取り出す(`L`が32バイト以内の場合)。Encryptionの手順は次の通りです。44/45が対応する仕様と同じになります。Push暗号化に対応するにはフラグを有効化する必要がありますので、実用上はあまり意識しなくても良いかもしれません。ブラウザから公開鍵とauthを取得する。アプリケーションサーバ用の鍵ペアを生成する。ブラウザの公開鍵とアプリケーションサーバの秘密鍵から共有鍵を生成し、そのバイナリデータを`IKM`とする。16バイト(128ビット)の乱数を生成し、これをsaltとする。IKM)`を計算し、9.に進む。IKM)`を計算する。salt)`を計算する。(サーバ公開鍵のバイナリデータ)`を取得する。16)`を計算し、得られる128ビットのバイナリデータをAES-GCMでの暗号鍵とする。12)`を計算し、得られる96ビットのバイナリデータをAES-GCMでのnonceとする。10.の暗号鍵と11.のnonceを使って128ビットAES-GCMでメッセージを暗号化する。50+)でメッセージを区切って暗号化する。49の場合は4079バイト単位。49の場合は`0x00`の1バイト)を付けて、実際には4080バイト単位で暗号化する。暗号化されたペイロードは16バイトのヘッダ込みで4096バイト単位となる。暗号鍵は11.で生成したものを利用する。先頭ブロックを`0`としてカウンタの値をブロック単位で1ずつ増加させる。AES暗号化で使うIVの値は、12.のnonceの下位6バイトについてカウンタの値とのXORを取って置き換えたものとする(上位6バイトはそのまま)。POSTで暗号化したメッセージを送信する。このとき、HTTPリクエストヘッダに次のパラメータを指定する。APIから取得できるGCMサーバのURLを次のように書き換える必要があります。(FCM)のものとなります(上記のような書き換えが不要になります)。Workerでプッシュ通知を受信アプリケーションサーバ側で正しくブラウザとの鍵共有とメッセージの暗号化ができていれば、プッシュサーバ経由でブラウザにメッセージをプッシュ通知し、ブラウザ側で暗号を解読して通知を表示することが出来ます。Pushに対応している場合は`evt.data`にメッセージ(ペイロード)の内容が格納されます。ペイロードが空であれば、`evt.data`が`null`となります。API](http://qiita.com/tomoyukilabs/items/9b464c53450acc0b9574)に似た感じで、`evt.data.text()`で文字列、`evt.data.json()`でJSONオブジェクト、`evt.data.arrayBuffer()`でArrayBuffer、`evt.data.blob()`でBlobとしてメッセージを受け取ることが可能です。APIと違い、Promiseではなくメソッドの戻り値として直接受け取ります。Capitan)で試してみた限りでは、ウィンドウが全て閉じられていると、通知をクリックしてもウィンドウが開かず、中身がなくても良いので何かウィンドウを開いていれば、通知クリックでそのWebアプリを開くことができるようになる、といった挙動でした。今後のバージョンアップで改善されることを願います。送信いただいたご意見への返信は行っておりません。お問い合わせください。