Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ dependencies {

implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'

implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
implementation 'com.auth0:jwks-rsa:0.22.1'

api 'com.squareup.okhttp3:okhttp:4.12.0'
api 'com.azure:azure-core:1.54.1'

Expand Down
4 changes: 4 additions & 0 deletions spotBugsExcludeFilter.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,8 @@ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubu
<Bug pattern="DCN_NULLPOINTER_EXCEPTION" />
<Class name="com.microsoft.graph.core.content.BatchResponseContentTest" />
</Match>
<Match>
<Bug pattern="CT_CONSTRUCTOR_THROW" />
<Class name="com.microsoft.graph.core.models.DiscoverUrlAdapter" />
</Match>
</FindBugsFilter>
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package com.microsoft.graph.core.models;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;

import javax.crypto.Cipher;
import javax.crypto.Mac;

import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import com.microsoft.kiota.serialization.Parsable;
import com.microsoft.kiota.serialization.ParsableFactory;
import com.microsoft.kiota.serialization.ParseNode;
import com.microsoft.kiota.serialization.ParseNodeFactoryRegistry;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;

/**
* DecryptableContent interface
*/
public interface DecryptableContent {

/**
* Sets the data
* @param data resource data
*/
public void setData(@Nullable final String data);
/**
* Gets the data
* @return the data
*/
public @Nullable String getData();
/**
* Sets the data key
* @param dataKey asymmetric key used to sign data
*/
public void setDataKey(@Nullable final String dataKey);
/**
* Gets the data key
* @return the data key
*/
public @Nullable String getDataKey();

/**
* Sets the data signature
* @param signature signature of the data
*/
public void setDataSignature(@Nullable final String signature);
/**
* Gets the data signature
* @return data signature
*/
public @Nullable String getDataSignature();
/**
* Sets the encryption certificate id
* @param encryptionCertificateId certificate Id used when subscribing
*/
public void setEncryptionCertificateId(@Nullable final String encryptionCertificateId);
/**
* Gets the encryption certificate id
* @return the encryption certificate id
*/
public @Nullable String getEncryptionCertificateId();
/**
* Sets the encryption certificate thumbprint
* @param encryptionCertificateThumbprint certificate thumbprint
*/
public void setEncryptionCertificateThumbprint(@Nullable final String encryptionCertificateThumbprint);
/**
* Gets the encryption certificate thumbprint
* @return the encryption certificate thumbprint
*/
public @Nullable String getEncryptionCertificateThumbprint();

/**
* Validates the signature of the resource data, decrypts resource data and deserializes the data to a Parsable
* https://learn.microsoft.com/en-us/graph/change-notifications-with-resource-data?tabs=csharp#decrypting-resource-data-from-change-notifications
*
* @param <T> Parsable type to return
* @param decryptableContent instance of DecryptableContent
* @param certificateKeyProvider provides an RSA Private Key for the certificate provided when subscribing
* @param factory ParsableFactory for the return type
* @return decrypted resource data
* @throws Exception if an error occurs while decrypting the data
*/
public static @Nonnull <T extends Parsable> T decrypt(@Nonnull final DecryptableContent decryptableContent, @Nonnull final CertificateKeyProvider certificateKeyProvider, @Nonnull final ParsableFactory<T> factory) throws Exception {
Objects.requireNonNull(certificateKeyProvider);
final String decryptedContent = decryptAsString(decryptableContent, certificateKeyProvider);
final ParseNode rootParseNode = ParseNodeFactoryRegistry.defaultInstance.getParseNode(
"application/json", new ByteArrayInputStream(decryptedContent.getBytes(StandardCharsets.UTF_8)));
return rootParseNode.getObjectValue(factory);
}

/**
* Validates the signature and decrypts resource data attached to the notification.
* https://learn.microsoft.com/en-us/graph/change-notifications-with-resource-data?tabs=csharp#decrypting-resource-data-from-change-notifications
*
* @param content instance of DecryptableContent
* @param certificateKeyProvider provides an RSA Private Key for the certificate provided when subscribing
* @return decrypted resource data
* @throws Exception if an error occurs while decrypting the data
*/
public static @Nonnull String decryptAsString(@Nonnull final DecryptableContent content, @Nonnull final CertificateKeyProvider certificateKeyProvider) throws Exception {
Objects.requireNonNull(certificateKeyProvider);
final Key privateKey = certificateKeyProvider.getCertificateKey(content.getEncryptionCertificateId(), content.getEncryptionCertificateThumbprint());
final Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
final byte[] decryptedSymmetricKey = cipher.doFinal(Base64.getDecoder().decode(content.getDataKey()));

final Mac sha256Mac = Mac.getInstance("HmacSHA256");
sha256Mac.init(new SecretKeySpec(decryptedSymmetricKey, "HmacSHA256"));
final byte[] hashedData = sha256Mac.doFinal(Base64.getDecoder().decode(content.getData()));

final String expectedSignature = Base64.getEncoder().encodeToString(hashedData);
if (!expectedSignature.equals(content.getDataSignature())) {
throw new Exception("Signature does not match");
}
return new String(aesDecrypt(Base64.getDecoder().decode(content.getData()), decryptedSymmetricKey), StandardCharsets.UTF_8);
}

/**
* Decrypts the resource data using the decrypted symmetric key
* @param data Base-64 decoded resource data
* @param key Decrypted symmetric key from DecryptableContent.getDataKey()
* @return decrypted resource data
* @throws Exception if an error occurs while decrypting the data
*/
public static @Nonnull byte[] aesDecrypt(@Nonnull final byte[] data, @Nonnull final byte[] key) throws Exception {
try {
@SuppressWarnings("java:S3329")
// Sonar warns that a random IV should be used for encryption
// but we are decrypting here.
final IvParameterSpec ivSpec = new IvParameterSpec(Arrays.copyOf(key, 16));
@SuppressWarnings("java:S5542")
// Sonar warns that cncryption algorithms should be used with secure mode and padding scheme
// but ChangeNotifications implementation uses this algorithm for decryption.
// https://learn.microsoft.com/en-us/graph/change-notifications-with-resource-data?tabs=java#decrypting-resource-data
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), ivSpec);
return cipher.doFinal(data);
} catch (Exception ex) {
throw new RuntimeException("Unexpected error occurred while trying to decrypt the data", ex);
}
}

/**
* Provides a private key for the certificate with the ID provided when creating the
* subscription and the thumbprint.
*/
@FunctionalInterface
public interface CertificateKeyProvider {
/**
* Returns the private key for an X.509 certificate with the given id and thumbprint
* @param certificateId certificate Id provided when subscribing
* @param certificateThumbprint certificate thumbprint
* @return Private key used to sign the certificate
*/
public @Nonnull Key getCertificateKey(@Nullable final String certificateId, @Nullable final String certificateThumbprint);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.microsoft.graph.core.models;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Key;
import java.util.Objects;

import org.slf4j.LoggerFactory;

import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.UrlJwkProvider;

import io.jsonwebtoken.JweHeader;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.LocatorAdapter;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;

/**
* DiscoverUrlAdapter class
*/
public class DiscoverUrlAdapter extends LocatorAdapter<Key> {

/**
* Key store
*/
private final JwkProvider keyStore;

/**
* Constructor
* @param keyDiscoveryUrl the JWKS endpoint to use to retrieve signing keys
* @throws URISyntaxException if uri is invalid
* @throws MalformedURLException if url is invalid
*/
public DiscoverUrlAdapter(@Nonnull final String keyDiscoveryUrl)
throws URISyntaxException, MalformedURLException {
this.keyStore =
new UrlJwkProvider(new URI(Objects.requireNonNull(keyDiscoveryUrl)).toURL());
}

@Override
protected @Nullable Key locate(@Nonnull JwsHeader header) {
Objects.requireNonNull(header);
try {
String keyId = header.getKeyId();
Jwk publicKey = keyStore.get(keyId);
return publicKey.getPublicKey();
} catch (final Exception e) {
throw new IllegalArgumentException("Could not locate key", e);
}
}

@Override
protected @Nullable Key locate(@Nonnull JweHeader header) {
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.microsoft.graph.core.models;

import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Objects;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;

/**
* EncryptableSubscription interface
*/
public interface EncryptableSubscription {

/**
* Sets the encryption certificate
* @param certificate Base-64 encoded certificate to be used by Microsoft Graph to encrypt resource data
*/
public void setEncryptionCertificate(@Nullable final String certificate);

/**
* Returns the encryption certificate
* @return encryption certificate
*/
public @Nullable String getEncryptionCertificate();

/**
* Converts an X.509 Certificate object to Base-64 string and adds to the encryptableSubscription provided
* @param subscription encryptable subscription
* @param certificate X.509 Certificate
* @throws CertificateEncodingException if the certificate cannot be encoded
*/
public static void addPublicEncryptionCertificate(@Nonnull final EncryptableSubscription subscription, @Nonnull final X509Certificate certificate) throws CertificateEncodingException {
Objects.requireNonNull(subscription);
Objects.requireNonNull(certificate);
subscription.setEncryptionCertificate(
Base64.getEncoder().encodeToString(certificate.getEncoded())
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.microsoft.graph.core.models;

/**
* Contains Decryptable content
* @param <T> The type of the decryptable content
*/
public interface EncryptedContentBearer<T extends DecryptableContent> {

/**
* Sets encrypted content
* @param encryptedContent encrypted content
*/
public void setEncryptedContent(T encryptedContent);

/**
* Return encrypted content
* @return encrypted content
*/
public T getEncryptedContent();

}
Loading