设置包含资源数据的更改通知
Microsoft Graph 允许应用通过 webhooks来订阅资源更改通知。 你可以设置订阅,以将更改后的资源数据(例如 Microsoft Teams 聊天消息的内容或 Microsoft Teams 状态信息)包括在更改通知中。 随后,应用程序可运行其业务逻辑,无需调用单独的 API 来获取更改的资源。 因此,应用程序的 API 调用更少,对于大型方案非常有用。
若要将资源数据作为更改通知的一部分,需要实现以下附加逻辑,来满足数据访问和安全要求:
- 处理特殊订阅生命周期通知,以保持数据的不间断流动。 Microsoft Graph 会不时发送生命周期通知,以要求应用重新授权,确保更改通知中所包含的数据不会意外发生访问问题。
- 验证来自 Microsoft Graph 的更改通知的真实性。
- 提供公共加密密钥并使用私钥解密通过更改通知所接收的资源数据。
通知负载中的资源数据
通常情况下,此类更改通知包括负载中的以下资源数据:
- resourceData属性中返回的已更改资源实例的ID和类型。
- 按照订阅中规定内容加密、在 encryptedContent 属性中返回的资源实例的所有属性值。
- 或者,具体取决于资源、resourceData属性中返回的特定属性。 若要仅获取特定属性,请使用
$select
参数,将其指定为订阅中的资源URL 的一部分。
支持的资源
Microsoft Teams chatMessage、onlineMeetings 和状态资源支持包含资源数据的更改通知。 Outlook 联系人、事件和邮件资源在预览版中拥有类似的支持。 具体来说,你可以为以下用例设置订阅。
在 v1.0 和 Beta 版终结点中可用:
特定 Teams 频道中新增或已更改的消息:
/teams/{id}/channels/{id}/messages
整个组织(租户)中所有团队频道中的新消息或已更改消息:
/teams/getAllMessages
指定团队聊天中的新增或已更改消息:
/chats/{id}/messages
整个组织(租户)中所有聊天的新消息或已更改消息:
/chats/getAllMessages
特定用户所属的所有 Teams 聊天中的新消息或更改的消息:
/users/{id}/chats/getAllMessages
所有 Teams 聊天中的新成员或已更改的成员:
/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}
仅在 Beta 版终结点中可用:
用户邮箱中的新联系人或已更改的个人联系人:
/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
包含 chatMessage、 onlineMeeting 或 状态资源数据的更改通知由已更改实例的所有属性组成。 它们不支持仅返回实例的所选属性。
联系人、事件或邮件资源的更改通知仅包括资源的一部分属性,必须使用 $select
查询参数在对应的订阅请求中指定这些属性。 有关订阅包邮件资源含资源数据的更改通知,请参阅 Microsoft Graph 中获取 Outlook 中变更通知。
本文其余部分将介绍订阅 Teams 通道中的 chatMessage 更改通知的示例,各更改通知包含已更改 chatMessage 实例的完整资源数据。 有关基于 chatMessage 的订阅的更多详细信息,请参阅获取聊天和频道消息的更改通知。
创建订阅
若要将资源数据包含在更改通知中,除了创建订阅时通常指定的属性外,必须指定下列属性:
- includeResourceData,应设置为
true
以明确请求资源数据。 - encryptionCertificate,仅包含 Microsoft Graph 用于加密资源数据的公钥。 保留相应的私钥,以解密内容。
- encryptionCertificateId,是证书的自有标识符。 使用此 ID 在各更改通知中匹配用于解密的证书。
请注意下列事项:
- 按通知终结点验证中所述,验证两个终结点。 如果选择针对两个终结点使用同一 URL,将收到并响应两个验证请求。
订阅请求示例
以下示例为订阅 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 将为每个不同的应用和在值数组中有项的租户对,生成单独的令牌。 请记住,通知可能包含使用同一 notificationUrl 订阅的各种应用和租户的混合项。
注意:如果要设置通过 Azure 事件中心传递的更改通知,Microsoft Graph将不会发送验证令牌。 Microsoft Graph不需要验证notificationUrl。
在以下示例中,更改通知包含同一应用和两个不同租户的两个项目,因此 validationTokens 数组包含两个需要验证的令牌。
{
"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。
如何验证
如果你没有使用过令牌验证,请参阅令牌验证原理以获取概述。 使用 SDK,例如用于 .NET 的 System.IdentityModel.Tokens.Jwt 库或用于不同平台的第三方库。
注意以下事项:
- 确保始终发送
HTTP 202 Accepted
状态代码作为更改通知响应的一部分。 - 可在验证更改通知前(例如,将更改通知存储在队列中以供后续处理)或在验证更改通知后(如果即时处理了通知)进行响应,即使验证失败也是如此。
- 接受更改通知可以防止不必要的传递重试,还可以防止任何潜在的恶意行为者查明他们是否通过了验证。 接受无效的更改通知后,你始终可以选择忽略它。
具体而言,针对 validationTokens 集合中的各个 JWT 令牌进行验证。 如果任何令牌失败,请考虑更改通知可疑并进一步调查。
使用下列步骤验证令牌和生成令牌的应用程序:
验证令牌是否未过期。
验证令牌未被篡改,并且由预期机构(Microsoft 标识平台)颁发:
- 从公用配置终结点获取签名密钥:
https://login.microsoftonline.com/common/.well-known/openid-configuration
。 此配置由应用程序缓存一段时间。 请注意,由于签名密钥每日都会轮换,因此配置会经常更新。 - 使用这些密钥验证 JWT 令牌的签名。
不要接受任何其他机构颁发的令牌。
- 从公用配置终结点获取签名密钥:
验证口令已为订阅更改通知的应用程序颁发。
下列步骤是 JWT 令牌库中标准验证逻辑的一部分,通常可作为单个函数调用执行。
- 在与应用程序ID匹配的令牌中验证“受众”。
- 如果有多个应用收到更改通知,请务必检查是否有多个 ID。
关键:验证生成令牌的应用程序是否代表着 Microsoft Graph 更改通知的发布者。
- 在与
0bf30f3b-4a52-48df-9a82-234910c4a086
期望值匹配的令牌中检查 appid 属性。 - 这样可以确保更改通知不会由不是 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);
}
});
});
}
还必须实现 JwkKeyResolver
,这样 Java 示例才能正常运行。
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 使用订阅中所提供密钥解密的完整资源数据。 此属性还含有验证和解密所需的数值。 这样做是为了提高通过更改通知访问的客户数据的安全性。 你有责任保护私钥,以确保客户数据不能由第三方解密,即使他们设法截取原始更改通知也是如此。
本节内容:
管理加密密钥
使用非对称密钥对获取证书。
可自行对证书进行签名,因为 Microsoft Graph 不会验证证书颁发者,并且仅将公共密钥用于加密。
使用Azure 密钥存储库作为创建、轮换和安全管理证书的解决方案。 确保密钥符合下列条件:
- 密钥必须属于类型
RSA
- 密钥大小必须在2048和4096位之间。
- 密钥必须属于类型
采用base64编码X.509格式导出证书,且仅包括公钥。
创建订阅时:
使用导入证书的base64基编码内容,在encryptionCertificate属性中提供证书。
在 encryptionCertificateId 属性中提供自己的标识符。
此标识符能够将你的证书与接收的更改通知匹配,并从证书存储中检索证书。 标识符最长 128 个字符。
安全地管理私钥,以便更改通知处理代码可以访问私钥来解密资源数据。
轮换密钥
若要将私钥泄露的风险降至最低,请定期更改非对称密钥。 请按照以下步骤介绍一对新密钥:
使用新非对称密钥对获取新证书。 将其用于创建的所有新订阅。
使用新的证书密钥更新现有订阅。
- 作为定期续订订阅的一部分执行此操作。
- 或者,枚举所有订阅并提供密钥。 使用订阅修补程序操作并更新encryptionCertificate和encryptionCertificateId属性。
请记住下列事项:
- 在一段时间内,旧证书仍可用于加密。 应用程序必须具有访问新旧证书的权限,以能够对内容进行解密。
- 使用各更改通知中的 encryptionCertificateId 属性来确定要使用的正确密钥。
- 只有在没有看到最近任何更改通知引用旧证书时,才可以丢弃旧证书。
解密资源数据
为优化性能,Microsoft Graph 使用两步加密过程:
- 它会生成一个使用对称密钥,并用来加密资源数据。
- 它使用公共非对称密钥(订阅时提供)加密对称密钥,并将之包含在订阅的各更改通知中。
始终假设更改通知中各项的对称密钥不同。
若要对资源数据进行解密,应用应使用各更改通知 encryptedContent 下的属性执行反向操作:
使用 encryptionCertificateId 属性标识要使用的证书。
使用私钥初始化 RSA 加密组件(如 .NET RSACryptoServiceProvider)。
解密更改通知中各项的 dataKey 属性中提供的对称密钥。
使用适用于解密算法的最佳非对称加密填充(OAEP)。
使用对称密钥计算数据中数值的 HMAC-SHA256 签名。
将其与 dataSignature中的值进行比较。 如果不匹配,则假定有效负载已被篡改,并且不对其进行解密。
将对称密钥与高级加密标准(AES)(例如 .NET AesCryptoServiceProvider)结合使用,解密 数据中的内容。
将以下解密参数用于 AES 算法:
- 填充: PKCS7
- 密码模式: CBC
通过复制用于解密的对称密钥的前16个字节来设置 "初始化向量"。
解密值是一个 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');