ee.ria.xroad.confproxy.util.OutputBuilder.java Source code

Java tutorial

Introduction

Here is the source code for ee.ria.xroad.confproxy.util.OutputBuilder.java

Source

/**
 * The MIT License
 * Copyright (c) 2015 Estonian Information System Authority (RIA), Population Register Centre (VRK)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package ee.ria.xroad.confproxy.util;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Date;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.input.TeeInputStream;

import org.eclipse.jetty.util.MultiPartWriter;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

import ee.ria.xroad.common.conf.globalconf.ConfigurationDirectory;
import ee.ria.xroad.common.conf.globalconf.ConfigurationPartMetadata;
import ee.ria.xroad.common.util.CryptoUtils;
import ee.ria.xroad.common.util.HashCalculator;
import ee.ria.xroad.common.util.MimeTypes;
import ee.ria.xroad.common.util.MultipartEncoder;
import ee.ria.xroad.confproxy.ConfProxyProperties;
import ee.ria.xroad.signer.protocol.SignerClient;
import ee.ria.xroad.signer.protocol.message.GetSignMechanism;
import ee.ria.xroad.signer.protocol.message.GetSignMechanismResponse;
import ee.ria.xroad.signer.protocol.message.Sign;
import ee.ria.xroad.signer.protocol.message.SignResponse;

import static ee.ria.xroad.common.util.CryptoUtils.calculateDigest;
import static ee.ria.xroad.common.util.CryptoUtils.encodeBase64;
import static ee.ria.xroad.common.util.MimeUtils.*;

/**
 * Utility class that encapsulates the process of signing the downloaded
 * global configuration and moving it to the target location.
 */
@Slf4j
public class OutputBuilder {

    public static final String SIGNED_DIRECTORY_NAME = "conf";

    private final ConfigurationDirectory confDir;
    private final ConfProxyProperties conf;
    private final int version;

    private Path tempConfPath;
    private HashCalculator hashCalculator;
    private String timestamp;
    private Path tempDirPath;

    private String dataBoundary;
    private String envelopeBoundary;
    private String envelopeHeader;

    /**
     * Constructs an output builder for the given global configuration directory
     * and configuration proxy instance configuration.
     * @param confDirectory global configuration to be processed
     * @param configuration configuration proxy instance configuration
     * @throws Exception in case of errors when a temporary directory
     */
    public OutputBuilder(final ConfigurationDirectory confDirectory, final ConfProxyProperties configuration,
            int version) throws Exception {
        this.confDir = confDirectory;
        this.conf = configuration;
        this.version = version;

        setup();
    }

    /**
     * Generates a signed directory MIME for the global configuration and
     * writes the directory contents to a temporary location.
     * @throws Exception if errors occur when reading global configuration files
     */
    public final void buildSignedDirectory() throws Exception {
        try (ByteArrayOutputStream mimeContent = new ByteArrayOutputStream()) {
            build(mimeContent);

            log.debug("Generated directory content:\n{}\n", mimeContent.toString());

            byte[] contentBytes = mimeContent.toByteArray();
            mimeContent.reset();
            sign(contentBytes, mimeContent);
            Files.write(tempConfPath, mimeContent.toByteArray());

            log.debug("Written signed directory to '{}'", tempConfPath);
        }
    }

    /**
     * Moves the signed global configuration to the location where it is
     * accessible to clients and cleans up any remaining temporary files.
     * @throws Exception in case of unsuccessful file operations
     */
    public final void moveAndCleanup() throws Exception {
        String path = conf.getConfigurationTargetPath();
        Path targetPath = Paths.get(path, timestamp);
        Path targetConf = Paths.get(path, String.format("%s-v%d", SIGNED_DIRECTORY_NAME, version));
        Files.createDirectories(targetPath.getParent());

        log.debug("Moving '{}' to '{}'", tempDirPath, targetPath);

        Files.move(tempDirPath, targetPath);

        log.debug("Moving '{}' to '{}'", tempConfPath, targetConf);

        Files.move(tempConfPath, targetConf, StandardCopyOption.ATOMIC_MOVE);
        FileUtils.deleteDirectory(tempDirPath.toFile());
    }

    /**
     * Setup reference data and temporary directory for the output builder.
     * @throws Exception if temporary directory could not be created
     */
    private void setup() throws Exception {
        String tempDir = conf.getTemporaryDirectoryPath();
        String hashAlgURI = conf.getHashAlgorithmURI();

        hashCalculator = new HashCalculator(hashAlgURI);
        timestamp = Long.toString(new Date().getTime());
        tempConfPath = Paths.get(tempDir, String.format("%s-v%d", SIGNED_DIRECTORY_NAME, version));
        tempDirPath = Paths.get(tempDir, timestamp);

        log.debug("Creating directories {}", tempDirPath);

        Files.createDirectories(tempDirPath);

        log.debug("Clean directory {}", tempDirPath);

        FileUtils.cleanDirectory(tempDirPath.toFile());

        dataBoundary = randomBoundary();
        envelopeBoundary = randomBoundary();
        envelopeHeader = HEADER_CONTENT_TYPE + ": "
                + mpRelatedContentType(envelopeBoundary, MultiPartWriter.MULTIPART_MIXED) + "\n\n";
    }

    /**
     * Generates global configuration directory content MIME.
     * @param mimeContent output stream to write to
     * @throws Exception if reading global configuration files fails
     */
    private void build(final ByteArrayOutputStream mimeContent) throws Exception {
        try (MultipartEncoder encoder = new MultipartEncoder(mimeContent, dataBoundary)) {
            DateTime expireDate = new DateTime().plusSeconds(conf.getValidityIntervalSeconds());
            encoder.startPart(null,
                    new String[] { HEADER_EXPIRE_DATE + ": " + expireDate.toDateTime(DateTimeZone.UTC),
                            HEADER_VERSION + ": " + String.format("%d", version) });

            String instance = conf.getInstance();

            confDir.eachFile((metadata, inputStream) -> {
                try (FileOutputStream fos = createFileOutputStream(tempDirPath, metadata)) {
                    TeeInputStream tis = new TeeInputStream(inputStream, fos);
                    appendFileContent(encoder, instance, metadata, tis);
                }
            });
        }
    }

    /**
     * Signs the global configuration directory content.
     * @param contentBytes configuration directory content bytes
     * @param mimeContent output stream to write to
     * @throws Exception if errors are encountered while writing
     * the signed directory content to a temporary location
     */
    private void sign(final byte[] contentBytes, final ByteArrayOutputStream mimeContent) throws Exception {
        String keyId = conf.getActiveSigningKey();
        String digestAlgorithmId = conf.getSignatureDigestAlgorithmId();
        String signAlgoId = getSignatureAlgorithmId(keyId, digestAlgorithmId);
        byte[] digest = calculateDigest(digestAlgorithmId, contentBytes);

        log.debug("Signing directory with signing key '{}' and signing algorithm '{}'", keyId, signAlgoId);

        String signature = getSignature(keyId, signAlgoId, digest);

        mimeContent.write(envelopeHeader.getBytes());

        try (MultipartEncoder encoder = new MultipartEncoder(mimeContent, envelopeBoundary)) {
            encoder.startPart(mpMixedContentType(dataBoundary));
            encoder.write(contentBytes);
            String algURI = CryptoUtils.getSignatureAlgorithmURI(signAlgoId);
            String hashURI = hashCalculator.getAlgoURI();
            Path verificationCertPath = conf.getCertPath(keyId);

            encoder.startPart(MimeTypes.BINARY,
                    new String[] { HEADER_CONTENT_TRANSFER_ENCODING + ": base64",
                            HEADER_SIG_ALGO_ID + ": " + algURI,
                            HEADER_VERIFICATION_CERT_HASH + ": " + getVerificationCertHash(verificationCertPath)
                                    + "; " + HEADER_HASH_ALGORITHM_ID + "=" + hashURI });
            encoder.write(signature.getBytes());
        }

        log.debug("Generated signed directory:\n{}\n", mimeContent.toString());

        Files.write(tempConfPath, mimeContent.toByteArray());

        log.debug("Written signed directory to '{}'", tempConfPath);
    }

    /**
     * Computes the verification hash of the certificate at the given path.
     * @param certPath path to the certificate file
     * @return verification hash for the certificate
     * @throws Exception if failed to open the certificate file
     */
    private String getVerificationCertHash(final Path certPath) throws Exception {
        try (InputStream is = new FileInputStream(certPath.toFile())) {
            byte[] certBytes = CryptoUtils.readCertificate(is).getEncoded();

            return hashCalculator.calculateFromBytes(certBytes);
        }
    }

    /**
     * Opens a stream for writing the configuration file describes by the metadata to the target location.
     * @param targetPath location to write the file to
     * @param metadata describes the configuration file
     * @return output stream for writing the file
     * @throws Exception if errors during file operations occur
     */
    private FileOutputStream createFileOutputStream(final Path targetPath, final ConfigurationPartMetadata metadata)
            throws Exception {
        Path filepath = targetPath
                .resolve(Paths.get(metadata.getInstanceIdentifier(), metadata.getContentLocation()));
        Files.createDirectories(filepath.getParent());
        Path newFile = Files.createFile(filepath);

        log.debug("Copying file '{}' to directory '{}'", newFile.toAbsolutePath(), targetPath);

        return new FileOutputStream(newFile.toAbsolutePath().toFile());
    }

    /**
     * Appends the metadata and hash of a configuration file to the content inside the encoder.
     * @param encoder generates the configuration directory mime from the given file content
     * @param instance configuration proxy instance name
     * @param metadata describes the configuration file
     * @param inputStream contents of the configuration file to compute the hash
     * @throws Exception if the configuration file content could not be appended
     */
    private void appendFileContent(final MultipartEncoder encoder, final String instance,
            final ConfigurationPartMetadata metadata, final InputStream inputStream) throws Exception {
        try {
            Path contentLocation = Paths.get(instance, timestamp, metadata.getInstanceIdentifier(),
                    metadata.getContentLocation());

            encoder.startPart(MimeTypes.BINARY,
                    new String[] { HEADER_CONTENT_TRANSFER_ENCODING + ": base64",
                            HEADER_CONTENT_IDENTIFIER + ": " + metadata.getContentIdentifier() + "; instance=\""
                                    + metadata.getInstanceIdentifier() + "\"",
                            HEADER_CONTENT_LOCATION + ": /" + contentLocation,
                            HEADER_HASH_ALGORITHM_ID + ": " + hashCalculator.getAlgoURI() });

            encoder.write(hashCalculator.calculateFromStream(inputStream).getBytes());
        } catch (Exception e) {
            log.error("Failed to append '{}' content to directory data", metadata.getContentFileName());

            throw e;
        }
    }

    private static String getSignatureAlgorithmId(String keyId, String digestAlgoId) throws Exception {
        GetSignMechanismResponse signMechanismResponse = SignerClient.execute(new GetSignMechanism(keyId));

        return CryptoUtils.getSignatureAlgorithmId(digestAlgoId, signMechanismResponse.getSignMechanismName());
    }

    /**
     * Generates the signature of the configuration directory data.
     * @param keyId id of the key used for signing
     * @param signatureAlgorithmId if of the algorithm used for signing
     * @param digest digest bytes of the directory content
     * @return the configuration directory signature string (base64)
     * @throws Exception if cryptographic operations fail
     */
    private String getSignature(final String keyId, final String signatureAlgorithmId, final byte[] digest)
            throws Exception {
        SignResponse response = SignerClient.execute(new Sign(keyId, signatureAlgorithmId, digest));

        return encodeBase64(response.getSignature());
    }
}