6 Commits 4e39eaa1f1 ... 85c2901484

Author SHA1 Message Date
  Armin Schrenk 85c2901484 Merge branch 'release/1.16.1' 2 weeks ago
  Armin Schrenk 434030b139 finalize 1.16.1 2 weeks ago
  Armin Schrenk e43bb37758 prepare 1.16.1 2 weeks ago
  Armin Schrenk 2eb9f0fca8 Fixes #3838 2 weeks ago
  Armin Schrenk 9503feb9c4 Feature: Use user and system certificate stores on macOS (#3837) 2 weeks ago
  Armin Schrenk 08e9f130e4 [skip ci] Merge branch 'main' into develop 2 weeks ago

+ 3 - 0
dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml

@@ -83,6 +83,9 @@
 	</content_rating>
 
 	<releases>
+		<release date="2025-04-30" version="1.16.1">
+			<url type="details">https://github.com/cryptomator/cryptomator/releases/1.16.1</url>
+		</release>
 		<release date="2025-04-29" version="1.16.0">
 			<url type="details">https://github.com/cryptomator/cryptomator/releases/1.16.0</url>
 		</release>

+ 2 - 2
pom.xml

@@ -3,7 +3,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.cryptomator</groupId>
 	<artifactId>cryptomator</artifactId>
-	<version>1.16.0</version>
+	<version>1.16.1</version>
 	<name>Cryptomator Desktop App</name>
 
 	<organization>
@@ -36,7 +36,7 @@
 		<cryptomator.cryptofs.version>2.9.0</cryptomator.cryptofs.version>
 		<cryptomator.integrations.version>1.5.1</cryptomator.integrations.version>
 		<cryptomator.integrations.win.version>1.3.0</cryptomator.integrations.win.version>
-		<cryptomator.integrations.mac.version>1.3.0</cryptomator.integrations.mac.version>
+		<cryptomator.integrations.mac.version>1.3.2</cryptomator.integrations.mac.version>
 		<cryptomator.integrations.linux.version>1.5.3</cryptomator.integrations.linux.version>
 		<cryptomator.fuse.version>5.0.5</cryptomator.fuse.version>
 		<cryptomator.webdav.version>2.0.10</cryptomator.webdav.version>

+ 185 - 0
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<String> engineAliases() {
+		var aliases = new LinkedHashSet<String>();
+		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.
+	}
+}

+ 12 - 1
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);
+		}
 	}
 }