mitm.common.security.certstore.dao.X509CertStoreDAOHibernate.java Source code

Java tutorial

Introduction

Here is the source code for mitm.common.security.certstore.dao.X509CertStoreDAOHibernate.java

Source

/*
 * Copyright (c) 2008-2011, Martijn Brinkers, Djigzo.
 * 
 * This file is part of Djigzo email encryption.
 *
 * Djigzo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License 
 * version 3, 19 November 2007 as published by the Free Software 
 * Foundation.
 *
 * Djigzo 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public 
 * License along with Djigzo. If not, see <http://www.gnu.org/licenses/>
 *
 * Additional permission under GNU AGPL version 3 section 7
 * 
 * If you modify this Program, or any covered work, by linking or 
 * combining it with aspectjrt.jar, aspectjweaver.jar, tyrex-1.0.3.jar, 
 * freemarker.jar, dom4j.jar, mx4j-jmx.jar, mx4j-tools.jar, 
 * spice-classman-1.0.jar, spice-loggerstore-0.5.jar, spice-salt-0.8.jar, 
 * spice-xmlpolicy-1.0.jar, saaj-api-1.3.jar, saaj-impl-1.3.jar, 
 * wsdl4j-1.6.1.jar (or modified versions of these libraries), 
 * containing parts covered by the terms of Eclipse Public License, 
 * tyrex license, freemarker license, dom4j license, mx4j license,
 * Spice Software License, Common Development and Distribution License
 * (CDDL), Common Public License (CPL) the licensors of this Program grant 
 * you additional permission to convey the resulting work.
 */
package mitm.common.security.certstore.dao;

import java.io.IOException;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertSelector;
import java.security.cert.CertStoreException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import javax.security.auth.x500.X500Principal;

import mitm.common.hibernate.DatabaseCriterion;
import mitm.common.hibernate.GenericHibernateDAO;
import mitm.common.hibernate.HibernateDatabaseCriterion;
import mitm.common.hibernate.SessionAdapter;
import mitm.common.hibernate.DatabaseCriterion.Restriction;
import mitm.common.locale.DefaultLocale;
import mitm.common.security.NoSuchProviderRuntimeException;
import mitm.common.security.certificate.X500PrincipalInspector;
import mitm.common.security.certificate.X509CertSelectorInspector;
import mitm.common.security.certificate.X509CertificateInspector;
import mitm.common.security.certstore.Expired;
import mitm.common.security.certstore.Match;
import mitm.common.security.certstore.MissingKeyAlias;
import mitm.common.security.certstore.hibernate.X509CertStoreEntryHibernate;
import mitm.common.util.BigIntegerUtils;
import mitm.common.util.CloseableIterator;
import mitm.common.util.CloseableIteratorException;
import mitm.common.util.HexUtils;

import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Property;
import org.hibernate.criterion.Restrictions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class X509CertStoreDAOHibernate extends GenericHibernateDAO<X509CertStoreEntryHibernate, Long>
        implements X509CertStoreDAO {
    private final static Logger logger = LoggerFactory.getLogger(X509CertStoreDAOHibernate.class);

    /*
     * The table columns 
     */
    private enum Field {
        ISSUER, ISSUER_FRIENDLY, SERIAL_NUMBER, SUBJECT, SUBJECT_FRIENDLY, SUBJECT_KEY_ID, KEY_ALIAS, THUMBPRINT, NOT_BEFORE, NOT_AFTER, CREATION_DATE, STORE_NAME
    };

    private final String storeName;
    private final String entityName;

    public X509CertStoreDAOHibernate(SessionAdapter session, String storeName, String entityName) {
        super(session);

        this.storeName = storeName;
        this.entityName = entityName;
    }

    public X509CertStoreDAOHibernate(SessionAdapter session, String storeName) {
        this(session, storeName, X509CertStoreEntryHibernate.ENTITY_NAME);
    }

    private Query createByEmailQuery(String baseQuery, String email, Match match, Expired expired,
            MissingKeyAlias missingKeyAlias, Date date) {
        if (email != null) {
            baseQuery = baseQuery + " join c.email e where e";
            baseQuery = baseQuery + (match == Match.EXACT ? " = :email" : " like :email");
        } else {
            /* if email is null we want to get all the entries without an email address */
            baseQuery = baseQuery + " left join c.email e where e is null";
        }

        if (expired == Expired.NOT_ALLOWED) {
            baseQuery = baseQuery + " and :time between notBefore and notAfter";
        }

        if (missingKeyAlias == MissingKeyAlias.NOT_ALLOWED) {
            baseQuery = baseQuery + " and keyAlias is not null";
        }

        baseQuery = baseQuery + " and storeName = :storeName";

        Query query = createQuery(baseQuery);

        if (email != null) {
            email = email.toLowerCase().trim();
            query.setString("email", email);
        }

        if (expired == Expired.NOT_ALLOWED) {
            query.setTimestamp("time", date);
        }

        query.setString(getColumnName(Field.STORE_NAME), storeName);

        return query;
    }

    @Override
    public CloseableIterator<X509CertStoreEntryHibernate> getByEmail(String email, Match match, Expired expired,
            MissingKeyAlias missingKeyAlias, Date date, Integer firstResult, Integer maxResults) {
        /*
         * we need to get distinct results using the distinct keyword. This is not supported by Criteria
         * AFAIK so we will need to use HQL
         */
        String baseQuery = "select distinct c from " + entityName + " c";

        Query query = createByEmailQuery(baseQuery, email, match, expired, missingKeyAlias, date);

        if (firstResult != null) {
            query.setFirstResult(firstResult);
        }

        if (maxResults != null) {
            query.setMaxResults(maxResults);
        }

        ScrollableResults results = query.scroll(ScrollMode.FORWARD_ONLY);

        return new X509CertStoreEntryIterator(results);
    }

    /**
     * returns the number of records that getByEmail would return
     */
    @Override
    public long getByEmailCount(String email, Match match, Expired expired, MissingKeyAlias missingKeyAlias,
            Date date) {
        /*
         * we need to get distinct results using the distinct keyword. This is not supported by Criteria
         * AFAIK so we will need to use HQL
         */
        String baseQuery = "select count (distinct c) from " + entityName + " c";

        Query query = createByEmailQuery(baseQuery, email, match, expired, missingKeyAlias, date);

        return (Long) query.uniqueResult();
    }

    /**
     * Searches the subject friendly name using ILIKE.
     */
    @Override
    public CloseableIterator<X509CertStoreEntryHibernate> searchBySubject(String subject, Expired expired,
            MissingKeyAlias missingKeyAlias, Date date, Integer firstResult, Integer maxResults) {
        return searchField(Field.SUBJECT_FRIENDLY, subject, expired, missingKeyAlias, date, firstResult,
                maxResults);
    }

    @Override
    public int getSearchBySubjectCount(String subject, Expired expired, MissingKeyAlias missingKeyAlias,
            Date date) {
        return getSearchFieldCount(Field.SUBJECT_FRIENDLY, subject, expired, missingKeyAlias, date);
    }

    /**
     * Searches the issuer friendly name using ILIKE.
     */
    @Override
    public CloseableIterator<X509CertStoreEntryHibernate> searchByIssuer(String issuer, Expired expired,
            MissingKeyAlias missingKeyAlias, Date date, Integer firstResult, Integer maxResults) {
        return searchField(Field.ISSUER_FRIENDLY, issuer, expired, missingKeyAlias, date, firstResult, maxResults);
    }

    @Override
    public int getSearchByIssuerCount(String issuer, Expired expired, MissingKeyAlias missingKeyAlias, Date date) {
        return getSearchFieldCount(Field.ISSUER_FRIENDLY, issuer, expired, missingKeyAlias, date);
    }

    /**
     * Helper function that searches the field using ILIKE.
     */
    private CloseableIterator<X509CertStoreEntryHibernate> searchField(Field field, String search, Expired expired,
            MissingKeyAlias missingKeyAlias, Date date, Integer firstResult, Integer maxResults) {
        Collection<DatabaseCriterion> criterion = new LinkedList<DatabaseCriterion>();

        criterion.add(createCriterion(field, search, Restriction.ILIKE));

        if (expired == Expired.NOT_ALLOWED) {
            criterion.add(createCriterion(Field.NOT_BEFORE, date, Restriction.LT));
            criterion.add(createCriterion(Field.NOT_AFTER, date, Restriction.GT));
        }

        if (missingKeyAlias == MissingKeyAlias.NOT_ALLOWED) {
            criterion.add(createCriterion(Field.KEY_ALIAS, Restriction.NOT_NULL));
        }

        ScrollableResults results = findByCriterionScrollable(criterion, firstResult, maxResults);

        return new X509CertStoreEntryIterator(results);
    }

    /**
     * Helper function that returns the number of records that searchField would return.
     */
    private int getSearchFieldCount(Field field, String search, Expired expired, MissingKeyAlias missingKeyAlias,
            Date date) {
        Criteria criteria = createCriteria(entityName);

        criteria.setProjection(Projections.rowCount());

        criteria.add(Restrictions.eq(getColumnName(Field.STORE_NAME), storeName));

        criteria.add(Restrictions.ilike(getColumnName(field), search));

        if (expired == Expired.NOT_ALLOWED) {
            criteria.add(Restrictions.lt(getColumnName(Field.NOT_BEFORE), date));
            criteria.add(Restrictions.gt(getColumnName(Field.NOT_AFTER), date));
        }

        if (missingKeyAlias == MissingKeyAlias.NOT_ALLOWED) {
            criteria.add(Restrictions.isNotNull(getColumnName(Field.KEY_ALIAS)));
        }

        return (Integer) criteria.uniqueResult();
    }

    @Override
    public X509CertStoreEntryHibernate getByThumbprint(String thumbprint) {
        if (thumbprint != null) {
            thumbprint = thumbprint.toUpperCase(DefaultLocale.getDefaultLocale());
        }

        return findByCriterionUniqueResult(createCriterion(Field.THUMBPRINT, thumbprint, Restriction.EQ));
    }

    @Override
    public X509CertStoreEntryHibernate getByCertificate(X509Certificate certificate) throws CertStoreException {
        String thumbprint;

        try {
            thumbprint = (certificate != null ? X509CertificateInspector.getThumbprint(certificate) : null);
        } catch (CertificateEncodingException e) {
            throw new CertStoreException(e);
        } catch (NoSuchAlgorithmException e) {
            throw new CertStoreException(e);
        } catch (NoSuchProviderException e) {
            throw new NoSuchProviderRuntimeException(e);
        }

        X509CertStoreEntryHibernate entry = getByThumbprint(thumbprint);

        return entry;
    }

    @Override
    public X509CertStoreEntryHibernate getLatest() {
        DetachedCriteria maxDateCriteria = createDetachedCriteria(entityName);

        maxDateCriteria.setProjection(Projections.max(getColumnName(Field.CREATION_DATE)));

        maxDateCriteria.add(Restrictions.eq(getColumnName(Field.STORE_NAME), storeName));

        Criteria criteria = createCriteria(entityName);

        criteria.add(Property.forName(getColumnName(Field.CREATION_DATE)).eq(maxDateCriteria));

        criteria.add(Restrictions.eq(getColumnName(Field.STORE_NAME), storeName));

        return (X509CertStoreEntryHibernate) criteria.uniqueResult();
    }

    @Override
    public int getRowCount() {
        Criteria criteria = createCriteria(entityName);

        criteria.setProjection(Projections.rowCount());

        criteria.add(Restrictions.eq(getColumnName(Field.STORE_NAME), storeName));

        return (Integer) criteria.uniqueResult();
    }

    @Override
    public int getRowCount(Expired expired, MissingKeyAlias missingKeyAlias, Date date) {
        Criteria criteria = createCriteria(entityName);

        criteria.setProjection(Projections.rowCount());

        criteria.add(Restrictions.eq(getColumnName(Field.STORE_NAME), storeName));

        if (expired == Expired.NOT_ALLOWED) {
            criteria.add(Restrictions.lt(getColumnName(Field.NOT_BEFORE), date));
            criteria.add(Restrictions.gt(getColumnName(Field.NOT_AFTER), date));
        }

        if (missingKeyAlias == MissingKeyAlias.NOT_ALLOWED) {
            criteria.add(Restrictions.isNotNull(getColumnName(Field.KEY_ALIAS)));
        }

        return (Integer) criteria.uniqueResult();
    }

    @Override
    public X509CertStoreEntryHibernate addCertificate(X509Certificate certificate, String keyAlias)
            throws CertStoreException {
        Date creationDate = new Date();

        X509CertStoreEntryHibernate entry;
        try {
            entry = new X509CertStoreEntryHibernate(certificate, keyAlias, storeName, creationDate);
        } catch (CertificateParsingException e) {
            throw new CertStoreException(e);
        } catch (IOException e) {
            throw new CertStoreException(e);
        }

        makePersistent(entry);

        return entry;
    }

    @Override
    public void removeCertificate(X509Certificate certificate) throws CertStoreException {
        X509CertStoreEntryHibernate entry = getByCertificate(certificate);

        if (entry != null) {
            this.delete(entry);
        }
    }

    @Override
    public void removeAllEntries() throws CertStoreException {
        /*
         * I tried using HQL and delete but that did not work because of a constraint violation.
         * Using delete seems not to cascade to the certificate_email table.
         */
        CloseableIterator<X509CertStoreEntryHibernate> iterator = getCertStoreIterator(null,
                MissingKeyAlias.ALLOWED, null, null);

        try {
            try {
                while (iterator.hasNext()) {
                    X509CertStoreEntryHibernate entry = iterator.next();

                    /* evict the entry to save memory */
                    this.evict(entry);
                    this.delete(entry);
                }
            } finally {
                /* need to close the iterator */
                iterator.close();
            }
        } catch (CloseableIteratorException e) {
            throw new CertStoreException(e);
        }
    }

    @Override
    public Collection<X509Certificate> getCertificates(CertSelector certSelector, MissingKeyAlias missingKeyAlias,
            Integer firstResult, Integer maxResults) throws CertStoreException {
        Collection<X509Certificate> foundCertificates = new LinkedList<X509Certificate>();

        CloseableIterator<X509Certificate> iterator = getCertificateIterator(certSelector, missingKeyAlias,
                firstResult, maxResults);

        try {
            try {
                while (iterator.hasNext()) {
                    X509Certificate certificate = iterator.next();

                    if (certificate != null && (certSelector == null || certSelector.match(certificate))) {
                        foundCertificates.add(certificate);
                    }
                }
            } finally {
                /* need to close the iterator */
                iterator.close();
            }
        } catch (CloseableIteratorException e) {
            throw new CertStoreException(e);
        }

        return foundCertificates;
    }

    @Override
    public CloseableIterator<X509Certificate> getCertificateIterator(CertSelector certSelector,
            MissingKeyAlias missingKeyAlias, Integer firstResult, Integer maxResults) throws CertStoreException {
        ScrollableResults results = getPreFilteredEntriesScrollable(certSelector, missingKeyAlias, firstResult,
                maxResults);

        return new X509CertStoreCertificateIterator(results, certSelector);
    }

    @Override
    public CloseableIterator<X509CertStoreEntryHibernate> getCertStoreIterator(CertSelector certSelector,
            MissingKeyAlias missingKeyAlias, Integer firstResult, Integer maxResults) throws CertStoreException {
        ScrollableResults results = getPreFilteredEntriesScrollable(certSelector, missingKeyAlias, firstResult,
                maxResults);

        return new X509CertStoreEntryIterator(results, certSelector);
    }

    private static String getColumnName(Field field) {
        switch (field) {
        case ISSUER:
            return "certificate.issuer";
        case ISSUER_FRIENDLY:
            return "certificate.issuerFriendly";
        case SERIAL_NUMBER:
            return "certificate.serial";
        case SUBJECT:
            return "certificate.subject";
        case SUBJECT_FRIENDLY:
            return "certificate.subjectFriendly";
        case SUBJECT_KEY_ID:
            return "certificate.subjectKeyIdentifier";
        case THUMBPRINT:
            return "certificate.thumbprint";
        case NOT_BEFORE:
            return "certificate.notBefore";
        case NOT_AFTER:
            return "certificate.notAfter";
        case KEY_ALIAS:
            return "keyAlias";
        case CREATION_DATE:
            return "creationDate";
        case STORE_NAME:
            return "storeName";
        default:
            throw new IllegalArgumentException("Unknown field: " + field);
        }
    }

    private DatabaseCriterion createCriterion(Field field, Object value, Restriction restriction) {
        return new HibernateDatabaseCriterion(getColumnName(field), value, restriction);
    }

    private DatabaseCriterion createCriterion(Field field, Restriction restriction) {
        return createCriterion(field, null, restriction);
    }

    @SuppressWarnings("unchecked")
    private List<X509CertStoreEntryHibernate> findByCriterion(Collection<DatabaseCriterion> criterions) {
        Criteria criteria = createCriteria(criterions);

        return criteria.list();
    }

    private ScrollableResults findByCriterionScrollable(Collection<DatabaseCriterion> criterions,
            Integer firstResult, Integer maxResults) {
        Criteria criteria = createCriteria(criterions);

        if (firstResult != null) {
            criteria.setFirstResult(firstResult);
        }

        if (maxResults != null) {
            criteria.setMaxResults(maxResults);
        }

        /*
         * Sort on creation date
         */
        criteria.addOrder(Order.asc(getColumnName(Field.CREATION_DATE)));

        return criteria.scroll(ScrollMode.FORWARD_ONLY);
    }

    private X509CertStoreEntryHibernate findByCriterionUniqueResult(Collection<DatabaseCriterion> criterions) {
        List<X509CertStoreEntryHibernate> entries = findByCriterion(criterions);

        X509CertStoreEntryHibernate entry = null;

        if (entries != null && entries.size() > 0) {
            entry = entries.get(0);
        }

        return entry;
    }

    private X509CertStoreEntryHibernate findByCriterionUniqueResult(DatabaseCriterion... criterions) {
        return findByCriterionUniqueResult(Arrays.asList(criterions));
    }

    private Criteria createCriteria(Collection<DatabaseCriterion> criterions) {
        Criteria criteria = createCriteria(entityName);

        for (DatabaseCriterion criterion : criterions) {
            if (!(criterion instanceof HibernateDatabaseCriterion)) {
                throw new IllegalArgumentException("criterion must be a HibernateDatabaseCriterion.");
            }

            criteria.add(((HibernateDatabaseCriterion) criterion).getCriterion());
        }

        /* make sure we only get results for just one store */
        criteria.add(Restrictions.eq(getColumnName(Field.STORE_NAME), storeName));

        return criteria;
    }

    /*
     * Creates a collection of DatabaseCriterions used for pre-filtering based on the
     * CertSelector. This speeds up the search process because the database will 
     * filter all relevant entries using database queries.
     */
    private Collection<DatabaseCriterion> createCriterion(X509CertSelector certSelector)
            throws CertStoreException, IOException {
        X500Principal issuer = certSelector.getIssuer();
        X500Principal subject = certSelector.getSubject();
        BigInteger serialNumber = certSelector.getSerialNumber();

        /* CertSelector store the subjectKeyIdentifier DER encoded */
        byte[] subjectKeyIdentifier = X509CertSelectorInspector.getSubjectKeyIdentifier(certSelector);

        /*
         * if a certificate is specified we need to get the properties from the
         * certificate
         */
        if (certSelector.getCertificate() != null) {
            X509Certificate certificate = certSelector.getCertificate();

            if (issuer == null) {
                issuer = certificate.getIssuerX500Principal();
            }

            if (subject == null) {
                subject = certificate.getSubjectX500Principal();
            }

            if (serialNumber == null) {
                serialNumber = certificate.getSerialNumber();
            }

            if (subjectKeyIdentifier == null) {
                subjectKeyIdentifier = X509CertificateInspector.getSubjectKeyIdentifier(certificate);
            }
        }

        Collection<DatabaseCriterion> criterion = new LinkedList<DatabaseCriterion>();

        if (issuer != null) {
            criterion.add(
                    createCriterion(Field.ISSUER, X500PrincipalInspector.getCanonical(issuer), Restriction.EQ));
        }

        if (subject != null) {
            criterion.add(
                    createCriterion(Field.SUBJECT, X500PrincipalInspector.getCanonical(subject), Restriction.EQ));
        }

        if (serialNumber != null) {
            criterion.add(
                    createCriterion(Field.SERIAL_NUMBER, BigIntegerUtils.hexEncode(serialNumber), Restriction.EQ));
        }

        if (subjectKeyIdentifier != null) {
            criterion.add(createCriterion(Field.SUBJECT_KEY_ID, HexUtils.hexEncode(subjectKeyIdentifier),
                    Restriction.EQ));
        }

        if (certSelector.getCertificateValid() != null) {
            criterion.add(createCriterion(Field.NOT_BEFORE, certSelector.getCertificateValid(), Restriction.LT));
            criterion.add(createCriterion(Field.NOT_AFTER, certSelector.getCertificateValid(), Restriction.GT));
        }

        return criterion;
    }

    /*
     * Return a pre filtered collection of X509CertStoreEntry object. The filtering is done using
     * database queries.
     */
    private ScrollableResults getPreFilteredEntriesScrollable(CertSelector certSelector,
            MissingKeyAlias missingKeyAlias, Integer firstResult, Integer maxResults) throws CertStoreException {
        Collection<DatabaseCriterion> criterion = new LinkedList<DatabaseCriterion>();

        /*
         * If the selector is a X509CertSelector we can do some pre-filtering to
         * speed the search process
         */
        if (certSelector instanceof X509CertSelector) {
            try {
                criterion = createCriterion((X509CertSelector) certSelector);
            } catch (IOException e) {
                throw new CertStoreException(e);
            }
        }

        if (missingKeyAlias == MissingKeyAlias.NOT_ALLOWED) {
            criterion.add(createCriterion(Field.KEY_ALIAS, Restriction.NOT_NULL));
        }

        if (criterion.size() == 0) {
            logger.debug("No optimization possible so step through all certificates.");
        }

        return findByCriterionScrollable(criterion, firstResult, maxResults);
    }
}