From 1230bb59c899db30617e898565b1a1023c18c995 Mon Sep 17 00:00:00 2001 From: Martin Vogel <martin.vogel@sinc.de> Date: Wed, 8 Jun 2022 12:26:04 +0200 Subject: [PATCH] #414 Adds JWE crypto service --- api/pom.xml | 11 ++ api/src/main/java/fitconnect/api/Sender.java | 32 ++--- .../client/impl/SubmissionSender.java | 34 ++++++ .../impl/crypto/FitCoCryptoService.java | 110 ++++++++++++++++++ .../client/crypto/JWECryptoServiceTest.java | 56 +++++++++ pom.xml | 25 ++++ 6 files changed, 244 insertions(+), 24 deletions(-) create mode 100644 client/src/main/java/fitconnect/client/impl/SubmissionSender.java create mode 100644 client/src/main/java/fitconnect/client/impl/crypto/FitCoCryptoService.java create mode 100644 client/src/test/java/fitconnect/client/crypto/JWECryptoServiceTest.java diff --git a/api/pom.xml b/api/pom.xml index 10f9df333..f1720e292 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -16,4 +16,15 @@ <maven.compiler.target>17</maven.compiler.target> </properties> + <dependencies> + <dependency> + <groupId>com.nimbusds</groupId> + <artifactId>nimbus-jose-jwt</artifactId> + </dependency> + <dependency> + <groupId>org.zalando</groupId> + <artifactId>problem</artifactId> + </dependency> + </dependencies> + </project> \ No newline at end of file diff --git a/api/src/main/java/fitconnect/api/Sender.java b/api/src/main/java/fitconnect/api/Sender.java index c5d1061b0..0e76e0a38 100644 --- a/api/src/main/java/fitconnect/api/Sender.java +++ b/api/src/main/java/fitconnect/api/Sender.java @@ -1,11 +1,7 @@ package fitconnect.api; +import com.nimbusds.jose.jwk.RSAKey; import fitconnect.api.auth.OAuthToken; -import fitconnect.api.data.Data; -import fitconnect.api.data.Metadata; -import fitconnect.api.validation.ValidationResult; - -import java.io.ByteArrayInputStream; /** * A technical system that creates a submission via the Submission API. @@ -22,31 +18,19 @@ public interface Sender { */ OAuthToken retrieveAuthenticationToken(final String clientId, final String clientSecret, String... scope); - // TODO - ValidationResult validateCertificateChain(byte[] chain); - /** * Encrypts the submission data with JWE. * - * @param unencryptedData - unencrypted JSON or XML data - * @return encrypted JSON or XML data + * @param unencryptedData - unencrypted JSON or XML data as string + * @return encrypted JSON or XML data as string */ - Data encryptData(final Data unencryptedData); + String encryptData(RSAKey publicKey, String unencryptedData); /** - * Encrypts the submission attachment with JWE. + * Encrypts a submission attachment with JWE. * - * @param unencryptedAttachmentBinary - the unencrypted binary data of the attachment - * @return the encrypted binary of the attachment + * @param unencryptedAttachment - unencrypted bytes of the attachment + * @return encrypted JWE string serialization of the attachment */ - ByteArrayInputStream encryptAttachment(final ByteArrayInputStream unencryptedAttachmentBinary); - - /** - * Generates the metadata consisting of the attachment and its hash values. - * - * @param attachment - the unencrypted binary data of the attachment - * @return metadata object - */ - Metadata generateMetadata(byte[] attachment); - + String encryptAttachment(RSAKey publicKey, byte[] unencryptedAttachment); } diff --git a/client/src/main/java/fitconnect/client/impl/SubmissionSender.java b/client/src/main/java/fitconnect/client/impl/SubmissionSender.java new file mode 100644 index 000000000..dc26e74d6 --- /dev/null +++ b/client/src/main/java/fitconnect/client/impl/SubmissionSender.java @@ -0,0 +1,34 @@ +package fitconnect.client.impl; + +import com.nimbusds.jose.jwk.RSAKey; +import fitconnect.api.Sender; +import fitconnect.api.auth.OAuthService; +import fitconnect.api.auth.OAuthToken; +import fitconnect.api.crypto.JWECryptoService; + +public class SubmissionSender implements Sender { + + private final OAuthService authService; + private final JWECryptoService cryptoService; + + public SubmissionSender(final OAuthService authService, JWECryptoService cryptoService){ + this.authService = authService; + this.cryptoService = cryptoService; + } + + @Override + public OAuthToken retrieveAuthenticationToken(String clientId, String clientSecret, String... scope) { + return authService.authenticate(clientId, clientSecret, scope).orElseThrow(); + } + + @Override + public String encryptData(RSAKey publicKey, String unencryptedData) { + return cryptoService.encryptString(publicKey, unencryptedData); + } + + @Override + public String encryptAttachment(RSAKey publicKey, byte[] unencryptedAttachment) { + return cryptoService.encryptBytes(publicKey, unencryptedAttachment); + } + +} diff --git a/client/src/main/java/fitconnect/client/impl/crypto/FitCoCryptoService.java b/client/src/main/java/fitconnect/client/impl/crypto/FitCoCryptoService.java new file mode 100644 index 000000000..0d03aa5f7 --- /dev/null +++ b/client/src/main/java/fitconnect/client/impl/crypto/FitCoCryptoService.java @@ -0,0 +1,110 @@ +package fitconnect.client.impl.crypto; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.crypto.RSADecrypter; +import com.nimbusds.jose.crypto.RSAEncrypter; +import com.nimbusds.jose.jwk.RSAKey; +import fitconnect.api.crypto.JWECryptoService; +import fitconnect.api.problems.DecryptionProblem; +import fitconnect.api.problems.EncryptionProblem; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; + +public class FitCoCryptoService implements JWECryptoService { + + public static final JWEAlgorithm ALGORITHM = JWEAlgorithm.RSA_OAEP_256; + public static final EncryptionMethod ENCRYPTION_METHOD = EncryptionMethod.A256GCM; + + @Override + public String encryptString(RSAKey publicKey, String data) throws EncryptionProblem { + final Payload payload = getPayloadString(data); + return encrypt(publicKey, payload); + } + + @Override + public String decryptString(RSAKey privateKey, String encryptedData) throws DecryptionProblem { + return decrypt(privateKey, encryptedData).toString(); + } + + @Override + public String encryptBytes(RSAKey publicKey, byte[] bytes) throws EncryptionProblem { + final Payload payload = getPayloadBytes(bytes); + return encrypt(publicKey, payload); + } + + @Override + public byte[] decryptBytes(RSAKey privateKey, String encryptedData) throws DecryptionProblem { + return decrypt(privateKey, encryptedData).toBytes(); + } + + private String encrypt(RSAKey publicKey, Payload payload) throws EncryptionProblem { + try { + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(ENCRYPTION_METHOD.cekBitLength()); + final SecretKey cek = keyGenerator.generateKey(); + final String keyID = getIdFromPublicKey(publicKey); + return encryptPayload(publicKey,payload, cek, keyID); + } catch (NoSuchAlgorithmException | JOSEException e) { + throw new EncryptionProblem(e.getMessage()); + } + } + + private Payload decrypt(RSAKey privateKey, String encData) throws DecryptionProblem { + try { + final JWEObject jwe = JWEObject.parse(encData); + jwe.decrypt(new RSADecrypter(privateKey)); + return jwe.getPayload(); + } catch (ParseException | JOSEException e) { + throw new DecryptionProblem(e.getMessage()); + } + } + + private JWEObject getJWEObject(String keyID, Payload payload) { + return new JWEObject(getJWEHeader(keyID), payload); + } + + private JWEHeader getJWEHeader(String keyID) { + return new JWEHeader.Builder(ALGORITHM, ENCRYPTION_METHOD) + .compressionAlgorithm(CompressionAlgorithm.DEF) + .contentType("application/json") + .keyID(keyID) + .build(); + } + + private RSAEncrypter getEncrypter(RSAPublicKey publicKey, SecretKey cek) { + return new RSAEncrypter(publicKey, cek); + } + + private Payload getPayloadString(String data) { + return new Payload(data); + } + + private Payload getPayloadBytes(byte[] bytes) { + return new Payload(bytes); + } + + private String encryptPayload(RSAKey publicKey, Payload payload, SecretKey cek, String keyID) throws JOSEException { + JWEObject jwe = getJWEObject(keyID, payload); + jwe.encrypt(getEncrypter(publicKey.toRSAPublicKey(), cek)); + checkIfJWEObjectIsEncrypted(jwe); + return jwe.serialize(); + } + + private void checkIfJWEObjectIsEncrypted(JWEObject jwe) { + if (!jwe.getState().equals(JWEObject.State.ENCRYPTED)) { + throw new EncryptionProblem("JWE object is not encrypted"); + } + } + + private String getIdFromPublicKey(RSAKey publicKey) { + final String keyID = publicKey.getKeyID(); + if (keyID == null || keyID.isEmpty()) { + throw new EncryptionProblem("public key has no keyID"); + } + return keyID; + } +} diff --git a/client/src/test/java/fitconnect/client/crypto/JWECryptoServiceTest.java b/client/src/test/java/fitconnect/client/crypto/JWECryptoServiceTest.java new file mode 100644 index 000000000..191cd1103 --- /dev/null +++ b/client/src/test/java/fitconnect/client/crypto/JWECryptoServiceTest.java @@ -0,0 +1,56 @@ +package fitconnect.client.crypto; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; +import fitconnect.client.impl.crypto.FitCoCryptoService; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class JWECryptoServiceTest { + + final fitconnect.api.crypto.JWECryptoService underTest = new FitCoCryptoService(); + + @Test + void encryptData() throws JOSEException { + + // Given + final RSAKey privateKey = generateRsaKey(4096); + final RSAKey publicKey = privateKey.toPublicJWK(); + + // When + String encryptedData = underTest.encryptString(publicKey, "some foo"); + + // Then + assertNotNull(encryptedData); + assertNotEquals("some foo", encryptedData); + } + + @Test + void decryptData() throws JOSEException { + + // Given + final RSAKey privateKey = generateRsaKey(4096); + final RSAKey publicKey = privateKey.toPublicJWK(); + String encryptedData = underTest.encryptString(publicKey, "some foo"); + + // When + String decrypted = underTest.decryptString(privateKey, encryptedData); + + // Then + assertNotNull(decrypted); + assertEquals("some foo", decrypted); + } + + public static RSAKey generateRsaKey(int size) throws JOSEException { + return new RSAKeyGenerator(size) + .keyUse(KeyUse.ENCRYPTION) + .keyID(UUID.randomUUID().toString()) + .generate(); + } + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2578a5c4d..e32292b0c 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,17 @@ <artifactId>guava</artifactId> <version>31.1-jre</version> </dependency> + <dependency> + <groupId>com.nimbusds</groupId> + <artifactId>nimbus-jose-jwt</artifactId> + <version>9.19</version> + </dependency> + <dependency> + <groupId>org.zalando</groupId> + <artifactId>problem</artifactId> + <version>0.27.1</version> + </dependency> + <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> @@ -60,4 +71,18 @@ </dependencies> </dependencyManagement> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>17</source> + <target>17</target> + </configuration> + </plugin> + </plugins> + </build> + + </project> \ No newline at end of file -- GitLab