ウェブプッシュ プロトコル

ここまで、ライブラリを使用して push メッセージをトリガーする方法を説明してきましたが、 どういうことでしょうか

ネットワーク リクエストを作成して、 選択できるようになりますこのネットワーク リクエストを定義する仕様は、 ウェブプッシュ プロトコル

サーバーから push への push メッセージの送信を示す図
サービス

このセクションでは、サーバーがアプリケーションによって自身を識別する方法について説明します。 暗号化されたペイロードと関連データの送信方法が含まれます。

これはウェブプッシュの面白さとは言えませんし、暗号化については私には専門的ではありませんが、もう少し詳しく見てみましょう。 これらのライブラリが内部で何を行っているかを把握しておくと便利です。

アプリケーション サーバーキー

ユーザーを登録するときは、applicationServerKey を渡します。このキーは、 push サービスに渡され、サブスクライブしたアプリケーションが ユーザーが push メッセージをトリガーしているアプリケーションでもあります。

push メッセージのトリガー時には push サービスがアプリケーションを認証できるようにします。(これは VAPID 仕様に準拠しています)。

具体的にはどういうことでしょうか。また、どのようなことが起きるのでしょうか。これが アプリケーション サーバー認証:

  1. アプリケーション サーバーは、限定公開アプリケーション キーを使用して一部の JSON 情報に署名します。
  2. この署名付き情報は、POST リクエストのヘッダーとして push サービスに送信されます。
  3. プッシュ サービスは、受け取った保存済み公開鍵を pushManager.subscribe(): 受信した情報が次の署名者による署名であることを確認します。 公開鍵に関連する秘密鍵を指定します。注意: 公開鍵は、 サブスクライブ呼び出しに渡された applicationServerKey
  4. 署名された情報が有効な場合、push サービスは push を というメッセージです。

この情報フローの例を以下に示します。( あります。

アプリケーション サーバーの秘密鍵が Google から送信される
メッセージ

署名された情報JSON Web Token です。

JSON Web Token

JSON Web Token(略して JWT)は、 受信側が検証できるようにメッセージをサードパーティに送信する 詳細情報を確認できます

サードパーティは、メールを受信したときに JWT の署名を検証できます。もし 署名が有効である場合、JWT が一致する署名で署名されている必要があります。 想定される送信者からのものである必要があります。

https://jwt.io/ には、 署名はあなたに代わって行うことをおすすめします できます。完全性を期すために、署名付き JWT を手動で作成する方法を見てみましょう。

ウェブ push と署名付き JWT

署名付き JWT は単なる文字列ですが、3 つの文字列を結合したものと考えることができます。 表示されます。

JSON Web での文字列のイラスト
トークン

1 番目と 2 番目の文字列(JWT 情報と JWT データ)は、 Base64 でエンコードされた JSON(一般公開で読み取り可能)。

最初の文字列は、JWT 自体に関する情報で、どのアルゴリズムを使用するかを示します。 を使用して署名が作成されました。

ウェブ push 用の JWT 情報には、次の情報が含まれている必要があります。

{
  "typ": "JWT",
  "alg": "ES256"
}

2 番目の文字列は JWT データです。これにより、JWT の送信者に関する情報が得られます。 有効期間を指定します

ウェブ push の場合、データの形式は次のようになります。

{
  "aud": "https://some-push-service.org",
  "exp": "1469618703",
  "sub": "mailto:example@web-push-book.org"
}

aud の値は、「オーディエンス」(JWT の対象となるユーザー)です。ウェブの場合は、 Audience は push サービスであるため、これをpush の送信元 サービス

exp 値は JWT の有効期限です。これにより、スヌーパーが JWT がインターセプトした場合、その JWT を再利用できます。有効期限は、 24 時間以下にする必要があります

Node.js では、次のように有効期限が設定されます。

Math.floor(Date.now() / 1000) + 12 * 60 * 60;

避けるべき時間は 24 時間ではなく 12 時間です 送信アプリケーションと push サービス間のクロック相違の問題を

最後に、sub の値は、URL か mailto のメールアドレスのいずれかにする必要があります。 これは、push サービスが送信者に連絡する必要がある場合に、 JWT の連絡先情報。(この理由から、ウェブ push ライブラリには 。

JWT Info と同様に、JWT データは URL セーフの base64 としてエンコードされます。 使用します。

3 番目の文字列(シグネチャ)は、最初の 2 つの文字列を取った結果 (JWT 情報と JWT データ)があります。これらをドット文字で結合します。 「未署名のトークン」を呼び出して署名します。

署名プロセスでは、「未署名のトークン」を暗号化する必要があるES256 を使用します。JWT によると、 仕様です。ES256 は、「P-256 曲線と ECDSA を使用した SHA-256 ハッシュ アルゴリズムが必要です。ウェブ暗号を使用すると、次のように署名を作成できます。

// Utility function for UTF-8 encoding a string to an ArrayBuffer.
const utf8Encoder = new TextEncoder('utf-8');

// The unsigned token is the concatenation of the URL-safe base64 encoded
// header and body.
const unsignedToken = .....;

// Sign the |unsignedToken| using ES256 (SHA-256 over ECDSA).
const key = {
  kty: 'EC',
  crv: 'P-256',
  x: window.uint8ArrayToBase64Url(
    applicationServerKeys.publicKey.subarray(1, 33)),
  y: window.uint8ArrayToBase64Url(
    applicationServerKeys.publicKey.subarray(33, 65)),
  d: window.uint8ArrayToBase64Url(applicationServerKeys.privateKey),
};

// Sign the |unsignedToken| with the server's private key to generate
// the signature.
return crypto.subtle.importKey('jwk', key, {
  name: 'ECDSA', namedCurve: 'P-256',
}, true, ['sign'])
.then((key) => {
  return crypto.subtle.sign({
    name: 'ECDSA',
    hash: {
      name: 'SHA-256',
    },
  }, key, utf8Encoder.encode(unsignedToken));
})
.then((signature) => {
  console.log('Signature: ', signature);
});

push サービスでは、公開アプリケーション サーバー鍵を使用して JWT を検証できます を使用して、署名を復号し、復号された文字列が 「未署名のトークン」として(JWT の最初の 2 つの文字列)。

署名付き JWT(ドットで結合された 3 つの文字列すべて)がウェブに送信される 次のように、WebPush を先頭にした Authorization ヘッダーとしてサービスを push します。

Authorization: 'WebPush [JWT Info].[JWT Data].[Signature]';

また、Web Push Protocol には、アプリケーション サーバーの公開鍵を鍵で管理する必要があると規定されています。 Crypto-Key ヘッダーで、URL セーフの Base64 エンコード文字列として 先頭に p256ecdsa= が付加されています。

Crypto-Key: p256ecdsa=[URL Safe Base64 Public Application Server Key]

ペイロード暗号化

次に、push メッセージを使用してペイロードを送信し、 プッシュ メッセージを受信すると、受信したデータにアクセスできるようになります。

他の push サービスを使用したことがあれば、ウェブが push される理由について 暗号化する必要はあるか?ネイティブ アプリでは、プッシュ メッセージでデータを書式なしテキストとして送信できます。

ウェブプッシュの優れた点の一つは、すべてのプッシュ サービスが 同じ API(ウェブ プッシュ プロトコル)が使用されるため、開発者は 構成されます。適切な形式でリクエストを行うことができます。 プッシュ メッセージを送信します。この方法の欠点は、デベロッパーが 信頼できないプッシュ サービスにメッセージを送信することが考えられます。方法 push サービスは送信されたデータを読み取ることができません。 情報を復号できるのはブラウザだけです。これにより 分析できます

ペイロードの暗号化は、Message Encryption 仕様をご覧ください。

push メッセージ ペイロードを暗号化する具体的な手順を説明する前に、 暗号化の際に使用されるいくつかの手法について プロセスです(Matt Scales はプッシュに関する優れた記事を執筆しているため、帽子の大きなヒントが投稿されています。 encryption.)

ECDH と HKDF

ECDH と HKDF はどちらも暗号化プロセス全体で使用されるため、暗号化には 目的としています。

ECDH: 楕円曲線 Diffie-Hellman 鍵交換

アリスとボブという 2 人が情報を共有したいと考えているとします。 アリスとボブはともに独自の公開鍵と秘密鍵を持っています。アリスとボブ 公開鍵を共有する必要があります。

ECDH で生成された鍵の有用な特性は、Alice は ECDH を使用して 使用して、シークレット値「X」を作成します。ボブはできる アリスの秘密鍵とアリスの公開鍵を 独立した同じ値「X」を作成します。これで「X」が共有シークレット その際、Alice と Bob は公開鍵を共有するだけで済みました。ボブとアリス 「X」を使用できます暗号鍵を使用して、それらの間でメッセージを暗号化および復号できます。

ECDH は、私の知る限りでは、この「特徴」を可能にする曲線の特性を定義している シークレット「X」を作成します。

ここでは ECDH の概要を説明します。詳細については、こちらの動画をご覧ください。

コードに関しては、ほとんどの言語 / プラットフォームには、 簡単に生成できます。

ノードでは、次の処理を行います。

const keyCurve = crypto.createECDH('prime256v1');
keyCurve.generateKeys();

const publicKey = keyCurve.getPublicKey();
const privateKey = keyCurve.getPrivateKey();

HKDF: HMAC ベースの鍵導出関数

ウィキペディアには、HKDF についての簡潔な説明があります。

HKDF は、脆弱な鍵を変換する HMAC ベースの鍵導出関数 鍵マテリアルを暗号的に強い鍵マテリアルに変換できます。これは Diffie Hellman は、共有されたシークレットを鍵マテリアルに 暗号化、整合性チェック、認証に適した形式です。

基本的に、HKDF は特に安全ではない入力を受け取り、安全性を高めます。

この暗号化を定義する仕様では、ハッシュ アルゴリズムとして SHA-256 を使用する必要があります。 ウェブプッシュで生成される HKDF のキーは 256 ビット以下にする必要があります (32 バイト)。

Node では、次のように実装できます。

// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
  // Extract
  const keyHmac = crypto.createHmac('sha256', salt);
  keyHmac.update(ikm);
  const key = keyHmac.digest();

  // Expand
  const infoHmac = crypto.createHmac('sha256', key);
  infoHmac.update(info);

  // A one byte long buffer containing only 0x01
  const ONE_BUFFER = new Buffer(1).fill(1);
  infoHmac.update(ONE_BUFFER);

  return infoHmac.digest().slice(0, length);
}

このコード例に関する Mat Scale の記事をご覧ください。

ECDH を大まかにカバーしています。 および HKDF

ECDH は、公開鍵を共有し、共有シークレットを生成する安全な方法です。HKDF は 保護します。

これはペイロードの暗号化時に使用されます。次に、Google Cloud が 暗号化する方法も教えてください。

入力

ペイロードとともにプッシュ メッセージをユーザーに送信するには、次の 3 つの入力が必要です。

  1. ペイロード自体。
  2. PushSubscriptionauth Secret。
  3. PushSubscriptionp256dh キー。

PushSubscription から取得される authp256dh の値を見てきましたが、 定期購入の場合、次の値が必要になります。

subscription.toJSON().keys.auth;
subscription.toJSON().keys.p256dh;

subscription.getKey('auth');
subscription.getKey('p256dh');

auth 値はシークレットとして扱い、アプリケーションの外部で共有しないでください。

p256dh キーは公開鍵であり、クライアントの公開鍵と呼ばれることもあります。現在地 サブスクリプションの公開鍵として p256dh を使用します。サブスクリプションの公開鍵は、 表示されます。ブラウザは秘密鍵を秘密にし、それを使って鍵を復号します 記述できます。

これらの 3 つの値、authp256dhpayload は入力として必要であり、 暗号化プロセスには、暗号化されたペイロード、ソルト値、公開鍵を データの暗号化を行います。

ソルト

ソルトには 16 バイトのランダムデータが必要です。NodeJS では、次の手順でソルトを作成します。

const salt = crypto.randomBytes(16);

公開鍵 / 秘密鍵

公開鍵と秘密鍵は、P-256 楕円曲線を使用して生成する必要があります。 Node では次のようになります。

const localKeysCurve = crypto.createECDH('prime256v1');
localKeysCurve.generateKeys();

const localPublicKey = localKeysCurve.getPublicKey();
const localPrivateKey = localKeysCurve.getPrivateKey();

これらの鍵を「ローカル鍵」と呼びます。暗号化にのみ使用され、 アプリケーション サーバーのキーには関係ありません。

ペイロードでは、認証シークレットとサブスクリプション公開鍵を入力として、また新しく生成されたキーを使って 使用すると、暗号化を行う準備が整います。

共有シークレット

まず、サブスクリプションの公開鍵と新しいサブネットを使用して、 (Alice と Bob の ECDH の説明を思い出してください。。

const sharedSecret = localKeysCurve.computeSecret(
  subscription.keys.p256dh,
  'base64',
);

これは、次のステップで擬似ランダムキー(PRK)を計算するために使用されます。

疑似ランダム鍵

疑似ランダム鍵(PRK)は、push サブスクリプションの認証と 先ほど作成した共有シークレットが含まれます。

const authEncBuff = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(subscription.keys.auth, sharedSecret, authEncBuff, 32);

Content-Encoding: auth\0 文字列は何のためのものですか? 要するに、ブラウザには明確な目的はありませんが、 受信メッセージを復号し、想定されているコンテンツ エンコーディングを探す \0 は、値が 0 のバイトをバッファの最後に追加します。これは、 非常に多くのバイト数が想定され、ブラウザがメッセージを復号すると コンテンツ エンコーディングで、値が 0 のバイトが続き、 暗号化します。

疑似ランダムキーは、認証、共有シークレット、エンコーディング情報を実行するだけです。 (暗号強度を高める)必要があります。

コンテキスト

「コンテキスト」暗号化で後から 2 つの値を計算するために使用されるバイトのセット できます。基本的には、サブスクリプションの公開鍵を含むバイトの配列です。 あります。

const keyLabel = new Buffer('P-256\0', 'utf8');

// Convert subscription public key into a buffer.
const subscriptionPubKey = new Buffer(subscription.keys.p256dh, 'base64');

const subscriptionPubKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = subscriptionPubKey.length;

const localPublicKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = localPublicKey.length;

const contextBuffer = Buffer.concat([
  keyLabel,
  subscriptionPubKeyLength.buffer,
  subscriptionPubKey,
  localPublicKeyLength.buffer,
  localPublicKey,
]);

最後のコンテキスト バッファはラベル、つまりサブスクリプション公開鍵のバイト数です。 その後にキー自体、ローカル公開鍵のバイト数、キーが続きます できます。

このコンテキスト値により、ノンスとコンテンツ暗号鍵の作成時にそれを使用できます。 (CEK)。

コンテンツ暗号鍵とノンス

ノンスはリプレイを防止する値です。 防ぐことができます。

コンテンツ暗号鍵(CEK)は、最終的にペイロードを暗号化するために使用される鍵です。

まず、ノンスと CEK のデータバイトを作成する必要があります。CEK は、 エンコード文字列の後に、先ほど計算したコンテキスト バッファが続きます。

const nonceEncBuffer = new Buffer('Content-Encoding: nonce\0', 'utf8');
const nonceInfo = Buffer.concat([nonceEncBuffer, contextBuffer]);

const cekEncBuffer = new Buffer('Content-Encoding: aesgcm\0');
const cekInfo = Buffer.concat([cekEncBuffer, contextBuffer]);

この情報は、ソルトと PRK を nonceInfo および cekInfo と組み合わせて HKDF を介して実行されます。

// The nonce should be 12 bytes long
const nonce = hkdf(salt, prk, nonceInfo, 12);

// The CEK should be 16 bytes long
const contentEncryptionKey = hkdf(salt, prk, cekInfo, 16);

これにより、ノンスとコンテンツ暗号鍵が提供されます。

暗号化を実行する

コンテンツ暗号鍵が準備できたので、ペイロードを暗号化できます。

コンテンツ暗号鍵を使用して AES128 暗号を作成します。 ノンスは初期化ベクトルです。

Node では次のようになります。

const cipher = crypto.createCipheriv(
  'id-aes128-GCM',
  contentEncryptionKey,
  nonce,
);

ペイロードを暗号化する前に、必要なパディング量を定義する必要があります。 先頭に追加します。パディングを追加する理由は 傍受者が自分の発言を盗まれて "types"ペイロードのサイズに応じて 自動的にスケールします

追加するパディングの長さを示すために、2 バイトのパディングを追加する必要があります。

たとえば、パディングを追加しなかった場合、値が 0 の 2 バイト(パディングが存在しない)になり、この 2 バイトの後にペイロードが読み取られることになります。5 バイトのパディングを追加した場合、最初の 2 バイトの値は 5 になるため、コンシューマはさらに 5 バイトを読み取り、ペイロードの読み取りを開始します。

const padding = new Buffer(2 + paddingLength);
// The buffer must be only zeros, except the length
padding.fill(0);
padding.writeUInt16BE(paddingLength, 0);

次に、この暗号でパディングとペイロードを実行します。

const result = cipher.update(Buffer.concat(padding, payload));
cipher.final();

// Append the auth tag to the result -
// https://nodejs.org/api/crypto.html#crypto_cipher_getauthtag
const encryptedPayload = Buffer.concat([result, cipher.getAuthTag()]);

これで、暗号化されたペイロードが完成しました。正解です。

あとは、このペイロードを push サービスに送信する方法を指定するだけです。

暗号化されたペイロード ヘッダーと本体

この暗号化されたペイロードを push サービスに送信するには、いくつかの POST リクエスト内の異なるヘッダーを使用します

暗号化ヘッダー

「暗号化」は、ヘッダーには、ペイロードの暗号化に使用する salt が含まれている必要があります。

次のように、16 バイトのソルトを Base64 で URL セーフとしてエンコードし、Encryption ヘッダーに追加する必要があります。

Encryption: salt=[URL Safe Base64 Encoded Salt]

Crypto-Key ヘッダー

[Application Server Keys] で Crypto-Key ヘッダーが使用されていることを確認しました。 セクションにアプリケーション サーバーの公開キーを含めます。

このヘッダーは、暗号化に使用するローカル公開鍵の共有にも使用されます。 記述できます。

結果のヘッダーは次のようになります。

Crypto-Key: dh=[URL Safe Base64 Encoded Local Public Key String]; p256ecdsa=[URL Safe Base64 Encoded Public Application Server Key]

コンテンツの種類、長さ、エンコード ヘッダー

Content-Length ヘッダーは、暗号化されたパケットのバイト数です。 記述できます。'コンテンツタイプ'「Content-Encoding」をヘッダーは固定値です。 以下にその例を示します。

Content-Length: [Number of Bytes in Encrypted Payload]
Content-Type: 'application/octet-stream'
Content-Encoding: 'aesgcm'

これらのヘッダーを設定して、暗号化されたペイロードを本文として送信する必要があります。 表示されます。Content-Type が次のように設定されています。 application/octet-stream。これは、暗号化されたペイロードが、外部 IP アドレスを持つ バイトストリームとして送信されます

NodeJS では、次のようにします。

const pushRequest = https.request(httpsOptions, function(pushResponse) {
pushRequest.write(encryptedPayload);
pushRequest.end();

ヘッダーの追加

JWT / アプリケーション サーバーキーに使用されるヘッダー( push サービスを使用するアプリケーションなど)で暗号化されるほか、暗号化されたデータを送信して、 記述できます。

追加のヘッダーを使用して、サービスを push し、 確認することもできます。これらのヘッダーには、必須のものと任意のものがあります。

TTL ヘッダー

必須

TTL(有効期間)は、秒数を指定する整数です。 push サービスでメッセージが配信される前に 提供します。TTL の期限が切れると、メッセージは push サービス キューに登録され、サービスは配信されません。

TTL: [Time to live in seconds]

TTL をゼロに設定すると、push サービスは すぐにメッセージが表示されますが、デバイスにアクセスできない場合は、 直ちに push サービス キューから削除されます。

push サービスは技術的には、push メッセージの TTL を 必要があります。これが発生したかどうかは、TTL ヘッダーを調べることで確認できます。 push サービスからのレスポンスを返します。

トピック

任意

トピックは、保留中のメッセージを 新しいメッセージが返されます。

これは、メッセージ中に複数のメッセージが デバイスがオフラインのため、ユーザーに最新状況の メッセージが表示されます。

緊急度

任意

緊急度は、ユーザーに対するメッセージの重要度を push サービスに伝えます。この をプッシュ サービスで使用できます。これにより、ユーザーのデバイスの バッテリー残量が低下しているときに、重要なメッセージを確認するためにスリープモードから復帰させることができます。

ヘッダー値の定義は次のとおりです。デフォルト値は normal です。

Urgency: [very-low | low | normal | high]

すべてが 1 か所に

この仕組みについてご不明な点がある場合は、ライブラリがどのようにトリガーされるかをいつでも確認できます。 web-push-libs 組織にメッセージを push します。

暗号化されたペイロードと上記のヘッダーを入手したら、POST リクエストを行うだけです。 PushSubscriptionendpoint にマッピングします。

では、この POST リクエストに対するレスポンスはどうなるでしょうか。

push サービスからのレスポンス

push サービスにリクエストを送信したら、ステータス コードを確認する必要があります。 リクエストが成功したかどうかがわかります。 判断できます

ステータス コード 説明
201 作成しました。プッシュ メッセージの送信リクエストが受信され、承認されました。
429 リクエスト数が多すぎます。つまり、アプリケーション サーバーが一定の割合に達した push サービスで制限できます。push サービスに「Retry-After」属性を含める必要がある ヘッダーを使用して、別のリクエストを実行できるまでの時間を指定します。
400 無効なリクエストです。これは通常、いずれかのヘッダーが無効であることを示します。 不適切な形式の画像を報告できます
404 見つかりませんでした。これは、サブスクリプションが期限切れであることを示します。 使用できませんこの場合、「PushSubscription」を削除して、 クライアントがユーザーに再度登録するのを待ちます。
410 なくなった。定期購入は無効になっているため、削除する必要があります 取得できるからですこの問題は、次を呼び出して再現できます。 `PushSubscription` の `unsubscribe()`。
413 ペイロード サイズが大きすぎます。push サービスに必要な最小サイズのペイロード サポートは 4,096 バイトです。 (または 4 KB)。

次のステップ

Codelab