From 9503feb9c41e0d70318539e199959e8261458a51 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Tue, 29 Apr 2025 17:02:14 +0200 Subject: [PATCH] Feature: Use user and system certificate stores on macOS (#3837) --- .../networking/CombinedKeyStoreSpi.java | 185 ++++++++++++++++++ .../networking/SSLContextWithMacKeychain.java | 13 +- 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/cryptomator/networking/CombinedKeyStoreSpi.java diff --git a/src/main/java/org/cryptomator/networking/CombinedKeyStoreSpi.java b/src/main/java/org/cryptomator/networking/CombinedKeyStoreSpi.java new file mode 100644 index 000000000..426dd5304 --- /dev/null +++ b/src/main/java/org/cryptomator/networking/CombinedKeyStoreSpi.java @@ -0,0 +1,185 @@ +package org.cryptomator.networking; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.concurrent.atomic.AtomicInteger; + +public class CombinedKeyStoreSpi extends KeyStoreSpi { + + private final KeyStore primary; + private final KeyStore fallback; + + public static CombinedKeyStoreSpi create(KeyStore primary, KeyStore fallback) { + checkIfLoaded(primary); + checkIfLoaded(fallback); + return new CombinedKeyStoreSpi(primary, fallback); + } + + private static void checkIfLoaded(KeyStore s) { + try { + s.aliases(); + } catch (KeyStoreException e) { + throw new IllegalArgumentException("Keystore %s is not loaded.".formatted(s.getType())); + } + } + + private CombinedKeyStoreSpi(KeyStore primary, KeyStore fallback) { + this.primary = primary; + this.fallback = fallback; + } + + @Override + public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException { + try { + Key key = primary.getKey(alias, password); + if (key == null) { + key = fallback.getKey(alias, password); + } + return key; + } catch (KeyStoreException e) { + throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e); + } + } + + @Override + public Certificate[] engineGetCertificateChain(String alias) { + try { + Certificate[] chain = primary.getCertificateChain(alias); + if (chain == null) { + chain = fallback.getCertificateChain(alias); + } + return chain; + } catch (KeyStoreException e) { + throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e); + } + } + + @Override + public Certificate engineGetCertificate(String alias) { + try { + Certificate cert = primary.getCertificate(alias); + if (cert == null) { + cert = fallback.getCertificate(alias); + } + return cert; + } catch (KeyStoreException e) { + throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e); + } + } + + @Override + public Date engineGetCreationDate(String alias) { + try { + Date date = primary.getCreationDate(alias); + if (date == null) { + date = fallback.getCreationDate(alias); + } + return date; + } catch (KeyStoreException e) { + throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e); + } + } + + @Override + public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) throws KeyStoreException { + throw new UnsupportedOperationException("Read-only KeyStore"); + } + + @Override + public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException { + throw new UnsupportedOperationException("Read-only KeyStore"); + } + + @Override + public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException { + throw new UnsupportedOperationException("Read-only KeyStore"); + } + + @Override + public void engineDeleteEntry(String alias) throws KeyStoreException { + throw new UnsupportedOperationException("Read-only KeyStore"); + } + + @Override + public Enumeration engineAliases() { + var aliases = new LinkedHashSet(); + try { + primary.aliases().asIterator().forEachRemaining(aliases::add); + fallback.aliases().asIterator().forEachRemaining(aliases::add); + return Collections.enumeration(aliases); + } catch (KeyStoreException e) { + throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e); + } + } + + @Override + public boolean engineContainsAlias(String alias) { + try { + return primary.containsAlias(alias) || fallback.containsAlias(alias); + } catch (KeyStoreException e) { + throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e); + } + } + + @Override + public int engineSize() { + var aliases = engineAliases(); + var i = new AtomicInteger(0); + aliases.asIterator().forEachRemaining(_ -> i.incrementAndGet()); + return i.get(); + } + + @Override + public boolean engineIsKeyEntry(String alias) { + try { + return primary.isKeyEntry(alias) || fallback.isKeyEntry(alias); + } catch (KeyStoreException e) { + throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e); + } + } + + @Override + public boolean engineIsCertificateEntry(String alias) { + try { + return primary.isCertificateEntry(alias) || fallback.isCertificateEntry(alias); + } catch (KeyStoreException e) { + throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e); + } + } + + @Override + public String engineGetCertificateAlias(Certificate cert) { + try { + String alias = primary.getCertificateAlias(cert); + if (alias == null) { + alias = fallback.getCertificateAlias(cert); + } + return alias; + } catch (KeyStoreException e) { + throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e); + } + } + + @Override + public void engineStore(OutputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException { + throw new UnsupportedOperationException("Read-only KeyStore"); + } + + @Override + public void engineLoad(InputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException { + // Nothing to do; the real keystores are already loaded. + } +} diff --git a/src/main/java/org/cryptomator/networking/SSLContextWithMacKeychain.java b/src/main/java/org/cryptomator/networking/SSLContextWithMacKeychain.java index 0078fdfd3..db52481e2 100644 --- a/src/main/java/org/cryptomator/networking/SSLContextWithMacKeychain.java +++ b/src/main/java/org/cryptomator/networking/SSLContextWithMacKeychain.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.Provider; import java.security.cert.CertificateException; /** @@ -16,6 +17,16 @@ public class SSLContextWithMacKeychain extends SSLContextDifferentTrustStoreBase @Override KeyStore getTruststore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { - return KeyStore.getInstance("KeychainStore-ROOT"); + var userKeyStore = KeyStore.getInstance("KeychainStore"); + var systemRootKeyStore = KeyStore.getInstance("KeychainStore-ROOT"); + userKeyStore.load(null); + systemRootKeyStore.load(null); + try { + CombinedKeyStoreSpi spi = CombinedKeyStoreSpi.create(userKeyStore, systemRootKeyStore); + Provider dummyProvider = new Provider("CombinedKeyStoreProvider", "1.0", "Provides a combined, read-only KeyStore") {}; + return new KeyStore(spi, dummyProvider, "CombinedKeyStoreProvider") {}; + } catch (IllegalArgumentException e) { + throw new KeyStoreException(e); + } } }