package com.fsck.k9.mail.ssl; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import org.apache.commons.io.IOUtils; import timber.log.Timber; public class LocalKeyStore { private static final int KEY_STORE_FILE_VERSION = 1; private static String sKeyStoreLocation; public static void setKeyStoreLocation(String directory) { sKeyStoreLocation = directory; } private static class LocalKeyStoreHolder { static final LocalKeyStore INSTANCE = new LocalKeyStore(); } public static LocalKeyStore getInstance() { return LocalKeyStoreHolder.INSTANCE; } private File mKeyStoreFile; private KeyStore mKeyStore; private LocalKeyStore() { try { upgradeKeyStoreFile(); setKeyStoreFile(null); } catch (CertificateException e) { /* * Can happen if setKeyStoreLocation(String directory) has not been * called before the first call to getInstance(). Not necessarily an * error, presuming setKeyStoreFile(File) is called next with a * non-null File. */ Timber.w("Local key store has not been initialized"); } } /** * Reinitialize the local key store with certificates contained in * {@code file} * * @param file * {@link File} containing locally saved certificates. May be 0 * length, in which case it is deleted and recreated. May be * {@code null}, in which case a default file location is used. * @throws CertificateException * Occurs if {@code file == null} and * {@code setKeyStoreLocation(directory)} was not called previously. */ public synchronized void setKeyStoreFile(File file) throws CertificateException { if (file == null) { file = new File(getKeyStoreFilePath(KEY_STORE_FILE_VERSION)); } if (file.length() == 0) { /* * The file may be empty (e.g., if it was created with * File.createTempFile). We can't pass an empty file to * Keystore.load. Instead, we let it be created anew. */ if (file.exists() && !file.delete()) { Timber.d("Failed to delete empty keystore file: %s", file.getAbsolutePath()); } } FileInputStream fis = null; try { fis = new FileInputStream(file); } catch (FileNotFoundException e) { // If the file doesn't exist, that's fine, too } try { KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType()); store.load(fis, "".toCharArray()); mKeyStore = store; mKeyStoreFile = file; } catch (Exception e) { Timber.e(e, "Failed to initialize local key store"); // Use of the local key store is effectively disabled. mKeyStore = null; mKeyStoreFile = null; } finally { IOUtils.closeQuietly(fis); } } public synchronized void addCertificate(String host, int port, X509Certificate certificate) throws CertificateException { if (mKeyStore == null) { throw new CertificateException( "Certificate not added because key store not initialized"); } try { mKeyStore.setCertificateEntry(getCertKey(host, port), certificate); } catch (KeyStoreException e) { throw new CertificateException( "Failed to add certificate to local key store", e); } writeCertificateFile(); } private void writeCertificateFile() throws CertificateException { java.io.OutputStream keyStoreStream = null; try { keyStoreStream = new java.io.FileOutputStream(mKeyStoreFile); mKeyStore.store(keyStoreStream, "".toCharArray()); } catch (FileNotFoundException e) { throw new CertificateException("Unable to write KeyStore: " + e.getMessage()); } catch (CertificateException e) { throw new CertificateException("Unable to write KeyStore: " + e.getMessage()); } catch (IOException e) { throw new CertificateException("Unable to write KeyStore: " + e.getMessage()); } catch (NoSuchAlgorithmException e) { throw new CertificateException("Unable to write KeyStore: " + e.getMessage()); } catch (KeyStoreException e) { throw new CertificateException("Unable to write KeyStore: " + e.getMessage()); } finally { IOUtils.closeQuietly(keyStoreStream); } } public synchronized boolean isValidCertificate(Certificate certificate, String host, int port) { if (mKeyStore == null) { return false; } Certificate storedCert = null; try { storedCert = mKeyStore.getCertificate(getCertKey(host, port)); return (storedCert != null && storedCert.equals(certificate)); } catch (KeyStoreException e) { return false; } } private static String getCertKey(String host, int port) { return host + ":" + port; } public synchronized void deleteCertificate(String oldHost, int oldPort) { if (mKeyStore == null) { return; } try { mKeyStore.deleteEntry(getCertKey(oldHost, oldPort)); writeCertificateFile(); } catch (KeyStoreException e) { // Ignore: most likely there was no cert. found } catch (CertificateException e) { Timber.e(e, "Error updating the local key store file"); } } private void upgradeKeyStoreFile() throws CertificateException { if (KEY_STORE_FILE_VERSION > 0) { // Blow away version "0" because certificate aliases have changed. File versionZeroFile = new File(getKeyStoreFilePath(0)); if (versionZeroFile.exists() && !versionZeroFile.delete()) { Timber.d("Failed to delete old key-store file: %s", versionZeroFile.getAbsolutePath()); } } } private String getKeyStoreFilePath(int version) throws CertificateException { if (sKeyStoreLocation == null) { throw new CertificateException("Local key store location has not been initialized"); } if (version < 1) { return sKeyStoreLocation + File.separator + "KeyStore.bks"; } else { return sKeyStoreLocation + File.separator + "KeyStore_v" + version + ".bks"; } } }