All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.fitko.fitconnect.client.subscriber.SubmissionReceiver Maven / Gradle / Ivy

Go to download

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));
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy