Skip to content

Commit 7617f8d

Browse files
committed
add DecryptableContent interface and static methods
1 parent 2ec2b30 commit 7617f8d

File tree

1 file changed

+156
-0
lines changed

1 file changed

+156
-0
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package com.microsoft.graph.core.models;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.nio.charset.Charset;
5+
import java.nio.charset.StandardCharsets;
6+
import java.security.interfaces.RSAPrivateKey;
7+
import java.util.Arrays;
8+
import java.util.Base64;
9+
import java.util.Objects;
10+
11+
import javax.crypto.Cipher;
12+
import javax.crypto.Mac;
13+
14+
import javax.crypto.spec.IvParameterSpec;
15+
import javax.crypto.spec.SecretKeySpec;
16+
17+
import com.microsoft.kiota.serialization.Parsable;
18+
import com.microsoft.kiota.serialization.ParsableFactory;
19+
import com.microsoft.kiota.serialization.ParseNode;
20+
import com.microsoft.kiota.serialization.ParseNodeFactoryRegistry;
21+
22+
import jakarta.annotation.Nonnull;
23+
24+
public interface DecryptableContent {
25+
26+
/**
27+
* Sets the data
28+
* @param data resource data
29+
*/
30+
public void setData(String data);
31+
/**
32+
* Gets the data
33+
* @return the data
34+
*/
35+
public String getData();
36+
/**
37+
* Sets the data key
38+
* @param dataKey asymmetric key used to sign data
39+
*/
40+
public void setDataKey(String dataKey);
41+
/**
42+
* Gets the data key
43+
* @return the data key
44+
*/
45+
public String getDataKey();
46+
47+
/**
48+
* Sets the data signature
49+
* @param signature signature of the data
50+
*/
51+
public void setDataSignature(String signature);
52+
/**
53+
* Gets the data signature
54+
* @return data signature
55+
*/
56+
public String getDataSignature();
57+
/**
58+
* Sets the encryption certificate id
59+
* @param encryptionCertificateId certificate Id used when subscribing
60+
*/
61+
public void setEncryptionCertificateId(String encryptionCertificateId);
62+
/**
63+
* Gets the encryption certificate id
64+
* @return the encryption certificate id
65+
*/
66+
public String getEncryptionCertificateId();
67+
/**
68+
* Sets the encryption certificate thumbprint
69+
* @param encryptionCertificateThumbprint certificate thumbprint
70+
*/
71+
public void setEncryptionCertificateThumbprint(String encryptionCertificateThumbprint);
72+
/**
73+
* Gets the encryption certificate thumbprint
74+
* @return the encryption certificate thumbprint
75+
*/
76+
public String getEncryptionCertificateThumbprint();
77+
78+
/**
79+
* Validates the signature of the resource data, decrypts resource data and deserializes the data to a Parsable
80+
* https://learn.microsoft.com/en-us/graph/change-notifications-with-resource-data?tabs=csharp#decrypting-resource-data-from-change-notifications
81+
*
82+
* @param <T> Parsable type to return
83+
* @param decryptableContent instance of DecryptableContent
84+
* @param privateKeyProvider provides an RSA Private Key for the certificate provided when subscribing
85+
* @param factory ParsableFactory for the return type
86+
* @return
87+
* @throws Exception
88+
*/
89+
public static <T extends Parsable> T decrypt(@Nonnull final DecryptableContent decryptableContent, @Nonnull final PrivateKeyProvider privateKeyProvider, @Nonnull final ParsableFactory<T> factory) throws Exception {
90+
Objects.requireNonNull(privateKeyProvider);
91+
final String decryptedContent = decryptAsString(decryptableContent, privateKeyProvider);
92+
final ParseNode rootParseNode = ParseNodeFactoryRegistry.defaultInstance.getParseNode(
93+
"application/json", new ByteArrayInputStream(decryptedContent.getBytes(StandardCharsets.UTF_8)));
94+
return rootParseNode.getObjectValue(factory);
95+
}
96+
97+
/**
98+
* Validates the signature and decrypts resource data attached to the notification.
99+
* https://learn.microsoft.com/en-us/graph/change-notifications-with-resource-data?tabs=csharp#decrypting-resource-data-from-change-notifications
100+
*
101+
* @param content instance of DecryptableContent
102+
* @param privateKeyProvider provides an RSA Private Key for the certificate provided when subscribing
103+
* @return decrypted resource data
104+
* @throws Exception
105+
*/
106+
public static String decryptAsString(@Nonnull final DecryptableContent content, @Nonnull final PrivateKeyProvider privateKeyProvider) throws Exception {
107+
Objects.requireNonNull(privateKeyProvider);
108+
final RSAPrivateKey privateKey = privateKeyProvider.getCertificatePrivateKey(content.getEncryptionCertificateId(), content.getEncryptionCertificateThumbprint());
109+
final Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
110+
cipher.init(Cipher.DECRYPT_MODE, privateKey);
111+
final byte[] decryptedSymmetricKey = cipher.doFinal(Base64.getDecoder().decode(content.getDataKey()));
112+
113+
final Mac sha256Mac = Mac.getInstance("HmacSHA256");
114+
sha256Mac.init(new SecretKeySpec(decryptedSymmetricKey, "HmacSHA256"));
115+
final byte[] hashedData = sha256Mac.doFinal(Base64.getDecoder().decode(content.getData()));
116+
117+
final String expectedSignature = Base64.getEncoder().encodeToString(hashedData);
118+
if (!expectedSignature.equals(content.getDataSignature())) {
119+
throw new Exception("Signature does not match");
120+
}
121+
return new String(aesDecrypt(Base64.getDecoder().decode(content.getData()), decryptedSymmetricKey), StandardCharsets.UTF_8);
122+
}
123+
124+
/**
125+
* Decrypts the resource data using the decrypted symmetric key
126+
* @param data Base-64 decoded resource data
127+
* @param key Decrypted symmetric key from DecryptableContent.getDataKey()
128+
* @return decrypted resource data
129+
* @throws Exception
130+
*/
131+
public static byte[] aesDecrypt(byte[] data, byte[] key) throws Exception {
132+
try {
133+
final IvParameterSpec ivSpec = new IvParameterSpec(Arrays.copyOf(key, 16));
134+
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
135+
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), ivSpec);
136+
return cipher.doFinal(data);
137+
} catch (Exception ex) {
138+
throw new RuntimeException("Unexpected error occurred while trying to decrypt the data", ex);
139+
}
140+
}
141+
142+
/**
143+
* Provides an RSA Private Key for the certificate with the ID provided when creating the
144+
* subscription and the thumbprint.
145+
*/
146+
@FunctionalInterface
147+
public interface PrivateKeyProvider {
148+
/**
149+
* Returns the RSAPrivateKey for an X.509 certificate with the given id and thumbprint
150+
* @param certificateId certificate Id provided when subscribing
151+
* @param certtificateThumbprint certificate thumbprint
152+
* @return RSA private key used to sign the certificate
153+
*/
154+
public RSAPrivateKey getCertificatePrivateKey(String certificateId, String certtificateThumbprint);
155+
}
156+
}

0 commit comments

Comments
 (0)