dev.fitko.fitconnect.client.subscriber.SubmissionReceiver Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of client Show documentation
Show all versions of client Show documentation
Library that provides client access to the FIT-Connect api-endpoints for sending, subscribing and
routing
The newest version!
package dev.fitko.fitconnect.client.subscriber;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import dev.fitko.fitconnect.api.FitConnectService;
import dev.fitko.fitconnect.api.config.ApplicationConfig;
import dev.fitko.fitconnect.api.domain.model.attachment.Attachment;
import dev.fitko.fitconnect.api.domain.model.event.EventPayload;
import dev.fitko.fitconnect.api.domain.model.event.Status;
import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags;
import dev.fitko.fitconnect.api.domain.model.event.problems.Problem;
import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataEncryptionIssue;
import dev.fitko.fitconnect.api.domain.model.event.problems.metadata.MetadataEncryptionIssue;
import dev.fitko.fitconnect.api.domain.model.event.problems.metadata.MetadataJsonSyntaxViolation;
import dev.fitko.fitconnect.api.domain.model.event.problems.submission.InvalidEventLog;
import dev.fitko.fitconnect.api.domain.model.event.problems.submission.MissingAuthenticationTags;
import dev.fitko.fitconnect.api.domain.model.event.problems.submission.NotExactlyOneSubmitEvent;
import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentForValidation;
import dev.fitko.fitconnect.api.domain.model.submission.Submission;
import dev.fitko.fitconnect.api.domain.subscriber.ReceivedSubmission;
import dev.fitko.fitconnect.api.domain.subscriber.ReceivedSubmissionData;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.exceptions.internal.AuthenticationTagsEmptyException;
import dev.fitko.fitconnect.api.exceptions.internal.DecryptionException;
import dev.fitko.fitconnect.api.exceptions.internal.EventLogException;
import dev.fitko.fitconnect.api.exceptions.internal.RestApiException;
import dev.fitko.fitconnect.api.exceptions.internal.SubmissionRequestException;
import dev.fitko.fitconnect.api.exceptions.internal.SubmitEventNotFoundException;
import dev.fitko.fitconnect.client.attachments.download.AttachmentDownloader;
import dev.fitko.fitconnect.client.util.AttachmentMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
import static com.nimbusds.jose.jwk.KeyOperation.UNWRAP_KEY;
import static dev.fitko.fitconnect.client.util.AttachmentMapper.getSubmissionDataFromAttachments;
public class SubmissionReceiver {
private static final Logger LOGGER = LoggerFactory.getLogger(SubmissionReceiver.class);
private static final ObjectMapper MAPPER = new ObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
private final FitConnectService fitConnectService;
private final ApplicationConfig config;
private final RSAKey privateKey;
private final AttachmentDownloader attachmentDownloader;
public SubmissionReceiver(final FitConnectService fitConnectService, final RSAKey privateKey, final ApplicationConfig config, final AttachmentDownloader attachmentDownloader) {
this.config = config;
this.privateKey = privateKey;
this.fitConnectService = fitConnectService;
this.attachmentDownloader = attachmentDownloader;
}
public ReceivedSubmission receiveSubmission(final UUID submissionId) {
LOGGER.info("Requesting submission ...");
final Submission submission = loadSubmission(submissionId);
LOGGER.info("Loading authentication tags from event log ...");
final AuthenticationTags authenticationTags = loadAuthTagsForSubmitEvent(submission);
LOGGER.info("Decrypting metadata ...");
final Metadata metadata = decryptMetadata(submission);
validateMetadata(metadata, submission, authenticationTags);
LOGGER.info("Loading and decrypting attachments ...");
final List attachments = loadAttachments(submission, metadata);
validateAttachments(attachments, submission, authenticationTags);
LOGGER.info("Decrypting data ...");
final byte[] decryptedData = decryptData(submission, attachments);
validateData(submission, metadata, decryptedData, authenticationTags.getData());
LOGGER.info("SUCCESSFULLY RECEIVED SUBMISSION ! \n");
return buildReceivedSubmission(submission, metadata, decryptedData, attachments);
}
private List loadAttachments(final Submission submission, final Metadata metadata) {
checkPrivateKey();
return attachmentDownloader.loadAttachments(submission, privateKey, metadata.getContentStructure().getAttachments());
}
private Submission loadSubmission(final UUID submissionId) {
try {
return fitConnectService.getSubmission(submissionId);
} catch (final RestApiException e) {
throw new SubmissionRequestException(e.getMessage(), e);
}
}
private AuthenticationTags loadAuthTagsForSubmitEvent(final Submission submission) {
try {
return fitConnectService.getSubmissionAuthenticationTags(submission);
} catch (final EventLogException e) {
// https://docs.fitko.de/fit-connect/docs/receiving/verification/#struktur--und-signaturpr%C3%BCfung-der-security-event-tokens
final InvalidEventLog problem = new InvalidEventLog();
rejectSubmissionWithProblem(submission, problem);
throw new SubmissionRequestException(problem.getDetail(), e);
} catch (final SubmitEventNotFoundException e) {
// https://docs.fitko.de/fit-connect/docs/receiving/verification/#genau-ein-submit-event
final NotExactlyOneSubmitEvent problem = new NotExactlyOneSubmitEvent();
rejectSubmissionWithProblem(submission, problem);
throw new SubmissionRequestException(problem.getDetail());
} catch (final AuthenticationTagsEmptyException e) {
// https://docs.fitko.de/fit-connect/docs/receiving/verification/#authentication-tags-im-submit-event
final MissingAuthenticationTags problem = new MissingAuthenticationTags();
rejectSubmissionWithProblem(submission, problem);
throw new SubmissionRequestException(problem.getDetail());
}
}
private void validateMetadata(final Metadata metadata, final Submission submission, final AuthenticationTags authenticationTags) {
final ValidationResult validationResult = fitConnectService.validateSubmissionMetadata(metadata, submission, authenticationTags);
evaluateValidationResult(submission, validationResult);
}
private void validateAttachments(final List attachmentForValidation, final Submission submission, final AuthenticationTags authenticationTags) {
final ValidationResult validationResult = fitConnectService.validateAttachments(attachmentForValidation, authenticationTags);
evaluateValidationResult(submission, validationResult);
}
private void validateData(final Submission submission, final Metadata metadata, final byte[] decryptedData, final String authenticationTags) {
final ValidationResult validationResult = fitConnectService.validateData(decryptedData, submission.getEncryptedData(), metadata, authenticationTags);
evaluateValidationResult(submission, validationResult);
}
private void evaluateValidationResult(final Submission submission, final ValidationResult validationResult) throws SubmissionRequestException {
if (validationResult.hasProblems()) {
rejectSubmissionWithProblem(submission, validationResult.getProblems().toArray(new Problem[0]));
throw new SubmissionRequestException(validationResult.getProblems().stream().map(Problem::getDetail).collect(Collectors.joining()), validationResult.getError());
} else if (validationResult.hasError()) {
LOGGER.error(validationResult.getError().getMessage(), validationResult.getError());
throw new SubmissionRequestException(validationResult.getError().getMessage(), validationResult.getError());
}
}
private ReceivedSubmission buildReceivedSubmission(final Submission submission, final Metadata metadata, final byte[] decryptedData, final List attachments) {
final List receivedAttachments = attachments.stream().map(AttachmentMapper::toApiAttachment).collect(Collectors.toList());
final Map attachmentAuthTags = AttachmentMapper.mapAttachmentIdsToAuthTags(attachments);
final Status submitState = fitConnectService.getSubmitState(submission);
final ReceivedSubmissionData receivedSubmissionData = ReceivedSubmissionData.builder()
.attachmentAuthTags(attachmentAuthTags)
.attachments(receivedAttachments)
.submissionStatus(submitState)
.submission(submission)
.metadata(metadata)
.data(decryptedData)
.build();
return new ReceivedSubmission(fitConnectService, receivedSubmissionData);
}
private byte[] decryptData(final Submission submission, List attachments) {
checkPrivateKey();
try {
return getSubmissionDataFromAttachments(attachments).orElse(fitConnectService.decryptString(privateKey, submission.getEncryptedData()));
} catch (final DecryptionException e) {
// https://docs.fitko.de/fit-connect/docs/receiving/verification/#entschl%C3%BCsselung-1
rejectSubmissionWithProblem(submission, new DataEncryptionIssue());
throw new SubmissionRequestException(e.getMessage(), e);
} catch (IOException e) {
throw new SubmissionRequestException(e.getMessage(), e);
}
}
private Metadata decryptMetadata(final Submission submission) {
checkPrivateKey();
try {
final byte[] metadataBytes = fitConnectService.decryptString(privateKey, submission.getEncryptedMetadata());
return MAPPER.readValue(metadataBytes, Metadata.class);
} catch (final IOException e) {
rejectSubmissionWithProblem(submission, new MetadataJsonSyntaxViolation());
throw new SubmissionRequestException(e.getMessage(), e);
} catch (final DecryptionException e) {
// https://docs.fitko.de/fit-connect/docs/receiving/verification/#entschl%C3%BCsselung
rejectSubmissionWithProblem(submission, new MetadataEncryptionIssue());
throw new SubmissionRequestException(e.getMessage(), e);
}
}
private void checkPrivateKey() {
if (!isUnwrapKey(privateKey) && !isEncryptionKeyUse(privateKey)) {
throw new SubmissionRequestException("Private key is not suitable for decryption, could not find key_operation unwrapKey or key_use encryption");
}
}
private static boolean isEncryptionKeyUse(final RSAKey privateKey) {
return privateKey.getKeyUse() != null && privateKey.getKeyUse().equals(KeyUse.ENCRYPTION);
}
private static boolean isUnwrapKey(final RSAKey privateKey) {
return privateKey.getKeyOperations() != null && privateKey.getKeyOperations().stream()
.anyMatch(keyOp -> keyOp.identifier().equals(UNWRAP_KEY.identifier()));
}
private void rejectSubmissionWithProblem(final Submission submission, final Problem... problem) {
reject(submission, List.of(problem));
}
private void reject(final Submission submission, final List problems) {
if (config.isAutoRejectEnabled()) {
LOGGER.info("Auto-rejecting submission due the following problem(s): {}", problems.stream().map(Problem::getDetail).collect(Collectors.joining("\n")));
fitConnectService.rejectSubmission(EventPayload.forRejectEvent(submission, problems));
}
}
}