org.bitrepository.protocol.security.BasicSecurityManager.java Source code

Java tutorial

Introduction

Here is the source code for org.bitrepository.protocol.security.BasicSecurityManager.java

Source

/*
 * #%L
 * Bitrepository Protocol
 * %%
 * Copyright (C) 2010 - 2012 The State and University Library, The Royal Library and The State Archives, Denmark
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation, either version 2.1 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
 * #L%
 */
package org.bitrepository.protocol.security;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

import org.bitrepository.common.ArgumentValidator;
import org.bitrepository.protocol.security.exception.CertificateUseException;
import org.bitrepository.protocol.security.exception.MessageAuthenticationException;
import org.bitrepository.protocol.security.exception.MessageSigningException;
import org.bitrepository.protocol.security.exception.OperationAuthorizationException;
import org.bitrepository.protocol.security.exception.SecurityException;
import org.bitrepository.protocol.security.exception.UnregisteredPermissionException;
import org.bitrepository.settings.repositorysettings.InfrastructurePermission;
import org.bitrepository.settings.repositorysettings.Permission;
import org.bitrepository.settings.repositorysettings.PermissionSet;
import org.bitrepository.settings.repositorysettings.RepositorySettings;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerId;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class to handle:
 * - loading of certificates
 * - setup of SSLContext
 * - Authentication of signatures
 * - Signature generation 
 * - Authorization of operations
 */
public class BasicSecurityManager implements SecurityManager {
    private final Logger log = LoggerFactory.getLogger(BasicSecurityManager.class);
    /** Default password for the in-memory keystore */
    private static final String defaultPassword = "123456";
    /** path to file containing the components private key and certificate */
    private final String privateKeyFile;
    /** RepositorySettings */
    private final RepositorySettings repositorySettings;
    /** Object to authenticate messages */
    private final MessageAuthenticator authenticator;
    /** Object to sign messages */
    private final MessageSigner signer;
    /** Object to authorize operations */
    private final OperationAuthorizor authorizer;
    /** Object storing permissions and certificates */
    private final PermissionStore permissionStore;
    /** int value to keep track of the next keystore alias */
    private static int aliasID = 0;
    /** In memory keyStore */
    private KeyStore keyStore;
    /** Member for holding the PrivateKeyEntry containing the from privateKeyFile loaded key and certificate */
    private PrivateKeyEntry privateKeyEntry;
    /** The ID of the component where this instance of the BasicSecurityManager is running */
    private final String componentID;

    /**
     * Constructor for the SecurityManager.
     * @param repositorySettings the collection settings to retrieve settings from
     * @param privateKeyFile path to the file containing the components private key and certificate, may be null if not using
     *        certificates and encryption.
     * @param authenticator MessageAuthenticator for authenticating messages
     * @param signer MessageSigner for signing messages.
     * @param authorizer OperationAuthorizer to authorize operations
     * @param permissionStore the PermissionStore to hold certificates and adjoining permissions
     */
    public BasicSecurityManager(RepositorySettings repositorySettings, String privateKeyFile,
            MessageAuthenticator authenticator, MessageSigner signer, OperationAuthorizor authorizer,
            PermissionStore permissionStore, String componentID) {
        ArgumentValidator.checkNotNull(repositorySettings, "repositorySettings");
        ArgumentValidator.checkNotNull(authenticator, "authenticator");
        ArgumentValidator.checkNotNull(signer, "signer");
        ArgumentValidator.checkNotNull(authorizer, "authorizer");
        ArgumentValidator.checkNotNull(permissionStore, "permissionStore");
        this.privateKeyFile = privateKeyFile;
        this.repositorySettings = repositorySettings;
        this.authenticator = authenticator;
        this.signer = signer;
        this.authorizer = authorizer;
        this.permissionStore = permissionStore;
        this.componentID = componentID;
        initialize();
    }

    @Override
    public SignerId authenticateMessage(String message, String signature) throws MessageAuthenticationException {
        if (repositorySettings.getProtocolSettings().isRequireMessageAuthentication()) {
            if (signature != null) {
                try {
                    byte[] decodedSig = Base64
                            .decode(signature.getBytes(SecurityModuleConstants.defaultEncodingType));
                    byte[] decodeMessage = message.getBytes(SecurityModuleConstants.defaultEncodingType);
                    return authenticator.authenticateMessage(decodeMessage, decodedSig);
                } catch (UnsupportedEncodingException e) {
                    throw new SecurityException(
                            SecurityModuleConstants.defaultEncodingType + " encoding not supported", e);
                }
            } else {
                throw new MessageAuthenticationException(
                        "Received unsigned message, but authentication is required");
            }
        }
        return null;
    }

    /**
     * Method to sign a message
     * @param message the message to sign
     * @return the signature for the message, or null if authentication is disabled.
     * @throws MessageSigningException if signing of the message fails.   
     */
    public String signMessage(String message) throws MessageSigningException {
        if (repositorySettings.getProtocolSettings().isRequireMessageAuthentication()) {
            try {
                byte[] signature = signer
                        .signMessage(message.getBytes(SecurityModuleConstants.defaultEncodingType));
                return new String(Base64.encode(signature));
            } catch (UnsupportedEncodingException e) {
                throw new SecurityException(SecurityModuleConstants.defaultEncodingType + " encoding not supported",
                        e);
            }
        } else {
            return null;
        }
    }

    /** 
     * Method to authorize the use of a certificate
     * @param certificateUser the user who signed the message
     * @param messageData the data of the message request.
     * @param signature the signature belonging to the message request.
     * @throws CertificateUseException in case the certificate use could not be authorized. 
     */
    public void authorizeCertificateUse(String certificateUser, String messageData, String signature)
            throws CertificateUseException {
        if (repositorySettings.getProtocolSettings().isRequireOperationAuthorization()) {
            byte[] decodeSig = Base64.decode(signature.getBytes());
            CMSSignedData s;
            try {
                s = new CMSSignedData(new CMSProcessableByteArray(messageData.getBytes()), decodeSig);
            } catch (CMSException e) {
                throw new SecurityException(e.getMessage(), e);
            }

            SignerInformation signer = (SignerInformation) s.getSignerInfos().getSigners().iterator().next();
            authorizer.authorizeCertificateUse(certificateUser, signer.getSID());
        }
    }

    @Override
    public String getCertificateFingerprint(SignerId signer) throws UnregisteredPermissionException {
        return permissionStore.getCertificateFingerprint(signer);
    }

    /**
     * Method to authorize an operation 
     * @param operationType the type of operation that is to be authorized.
     * @param messageData the data of the message request.
     * @param signature the signature belonging to the message request.
     * @throws OperationAuthorizationException in case of failure. 
     */
    public void authorizeOperation(String operationType, String messageData, String signature)
            throws OperationAuthorizationException {
        if (repositorySettings.getProtocolSettings().isRequireOperationAuthorization()) {
            byte[] decodeSig = Base64.decode(signature.getBytes());
            CMSSignedData s;
            try {
                s = new CMSSignedData(new CMSProcessableByteArray(messageData.getBytes()), decodeSig);
            } catch (CMSException e) {
                throw new SecurityException(e.getMessage(), e);
            }

            SignerInformation signer = (SignerInformation) s.getSignerInfos().getSigners().iterator().next();
            try {
                authorizer.authorizeOperation(operationType, signer.getSID());
            } catch (UnregisteredPermissionException e) {
                log.info(e.getMessage());
            }

        }
    }

    /**
     * Do initialization work
     * - Creates keystore
     * - Loads private key and certificate
     * - Loads permissions and certificates
     * - Sets up SSLContext
     */
    private void initialize() {
        Security.addProvider(new BouncyCastleProvider());
        try {
            keyStore = KeyStore.getInstance(SecurityModuleConstants.keyStoreType);
            keyStore.load(null);
            loadPrivateKey(privateKeyFile);
            loadInfrastructureCertificates(repositorySettings.getPermissionSet());
            permissionStore.loadPermissions(repositorySettings.getPermissionSet(), componentID);
            signer.setPrivateKeyEntry(privateKeyEntry);
            setupDefaultSSLContext();
        } catch (Exception e) {
            throw new SecurityException(e.getMessage(), e);
        }
    }

    /**
     * Alias generator for the keystore entries.
     * @return returns a String containing the alias for the next keystore entry 
     */
    private String getNewAlias() {
        return "" + aliasID++;
    }

    /**
     * Attempts to load the pillars private key and certificate from a PEM formatted file. 
     * @param privateKeyFile, path to the file containing the components private key and certificate, may be null
     * @throws IOException if the file cannot be found or read. 
     * @throws KeyStoreException if there is problems with adding the privateKeyEntry to keyStore
     * @throws CertificateException 
     */
    private void loadPrivateKey(String privateKeyFile) throws IOException, KeyStoreException, CertificateException {
        PrivateKey privKey = null;
        X509Certificate privCert = null;
        if (!(new File(privateKeyFile)).isFile()) {
            log.info("Key file '" + privateKeyFile + "' with private key and certificate does not exist!");
            return;
        }
        BufferedReader bufferedReader = new BufferedReader(new FileReader(privateKeyFile));
        PEMParser pemParser = new PEMParser(bufferedReader);
        Object pemObj = pemParser.readObject();

        while (pemObj != null) {
            if (pemObj instanceof X509Certificate) {
                log.debug("Certificate for PrivateKeyEntry found");
                privCert = (X509Certificate) pemObj;
            } else if (pemObj instanceof PrivateKey) {
                log.debug("Key for PrivateKeyEntry found");
                privKey = (PrivateKey) pemObj;
            } else if (pemObj instanceof X509CertificateHolder) {
                log.debug("X509CertificateHolder found");
                privCert = new JcaX509CertificateConverter().setProvider("BC")
                        .getCertificate((X509CertificateHolder) pemObj);
            } else if (pemObj instanceof PrivateKeyInfo) {
                log.debug("PrivateKeyInfo found");
                PrivateKeyInfo pki = (PrivateKeyInfo) pemObj;
                JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
                privKey = converter.getPrivateKey(pki);
            } else {
                log.debug("Got something, that we don't (yet) recognize. Class: "
                        + pemObj.getClass().getSimpleName());
            }
            pemObj = pemParser.readObject();
        }

        pemParser.close();
        if (privKey == null || privCert == null) {
            log.info("No material to create private key entry found!");
        } else {
            privCert.checkValidity();
            privateKeyEntry = new PrivateKeyEntry(privKey, new Certificate[] { privCert });
            keyStore.setEntry(SecurityModuleConstants.privateKeyAlias, privateKeyEntry,
                    new KeyStore.PasswordProtection(defaultPassword.toCharArray()));
        }
    }

    /**
     * Load the appropriate certificates from PermissionSet into trust/keystore 
     * @throws CertificateException if certificate cannot be created from the data
     * @throws KeyStoreException if certificate cannot be put into the keyStore
     */
    private void loadInfrastructureCertificates(PermissionSet permissions)
            throws CertificateException, KeyStoreException {
        ByteArrayInputStream bs;
        if (permissions == null) {
            log.info("The provided PermissionSet is empty. Continuing without permissions!");
            return;
        }
        for (Permission permission : permissions.getPermission()) {
            if (permission.getInfrastructurePermission().contains(InfrastructurePermission.MESSAGE_BUS_SERVER)) {
                try {
                    bs = new ByteArrayInputStream(permission.getCertificate().getCertificateData());
                    X509Certificate certificate = (X509Certificate) CertificateFactory
                            .getInstance(SecurityModuleConstants.CertificateType).generateCertificate(bs);
                    keyStore.setEntry(getNewAlias(), new KeyStore.TrustedCertificateEntry(certificate),
                            SecurityModuleConstants.nullProtectionParameter);

                    bs.close();
                } catch (IOException e) {

                }
            }
            if (permission.getInfrastructurePermission().contains(InfrastructurePermission.FILE_EXCHANGE_SERVER)) {
                try {
                    bs = new ByteArrayInputStream(permission.getCertificate().getCertificateData());
                    X509Certificate certificate = (X509Certificate) CertificateFactory
                            .getInstance(SecurityModuleConstants.CertificateType).generateCertificate(bs);
                    keyStore.setEntry(getNewAlias(), new KeyStore.TrustedCertificateEntry(certificate),
                            SecurityModuleConstants.nullProtectionParameter);
                    bs.close();
                } catch (IOException e) {
                    log.debug("Failed closing ByteArrayInputStream", e);
                }
            }
        }
    }

    /**
     * Sets up the Default SSL context  
     * @throws NoSuchAlgorithmException
     * @throws KeyStoreException
     * @throws UnrecoverableKeyException
     * @throws KeyManagementException
     */
    private void setupDefaultSSLContext()
            throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException {
        TrustManagerFactory tmf;
        KeyManagerFactory kmf;
        SSLContext context;
        tmf = TrustManagerFactory.getInstance(SecurityModuleConstants.keyTrustStoreAlgorithm);
        tmf.init(keyStore);
        kmf = KeyManagerFactory.getInstance(SecurityModuleConstants.keyTrustStoreAlgorithm);
        kmf.init(keyStore, defaultPassword.toCharArray());
        context = SSLContext.getInstance(SecurityModuleConstants.defaultSSLProtocol);
        context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), SecurityModuleConstants.defaultRandom);
        SSLContext.setDefault(context);
    }
}