package org.jivesoftware.sparkimpl.certificates; import java.awt.HeadlessException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Enumeration; import java.util.LinkedList; import java.util.List; import javax.naming.InvalidNameException; import javax.swing.JOptionPane; import javax.swing.JTable; import javax.swing.SwingUtilities; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableColumnModel; import org.jivesoftware.Spark; import org.jivesoftware.resource.Res; import org.jivesoftware.spark.ui.login.CertificateDialog; import org.jivesoftware.spark.ui.login.CertificatesManagerSettingsPanel; import org.jivesoftware.spark.util.log.Log; import org.jivesoftware.sparkimpl.settings.local.LocalPreferences; /** * This class serve to extract certificates, storage them during runtime and format them and support management of them. * Together with CertificateManagerSettingsPanel and CertificateModel Classes this apply MVC pattern. * * @author Paweł Ścibiorski * */ public class CertificateController extends CertManager { /** * There are 7 KeyStores: * TRUSTED contain user's trusted certificates * EXCEPTIONS contain user's certificates that are added to exceptions (their's validity isn't checked) * CACERTS contain only JRE default certificates, data is only read from it, never saved to this file * BLACKLIST used for revoked certificates, part of super class CertManager * DISTRUSTED_CACERTS when user remove JRE certificate then really copy of this is created in this KeyStore * CACERTS_EXCEPTIONS used for JRE certificates that are added to exceptions (their's validity isn;t checked) * DISPLAYED_CACERTS isn't used de facto as file as it is never saved but this object helps in keystore management. * It contain CACERTS - (DISTRUSTED_CACERTS + CACERTSEXCEPTIONS) * */ public final static File TRUSTED = new File(Spark.getSparkUserHome() + File.separator + "security" + File.separator + "truststore"); public final static File EXCEPTIONS = new File(Spark.getSparkUserHome() + File.separator + "security" + File.separator + "exceptions"); public final static File DISTRUSTED_CACERTS = new File(Spark.getSparkUserHome() + File.separator + "security" + File.separator + "distrusted_cacerts"); public final static File CACERTS_EXCEPTIONS = new File(Spark.getSparkUserHome() + File.separator + "security" + File.separator + "cacerts_exceptions"); public final static File DISPLAYED_CACERTS = new File(Spark.getSparkUserHome() + File.separator + "security" + File.separator + "displayed_cacerts"); //CACERTS should be used only for read public final static File CACERTS = new File(System.getProperty("java.home") + File.separator + "lib" + File.separator + "security" + File.separator + "cacerts"); private KeyStore trustStore, exceptionsStore, displayCaStore, distrustedCaStore, exceptionsCaStore; private List<CertificateModel> trustedCertificates = new LinkedList<>(); // contain certificates which aren't revoked or exempted private List<CertificateModel> exemptedCertificates = new LinkedList<>(); // contain only certificates from exempted list private List<CertificateModel> exemptedCacerts = new LinkedList<>(); // contain only exempted cacerts certificates private List<CertificateModel> displayCaCertificates = new LinkedList<>(); // contain cacerts displayed certificates that aren't exempted private static final String[] COLUMN_NAMES = { Res.getString("table.column.certificate.subject"), Res.getString("table.column.certificate.validity"), Res.getString("table.column.certificate.exempted") }; private static final int NUMBER_OF_COLUMNS = COLUMN_NAMES.length; public CertificateController(LocalPreferences localPreferences) { if (localPreferences == null) { throw new IllegalArgumentException("localPreferences cannot be null"); } this.localPreferences = localPreferences; } /** * Load KeyStores files. */ @Override public void loadKeyStores() { blackListStore = openKeyStore(BLACKLIST); trustStore = openKeyStore(TRUSTED); exceptionsStore = openKeyStore(EXCEPTIONS); distrustedCaStore = openKeyStore(DISTRUSTED_CACERTS); exceptionsCaStore = openKeyStore(CACERTS_EXCEPTIONS); displayCaStore = openCacertsKeyStore(); trustedCertificates = fillTableListWithKeyStoreContent(trustStore, trustedCertificates); exemptedCertificates = fillTableListWithKeyStoreContent(exceptionsStore, exemptedCertificates); displayCaCertificates = fillTableListWithKeyStoreContent(displayCaStore, displayCaCertificates); exemptedCacerts = fillTableListWithKeyStoreContent(exceptionsCaStore, exemptedCacerts); } public KeyStore openCacertsKeyStore() { KeyStore caStore = openKeyStore(CACERTS); KeyStore distrustedCaStore = openKeyStore(DISTRUSTED_CACERTS); KeyStore exceptionsCaStore = openKeyStore(CACERTS_EXCEPTIONS); KeyStore displayCerts = null; // displayCerts keyStore is meant to contain certificates that are in cacerts and aren't distrusted try { displayCerts = KeyStore.getInstance("JKS"); displayCerts.load(null, passwd); if (caStore != null) { Enumeration<String> store; store = caStore.aliases(); while (store.hasMoreElements()) { String alias = (String) store.nextElement(); X509Certificate certificate = (X509Certificate) caStore.getCertificate(alias); // if getCertificateAlias return null then entry doesn't exist in distrustedCaStore (Java's default). if (distrustedCaStore.getCertificateAlias(certificate) == null) { displayCerts.setCertificateEntry(alias, certificate); } } } } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) { Log.error("Cannot read KeyStore", e); } return displayCerts; } @Override public void overWriteKeyStores() { saveKeyStore(trustStore, TRUSTED); saveKeyStore(exceptionsStore, EXCEPTIONS); saveKeyStore(blackListStore, BLACKLIST); saveKeyStore(distrustedCaStore, DISTRUSTED_CACERTS); saveKeyStore(exceptionsCaStore, CACERTS_EXCEPTIONS); } @Override public void createTableModel(){ tableModel = new DefaultTableModel() { // return adequate classes for columns so last column is Boolean // displayed as checkbox @Override public Class<?> getColumnClass(int column) { switch (column) { case 0: return String.class; case 1: return String.class; case 2: return Boolean.class; default: throw new RuntimeException("Cannot assign classes for columns"); } } @Override public boolean isCellEditable(int row, int column) { return column !=2 ? false:true; } }; tableModel.setColumnIdentifiers(COLUMN_NAMES); Object[] certEntry = new Object[NUMBER_OF_COLUMNS]; addRowsToTableModel(trustedCertificates, certEntry); addRowsToTableModel(displayCaCertificates, certEntry); } /** * Adds list to the certificate table so it is displayed in the table. * * @param certList is list with CertificateModel object that are added to the * @param certEntry serves as table row model. Each element of that array is corresponding to the column in table */ private void addRowsToTableModel(List<CertificateModel> certList, Object[] certEntry){ if (certList != null) { // put certificate from arrayList into rows with chosen columns for (CertificateModel cert : certList) { tableModel.addRow(fillTableWithList(certEntry, cert)); } } } /** * Create certificate entry, which can be added in row of the certificate table. * * @param certEntry serves as table row model. Each element of that array is corresponding to the column in table * @param cert is CertificateModel for which this class will return object representing table's row * @return certificate entry which is array of objects which values depends on this method. Elements are: [0] String [1] String [2] boolean */ private Object[] fillTableWithList(Object[] certEntry, CertificateModel cert) { if (cert.getSubjectCommonName() != null) { certEntry[0] = cert.getSubjectCommonName(); } else { certEntry[0] = cert.getSubject(); } certEntry[1] = cert.getValidityStatus(); certEntry[2] = isOnExceptionList(cert); return certEntry; } /** * Adds certificate with given entry to exceptions KeyStore * @param alias of the certificate, assuming that certificate is in TrustStore */ public void addCertToExceptions(String alias) { try { X509Certificate cert = (X509Certificate) trustStore.getCertificate(alias); exceptionsStore.setCertificateEntry(alias, cert); } catch (KeyStoreException ex) { Log.error("Error at moving certificate from trusted list to the exceptions list", ex); } } /** * Removes certificate with the given alias from the exceptions list * @param alias of the certificate */ private void removeCertFromExceptions(String alias) { try { X509Certificate cert = (X509Certificate) trustStore.getCertificate(alias); exceptionsStore.deleteEntry(alias); } catch (KeyStoreException ex) { Log.error("Error at moving certificate from exceptions list to trusted list", ex); } } /** * Adds CA certificate with given entry to CA exceptions KeyStore * @param alias of the certificate */ private void addCaCertToExceptions(String alias) { try { X509Certificate cert = (X509Certificate) displayCaStore.getCertificate(alias); exceptionsCaStore.setCertificateEntry(alias, cert); } catch (KeyStoreException ex) { Log.error("Error at moving certificate from trusted list to the exception list", ex); } } /** * Removes CA certificate with the given alias from the exceptions list * @param alias of the certificate */ private void removeCaCertFromExceptions(String alias) { try { X509Certificate cert = (X509Certificate) displayCaStore.getCertificate(alias); exceptionsCaStore.deleteEntry(alias); } catch (KeyStoreException ex) { Log.error("Error at moving certificate from exceptions list to trusted list", ex); } } /** * If argument is true then move certificate to the exceptions Keystore, if false then move to the trusted Keystore. * Useful for checkboxes where it's selected value indicates where certificate should be moved. * @param checked should it be moved? */ @Override public void addOrRemoveFromExceptionList(boolean checked) { String alias = allCertificates.get(getTranslatedRow()).getAlias(); if (getAliasKeyStorePath(alias).equals(TRUSTED)) { if(checked) { addCertToExceptions(alias); } else { removeCertFromExceptions(alias); } }else if (getAliasKeyStorePath(alias).equals(DISPLAYED_CACERTS)) { if(checked) { addCaCertToExceptions(alias); } else { removeCaCertFromExceptions(alias); } } } public boolean isInTrustStore(CertificateModel cert) { try { if (trustStore.getCertificateAlias(cert.getCertificate()) != null) { return true; } else if (displayCaStore.getCertificateAlias(cert.getCertificate()) != null) { return true; } else { return false; } } catch (KeyStoreException e) { return false; } } /** * Return information if certificate is on exception list. * * @param Certificate * Model entry */ @Override public boolean isOnExceptionList(CertificateModel cert) { try { if (exceptionsStore.getCertificateAlias(cert.getCertificate()) != null) { return true; } else if (exceptionsCaStore.getCertificateAlias(cert.getCertificate()) != null) { return true; } else { return false; } } catch (KeyStoreException e) { return false; } } /** * Return information if certificate is on blacklist (revoked). * * @param Certificate Model entry */ public boolean isOnBlackList(CertificateModel cert) { return blackListedCertificates.contains(cert); } /** * Return file path which contains certificate with given alias; * * @param alias of the certificate * @return File path of KeyStore with certificate */ private KeyStore getAliasKeyStore(String alias) { try { if (exceptionsStore.containsAlias(alias)) { return exceptionsStore; } if (blackListStore.containsAlias(alias)) { return blackListStore; } if (trustStore.containsAlias(alias)) { return trustStore; } if (exceptionsCaStore.containsAlias(alias)) { return exceptionsCaStore; } if (displayCaStore.containsAlias(alias)) { return displayCaStore; } } catch (KeyStoreException e) { Log.error(e); return null; } return null; } /** * Return file path which contains certificate with given alias; * * @param alias of the certificate * @return File path of KeyStore with certificate */ private File getAliasKeyStorePath(String alias) { try { if (blackListStore.containsAlias(alias)) { return BLACKLIST; } if (trustStore.containsAlias(alias)) { return TRUSTED; } if (exceptionsStore.containsAlias(alias)) { return EXCEPTIONS; } if (displayCaStore.containsAlias(alias)) { return DISPLAYED_CACERTS; } if (exceptionsCaStore.containsAlias(alias)) { return CACERTS_EXCEPTIONS; } } catch (KeyStoreException e) { Log.error(e); return null; } return null; } /** * This method delete certificate with provided alias from the Truststore * * @param alias Alias of the certificate to delete * @throws KeyStoreException * @throws IOException * @throws NoSuchAlgorithmException * @throws CertificateException */ @Override public void deleteEntry(String alias) throws KeyStoreException { int dialogButton = JOptionPane.YES_NO_OPTION; int dialogValue = JOptionPane.showConfirmDialog(null, Res.getString("dialog.certificate.sure.to.delete"), null, dialogButton); if (dialogValue == JOptionPane.YES_OPTION) { KeyStore store = getAliasKeyStore(alias); if(store.equals(displayCaStore) || store.equals(exceptionsCaStore)){ // adds entry do distrusted store so it will be not displayed next time distrustedCaStore.setCertificateEntry(alias, store.getCertificate(alias)); } store.deleteEntry(alias); if(store.equals(trustStore) ) { removeCertFromExceptions(alias); } JOptionPane.showMessageDialog(null, Res.getString("dialog.certificate.has.been.deleted")); CertificateModel model = null; for (CertificateModel certModel : allCertificates) { if (certModel.getAlias().equals(alias)) { model = certModel; } } exemptedCertificates.remove(model); trustedCertificates.remove(model); blackListedCertificates.remove(model); displayCaCertificates.remove(model); exemptedCacerts.remove(model); allCertificates.remove(model); } refreshCertTable(); } /** * Refresh certificate table to make visible changes in it's model */ @Override public void refreshCertTable() { createTableModel(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { resizeColumnWidth(CertificatesManagerSettingsPanel.getCertTable()); CertificatesManagerSettingsPanel.getCertTable().setModel(tableModel); tableModel.fireTableDataChanged(); } }); } /** * Resizes certificate table to preferred width. */ public void resizeColumnWidth(JTable table) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { final TableColumnModel columnModel = table.getColumnModel(); final int maxWidth = table.getParent().getWidth(); columnModel.getColumn(1).setPreferredWidth(80); columnModel.getColumn(2).setPreferredWidth(60); columnModel.getColumn(0).setPreferredWidth(maxWidth - 140); } }); } public void addEntryToKeyStore(X509Certificate cert, boolean exempted) throws HeadlessException, InvalidNameException, KeyStoreException { if (cert == null){ throw new IllegalArgumentException("Cert cannot be null"); } addEntryToKeyStoreImpl(new CertificateModel(cert), exempted); } /** * This method add certificate entry to the TrustStore. * @param cert Certificate which is added. * @throws HeadlessException * @throws InvalidNameException * @throws KeyStoreException */ public void addEntryToKeyStore(X509Certificate cert, CertificateDialogReason reason) throws HeadlessException, InvalidNameException, KeyStoreException { if (cert == null){ throw new IllegalArgumentException("Cert cannot be null"); } if(reason != null) { addEntryToKeyStoreImpl(cert, reason); } else { addEntryToKeyStoreImpl(new CertificateModel(cert), false); } } /** * This method add certificate from file (*.cer), (*.crt), (*.der), (*.pem) to TrustStore. * * @param file File with certificate that is added * @throws KeyStoreException * @throws CertificateException * @throws NoSuchAlgorithmException * @throws IOException * @throws InvalidNameException * @throws HeadlessException */ @Override public void addEntryFileToKeyStore(File file) throws IOException, CertificateException, KeyStoreException, HeadlessException, InvalidNameException { if (file == null) { throw new IllegalArgumentException("File cannot be null"); } X509Certificate addedCert = certificateFromFile(file); addEntryToKeyStoreImpl(addedCert, CertificateDialogReason.ADD_CERTIFICATE); } //Opens new dialog which will ask to add certificates from chain public void addChain(X509Certificate[] chain){ try { InBandCertificateChainDialog chainDialog = new InBandCertificateChainDialog(chain, this); } catch (Exception e) { Log.error("Cannot open InBandCertificateChainDialog", e); } } public void addCertificateAsExempted(CertificateModel certModel) throws HeadlessException, InvalidNameException, KeyStoreException { addEntryToKeyStoreImpl(certModel, true); } /** * This method add certificate to KeyStore. If it is invalid, revoked or self-signed it will be added as exempted certificate. * It can be added as exempted also on purpose by setting on true exempted. * @param certModel CertificateModel * @param exempted if it is set on true then certificate will be also added to exempted certificates * @throws HeadlessException * @throws InvalidNameException * @throws KeyStoreException */ private void addEntryToKeyStoreImpl(CertificateModel certModel, boolean exempted) throws HeadlessException, InvalidNameException, KeyStoreException { String alias = useCommonNameAsAlias(certModel.getCertificate()); //if certificate is invalid in some way then it is added to exceptions, also it can be intentionally set as exempted if (!certModel.isValid() || checkRevocation(certModel.getCertificate()) || certModel.isSelfSigned() || exempted) { exceptionsStore.setCertificateEntry(alias, certModel.getCertificate()); exemptedCertificates.add(certModel); } trustStore.setCertificateEntry(alias, certModel.getCertificate()); trustedCertificates.add(certModel); if (tableModel != null) { refreshCertTable(); } } /** * This method takes certificate and add it to the TrustStore * @param addedCert certificate which is added * @param reason changes displayed text in certificate dialog * @throws HeadlessException * @throws InvalidNameException * @throws KeyStoreException */ private void addEntryToKeyStoreImpl(X509Certificate addedCert, CertificateDialogReason reason) throws HeadlessException, InvalidNameException, KeyStoreException{ CertificateModel certModel = new CertificateModel(addedCert); CertificateDialog certDialog = null; if (checkForSameCertificate(addedCert) == false) { certDialog = showCertificate(certModel, reason); } if (certDialog != null && certDialog.isAddCert()) { addEntryToKeyStoreImpl(certModel, false); JOptionPane.showMessageDialog(null, Res.getString("dialog.certificate.has.been.added")); } } /** * Takes file with certificate and return X509Representation of the certificate. * @param file with certificate * @return X509Certificate from file * @throws FileNotFoundException * @throws IOException * @throws CertificateException */ private X509Certificate certificateFromFile(File file) throws FileNotFoundException, IOException, CertificateException{ X509Certificate cert; try (InputStream inputStream = new FileInputStream(file)) { CertificateFactory cf = CertificateFactory.getInstance("X509"); cert = (X509Certificate) cf.generateCertificate(inputStream); } return cert; } /** * Check if there is certificate entry in Truststore with the same alias. * * @param alias Alias of the certificate which is looked for in the model list * @return True if KeyStore contain the same alias. * @throws HeadlessException * @throws KeyStoreException */ @Override protected boolean checkForSameAlias(String alias) throws HeadlessException, KeyStoreException { for(CertificateModel model: allCertificates){ if(model.getAlias().equals(alias)){ return true; } } return false; } /** * Open dialog with certificate. */ @Override public void showCertificate() { CertificateDialog certDialog = new CertificateDialog(localPreferences, allCertificates.get(getTranslatedRow()), this, CertificateDialogReason.SHOW_CERTIFICATE); } /** * Gets index of element in table model in selected row of table after sorting. * @return index of row */ private int getTranslatedRow() { int selectedRow = CertificatesManagerSettingsPanel.getCertTable().getSelectedRow(); return CertificatesManagerSettingsPanel.getCertTable().convertRowIndexToModel(selectedRow); } public List<CertificateModel> getAllCertificates() { return allCertificates; } public void setTableModel(DefaultTableModel tableModel) { this.tableModel = tableModel; } }