diff --git a/pom.xml b/pom.xml
index 1369c5611..581a4c70c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,6 +39,7 @@
16
3.12.0
+ 1.69
3.18.1
2.2
30.1.1-jre
@@ -128,6 +129,13 @@
${commons-lang3.version}
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+ ${bouncycastle.version}
+
+
com.auth0
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 6ba69a6ef..4caaf5592 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -23,6 +23,8 @@ module org.cryptomator.desktop {
requires org.apache.commons.lang3;
requires dagger;
requires com.auth0.jwt;
+ requires org.bouncycastle.provider;
+ requires org.bouncycastle.pkix;
/* TODO: filename-based modules: */
requires static javax.inject; /* ugly dagger/guava crap */
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java
new file mode 100644
index 000000000..b25a7b188
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java
@@ -0,0 +1,108 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.X509Certificate;
+import java.security.spec.ECGenParameterSpec;
+
+class P12AccessHelper {
+
+ private static final String EC_ALG = "EC";
+ private static final String EC_CURVE_NAME = "secp256r1";
+ private static final String SIGNATURE_ALG = "SHA256withECDSA";
+ private static final String KEYSTORE_ALIAS_KEY = "key";
+ private static final String KEYSTORE_ALIAS_CERT = "crt";
+
+ private P12AccessHelper() {}
+
+ /**
+ * Creates a new key pair and stores it in PKCS#12 format at the given path.
+ *
+ * @param p12File The path of the .p12 file
+ * @param pw The password to protect the key material
+ * @throws IOException In case of I/O errors
+ * @throws MasterkeyLoadingFailedException If any cryptographic operation fails
+ */
+ public static KeyPair createNew(Path p12File, char[] pw) throws IOException, MasterkeyLoadingFailedException {
+ try {
+ var keyPair = getKeyPairGenerator().generateKeyPair();
+ var keyStore = getKeyStore();
+ keyStore.load(null, pw);
+ var cert = X509Helper.createSelfSignedCert(keyPair, SIGNATURE_ALG);
+ var chain = new X509Certificate[]{cert};
+ keyStore.setKeyEntry(KEYSTORE_ALIAS_KEY, keyPair.getPrivate(), pw, chain);
+ keyStore.setCertificateEntry(KEYSTORE_ALIAS_CERT, cert);
+ var tmpFile = p12File.resolveSibling(p12File.getFileName().toString() + ".tmp");
+ try (var out = Files.newOutputStream(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
+ keyStore.store(out, pw);
+ }
+ Files.move(tmpFile, p12File, StandardCopyOption.REPLACE_EXISTING);
+ return keyPair;
+ } catch (GeneralSecurityException e) {
+ throw new MasterkeyLoadingFailedException("Failed to store PKCS12 file.", e);
+ }
+ }
+
+ /**
+ * Loads a key pair from a PKCS#12 file located at the given path.
+ *
+ * @param p12File The path of the .p12 file
+ * @param pw The password to protect the key material
+ * @throws IOException In case of I/O errors
+ * @throws InvalidPassphraseException If the supplied password is incorrect
+ * @throws MasterkeyLoadingFailedException If any cryptographic operation fails
+ */
+ public static KeyPair loadExisting(Path p12File, char[] pw) throws IOException, InvalidPassphraseException, MasterkeyLoadingFailedException {
+ try (var in = Files.newInputStream(p12File, StandardOpenOption.READ)) {
+ var keyStore = getKeyStore();
+ keyStore.load(in, pw);
+ var sk = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS_KEY, pw);
+ var pk = keyStore.getCertificate(KEYSTORE_ALIAS_CERT).getPublicKey();
+ return new KeyPair(pk, sk);
+ } catch (UnrecoverableKeyException e) {
+ throw new InvalidPassphraseException();
+ } catch (IOException e) {
+ if (e.getCause() instanceof UnrecoverableKeyException) {
+ throw new InvalidPassphraseException();
+ } else {
+ throw e;
+ }
+ } catch (GeneralSecurityException e) {
+ throw new MasterkeyLoadingFailedException("Failed to load PKCS12 file.", e);
+ }
+ }
+
+ private static KeyPairGenerator getKeyPairGenerator() {
+ try {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance(EC_ALG);
+ keyGen.initialize(new ECGenParameterSpec(EC_CURVE_NAME));
+ return keyGen;
+ } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
+ throw new IllegalStateException("secp256r1 curve not supported");
+ }
+ }
+
+ private static KeyStore getKeyStore() {
+ try {
+ return KeyStore.getInstance("PKCS12");
+ } catch (KeyStoreException e) {
+ throw new IllegalStateException("Every implementation of the Java platform is required to support PKCS12.");
+ }
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/X509Helper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/X509Helper.java
new file mode 100644
index 000000000..d9b7b953a
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/X509Helper.java
@@ -0,0 +1,81 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.sql.Date;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.UUID;
+
+class X509Helper {
+
+ private static final X500Name ISSUER = new X500Name("CN=Cryptomator");
+ private static final X500Name SUBJECT = new X500Name("CN=Self Signed Cert");
+ private static final ASN1ObjectIdentifier ASN1_SUBJECT_KEY_ID = new ASN1ObjectIdentifier("2.5.29.14");
+
+ private X509Helper() {}
+
+ /**
+ * Creates a self-signed X509Certificate containing the public key and signed with the private key of a given key pair.
+ *
+ * @param keyPair A key pair
+ * @param signatureAlg A signature algorithm suited for the given key pair (see available algorithms)
+ * @return A self-signed X509Certificate
+ * @throws CertificateException If certificate generation failed, e.g. because of unsupported algorithms
+ */
+ public static X509Certificate createSelfSignedCert(KeyPair keyPair, String signatureAlg) throws CertificateException {
+ try {
+ X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder( //
+ ISSUER, //
+ randomSerialNo(), //
+ Date.from(Instant.now()), //
+ Date.from(Instant.now().plus(3650, ChronoUnit.DAYS)), //
+ SUBJECT, //
+ keyPair.getPublic());
+ certificateBuilder.addExtension(ASN1_SUBJECT_KEY_ID, false, getX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
+ var signer = new JcaContentSignerBuilder(signatureAlg).build(keyPair.getPrivate());
+ var cert = certificateBuilder.build(signer);
+ try (InputStream in = new ByteArrayInputStream(cert.getEncoded())) {
+ return (X509Certificate) getCertFactory().generateCertificate(in);
+ }
+ } catch (IOException | OperatorCreationException e) {
+ throw new CertificateException(e);
+ }
+ }
+
+ private static BigInteger randomSerialNo() {
+ return BigInteger.valueOf(UUID.randomUUID().getMostSignificantBits());
+ }
+
+ private static JcaX509ExtensionUtils getX509ExtensionUtils() {
+ try {
+ return new JcaX509ExtensionUtils();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Every implementation of the Java platform is required to support SHA-1.");
+ }
+ }
+
+ private static CertificateFactory getCertFactory() {
+ try {
+ return CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new IllegalStateException("Every implementation of the Java platform is required to support X.509.");
+ }
+ }
+
+}
diff --git a/src/main/resources/license/THIRD-PARTY.txt b/src/main/resources/license/THIRD-PARTY.txt
index 2b18cc3f1..e109eb172 100644
--- a/src/main/resources/license/THIRD-PARTY.txt
+++ b/src/main/resources/license/THIRD-PARTY.txt
@@ -11,7 +11,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see http://www.gnu.org/licenses/.
-Cryptomator uses 40 third-party dependencies under the following licenses:
+Cryptomator uses 43 third-party dependencies under the following licenses:
Apache License v2.0:
- jffi (com.github.jnr:jffi:1.2.23 - http://github.com/jnr/jffi)
- jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm)
@@ -41,6 +41,10 @@ Cryptomator uses 40 third-party dependencies under the following licenses:
- asm-commons (org.ow2.asm:asm-commons:7.1 - http://asm.ow2.org/)
- asm-tree (org.ow2.asm:asm-tree:7.1 - http://asm.ow2.org/)
- asm-util (org.ow2.asm:asm-util:7.1 - http://asm.ow2.org/)
+ Bouncy Castle Licence:
+ - Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk15on:1.69 - https://www.bouncycastle.org/java.html)
+ - Bouncy Castle Provider (org.bouncycastle:bcprov-jdk15on:1.69 - https://www.bouncycastle.org/java.html)
+ - Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk15on:1.69 - https://www.bouncycastle.org/java.html)
Eclipse Public License - Version 1.0:
- Jetty :: Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-servlet-api:4.0.6 - https://eclipse.org/jetty/jetty-servlet-api)
Eclipse Public License - Version 2.0:
diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/P12AccessHelperTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/P12AccessHelperTest.java
new file mode 100644
index 000000000..a92eb40b3
--- /dev/null
+++ b/src/test/java/org/cryptomator/ui/keyloading/hub/P12AccessHelperTest.java
@@ -0,0 +1,52 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class P12AccessHelperTest {
+
+ @Test
+ public void testCreate(@TempDir Path tmpDir) throws IOException {
+ var p12File = tmpDir.resolve("test.p12");
+
+ var keyPair = P12AccessHelper.createNew(p12File, "asd".toCharArray());
+
+ Assertions.assertNotNull(keyPair);
+ Assertions.assertTrue(Files.exists(p12File));
+ }
+
+ @Nested
+ public class ExistingFile {
+
+ private Path p12File;
+
+ @BeforeEach
+ public void setup(@TempDir Path tmpDir) throws IOException {
+ p12File = tmpDir.resolve("test.p12");
+ P12AccessHelper.createNew(p12File, "foo".toCharArray());
+ }
+
+ @Test
+ public void testLoadWithWrongPassword() {
+ Assertions.assertThrows(InvalidPassphraseException.class, () -> {
+ P12AccessHelper.loadExisting(p12File, "bar".toCharArray());
+ });
+ }
+
+ @Test
+ public void testLoad() throws IOException {
+ var keyPair = P12AccessHelper.loadExisting(p12File, "foo".toCharArray());
+
+ Assertions.assertNotNull(keyPair);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/X509HelperTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/X509HelperTest.java
new file mode 100644
index 000000000..79abc82f4
--- /dev/null
+++ b/src/test/java/org/cryptomator/ui/keyloading/hub/X509HelperTest.java
@@ -0,0 +1,25 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPairGenerator;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.spec.ECGenParameterSpec;
+
+public class X509HelperTest {
+
+ @Test
+ public void testCreateCert() throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, InvalidAlgorithmParameterException {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
+ keyGen.initialize(new ECGenParameterSpec("secp256r1"));
+ var keyPair = keyGen.generateKeyPair();
+ var cert = X509Helper.createSelfSignedCert(keyPair, "SHA256withECDSA");
+ Assertions.assertNotNull(cert);
+ }
+
+}
\ No newline at end of file