Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • fit-connect/sdk-java
1 result
Show changes
Commits on Source (31)
Showing
with 182 additions and 51 deletions
......@@ -150,9 +150,13 @@ final EncryptedSubmissionPayload frontendEncryptedPayload = EncryptedSubmissionB
.build();
final SentSubmission sentSubmission = senderClient.submit(frontendEncryptedPayload);
```
| **Important** |
| ------------- |
| If destination id (`destinationId`) and service type (`leikaKey`) are provided by a frontend component, they MUST NOT be blindly trusted. Instead, the sender's backend MUST check if sending submissions of this service type to the specified destination is allowed. |
### Hand in a new submission with unencrypted data
If all data, metadata and attachments are encrypted in the sender using the SDK, the client automatically handles the encryption.
......
package dev.fitko.fitconnect.api.config;
import lombok.Data;
@Data
public class BuildInfo {
private String productName;
private String productVersion;
private String commit;
}
......@@ -51,6 +51,17 @@ public interface Sender {
*/
ValidationResult validateMetadata(Metadata metadata);
/**
* Checks if a received callback can be trusted by validating the provided request data
*
* @param hmac authentication code provided by the callback
* @param timestamp timestamp provided by the callback
* @param httpBody HTTP body provided by the callback
* @param callbackSecret secret owned by the client, which is used to calculate the hmac
* @return {@code true} if hmac and timestamp provided by the callback meet the required conditions
*/
ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret);
/**
* Encrypts the submission data payload (json or xml) with JWE (JSON-Web-Encryption).
*
......
......@@ -85,6 +85,17 @@ public interface Subscriber {
*/
ValidationResult validateHashIntegrity(String originalHash, byte[] data);
/**
* Checks if a received callback can be trusted by validating the provided request data
*
* @param hmac authentication code provided by the callback
* @param timestamp timestamp provided by the callback
* @param httpBody HTTP body provided by the callback
* @param callbackSecret secret owned by the client, which is used to calculate the hmac
* @return {@code true} if hmac and timestamp provided by the callback meet the required conditions
*/
ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret);
/**
* Sends a confirmation event if the received submission matches all validations.
*
......
......@@ -37,4 +37,13 @@ public interface MessageDigestService {
* @return byte[] of the given hex string
*/
byte[] fromHexString(String hexString);
/**
* Creates an HMAC
*
* @param data raw data
* @param key secret key
* @return HMAC according to https://datatracker.ietf.org/doc/html/rfc2104
*/
String calculateHMAC(String data, String key);
}
......@@ -80,5 +80,14 @@ public interface ValidationService {
*/
ValidationResult validateXmlFormat(String xml);
/**
* Checks if a received callback can be trusted by validating the provided request data
*
* @param hmac authentication code provided by the callback
* @param timestamp timestamp provided by the callback
* @param httpBody HTTP body provided by the callback
* @param callbackSecret secret owned by the client, which is used to calculate the hmac
* @return {@code true} if hmac and timestamp provided by the callback meet the required conditions
*/
ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret);
}
......@@ -71,6 +71,14 @@
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
......@@ -105,8 +113,23 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.github.git-commit-id</groupId>
<artifactId>git-commit-id-maven-plugin</artifactId>
<executions>
<execution>
<id>get-the-git-infos</id>
<goals>
<goal>revision</goal>
</goals>
<phase>initialize</phase>
</execution>
</executions>
<configuration>
<commitIdGenerationMode>full</commitIdGenerationMode>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
......@@ -8,6 +8,7 @@ import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags;
import dev.fitko.fitconnect.api.domain.model.submission.SentSubmission;
import dev.fitko.fitconnect.api.domain.model.submission.Submission;
import dev.fitko.fitconnect.api.domain.model.submission.SubmitSubmission;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.services.Sender;
import dev.fitko.fitconnect.client.sender.model.EncryptedSubmissionPayload;
import dev.fitko.fitconnect.client.sender.model.SubmissionPayload;
......@@ -69,6 +70,10 @@ public class SenderClient {
return sender.getLastedEvent(destinationId, caseId, submissionId, authenticationTags);
}
public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) {
return this.sender.validateCallback(hmac, timestamp, httpBody, callbackSecret);
}
/**
* Send submission with attachments and data to FIT-Connect API. Encryption is handled by the SDK.
*
......
......@@ -144,6 +144,10 @@ public class SubscriberClient {
return null;
}
public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) {
return this.subscriber.validateCallback(hmac, timestamp, httpBody, callbackSecret);
}
private ReceivedSubmission buildReceivedSubmission(final Submission submission, final Metadata metadata, final byte[] decryptedData, final List<DecryptedAttachmentPayload> attachments) {
final MimeType mimeType = metadata.getContentStructure().getData().getSubmissionSchema().getMimeType();
final ReceivedData receivedData = new ReceivedData(new String(decryptedData, StandardCharsets.UTF_8), mimeType);
......
package dev.fitko.fitconnect.client.factory;
import dev.fitko.fitconnect.api.config.ApplicationConfig;
import dev.fitko.fitconnect.api.config.BuildInfo;
import dev.fitko.fitconnect.api.exceptions.InitializationException;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
......@@ -11,13 +12,15 @@ import java.nio.file.Path;
public final class ApplicationConfigLoader {
private ApplicationConfigLoader() { }
private static final String BUILD_INFO_PATH = "buildinfo.yaml";
private ApplicationConfigLoader() {
}
/**
* Load ApplicationConfig from path.
*
* @param configPath path to config file
*
* @return ApplicationConfig
* @throws InitializationException if the config file could not be loaded
*/
......@@ -33,24 +36,31 @@ public final class ApplicationConfigLoader {
* Load ApplicationConfig from path string.
*
* @param configPath string of path to config file
*
* @return ApplicationConfig
* @throws InitializationException if the config file could not be loaded
*/
public static ApplicationConfig loadConfig(final String configPath) {
return loadConfig(Path.of(configPath));
return loadConfig(Path.of(configPath));
}
/**
* Load ApplicationConfig from yaml string.
*
* @param configYaml string content of the yaml config file
*
* @return ApplicationConfig
*/
public static ApplicationConfig loadConfigFromYaml(final String configYaml) {
final Constructor constructor = new Constructor(ApplicationConfig.class);
final Yaml yaml = new Yaml(constructor);
return yaml.load(configYaml);
final Yaml applicationPropertiesYaml = new Yaml(new Constructor(ApplicationConfig.class));
ApplicationConfig applicationConfig = applicationPropertiesYaml.load(configYaml);
return applicationConfig;
}
public static BuildInfo loadBuildInfo() {
final Yaml buildInfoYaml = new Yaml(new Constructor(BuildInfo.class));
return buildInfoYaml.load(ApplicationConfigLoader.class.getClassLoader().getResourceAsStream(BUILD_INFO_PATH));
}
}
......@@ -3,6 +3,7 @@ package dev.fitko.fitconnect.client.factory;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.RSAKey;
import dev.fitko.fitconnect.api.config.ApplicationConfig;
import dev.fitko.fitconnect.api.config.BuildInfo;
import dev.fitko.fitconnect.api.config.SchemaConfig;
import dev.fitko.fitconnect.api.config.SubscriberConfig;
import dev.fitko.fitconnect.api.exceptions.InitializationException;
......@@ -64,8 +65,7 @@ public final class ClientFactory {
* @return the sender client
*/
public static SenderClient senderClient() {
final ApplicationConfig config = loadConfig();
return senderClient(config);
return senderClient(loadConfig());
}
/**
......@@ -75,7 +75,7 @@ public final class ClientFactory {
*/
public static SenderClient senderClient(final ApplicationConfig config) {
LOGGER.info("Initializing sender client ...");
return new SenderClient(getSender(config));
return new SenderClient(getSender(config, ApplicationConfigLoader.loadBuildInfo()));
}
......@@ -85,8 +85,7 @@ public final class ClientFactory {
* @return the subscriber client
*/
public static SubscriberClient subscriberClient() {
final ApplicationConfig config = loadConfig();
return subscriberClient(config);
return subscriberClient(loadConfig());
}
/**
......@@ -96,7 +95,7 @@ public final class ClientFactory {
*/
public static SubscriberClient subscriberClient(final ApplicationConfig config) {
LOGGER.info("Initializing subscriber client ...");
final Subscriber subscriber = getSubscriber(config);
final Subscriber subscriber = getSubscriber(config, ApplicationConfigLoader.loadBuildInfo());
final SubscriberConfig subscriberConfig = config.getSubscriberConfig();
LOGGER.info("Reading private decryption key from {} ", subscriberConfig.getPrivateDecryptionKeyPath());
......@@ -106,8 +105,8 @@ public final class ClientFactory {
return new SubscriberClient(subscriber, privateKey);
}
private static Subscriber getSubscriber(final ApplicationConfig config) {
final RestTemplate restTemplate = getRestTemplate(config);
private static Subscriber getSubscriber(final ApplicationConfig config, final BuildInfo buildInfo) {
final RestTemplate restTemplate = getRestTemplate(config, buildInfo);
final SchemaProvider schemaProvider = getSchemaProvider();
final MessageDigestService messageDigestService = getMessageDigestService();
......@@ -124,8 +123,8 @@ public final class ClientFactory {
return new SubmissionSubscriber(submissionService, eventLogService, cryptoService, validator, setService);
}
private static Sender getSender(final ApplicationConfig config) {
final RestTemplate restTemplate = getRestTemplate(config);
private static Sender getSender(final ApplicationConfig config, final BuildInfo buildInfo) {
final RestTemplate restTemplate = getRestTemplate(config, buildInfo);
final SchemaProvider schemaProvider = getSchemaProvider();
final MessageDigestService messageDigestService = getMessageDigestService();
......@@ -169,8 +168,8 @@ public final class ClientFactory {
return new JWECryptoService(messageDigestService);
}
private static RestTemplate getRestTemplate(final ApplicationConfig config) {
final ProxyConfig proxyConfig = new ProxyConfig(config.getHttpProxyHost(), config.getHttpProxyPort());
private static RestTemplate getRestTemplate(final ApplicationConfig config, final BuildInfo buildInfo) {
final ProxyConfig proxyConfig = new ProxyConfig(config.getHttpProxyHost(), config.getHttpProxyPort(), buildInfo);
return proxyConfig.proxyRestTemplate();
}
......
productName: "${project.name}"
productVersion: "${project.version}"
commit: "${git.commit.id.full}"
\ No newline at end of file
......@@ -22,8 +22,8 @@ import dev.fitko.fitconnect.api.services.crypto.CryptoService;
import dev.fitko.fitconnect.client.factory.ClientFactory;
import dev.fitko.fitconnect.client.sender.EncryptedSubmissionBuilder;
import dev.fitko.fitconnect.client.sender.SubmissionBuilder;
import dev.fitko.fitconnect.client.subscriber.ReceivedSubmission;
import dev.fitko.fitconnect.client.sender.model.AttachmentPayload;
import dev.fitko.fitconnect.client.subscriber.ReceivedSubmission;
import dev.fitko.fitconnect.client.util.SubmissionUtil;
import dev.fitko.fitconnect.core.auth.DefaultOAuthService;
import dev.fitko.fitconnect.core.crypto.HashService;
......
......@@ -18,12 +18,7 @@ import dev.fitko.fitconnect.api.domain.model.submission.SentSubmission;
import dev.fitko.fitconnect.api.domain.model.submission.Submission;
import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.exceptions.EncryptionException;
import dev.fitko.fitconnect.api.exceptions.EventLogException;
import dev.fitko.fitconnect.api.exceptions.KeyNotRetrievedException;
import dev.fitko.fitconnect.api.exceptions.RestApiException;
import dev.fitko.fitconnect.api.exceptions.SubmissionNotCreatedException;
import dev.fitko.fitconnect.api.exceptions.ValidationException;
import dev.fitko.fitconnect.api.exceptions.*;
import dev.fitko.fitconnect.api.services.Sender;
import dev.fitko.fitconnect.client.sender.EncryptedSubmissionBuilder;
import dev.fitko.fitconnect.client.sender.SubmissionBuilder;
......@@ -35,24 +30,15 @@ import org.junit.jupiter.api.Test;
import java.io.File;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
public class SenderClientTest {
......@@ -636,6 +622,18 @@ public class SenderClientTest {
}
@Test
void validateCallback() {
when(this.senderMock.validateCallback(anyString(), anyLong(), anyString(), anyString())).thenReturn(ValidationResult.ok());
ValidationResult validationResult = this.senderClient.validateCallback("hmac", 0L, "body", "secret");
verify(this.senderMock, times(1)).validateCallback(anyString(), anyLong(), anyString(), anyString());
assertTrue(validationResult.isValid());
assertFalse(validationResult.hasError());
}
private UUID setupTestMocks() throws JOSEException {
final var destinationId = UUID.randomUUID();
final var destination = getDestination(destinationId);
......
......@@ -550,6 +550,18 @@ class SubscriberClientTest {
}
@Test
void validateCallback() {
when(this.subscriberMock.validateCallback(anyString(), anyLong(), anyString(), anyString())).thenReturn(ValidationResult.ok());
ValidationResult validationResult = this.underTest.validateCallback("hmac", 0L, "body", "secret");
verify(this.subscriberMock, times(1)).validateCallback(anyString(), anyLong(), anyString(), anyString());
assertTrue(validationResult.isValid());
assertFalse(validationResult.hasError());
}
private String getResourceAsString(final String name) throws IOException {
final ClassLoader classLoader = getClass().getClassLoader();
final File file = new File(Objects.requireNonNull(classLoader.getResource(name)).getFile());
......
......@@ -16,9 +16,7 @@ import java.nio.file.Path;
import java.util.Objects;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class ApplicationConfigLoaderTest {
......
package dev.fitko.fitconnect.client.factory;
import dev.fitko.fitconnect.api.config.ApplicationConfig;
import dev.fitko.fitconnect.api.config.Environment;
import dev.fitko.fitconnect.api.config.EnvironmentName;
import dev.fitko.fitconnect.api.config.SenderConfig;
import dev.fitko.fitconnect.api.config.SubscriberConfig;
import dev.fitko.fitconnect.api.config.*;
import dev.fitko.fitconnect.api.exceptions.InitializationException;
import dev.fitko.fitconnect.api.exceptions.InvalidKeyException;
import org.junit.jupiter.api.Test;
......
......@@ -59,6 +59,11 @@ public class SubmissionSender implements Sender {
return validationService.validateMetadataSchema(metadata);
}
@Override
public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) {
return this.validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret);
}
@Override
public String encryptBytes(final RSAKey publicKey, final byte[] data) {
LOGGER.info("Encrypting {} bytes", data.length);
......
......@@ -85,6 +85,11 @@ public class SubmissionSubscriber implements Subscriber {
return validationService.validateHashIntegrity(originalHash, data);
}
@Override
public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) {
return this.validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret);
}
@Override
public void acceptSubmission(final EventPayload eventPayload) {
LOGGER.info("Accepting submission");
......
package dev.fitko.fitconnect.core.crypto;
import dev.fitko.fitconnect.api.exceptions.EncryptionException;
import dev.fitko.fitconnect.api.exceptions.InitializationException;
import dev.fitko.fitconnect.api.services.crypto.MessageDigestService;
import dev.fitko.fitconnect.core.util.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class HashService implements MessageDigestService {
static final String DEFAULT_ALGORITHM = "SHA-512"; // Currently, only SHA-512 is supported.
private static final String HMAC_SHA512 = "HmacSHA512";
private static final Logger LOGGER = LoggerFactory.getLogger(HashService.class);
......@@ -67,6 +72,19 @@ public class HashService implements MessageDigestService {
return bytes;
}
@Override
public String calculateHMAC(String data, String key) {
try {
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), HMAC_SHA512);
Mac mac = Mac.getInstance(HMAC_SHA512);
mac.init(secretKeySpec);
return toHexString(mac.doFinal(data.getBytes()));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new EncryptionException("Calculation of HMAC failed.", e);
}
}
private byte hexToByte(final String hexString) {
final int firstDigit = toDigit(hexString.charAt(0));
final int secondDigit = toDigit(hexString.charAt(1));
......