リソース データが含まれる変更通知を設定する

Microsoft Graph を使用すると、アプリは webhook 経由でリソースの変更通知をサブスクライブできるようになります。 変更されたリソース データ (Microsoft Teams のチャット メッセージや Microsoft Teams のプレゼンス情報のコンテンツなど) を変更通知に含めるようにサブスクリプションを設定できます。 これにより、アプリでは、変更されたリソースを取得するために改めて API 呼び出しを行うことなくビジネス ロジックを実行できます。 その結果、API 呼び出しの数が減るためにアプリのパフォーマンスが向上し、これは大規模なシナリオで役に立ちます。

変更通知の一部としてリソース データを含めるには、次の追加のロジックを実装してデータ アクセスおよびセキュリティの要件を満たす必要があります。

  • サブスクリプションの特別なライフサイクル通知を 処理 して継続的なデータの流れを維持します。 Microsoft Graph では、変更通知にリソース データを含めることで予期せぬアクセスの問題が発生しないよう、ライフサイクル通知を随時送信してアプリが再承認されるようにしています。
  • 変更通知が Microsoft Graph から発信されたものであることの真正性を 検証 します。
  • 公開暗号キーを 提供 し、変更通知で受け取ったリソース データの復号には秘密キーを使用します。

通知ペイロードのリソース データ

通常、この種類の変更通知では、次のリソース データがペイロードに含まれます。

  • resourceData プロパティにより返される、変更されたリソース インスタンスの ID および種類。
  • そのリソース インスタンスのすべてのプロパティ値。これはサブスクリプションの指定に従って暗号化され、encryptedContent プロパティで返されます。
  • または、リソースによっては、resourceData プロパティで返される特定のプロパティ。 特定のプロパティのみを取得するには、$select パラメーターを使用して、サブスクリプションのリソース URL の一部としてそれらのプロパティを指定します。

サポートされているリソース

Microsoft Teams の chatMessageonlineMeetings、および presence リソースは、リソース データを使用した変更通知をサポートします。 Outlook の連絡先イベントメッセージの各リソースは、プレビューに同様のサポートがあります。 具体的には、次のユース ケースのサブスクリプションを設定できます。

v1.0 およびベータ版のエンドポイントで使用できます。

  • 特定の Teams チャネル内の新しいまたは変更されたメッセージ: /teams/{id}/channels/{id}/messages

  • 組織 (テナント) 全体のすべての Teams チャネル内の新しいまたは変更されたメッセージ: /teams/getAllMessages

  • 特定の Teams チャット内の新しいまたは変更されたメッセージ: /chats/{id}/messages

  • 組織 (テナント) 全体のすべてのチャット内の新しいまたは変更されたメッセージ: /chats/getAllMessages

  • 特定のユーザーが参加しているすべての Teams チャットの新しいメッセージまたは変更されたメッセージ: /users/{id}/chats/getAllMessages

  • 組織 (テナント) 全体のすべてのチャット内の新しいまたは変更されたメッセージ: /chats/getAllMembers

  • 特定の Teams チャット内の新しいまたは変更されたメッセージ: /chats/{id}/members

  • テナント全体で新規または変更されたチャット: /chats

  • 特定のチャットでのプロパティの変更: /chats/{id}

  • 特定のチームのすべてのチャネルの新規または変更されたメンバー: /teams/{id}/channels/getAllMembers

  • 特定のチームの新規または変更されたメンバー: /teams/{id}/members

  • テナント全体の新規または変更されたチーム: /teams

  • 特定のチームでのプロパティの変更: /teams/{id}

  • すべての Teams チームの新しいチャネルまたは変更されたチャネル: /teams/getAllChannels

  • 特定のチームの新規または変更されたチャネル: /teams/{id}/channels

  • ユーザーのプレゼンス情報の更新: /communications/presences/{id}

ベータ版のエンドポイントでのみ使用できます。

  • ユーザーのメールボックス内の個人の連絡先を新規作成または変更します: /users/{id}/contacts

  • ユーザーの contactFolder で個人の連絡先を新規作成または変更しました: /users/{id}/contactFolders/{id}/contacts

  • ユーザーのメールボックス内の新しいイベントまたは変更されたイベント: /users/{id}/events

  • ユーザーのメールボックス内の新しいメッセージまたは変更されたメッセージ: /users/{id}/messages

  • ユーザーの mailFolder 内の新しいメッセージまたは変更されたメッセージ: /users/{id}/mailFolders/{id}/messages

  • Teams 会議の状態に関する情報の更新:/communications/onlineMeetings/?$filter=JoinWebUrl eq '{joinWebUrl}

  • テナント全体のすべてのチャネルの新規または変更されたメンバー: /teams/getAllChannels/getAllMembers

chatMessageonlineMeeting、または presence リソース データを含む変更通知は、変更されたインスタンスのすべてのプロパティで構成されます。 インスタンスの選択したプロパティのみを取得することはサポートされていません。

連絡先イベント、またはメッセージ リソースの変更通知には、リソースのプロパティのサブセットのみが含まれます。これは、$select クエリ パラメーターを使用して対応するサブスクリプション要求で指定する必要があります。 メッセージ リソースのリソース データを使用して変更通知をサブスクライブする方法の詳細と例については、「Microsoft Outlook Graph のリソースの通知を変更する」を参照してください。

この記事の残りの部分では、Teams チャネルの chatMessage リソースでメッセージの変更通知をサブスクライブする例を説明します。各変更通知には、変更された chatMessage インスタンスの完全なリソース データが含まれます。 chatMessage サブスクリプションの詳細については、「チャットおよびチャネル メッセージの変更通知を取得する」を参照してください。

サブスクリプションの作成

リソース データを変更通知に含めるには、通常はサブスクリプションの作成時に指定されるプロパティに加え、次のプロパティを指定する必要があります

  • includeResourceData: これは、true に設定して、リソース データを明示的に要求する必要があります。
  • encryptionCertificate: リソース データを暗号化するために Microsoft Graph で使用される公開キーのみがこれに含まれます。 コンテンツを復号するために、対応する秘密キーを保持しておきます。
  • encryptionCertificateId: 証明書用の独自の識別子です。 この ID は、復号に使用する証明書を照合するために各変更通知で使用します。

以下の点にご注意ください:

  • 通知エンドポイントの検証の説明に従って、両方の通知エンドポイントを検証します。 両エンドポイントに同じ URL を使用することを選択した場合は、検証要求が 2 通配信されるので、両方に応答します。

サブスクリプション要求の例

Microsoft Teams で作成または更新されるチャネル メッセージをサブスクライブする例は次の通りです。

POST https://graph.microsoft.com/v1.0/subscriptions
Content-Type: application/json
{
  "changeType": "created,updated",
  "notificationUrl": "https://webhook.azurewebsites.net/api/resourceNotifications",
  "resource": "/teams/{id}/channels/{id}/messages",
  "includeResourceData": true,
  "encryptionCertificate": "{base64encodedCertificate}",
  "encryptionCertificateId": "{customId}",
  "expirationDateTime": "2019-09-19T11:00:00.0000000Z",
  "clientState": "{secretClientState}"
}

サブスクリプションの応答

HTTP/1.1 201 Created
Content-Type: application/json

{
  "changeType": "created,updated",
  "notificationUrl": "https://webhook.azurewebsites.net/api/resourceNotifications",
  "resource": "/teams/{id}/channels/{id}/messages",
  "includeResourceData": true,
  "encryptionCertificateId": "{custom ID}",
  "expirationDateTime": "2019-09-19T11:00:00.0000000Z",
  "clientState": "{secret client state}"
}

サブスクリプション ライフサイクル通知

既存のサブスクリプションでの変更通知フローは、特定のイベントにより妨げられる場合があります。 サブスクリプション ライフサイクル通知では、継続的なフローを維持するために実行する必要があるアクションが通知されます。 リソース インスタンスでの変更を通知するリソース変更通知とは異なり、ライフサイクル通知は、サブスクリプション自体と、サブスクリプションのライフサイクル内での現在の状態に関するものです。

ライフサイクル通知の受信および応答方法の詳細については、「不足しているサブスクリプションの削減と通知の変更」を参照してください。

通知の真正性を検証する

アプリでは多くの場合、変更通知に含まれるリソース データに基づいてビジネス ロジックが実行されます。 各変更通知の真正性を最初に確認しておくことが重要です。 これを行わない場合、第三者が偽の変更通知を用いてアプリに対してなりすましを行い、アプリのビジネス ロジックを不正に実行することができ、セキュリティ インシデントの発生につながる可能性があります。

リソース データが含まれていない基本的な変更通知は、「変更通知を処理する」の説明に従い、clientState 値に基づいて簡単に検証します。 これは、信頼できる Microsoft Graph 呼び出しを後から行ってリソース データにアクセスできるため、問題ありません。そのため、スプーフィングが試行された場合でも、その影響は限定的です。

リソース データを送信する変更通知の場合は、データを処理する前により徹底した検証を行います。

このセクションで説明する項目:

変更通知の検証トークン

リソース データが含まれる変更通知には、validationTokens という追加のプロパティが含まれており、これには、Microsoft Graph により生成される JWT トークンの配列が含まれています。 Microsoft Graph では、value 配列にアイテムが存在するアプリとテナントのペアそれぞれにつきトークンが 1 つ生成されます。 変更通知には、同じ notificationUrl を使用してサブスクライブを行ったさまざまなアプリやテナントのアイテムが混在している場合があることをご留意ください。

注:Azure Event Hubs を介して配信される変更通知を設定している場合、Microsoft Graph は検証トークンを送信しません。 Microsoft Graph は、notificationUrl を検証する必要はありません。

次の例では、1 つのアプリおよび 2 つの異なるテナントに対して 2 つのアイテムがこの変更通知には含まれているため、validationTokens 配列には検証が必要なトークンが 2 つ含まれています。

{
	"value": [
		  {
			"subscriptionId": "76619225-ff6b-4489-96ca-4ef547e78b22",
      "tenantId": "84bd8158-6d4d-4958-8b9f-9d6445542f95",
			"changeType": "created",
			...
		  },
      {
			"subscriptionId": "e990d58f-fd93-40af-acf7-a7c907c5d8ea",
      "tenantId": "46d9e3bd-6309-4177-a016-b256a411e30f",
			"changeType": "created",
			...
			}
	],
	"validationTokens": [
		"eyJ0eXAiOiJKV1QiLCJhb...",
    "cGlkYWNyIjoiMiIsImlkc..."
	]
}

注: 変更通知が配信されたときに送信されるデータの詳細については、changeNotificationCollection を参照してください。

検証方法

トークンの検証を初めて使用する場合は、「トークンの検証の原則」 で概要を確認してください。 .NET 用の System.IdentityModel.Tokens.Jwt ライブラリなどの SDK または他のプラットフォーム用のサードパーティ ライブラリを使用します。

次の点に注意してください:

  • 変更通知に対する応答の一部として HTTP 202 Accepted 状態コードを必ず送信するようにします。
  • 検証が失敗した場合でも、変更通知の検証前 (たとえば、後で処理するために変更通知をキューに保存する場合) または検証後 (その場ですぐ検証する場合) に実行します。
  • 変更通知を承認することにより、配信の不必要なリトライを防ぐことができ、悪意を持っている可能性がある者が検証に合格したかどうかを自ら確認できないようにもします。 無効な変更通知は、その通知を承認した後にいつでも無視することもできます。

特に、validationTokens コレクション内のすべての JWT トークンに対して検証を実行します。 合格しないトークンが含まれる変更通知は不審な通知とみなし、詳しく調査します。

次の手順を使用して、トークンおよびトークンを生成するアプリを検証します。

  1. トークンの有効期限が切れていないことを確認します。

  2. トークンが改ざんされていないこと、また、想定された機関である Microsoft ID プラットフォームにより発行されたものであることを検証します。

    • 共通の構成エンドポイント https://login.microsoftonline.com/common/.well-known/openid-configuration から、署名キーを取得します。 この構成は、アプリで一定期間キャッシュされます。 署名キーは毎日ローテーションされるため、構成は頻繁に更新される点にご注意ください。
    • これらのキーを使用して、JWT トークンの署名を確認します。

    他の機関が発行したトークンは承認しなでください。

  3. 変更通知をサブスクライブしているアプリ用に発行されたトークンであることを確認します。

    次の手順は、JWT トークン ライブラリでの標準的な検証ロジックの一部であり、通常は 1 つの関数呼び出しとして実行できます。

    • トークン内の "audience" がアプリ ID と一致していることを確認します。
    • 変更通知を受け取るアプリが複数ある場合は、複数の ID について確認する必要があります。
  4. 重要: トークンを生成したアプリが Microsoft Graph 変更通知の発行元を表していることを確認します。

    • トークンの appid プロパティが、想定値である 0bf30f3b-4a52-48df-9a82-234910c4a086 と一致していることを確認します。
    • これにより、変更通知が Microsoft Graph 以外の別のアプリから送信されたものでないことを確認できます。

JWT トークンの例

検証が必要な JWT トークンに含まれるプロパティの例を次に示します。

{
  // aud is your app's id 
  "aud": "8e460676-ae3f-4b1e-8790-ee0fb5d6148f",                           
  "iss": "https://sts.windows.net/84bd8158-6d4d-4958-8b9f-9d6445542f95/",
  "iat": 1565046813,
  "nbf": 1565046813,
  // Expiration date 
  "exp": 1565075913,                                                        
  "aio": "42FgYKhZ+uOZrHa7p+7tfruauq1HAA==",
  // appid represents the notification publisher and must always be the same value of 0bf30f3b-4a52-48df-9a82-234910c4a086 
  "appid": "0bf30f3b-4a52-48df-9a82-234910c4a086",                          
  "appidacr": "2",
  "idp": "https://sts.windows.net/84bd8158-6d4d-4958-8b9f-9d6445542f95/",
  "tid": "84bd8158-6d4d-4958-8b9f-9d6445542f95",
  "uti": "-KoJHevhgEGnN4kwuixpAA",
  "ver": "1.0"
}

例: 検証トークンの検証

// add Microsoft.IdentityModel.Protocols.OpenIdConnect and System.IdentityModel.Tokens.Jwt nuget packages to your project
public async Task<bool> ValidateToken(string token, string tenantId, IEnumerable<string> appIds)
{
    var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
    var openIdConfig = await configurationManager.GetConfigurationAsync();
    var handler = new JwtSecurityTokenHandler();
    try
    {
	handler.ValidateToken(token, new TokenValidationParameters
	{
	    ValidateIssuer = true,
	    ValidateAudience = true,
	    ValidateIssuerSigningKey = true,
	    ValidateLifetime = true,
	    ValidIssuer = $"https://sts.windows.net/{tenantId}/",
	    ValidAudiences = appIds,
	    IssuerSigningKeys = openIdConfig.SigningKeys
	}, out _);
	return true;
    }
    catch (Exception ex)
    {
	Trace.TraceError($"{ex.Message}:{ex.StackTrace}");
	return false;
    }
}
private boolean IsValidationTokenValid(String[] appIds, String tenantId, String serializedToken) {
	try {
	    JwkKeyResolver jwksResolver = new JwkKeyResolver();
	    Jws<Claims> token = Jwts.parserBuilder()
		.setSigningKeyResolver(jwksResolver)
		.build()
		.parseClaimsJws(serializedToken);
	    Claims body = token.getBody();
	    String audience = body.getAudience();
	    boolean isAudienceValid = false;
	    for(String appId : appIds) {
		isAudienceValid = isAudienceValid || appId.equals(audience);
	    }
	    boolean isTenantValid = body.getIssuer().endsWith(tenantId + "/");
	    return isAudienceValid  && isTenantValid; //nbf,exp and signature are already validated by library
	} catch (Exception e) {
	    LOGGER.error("could not validate token");
	    LOGGER.error(e.getMessage());
	    return false;
	}
}
import jwt from 'jsonwebtoken';
import jkwsClient from 'jwks-rsa';

const client = jkwsClient({
  jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
});

export function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    var signingKey = key.publicKey || key.rsaPublicKey;
    callback(null, signingKey);
  });
}

export function isTokenValid(token, appId, tenantId) {
  return new Promise((resolve) => {
    const options = {
      audience: [appId],
      issuer: [`https://sts.windows.net/${tenantId}/`]
    };
    jwt.verify(token, getKey, options, (err) => {
      if (err) {
        // eslint-disable-next-line no-console
        console.error(err);
        resolve(false);
      } else {
        resolve(true);
      }
    });
  });
}

Java サンプルが動作するには、JwkKeyResolver も実装する必要があります。

package com.example.restservice;

import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.UrlJwkProvider;
import com.auth0.jwk.Jwk;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import java.security.Key;
import java.net.URI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JwkKeyResolver extends SigningKeyResolverAdapter {
    private JwkProvider keyStore;
    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
    public JwkKeyResolver() throws java.net.URISyntaxException, java.net.MalformedURLException {
        this.keyStore = new UrlJwkProvider((new URI("https://login.microsoftonline.com/common/discovery/keys").toURL()));
    }
    @Override
    public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
        try {
            String keyId = jwsHeader.getKeyId();
            Jwk pub = keyStore.get(keyId);
            return pub.getPublicKey();
        } catch (Exception e) {
            LOGGER.error(e.getMessage());
            return null;
        }
    }
}

変更通知からのリソース データの復号

変更通知の resourceData プロパティには、リソース インスタンスの基本 ID と型情報のみが含まれています。 encryptedData プロパティには、サブスクリプションで提供されている公開キーを使用して Microsoft Graph により暗号化された完全なリソース データが含まれています。 このプロパティには、確認と復号に必要な値も含まれています。 これを行う理由は、変更通知経由でアクセスする顧客のデータのセキュリティを強化するためです。 たとえ元の変更通知が第三者により傍受された場合でもその者が顧客のデータを復号できないよう、管理者の責任において秘密キーの安全を確保する必要があります。

このセクションで説明する項目:

暗号化キーの管理

  1. 非対称キーのペアを使用して証明書を取得します。

    • 証明書は、自己署名することができます。これは、Microsoft Graph では証明書の発行元が検証されず、公開キーは暗号化のみに使用されるためです。

    • 証明書の作成、ローテーション、および安全な管理を行うためのソリューションとして、Azure Key Vault を使用します。 キーは、次の条件を満たしている必要があります。

      • キーの種類が RSA である必要があります。
      • キーのサイズが 2048 ビットから 4096 ビットの範囲である必要があります。
  2. 証明書を Base64 でエンコードされた X.509 形式でエクスポートし、公開キーのみを含めます

  3. サブスクリプションを作成するときに、以下を行います。

    • 証明書がエクスポートされた Base64 でエンコードされたコンテンツを使用して、encryptionCertificate プロパティで証明書を指定します。

    • encryptionCertificateId プロパティで独自の識別子を指定します。

      この識別子を使用することで、受信する変更通知に証明書を一致させ、証明書ストアから証明書を取得することができます。 識別子には、最大 128 文字まで使用できます。

  4. 変更通知処理コードが秘密キーにアクセスしてリソース データを復号できるよう、秘密キーを安全に管理にします。

キーのローテーション

秘密キーの漏洩リスクを最小限に抑えるために、非対称キーは定期的に変更します。 次の手順に従って、新しいキーのペアを導入します。

  1. 新しい非対称キーのペアを使用して新しい証明書を取得します。 作成するすべての新しいサブスクリプションでこれを使用します。

  2. 新しい証明書キーを使用して既存のサブスクリプションを更新します。

    • 定期的なサブスクリプション更新の一部としてこの作業を行います。
    • または、すべてのサブスクリプションを列挙して、キーを提供します。 サブスクリプション で PATCH 操作を使用して、encryptionCertificate プロパティと encryptionCertificateId プロパティを更新します。
  3. 以下の点に注意してください。

    • 一定の期間、暗号化のために古い証明書が使用される場合があります。 コンテンツを復号できるよう、アプリは古い証明書と新しい証明書の両方にアクセスできる必要があります。
    • 各変更通知で encryptionCertificateId プロパティを使用して、使用する正しいキーを特定します。
    • 古い証明書は、最近の変更通知で古い証明書への参照が行われなくなった場合にのみ破棄します。

リソース データの復号

パフォーマンスを最適化するために、Microsoft Graph では 2 段階の暗号化プロセスを使用しています。

  • このプロセスでは、1 回限りの使用の対称キーが生成され、リソース データの暗号化に使用されます。
  • このプロセスでは、対称公開キー (サブスクライブする際に指定したもの) を使用して対称キーが暗号化され、それが当該サブスクリプションの各変更通知に含められます。

変更通知の各アイテムで対称キーが異なることを常に想定してください。

リソース データを復号するには、アプリは各変更通知の encryptedContent のプロパティを使用して手順を逆に進める必要があります。

  1. encryptionCertificateId プロパティを使用して、使用する証明書を特定します。

  2. 秘密キーを使用して、RSA 暗号化コンポーネント (.NET RSACryptoServiceProvider など) を初期化します。

  3. 変更通知内の各アイテムの dataKey プロパティで提供された対称キーを復号します。

    復号アルゴリズムには、最適非対称暗号化パディング (OAEP) を使用します。

  4. 対称キーを使用して、データ内の値の HMAC-SHA256 署名を計算します。

    その署名を dataSignature の値と比較します。 これらが一致しない場合は、ペイロードが改ざんされたものとみなし、復号しないようにします。

  5. Advanced Encryption Standard (AES) (.NET AesCryptoServiceProvider など) を用いて対称キー使用し、データ内のコンテンツを復号します。

    • AES アルゴリズムでは、次の復号パラメーターを使用します。

      • パディング: PKCS7
      • 暗号モード: CBC
    • 復号に使用する対称キーの最初の 16 バイトをコピーして、"初期化ベクター" を設定します。

  6. 復号された値は、変更通知のリソース インスタンスを表す JSON 文字列です。

例: 暗号化されたリソース データが使用されている通知の復号

チャネル メッセージ内の chatMessage インスタンスの暗号化されたプロパティ値を含む変更通知例を次に示します。 インスタンスは、@odata.id 値によって指定されています。

{
	"value": [
		{
			"subscriptionId": "76222963-cc7b-42d2-882d-8aaa69cb2ba3",
			"changeType": "created",
			// Other properties typical in a resource change notification
			"resource": "teams('d29828b8-c04d-4e2a-b2f6-07da6982f0f0')/channels('19:f127a8c55ad949d1a238464d22f0f99e@thread.skype')/messages('1565045424600')/replies('1565047490246')",
			"resourceData": {
				"id": "1565293727947",
				"@odata.type": "#Microsoft.Graph.ChatMessage",
				"@odata.id": "teams('88cbc8fc-164b-44f0-b6a6-b59b4a1559d3')/channels('19:8d9da062ec7647d4bb1976126e788b47@thread.tacv2')/messages('1565293727947')/replies('1565293727947')"
			},
			"encryptedContent": {
				"data": "{encrypted data that produces a full resource}",
        "dataSignature": "<HMAC-SHA256 hash>",
				"dataKey": "{encrypted symmetric key from Microsoft Graph}",
				"encryptionCertificateId": "MySelfSignedCert/DDC9651A-D7BC-4D74-86BC-A8923584B0AB",
				"encryptionCertificateThumbprint": "07293748CC064953A3052FB978C735FB89E61C3D"
			}
		}
	],
	"validationTokens": [
		"eyJ0eXAiOiJKV1QiLCJhbGciOiJSU..."
	]
}

注: 変更通知が配信されたときに送信されるデータの詳細については、changeNotificationCollection を参照してください。

このセクションでは、復号の各ステージで役立つ、 C# および .NET を使用するコード スニペットをいくつか示します。

対称キーを復号する

// Initialize with the private key that matches the encryptionCertificateId.
RSACryptoServiceProvider rsaProvider = ...;        
byte[] encryptedSymmetricKey = Convert.FromBase64String(<value from dataKey property>);

// Decrypt using OAEP padding.
byte[] decryptedSymmetricKey = rsaProvider.Decrypt(encryptedSymmetricKey, fOAEP: true);

// Can now use decryptedSymmetricKey with the AES algorithm.
String storename = ""; //name/path of the jks store
String storepass = ""; //password used to open the jks store
String alias = ""; //alias of the certificate when store in the jks store, should be passed as encryptionCertificateId when subscribing and retrieved from the notification
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(storename), storepass.toCharArray());
Key asymmetricKey = ks.getKey(alias, storepass.toCharArray());
byte[] encryptedSymetricKey = Base64.decodeBase64("<value from dataKey property>");
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, asymmetricKey);
byte[] decryptedSymmetricKey = cipher.doFinal(encryptedSymetricKey);
// Can now use decryptedSymmetricKey with the AES algorithm.
const base64encodedKey = 'base 64 encoded dataKey value';
const asymetricPrivateKey = 'pem encoded private key';
const decodedKey = Buffer.from(base64encodedKey, 'base64');
const decryptedSymetricKey = crypto.privateDecrypt(asymetricPrivateKey, decodedKey);
// Can now use decryptedSymmetricKey with the AES algorithm.

HMAC-SHA256 を使用してデータの署名を比較する

byte[] decryptedSymmetricKey = <the aes key decrypted in the previous step>;
byte[] encryptedPayload = <the value from the data property, still encrypted>;
byte[] expectedSignature = <the value from the dataSignature property>;
byte[] actualSignature;

using (HMACSHA256 hmac = new HMACSHA256(decryptedSymmetricKey))
{
    actualSignature = hmac.ComputeHash(encryptedPayload);
}
if (actualSignature.SequenceEqual(expectedSignature))
{
    // Continue with decryption of the encryptedPayload.
}
else
{
    // Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
}
byte[] decryptedSymmetricKey = "<the aes key decrypted in the previous step>";
byte[] decodedEncryptedData = Base64.decodeBase64("data property from encryptedContent object");
Mac mac = Mac.getInstance("HMACSHA256");
SecretKey skey = new SecretKeySpec(decryptedSymmetricKey, "HMACSHA256");
mac.init(skey);
byte[] hashedData = mac.doFinal(decodedEncryptedData);
String encodedHashedData = new String(Base64.encodeBase64(hashedData));
if (comparisonSignature.equals(encodedHashedData))
{
    // Continue with decryption of the encryptedPayload.
}
else
{
    // Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
}
const decryptedSymetricKey = []; //Buffer provided by previous step
const base64encodedSignature = 'base64 encodded value from the dataSignature property';
const hmac = crypto.createHmac('sha256', decryptedSymetricKey);
hmac.write(base64encodedPayload, 'base64');
if(base64encodedSignature === hmac.digest('base64'))
{
    // Continue with decryption of the encryptedPayload.
}
else
{
    // Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
}

リソース データ コンテンツを復号する

AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider();
aesProvider.Key = decryptedSymmetricKey;
aesProvider.Padding = PaddingMode.PKCS7;
aesProvider.Mode = CipherMode.CBC;

// Obtain the intialization vector from the symmetric key itself.
int vectorSize = 16;
byte[] iv = new byte[vectorSize];
Array.Copy(decryptedSymmetricKey, iv, vectorSize);
aesProvider.IV = iv;

byte[] encryptedPayload = Convert.FromBase64String(<value from dataKey property>);

string decryptedResourceData;
// Decrypt the resource data content.
using (var decryptor = aesProvider.CreateDecryptor())
{
  using (MemoryStream msDecrypt = new MemoryStream(encryptedPayload))
  {
      using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
      {
          using (StreamReader srDecrypt = new StreamReader(csDecrypt))
          {
              decryptedResourceData = srDecrypt.ReadToEnd();
          }
      }
  }
}

// decryptedResourceData now contains a JSON string that represents the resource.
SecretKey skey = new SecretKeySpec(decryptedSymmetricKey, "AES");
IvParameterSpec ivspec = new IvParameterSpec(Arrays.copyOf(decryptedSymmetricKey, 16));
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, skey, ivspec);
String decryptedResourceData = new String(cipher.doFinal(Base64.decodeBase64(encryptedData)));
const base64encodedPayload = 'base64 encoded value from data property';
const decryptedSymetricKey = []; //Buffer provided by previous step
const iv = Buffer.alloc(16, 0);
decryptedSymetricKey.copy(iv, 0, 0, 16);
const decipher = crypto.createDecipheriv('aes-256-cbc', decryptedSymetricKey, iv);
let decryptedPayload = decipher.update(base64encodedPayload, 'base64', 'utf8');
decryptedPayload += decipher.final('utf8');

関連項目