From 2952733a11cdc269421e87847d14d0e05a063255 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 28 Jul 2021 17:04:12 +0200 Subject: [PATCH 01/55] add PKCS12 support for on-demand creation and storage of an EC keypair --- pom.xml | 8 ++ src/main/java/module-info.java | 2 + .../ui/keyloading/hub/P12AccessHelper.java | 108 ++++++++++++++++++ .../ui/keyloading/hub/X509Helper.java | 81 +++++++++++++ src/main/resources/license/THIRD-PARTY.txt | 6 +- .../keyloading/hub/P12AccessHelperTest.java | 52 +++++++++ .../ui/keyloading/hub/X509HelperTest.java | 25 ++++ 7 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/X509Helper.java create mode 100644 src/test/java/org/cryptomator/ui/keyloading/hub/P12AccessHelperTest.java create mode 100644 src/test/java/org/cryptomator/ui/keyloading/hub/X509HelperTest.java 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 From b21ea6134241801dfa72c27f61a6f15c98932026 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 28 Jul 2021 17:29:44 +0200 Subject: [PATCH 02/55] add first draft for `hub+http` / `hub+https` keyloading scheme --- .idea/runConfigurations/Cryptomator_Linux.xml | 2 +- .../Cryptomator_Linux_Dev.xml | 2 +- .../runConfigurations/Cryptomator_Windows.xml | 2 +- .../Cryptomator_Windows_Dev.xml | 2 +- .idea/runConfigurations/Cryptomator_macOS.xml | 2 +- .../Cryptomator_macOS_Dev.xml | 2 +- src/main/java/module-info.java | 1 + .../org/cryptomator/common/Environment.java | 5 + .../org/cryptomator/ui/common/FxmlFile.java | 1 + .../ui/controls/NiceSecurePasswordField.java | 4 + .../ui/controls/SecurePasswordField.java | 9 ++ .../ui/keyloading/KeyLoadingModule.java | 3 +- .../keyloading/hub/HubKeyLoadingModule.java | 87 +++++++++++++ .../keyloading/hub/HubKeyLoadingStrategy.java | 88 +++++++++++++ .../ui/keyloading/hub/P12Controller.java | 65 ++++++++++ .../keyloading/hub/P12CreateController.java | 118 ++++++++++++++++++ .../ui/keyloading/hub/P12LoadController.java | 106 ++++++++++++++++ src/main/resources/fxml/hub_p12.fxml | 19 +++ src/main/resources/fxml/hub_p12_create.fxml | 39 ++++++ src/main/resources/fxml/hub_p12_load.fxml | 48 +++++++ 20 files changed, 598 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java create mode 100644 src/main/resources/fxml/hub_p12.fxml create mode 100644 src/main/resources/fxml/hub_p12_create.fxml create mode 100644 src/main/resources/fxml/hub_p12_load.fxml diff --git a/.idea/runConfigurations/Cryptomator_Linux.xml b/.idea/runConfigurations/Cryptomator_Linux.xml index 735f60069..2d55618de 100644 --- a/.idea/runConfigurations/Cryptomator_Linux.xml +++ b/.idea/runConfigurations/Cryptomator_Linux.xml @@ -2,7 +2,7 @@ + + org.eclipse.jetty + jetty-server + ${jetty.version} + + com.auth0 diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index dfb03a6c4..33d384785 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -21,6 +21,7 @@ module org.cryptomator.desktop { requires com.nulabinc.zxcvbn; requires org.slf4j; requires org.apache.commons.lang3; + requires org.eclipse.jetty.server; requires dagger; requires com.auth0.jwt; requires org.bouncycastle.provider; diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java new file mode 100644 index 000000000..dfb37a2ab --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java @@ -0,0 +1,118 @@ +package org.cryptomator.ui.keyloading.hub; + +import com.google.common.io.BaseEncoding; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.AbstractHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.TransferQueue; +import java.util.function.Consumer; + +/** + * A basic implementation for RFC 8252, Section 7.3: + *

+ * We're spawning a local http server on a system-assigned high port and + * use http://127.0.0.1:{PORT}/success as a redirect URI. + *

+ * Furthermore, we can deliver a html response to inform the user that the + * auth workflow finished and she can close the browser tab. + */ +class AuthReceiver implements AutoCloseable { + + private static final String REDIRECT_SCHEME = "http"; + private static final String LOOPBACK_ADDR = "127.0.0.1"; + private static final String JSON_200 = """ + {"status": "success"} + """; + private static final String JSON_400 = """ + {"status": "missing param key"} + """; + + private final Server server; + private final ServerConnector connector; + private final Handler handler; + + private AuthReceiver(Server server, ServerConnector connector, Handler handler) { + assert server.isRunning(); + this.server = server; + this.connector = connector; + this.handler = handler; + } + + public URI getRedirectURL() { + try { + return new URI(REDIRECT_SCHEME, null, LOOPBACK_ADDR, connector.getLocalPort(), null, null, null); + } catch (URISyntaxException e) { + throw new IllegalStateException("URI constructed from well-formed components.", e); + } + } + + public static AuthReceiver start() throws Exception { + Server server = new Server(); + var handler = new Handler(); + var connector = new ServerConnector(server); + connector.setPort(0); + connector.setHost(LOOPBACK_ADDR); + server.setConnectors(new Connector[]{connector}); + server.setHandler(handler); + server.start(); + return new AuthReceiver(server, connector, handler); + } + + public String receive() throws InterruptedException { + return handler.receivedKeys.take(); + } + + @Override + public void close() throws Exception { + server.stop(); + } + + private static class Handler extends AbstractHandler { + + private final BlockingQueue receivedKeys = new LinkedBlockingQueue<>(); + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest req, HttpServletResponse res) throws IOException { + baseRequest.setHandled(true); + var key = req.getParameter("key"); + byte[] response; + if (key != null) { + res.setStatus(HttpServletResponse.SC_OK); + response = JSON_200.getBytes(StandardCharsets.UTF_8); + } else { + res.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response = JSON_400.getBytes(StandardCharsets.UTF_8); + } + res.setContentType("application/json;charset=utf-8"); + res.setContentLength(response.length); + res.getOutputStream().write(response); + res.getOutputStream().flush(); + + // the following line might trigger a server shutdown, + // so let's make sure the response is flushed first + if (key != null) { + receivedKeys.add(key); + } + } + } +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java index 367db4d6e..38a6643b0 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java @@ -1,5 +1,7 @@ package org.cryptomator.ui.keyloading.hub; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; import dagger.Lazy; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptolib.api.Masterkey; @@ -12,23 +14,28 @@ import org.cryptomator.ui.keyloading.KeyLoadingStrategy; import org.cryptomator.ui.unlock.UnlockCancelledException; import javax.inject.Inject; +import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.Window; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.security.KeyPair; +import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; @KeyLoading public class HubKeyLoadingStrategy implements KeyLoadingStrategy { - static final String SCHEME_HUB_HTTP = "hub+http"; - static final String SCHEME_HUB_HTTPS = "hub+https"; - private static final String SCHEME_HTTP = "http"; - private static final String SCHEME_HTTPS = "https"; + private static final String SCHEME_PREFIX = "hub+"; + static final String SCHEME_HUB_HTTP = SCHEME_PREFIX + "http"; + static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https"; + private final Application application; + private final ExecutorService executor; private final Vault vault; private final Stage window; private final Lazy p12LoadingScene; @@ -36,7 +43,9 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { private final AtomicReference keyPairRef; @Inject - public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy p12LoadingScene, UserInteractionLock p12LoadingLock, AtomicReference keyPairRef) { + public HubKeyLoadingStrategy(Application application, ExecutorService executor, @KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy p12LoadingScene, UserInteractionLock p12LoadingLock, AtomicReference keyPairRef) { + this.application = application; + this.executor = executor; this.vault = vault; this.window = window; this.p12LoadingScene = p12LoadingScene; @@ -46,23 +55,14 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { @Override public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException { - return switch (keyId.getScheme().toLowerCase()) { - case SCHEME_HUB_HTTP -> loadKey(keyId, SCHEME_HTTP); - case SCHEME_HUB_HTTPS -> loadKey(keyId, SCHEME_HTTPS); - default -> throw new IllegalArgumentException("Only supports keys with schemes " + SCHEME_HUB_HTTP + " or " + SCHEME_HUB_HTTPS); - }; - } - - private Masterkey loadKey(URI keyId, String adjustedScheme) { - try { - var foo = new URI(adjustedScheme, keyId.getSchemeSpecificPart(), keyId.getFragment()); - } catch (URISyntaxException e) { - throw new IllegalStateException("URI known to be valid, if old URI was valid", e); - } - + Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX)); try { loadP12(); LOG.info("keypair loaded {}", keyPairRef.get().getPublic()); + var task = new ReceiveEncryptedMasterkeyTask(redirectUri -> { + openBrowser(keyId, redirectUri); + }); + executor.submit(task); throw new UnlockCancelledException("not yet implemented"); // TODO } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -70,6 +70,18 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { } } + private void openBrowser(URI keyId, URI redirectUri) { + Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX)); + var httpScheme = keyId.getScheme().substring(SCHEME_PREFIX.length()); + var redirectParam = "redirect_uri="+ URLEncoder.encode(redirectUri.toString(), StandardCharsets.US_ASCII); + try { + var uri = new URI(httpScheme, keyId.getAuthority(), keyId.getPath(), redirectParam, null); + application.getHostServices().showDocument(uri.toString()); + } catch (URISyntaxException e) { + throw new IllegalStateException("URI constructed from params known to be valid", e); + } + } + private HubKeyLoadingModule.P12KeyLoading loadP12() throws InterruptedException { Platform.runLater(() -> { window.setScene(p12LoadingScene.get()); diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java new file mode 100644 index 000000000..70a08cca6 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java @@ -0,0 +1,31 @@ +package org.cryptomator.ui.keyloading.hub; + +import com.google.common.io.BaseEncoding; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javafx.concurrent.Task; +import java.net.URI; +import java.util.function.Consumer; + +class ReceiveEncryptedMasterkeyTask extends Task { + + private static final Logger LOG = LoggerFactory.getLogger(ReceiveEncryptedMasterkeyTask.class); + + private final Consumer redirectUriConsumer; + + public ReceiveEncryptedMasterkeyTask(Consumer redirectUriConsumer) { + this.redirectUriConsumer = redirectUriConsumer; + } + + @Override + protected byte[] call() throws Exception { + try (var receiver = AuthReceiver.start()) { + var redirectUri = receiver.getRedirectURL(); + LOG.debug("Waiting for key on {}", redirectUri); + redirectUriConsumer.accept(redirectUri); + var token = receiver.receive(); + return BaseEncoding.base64Url().decode(token); + } + } +} diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/AuthReceiverTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/AuthReceiverTest.java new file mode 100644 index 000000000..531f63415 --- /dev/null +++ b/src/test/java/org/cryptomator/ui/keyloading/hub/AuthReceiverTest.java @@ -0,0 +1,23 @@ +package org.cryptomator.ui.keyloading.hub; + +public class AuthReceiverTest { + + static { + System.setProperty("LOGLEVEL", "INFO"); + } + + public static void main(String[] args) { + try (var receiver = AuthReceiver.start()) { + System.out.println("Waiting on " + receiver.getRedirectURL()); + var token = receiver.receive(); + System.out.println("SUCCESS: " + token); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + System.out.println("CANCELLED"); + } catch (Exception e) { + System.out.println("ERROR"); + e.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 000000000..51bfcac67 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file From d938b1c3f72ac1b73bc7095972908572a109634c Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 30 Jul 2021 16:55:05 +0200 Subject: [PATCH 04/55] keep window open while waiting for http callback --- src/main/java/module-info.java | 13 +- .../org/cryptomator/ui/common/FxmlFile.java | 1 + .../ui/keyloading/hub/AuthController.java | 151 ++++++++++++++++++ .../ui/keyloading/hub/AuthParams.java | 12 ++ .../ui/keyloading/hub/AuthReceiveTask.java | 35 ++++ .../ui/keyloading/hub/AuthReceiver.java | 33 ++-- .../keyloading/hub/HubKeyLoadingModule.java | 29 +++- .../keyloading/hub/HubKeyLoadingStrategy.java | 46 ++---- .../ui/keyloading/hub/P12AccessHelper.java | 2 +- .../ui/keyloading/hub/P12Controller.java | 32 +--- .../keyloading/hub/P12CreateController.java | 25 ++- .../ui/keyloading/hub/P12LoadController.java | 25 ++- .../hub/ReceiveEncryptedMasterkeyTask.java | 31 ---- .../MasterkeyFileLoadingModule.java | 4 +- .../MasterkeyFileLoadingStrategy.java | 4 +- .../PassphraseEntryController.java | 4 +- .../SelectMasterkeyFileController.java | 4 +- src/main/resources/fxml/hub_auth.fxml | 44 +++++ src/main/resources/fxml/hub_p12_create.fxml | 2 +- src/main/resources/fxml/hub_p12_load.fxml | 2 +- 20 files changed, 357 insertions(+), 142 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/AuthController.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/AuthParams.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiveTask.java delete mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java create mode 100644 src/main/resources/fxml/hub_auth.fxml diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 33d384785..a2da43316 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -9,23 +9,26 @@ module org.cryptomator.desktop { requires org.cryptomator.frontend.fuse; requires org.cryptomator.frontend.webdav; requires org.cryptomator.integrations.api; + // jdk: requires java.desktop; requires java.net.http; requires javafx.base; requires javafx.graphics; requires javafx.controls; requires javafx.fxml; - requires com.tobiasdiez.easybind; + requires jdk.crypto.ec; + // 3rd party: + requires com.auth0.jwt; requires com.google.common; requires com.google.gson; requires com.nulabinc.zxcvbn; - requires org.slf4j; - requires org.apache.commons.lang3; - requires org.eclipse.jetty.server; + requires com.tobiasdiez.easybind; requires dagger; - requires com.auth0.jwt; + requires org.slf4j; requires org.bouncycastle.provider; requires org.bouncycastle.pkix; + requires org.apache.commons.lang3; + requires org.eclipse.jetty.server; /* TODO: filename-based modules: */ requires static javax.inject; /* ugly dagger/guava crap */ diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index a6c3a72e6..6fc36d48c 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -15,6 +15,7 @@ public enum FxmlFile { HEALTH_START_FAIL("/fxml/health_start_fail.fxml"), // HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), // HUB_P12("/fxml/hub_p12.fxml"), // + HUB_AUTH("/fxml/hub_auth.fxml"), // LOCK_FORCED("/fxml/lock_forced.fxml"), // LOCK_FAILED("/fxml/lock_failed.fxml"), // MAIN_WINDOW("/fxml/main_window.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthController.java new file mode 100644 index 000000000..cd4f8331c --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthController.java @@ -0,0 +1,151 @@ +package org.cryptomator.ui.keyloading.hub; + +import com.google.common.base.Preconditions; +import org.cryptomator.ui.common.ErrorComponent; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.UserInteractionLock; +import org.cryptomator.ui.keyloading.KeyLoading; +import org.cryptomator.ui.keyloading.KeyLoadingScoped; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.application.Application; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.ObjectBinding; +import javafx.beans.binding.StringBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.concurrent.WorkerStateEvent; +import javafx.fxml.FXML; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +@KeyLoadingScoped +public class AuthController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(AuthController.class); + + private final Application application; + private final ExecutorService executor; + private final Stage window; + private final KeyPair keyPair; + private final UserInteractionLock authFlowLock; + private final AtomicReference hubUriRef; + private final ErrorComponent.Builder errorComponent; + private final ObjectProperty redirectUriRef; + private final ObjectBinding authUri; + private final StringBinding authUriHost; + private final BooleanBinding ready; + private final AuthReceiveTask receiveTask; + + @Inject + public AuthController(Application application, ExecutorService executor, @KeyLoading Stage window, AtomicReference keyPairRef, UserInteractionLock authFlowLock, AtomicReference hubUriRef, ErrorComponent.Builder errorComponent) { + this.application = application; + this.executor = executor; + this.window = window; + this.keyPair = Objects.requireNonNull(keyPairRef.get()); + this.authFlowLock = authFlowLock; + this.hubUriRef = hubUriRef; + this.errorComponent = errorComponent; + this.redirectUriRef = new SimpleObjectProperty<>(); + this.authUri = Bindings.createObjectBinding(this::getAuthUri, redirectUriRef); + this.authUriHost = Bindings.createStringBinding(this::getAuthUriHost, authUri); + this.ready = authUri.isNotNull(); + this.receiveTask = new AuthReceiveTask(redirectUriRef::set); + this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); + } + + @FXML + public void initialize() { + Preconditions.checkState(hubUriRef.get() != null); + receiveTask.setOnSucceeded(this::receivedKey); + receiveTask.setOnFailed(this::keyRetrievalFailed); + executor.submit(receiveTask); + } + + private void keyRetrievalFailed(WorkerStateEvent workerStateEvent) { + LOG.error("Cryptomator Hub login failed with error", receiveTask.getException()); + authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.FAILED); + errorComponent.cause(receiveTask.getException()).window(window).build().showErrorScene(); + } + + private void receivedKey(WorkerStateEvent workerStateEvent) { + var authParams = receiveTask.getValue(); + LOG.info("Cryptomator Hub login succeeded: {} encrypted with {}", authParams, keyPair.getPublic()); + // TODO decrypt and return masterkey + authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.SUCCESS); + window.close(); + } + + @FXML + public void cancel() { + window.close(); + } + + private void windowClosed(WindowEvent windowEvent) { + // stop server, if it is still running + receiveTask.cancel(); + // if not already interacted, mark this workflow as cancelled: + if (authFlowLock.awaitingInteraction().get()) { + LOG.debug("Authorization cancelled by user."); + authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.CANCELLED); + } + } + + @FXML + public void openBrowser() { + assert getAuthUri() != null; + application.getHostServices().showDocument(getAuthUri().toString()); + } + + /* Getter/Setter */ + + public ObjectBinding authUriProperty() { + return authUri; + } + + public URI getAuthUri() { + var hubUri = hubUriRef.get(); + var redirectUri = redirectUriRef.get(); + if (hubUri == null || redirectUri == null) { + return null; + } + var redirectParam = "redirect_uri=" + URLEncoder.encode(redirectUri.toString(), StandardCharsets.US_ASCII); + try { + return new URI(hubUri.getScheme(), hubUri.getAuthority(), hubUri.getPath(), redirectParam, null); + } catch (URISyntaxException e) { + throw new IllegalStateException("URI constructed from params known to be valid", e); + } + } + + public StringBinding authUriHostProperty() { + return authUriHost; + } + + public String getAuthUriHost() { + var authUri = getAuthUri(); + if (authUri == null) { + return null; + } else { + return authUri.getHost(); + } + } + + public BooleanBinding readyProperty() { + return ready; + } + + public boolean isReady() { + return ready.get(); + } +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthParams.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthParams.java new file mode 100644 index 000000000..d08aa28a1 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthParams.java @@ -0,0 +1,12 @@ +package org.cryptomator.ui.keyloading.hub; + +/** + * Parameters required to decrypt the masterkey: + *

+ */ +record AuthParams(String m, String epk) { + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiveTask.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiveTask.java new file mode 100644 index 000000000..8ae71f81f --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiveTask.java @@ -0,0 +1,35 @@ +package org.cryptomator.ui.keyloading.hub; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javafx.application.Platform; +import javafx.concurrent.Task; +import java.net.URI; +import java.util.function.Consumer; + +class AuthReceiveTask extends Task { + + private static final Logger LOG = LoggerFactory.getLogger(AuthReceiveTask.class); + + private final Consumer redirectUriConsumer; + + /** + * Spawns a server and waits for the redirectUri to be called. + * + * @param redirectUriConsumer A callback invoked with the redirectUri, as soon as the server has started + */ + public AuthReceiveTask(Consumer redirectUriConsumer) { + this.redirectUriConsumer = redirectUriConsumer; + } + + @Override + protected AuthParams call() throws Exception { + try (var receiver = AuthReceiver.start()) { + var redirectUri = receiver.getRedirectURL(); + Platform.runLater(() -> redirectUriConsumer.accept(redirectUri)); + LOG.debug("Waiting for key on {}", redirectUri); + return receiver.receive(); + } + } +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java index dfb37a2ab..6e5ca5b56 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java @@ -1,6 +1,5 @@ package org.cryptomator.ui.keyloading.hub; -import com.google.common.io.BaseEncoding; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; @@ -13,19 +12,8 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; -import java.util.Queue; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.TransferQueue; -import java.util.function.Consumer; /** * A basic implementation for RFC 8252, Section 7.3: @@ -41,11 +29,11 @@ class AuthReceiver implements AutoCloseable { private static final String REDIRECT_SCHEME = "http"; private static final String LOOPBACK_ADDR = "127.0.0.1"; private static final String JSON_200 = """ - {"status": "success"} - """; + {"status": "success"} + """; private static final String JSON_400 = """ - {"status": "missing param key"} - """; + {"status": "missing param"} + """; private final Server server; private final ServerConnector connector; @@ -78,7 +66,7 @@ class AuthReceiver implements AutoCloseable { return new AuthReceiver(server, connector, handler); } - public String receive() throws InterruptedException { + public AuthParams receive() throws InterruptedException { return handler.receivedKeys.take(); } @@ -89,14 +77,15 @@ class AuthReceiver implements AutoCloseable { private static class Handler extends AbstractHandler { - private final BlockingQueue receivedKeys = new LinkedBlockingQueue<>(); + private final BlockingQueue receivedKeys = new LinkedBlockingQueue<>(); @Override public void handle(String target, Request baseRequest, HttpServletRequest req, HttpServletResponse res) throws IOException { baseRequest.setHandled(true); - var key = req.getParameter("key"); + var m = req.getParameter("m"); // encrypted masterkey + var epk = req.getParameter("epk"); // ephemeral public key byte[] response; - if (key != null) { + if (m != null && epk != null) { res.setStatus(HttpServletResponse.SC_OK); response = JSON_200.getBytes(StandardCharsets.UTF_8); } else { @@ -110,8 +99,8 @@ class AuthReceiver implements AutoCloseable { // the following line might trigger a server shutdown, // so let's make sure the response is flushed first - if (key != null) { - receivedKeys.add(key); + if (m != null && epk != null) { + receivedKeys.add(new AuthParams(m, epk)); } } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java index 132ef36d0..67f4e2fc4 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java @@ -18,6 +18,7 @@ import org.cryptomator.ui.keyloading.KeyLoadingScoped; import org.cryptomator.ui.keyloading.KeyLoadingStrategy; import javafx.scene.Scene; +import java.net.URI; import java.security.KeyPair; import java.util.ResourceBundle; import java.util.concurrent.atomic.AtomicReference; @@ -25,10 +26,10 @@ import java.util.concurrent.atomic.AtomicReference; @Module public abstract class HubKeyLoadingModule { - public enum P12KeyLoading { - LOADED, - CREATED, - CANCELED + public enum AuthFlow { + SUCCESS, + FAILED, + CANCELLED } @Provides @@ -39,10 +40,16 @@ public abstract class HubKeyLoadingModule { @Provides @KeyLoadingScoped - static UserInteractionLock provideP12KeyLoadingLock() { + static UserInteractionLock provideAuthFlowLock() { return new UserInteractionLock<>(null); } + @Provides + @KeyLoadingScoped + static AtomicReference provideHubUri() { + return new AtomicReference<>(); + } + @Binds @IntoMap @KeyLoadingScoped @@ -62,6 +69,13 @@ public abstract class HubKeyLoadingModule { return fxmlLoaders.createScene(FxmlFile.HUB_P12); } + @Provides + @FxmlScene(FxmlFile.HUB_AUTH) + @KeyLoadingScoped + static Scene provideHubAuthScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.HUB_AUTH); + } + @Binds @IntoMap @FxControllerKey(P12Controller.class) @@ -84,4 +98,9 @@ public abstract class HubKeyLoadingModule { return new NewPasswordController(resourceBundle, strengthRater); } + @Binds + @IntoMap + @FxControllerKey(AuthController.class) + abstract FxController bindAuthController(AuthController controller); + } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java index 38a6643b0..b3830d22d 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java @@ -1,7 +1,6 @@ package org.cryptomator.ui.keyloading.hub; import com.google.common.base.Preconditions; -import com.google.common.base.Splitter; import dagger.Lazy; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptolib.api.Masterkey; @@ -14,17 +13,13 @@ import org.cryptomator.ui.keyloading.KeyLoadingStrategy; import org.cryptomator.ui.unlock.UnlockCancelledException; import javax.inject.Inject; -import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.Window; import java.net.URI; import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.security.KeyPair; -import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; @KeyLoading @@ -34,55 +29,48 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { static final String SCHEME_HUB_HTTP = SCHEME_PREFIX + "http"; static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https"; - private final Application application; - private final ExecutorService executor; private final Vault vault; private final Stage window; private final Lazy p12LoadingScene; - private final UserInteractionLock p12LoadingLock; - private final AtomicReference keyPairRef; + private final UserInteractionLock userInteraction; + private final AtomicReference hubUriRef; @Inject - public HubKeyLoadingStrategy(Application application, ExecutorService executor, @KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy p12LoadingScene, UserInteractionLock p12LoadingLock, AtomicReference keyPairRef) { - this.application = application; - this.executor = executor; + public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy p12LoadingScene, UserInteractionLock userInteraction, AtomicReference hubUriRef) { this.vault = vault; this.window = window; this.p12LoadingScene = p12LoadingScene; - this.p12LoadingLock = p12LoadingLock; - this.keyPairRef = keyPairRef; + this.userInteraction = userInteraction; + this.hubUriRef = hubUriRef; } @Override public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException { - Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX)); + hubUriRef.set(getHubUri(keyId)); try { - loadP12(); - LOG.info("keypair loaded {}", keyPairRef.get().getPublic()); - var task = new ReceiveEncryptedMasterkeyTask(redirectUri -> { - openBrowser(keyId, redirectUri); - }); - executor.submit(task); - throw new UnlockCancelledException("not yet implemented"); // TODO + switch (auth()) { + case SUCCESS -> LOG.debug("TODO success"); // TODO return key + //case FAILED -> LOG.error("failed to load keypair"); + case CANCELLED -> throw new UnlockCancelledException("User cancelled auth workflow"); + } + throw new UnlockCancelledException("not yet implemented"); // TODO remove } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new UnlockCancelledException("Loading interrupted", e); } } - private void openBrowser(URI keyId, URI redirectUri) { + private URI getHubUri(URI keyId) { Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX)); - var httpScheme = keyId.getScheme().substring(SCHEME_PREFIX.length()); - var redirectParam = "redirect_uri="+ URLEncoder.encode(redirectUri.toString(), StandardCharsets.US_ASCII); + var hubUriScheme = keyId.getScheme().substring(SCHEME_PREFIX.length()); try { - var uri = new URI(httpScheme, keyId.getAuthority(), keyId.getPath(), redirectParam, null); - application.getHostServices().showDocument(uri.toString()); + return new URI(hubUriScheme, keyId.getSchemeSpecificPart(), null); } catch (URISyntaxException e) { throw new IllegalStateException("URI constructed from params known to be valid", e); } } - private HubKeyLoadingModule.P12KeyLoading loadP12() throws InterruptedException { + private HubKeyLoadingModule.AuthFlow auth() throws InterruptedException { Platform.runLater(() -> { window.setScene(p12LoadingScene.get()); window.show(); @@ -94,7 +82,7 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { window.centerOnScreen(); } }); - return p12LoadingLock.awaitInteraction(); + return userInteraction.awaitInteraction(); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java index b25a7b188..7e62c8a0c 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java @@ -93,7 +93,7 @@ class P12AccessHelper { keyGen.initialize(new ECGenParameterSpec(EC_CURVE_NAME)); return keyGen; } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { - throw new IllegalStateException("secp256r1 curve not supported"); + throw new IllegalStateException(EC_CURVE_NAME + " curve not supported"); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java b/src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java index 0505bde28..84df68749 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java @@ -1,35 +1,17 @@ package org.cryptomator.ui.keyloading.hub; -import com.google.common.base.Preconditions; import org.cryptomator.common.Environment; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.common.Destroyables; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.NewPasswordController; import org.cryptomator.ui.common.UserInteractionLock; -import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.cryptomator.ui.keyloading.KeyLoading; import org.cryptomator.ui.keyloading.KeyLoadingScoped; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanExpression; -import javafx.beans.binding.ObjectExpression; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.fxml.FXML; -import javafx.scene.control.ContentDisplay; -import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.stage.WindowEvent; -import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; -import java.security.KeyPair; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicReference; @KeyLoadingScoped public class P12Controller implements FxController { @@ -38,21 +20,21 @@ public class P12Controller implements FxController { private final Stage window; private final Environment env; - private final UserInteractionLock p12LoadingLock; + private final UserInteractionLock userInteraction; @Inject - public P12Controller(@KeyLoading Stage window, Environment env, AtomicReference keyPairRef, UserInteractionLock p12LoadingLock) { + public P12Controller(@KeyLoading Stage window, Environment env, UserInteractionLock userInteraction) { this.window = window; this.env = env; - this.p12LoadingLock = p12LoadingLock; - this.window.setOnHiding(this::windowClosed); + this.userInteraction = userInteraction; + this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); } private void windowClosed(WindowEvent windowEvent) { // if not already interacted, mark this workflow as cancelled: - if (p12LoadingLock.awaitingInteraction().get()) { - LOG.debug("P12 loading canceled by user."); - p12LoadingLock.interacted(HubKeyLoadingModule.P12KeyLoading.CANCELED); + if (userInteraction.awaitingInteraction().get()) { + LOG.debug("P12 loading cancelled by user."); + userInteraction.interacted(HubKeyLoadingModule.AuthFlow.CANCELLED); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java index d892d086f..735950d9e 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java @@ -1,9 +1,12 @@ package org.cryptomator.ui.keyloading.hub; import com.google.common.base.Preconditions; +import dagger.Lazy; import org.cryptomator.common.Environment; import org.cryptomator.cryptolib.common.Destroyables; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.NewPasswordController; import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.keyloading.KeyLoading; @@ -19,8 +22,10 @@ import javafx.beans.binding.ObjectExpression; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; +import javafx.scene.Scene; import javafx.scene.control.ContentDisplay; import javafx.stage.Stage; +import javafx.stage.WindowEvent; import java.io.IOException; import java.nio.file.Path; import java.security.KeyPair; @@ -35,25 +40,27 @@ public class P12CreateController implements FxController { private final Stage window; private final Environment env; private final AtomicReference keyPairRef; - private final UserInteractionLock p12LoadingLock; + private final Lazy authScene; + private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty(); private final ObjectBinding unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled); private final BooleanProperty readyToCreate = new SimpleBooleanProperty(); public NewPasswordController newPasswordController; - @Inject - public P12CreateController(@KeyLoading Stage window, Environment env, AtomicReference keyPairRef, UserInteractionLock p12LoadingLock) { + public P12CreateController(@KeyLoading Stage window, Environment env, AtomicReference keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH) Lazy authScene) { this.window = window; this.env = env; this.keyPairRef = keyPairRef; - this.p12LoadingLock = p12LoadingLock; + this.authScene = authScene; + this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); } @FXML public void initialize() { readyToCreate.bind(newPasswordController.goodPasswordProperty()); + newPasswordController.passwordField.requestFocus(); } @FXML @@ -61,6 +68,11 @@ public class P12CreateController implements FxController { window.close(); } + private void windowClosed(WindowEvent windowEvent) { + newPasswordController.passwordField.wipe(); + newPasswordController.reenterField.wipe(); + } + @FXML public void create() { Preconditions.checkState(newPasswordController.goodPasswordProperty().get()); @@ -70,15 +82,12 @@ public class P12CreateController implements FxController { var keyPair = P12AccessHelper.createNew(p12File, pw); setKeyPair(keyPair); LOG.debug("Created .p12 file {}", p12File); - p12LoadingLock.interacted(HubKeyLoadingModule.P12KeyLoading.CREATED); - window.close(); + window.setScene(authScene.get()); } catch (IOException e) { LOG.error("Failed to load .p12 file.", e); // TODO } finally { Arrays.fill(pw, '\0'); - newPasswordController.passwordField.wipe(); - newPasswordController.reenterField.wipe(); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java index bab2992bb..d774aa4c7 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java @@ -1,10 +1,13 @@ package org.cryptomator.ui.keyloading.hub; +import dagger.Lazy; import org.cryptomator.common.Environment; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.common.Destroyables; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.cryptomator.ui.keyloading.KeyLoading; @@ -20,8 +23,10 @@ import javafx.beans.binding.ObjectExpression; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; +import javafx.scene.Scene; import javafx.scene.control.ContentDisplay; import javafx.stage.Stage; +import javafx.stage.WindowEvent; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -37,18 +42,24 @@ public class P12LoadController implements FxController { private final Stage window; private final Environment env; private final AtomicReference keyPairRef; - private final UserInteractionLock p12LoadingLock; + private final Lazy authScene; private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty(); private final ObjectBinding unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled); public NiceSecurePasswordField passwordField; @Inject - public P12LoadController(@KeyLoading Stage window, Environment env, AtomicReference keyPairRef, UserInteractionLock p12LoadingLock) { + public P12LoadController(@KeyLoading Stage window, Environment env, AtomicReference keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH) Lazy authScene) { this.window = window; this.env = env; this.keyPairRef = keyPairRef; - this.p12LoadingLock = p12LoadingLock; + this.authScene = authScene; + this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); + } + + @FXML + public void initialize() { + passwordField.requestFocus(); } @FXML @@ -56,6 +67,10 @@ public class P12LoadController implements FxController { window.close(); } + private void windowClosed(WindowEvent windowEvent) { + passwordField.wipe(); + } + @FXML public void load() { char[] pw = passwordField.copyChars(); @@ -64,8 +79,7 @@ public class P12LoadController implements FxController { var keyPair = P12AccessHelper.loadExisting(p12File, pw); setKeyPair(keyPair); LOG.debug("Loaded .p12 file {}", p12File); - p12LoadingLock.interacted(HubKeyLoadingModule.P12KeyLoading.LOADED); - window.close(); + window.setScene(authScene.get()); } catch (InvalidPassphraseException e) { LOG.warn("Invalid passphrase entered for .p12 file"); Animations.createShakeWindowAnimation(window).playFromStart(); @@ -75,7 +89,6 @@ public class P12LoadController implements FxController { // TODO } finally { Arrays.fill(pw, '\0'); - passwordField.wipe(); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java deleted file mode 100644 index 70a08cca6..000000000 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveEncryptedMasterkeyTask.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.cryptomator.ui.keyloading.hub; - -import com.google.common.io.BaseEncoding; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javafx.concurrent.Task; -import java.net.URI; -import java.util.function.Consumer; - -class ReceiveEncryptedMasterkeyTask extends Task { - - private static final Logger LOG = LoggerFactory.getLogger(ReceiveEncryptedMasterkeyTask.class); - - private final Consumer redirectUriConsumer; - - public ReceiveEncryptedMasterkeyTask(Consumer redirectUriConsumer) { - this.redirectUriConsumer = redirectUriConsumer; - } - - @Override - protected byte[] call() throws Exception { - try (var receiver = AuthReceiver.start()) { - var redirectUri = receiver.getRedirectURL(); - LOG.debug("Waiting for key on {}", redirectUri); - redirectUriConsumer.accept(redirectUri); - var token = receiver.receive(); - return BaseEncoding.base64Url().decode(token); - } - } -} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java index 901eacfb9..d71cff54d 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java @@ -35,12 +35,12 @@ public abstract class MasterkeyFileLoadingModule { public enum PasswordEntry { PASSWORD_ENTERED, - CANCELED + CANCELLED } public enum MasterkeyFileProvision { MASTERKEYFILE_PROVIDED, - CANCELED + CANCELLED } @Provides diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java index 39db1cc04..b2725ef7e 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -95,7 +95,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { if (filePath.get() == null) { return switch (askUserForMasterkeyFilePath()) { case MASTERKEYFILE_PROVIDED -> filePath.get(); - case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled."); + case CANCELLED -> throw new UnlockCancelledException("Choosing masterkey file cancelled."); }; } else { return filePath.get(); @@ -121,7 +121,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { if (password.get() == null) { return switch (askForPassphrase()) { case PASSWORD_ENTERED -> CharBuffer.wrap(password.get()); - case CANCELED -> throw new UnlockCancelledException("Password entry cancelled."); + case CANCELLED -> throw new UnlockCancelledException("Password entry cancelled."); }; } else { // e.g. pre-filled from keychain or previous unlock attempt diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java index f6ce79e51..312118ad4 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java @@ -143,8 +143,8 @@ public class PassphraseEntryController implements FxController { private void windowClosed(WindowEvent windowEvent) { // if not already interacted, mark this workflow as cancelled: if (passwordEntryLock.awaitingInteraction().get()) { - LOG.debug("Unlock canceled by user."); - passwordEntryLock.interacted(PasswordEntry.CANCELED); + LOG.debug("Unlock cancelled by user."); + passwordEntryLock.interacted(PasswordEntry.CANCELLED); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java index 39be2b36e..cc103784b 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java @@ -45,8 +45,8 @@ public class SelectMasterkeyFileController implements FxController { private void windowClosed(WindowEvent windowEvent) { // if not already interacted, mark this workflow as cancelled: if (masterkeyFileProvisionLock.awaitingInteraction().get()) { - LOG.debug("Unlock canceled by user."); - masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELED); + LOG.debug("Unlock cancelled by user."); + masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELLED); } } diff --git a/src/main/resources/fxml/hub_auth.fxml b/src/main/resources/fxml/hub_auth.fxml new file mode 100644 index 000000000..f45203d01 --- /dev/null +++ b/src/main/resources/fxml/hub_auth.fxml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - diff --git a/src/main/resources/fxml/hub_p12_load.fxml b/src/main/resources/fxml/hub_p12_load.fxml deleted file mode 100644 index 98fde1dd0..000000000 --- a/src/main/resources/fxml/hub_p12_load.fxml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/license/THIRD-PARTY.txt b/src/main/resources/license/THIRD-PARTY.txt index 0ed1b3cff..6f06bbb39 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 46 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) @@ -44,10 +44,6 @@ Cryptomator uses 46 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/EciesHelperTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/EciesHelperTest.java deleted file mode 100644 index 0bb432608..000000000 --- a/src/test/java/org/cryptomator/ui/keyloading/hub/EciesHelperTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.cryptomator.ui.keyloading.hub; - -import com.google.common.io.BaseEncoding; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.extension.ParameterContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.converter.ArgumentConversionException; -import org.junit.jupiter.params.converter.ArgumentConverter; -import org.junit.jupiter.params.converter.ConvertWith; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.ValueSource; - -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; - -public class EciesHelperTest { - - @DisplayName("ECDH + KDF") - @ParameterizedTest - @ValueSource(ints = {16, 32, 44, 128}) - public void testEcdhAndKdf(int len) throws NoSuchAlgorithmException { - var alice = KeyPairGenerator.getInstance("EC").generateKeyPair(); - var bob = KeyPairGenerator.getInstance("EC").generateKeyPair(); - - byte[] result1 = EciesHelper.ecdhAndKdf(alice.getPrivate(), bob.getPublic(), len); - byte[] result2 = EciesHelper.ecdhAndKdf(bob.getPrivate(), alice.getPublic(), len); - - Assertions.assertArrayEquals(result1, result2); - } - - @DisplayName("ANSI-X9.63-KDF") - @ParameterizedTest - @CsvSource(value = { - "96c05619d56c328ab95fe84b18264b08725b85e33fd34f08, , 16, 443024c3dae66b95e6f5670601558f71", - "96f600b73ad6ac5629577eced51743dd2c24c21b1ac83ee4, , 16, b6295162a7804f5667ba9070f82fa522", - "22518b10e70f2a3f243810ae3254139efbee04aa57c7af7d, 75eef81aa3041e33b80971203d2c0c52, 128, c498af77161cc59f2962b9a713e2b215152d139766ce34a776df11866a69bf2e52a13d9c7c6fc878c50c5ea0bc7b00e0da2447cfd874f6cf92f30d0097111485500c90c3af8b487872d04685d14c8d1dc8d7fa08beb0ce0ababc11f0bd496269142d43525a78e5bc79a17f59676a5706dc54d54d4d1f0bd7e386128ec26afc21", - "7e335afa4b31d772c0635c7b0e06f26fcd781df947d2990a, d65a4812733f8cdbcdfb4b2f4c191d87, 128, c0bd9e38a8f9de14c2acd35b2f3410c6988cf02400543631e0d6a4c1d030365acbf398115e51aaddebdc9590664210f9aa9fed770d4c57edeafa0b8c14f93300865251218c262d63dadc47dfa0e0284826793985137e0a544ec80abf2fdf5ab90bdaea66204012efe34971dc431d625cd9a329b8217cc8fd0d9f02b13f2f6b0b", - }) - // test vectors from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/components/800-135testvectors/ansx963_2001.zip - public void testKdf(@ConvertWith(HexConverter.class) byte[] sharedSecret, @ConvertWith(HexConverter.class) byte[] sharedInfo, int outLen, @ConvertWith(HexConverter.class) byte[] expectedResult) { - byte[] result = EciesHelper.kdf(sharedSecret, sharedInfo, outLen); - Assertions.assertArrayEquals(expectedResult, result); - } - - public static class HexConverter implements ArgumentConverter { - - @Override - public byte[] convert(Object source, ParameterContext context) throws ArgumentConversionException { - if (source == null) { - return new byte[0]; - } else if (source instanceof String s) { - return BaseEncoding.base16().lowerCase().decode(s); - } else { - return null; - } - } - } - -} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/P12AccessHelperTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/P12AccessHelperTest.java deleted file mode 100644 index a92eb40b3..000000000 --- a/src/test/java/org/cryptomator/ui/keyloading/hub/P12AccessHelperTest.java +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 79abc82f4..000000000 --- a/src/test/java/org/cryptomator/ui/keyloading/hub/X509HelperTest.java +++ /dev/null @@ -1,25 +0,0 @@ -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 From 056990151a02eb3c1fca8c4284c85ba582927ec7 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 24 Aug 2021 12:39:29 +0200 Subject: [PATCH 21/55] adjusted vault config --- .../org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java | 4 ++-- .../java/org/cryptomator/ui/keyloading/hub/HubConfig.java | 4 ++-- .../ui/keyloading/hub/AuthFlowIntegrationTest.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java index 2f96f05a3..d2995b0f8 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java @@ -87,9 +87,9 @@ class AuthFlowReceiver implements AutoCloseable { res.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); if (error == null && code != null) { - res.setHeader("Location", hubConfig.unlockSuccessUrl); + res.setHeader("Location", hubConfig.authSuccessUrl); } else { - res.setHeader("Location", hubConfig.unlockErrorUrl); + res.setHeader("Location", hubConfig.authErrorUrl); } callback.add(new Callback(error, code, state)); diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java index 0e53351e5..050435327 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java @@ -7,7 +7,7 @@ public class HubConfig { public String authEndpoint; public String tokenEndpoint; public String deviceRegistrationUrl; - public String unlockSuccessUrl; - public String unlockErrorUrl; + public String authSuccessUrl; + public String authErrorUrl; } diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java index 2b0f93c25..1c875365a 100644 --- a/src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java +++ b/src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java @@ -23,8 +23,8 @@ public class AuthFlowIntegrationTest { hubConfig.authEndpoint = "http://localhost:8080/auth/realms/cryptomator/protocol/openid-connect/auth"; hubConfig.tokenEndpoint = "http://localhost:8080/auth/realms/cryptomator/protocol/openid-connect/token"; hubConfig.clientId = "cryptomator-hub"; - hubConfig.unlockSuccessUrl = "http://localhost:3000/#/unlock-success"; - hubConfig.unlockErrorUrl = "http://localhost:3000/#/unlock-error"; + hubConfig.authSuccessUrl = "http://localhost:3000/#/unlock-success"; + hubConfig.authErrorUrl = "http://localhost:3000/#/unlock-error"; try (var authFlow = AuthFlow.init(hubConfig)) { var token = authFlow.run(uri -> { From 1477bf07a9306e8c4ceaf1c0dadd6c14c9f0bcc2 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 24 Aug 2021 13:59:51 +0200 Subject: [PATCH 22/55] use public key hash as device id --- .../keyloading/hub/ReceiveKeyController.java | 24 +++++++++---------- .../hub/RegisterDeviceController.java | 11 +++++---- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java index 295aa9244..d14b59ced 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java @@ -1,12 +1,12 @@ package org.cryptomator.ui.keyloading.hub; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import com.google.common.io.BaseEncoding; -import com.google.gson.Gson; -import com.google.gson.JsonElement; import dagger.Lazy; +import org.cryptomator.common.settings.DeviceKey; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptolib.common.MessageDigestSupplier; +import org.cryptomator.cryptolib.common.P384KeyPair; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; @@ -19,15 +19,9 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; -import javafx.application.Application; import javafx.application.Platform; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; import javafx.fxml.FXML; import javafx.scene.Scene; -import javafx.scene.control.TextField; import javafx.stage.Stage; import javafx.stage.WindowEvent; import java.io.IOException; @@ -38,7 +32,6 @@ import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.security.KeyPair; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; @@ -50,6 +43,7 @@ public class ReceiveKeyController implements FxController { private static final String SCHEME_PREFIX = "hub+"; private final Stage window; + private final P384KeyPair keyPair; private final String bearerToken; private final AtomicReference eciesParamsRef; private final UserInteractionLock result; @@ -58,9 +52,11 @@ public class ReceiveKeyController implements FxController { private final URI vaultBaseUri; private final HttpClient httpClient; + @Inject - public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, @Named("bearerToken") AtomicReference tokenRef, AtomicReference eciesParamsRef, UserInteractionLock result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, ErrorComponent.Builder errorComponent) { + public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, DeviceKey deviceKey, @Named("bearerToken") AtomicReference tokenRef, AtomicReference eciesParamsRef, UserInteractionLock result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, ErrorComponent.Builder errorComponent) { this.window = window; + this.keyPair = Objects.requireNonNull(deviceKey.get()); this.bearerToken = Objects.requireNonNull(tokenRef.get()); this.eciesParamsRef = eciesParamsRef; this.result = result; @@ -73,7 +69,10 @@ public class ReceiveKeyController implements FxController { @FXML public void initialize() { - var keyUri = appendPath(vaultBaseUri, "/keys/desktop-app"); // TODO use actual device id + var deviceKey = keyPair.getPublic().getEncoded(); + var hashedKey = MessageDigestSupplier.SHA256.get().digest(deviceKey); + var deviceId = BaseEncoding.base16().encode(hashedKey); + var keyUri = appendPath(vaultBaseUri, "/keys/" + deviceId); var request = HttpRequest.newBuilder(keyUri) // .header("Authorization", "Bearer " + bearerToken) // .GET() // @@ -159,5 +158,4 @@ public class ReceiveKeyController implements FxController { throw new IllegalStateException("URI constructed from params known to be valid", e); } } - } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java index ba2c308c7..c4e33dbf5 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java @@ -2,6 +2,7 @@ package org.cryptomator.ui.keyloading.hub; import com.google.common.io.BaseEncoding; import org.cryptomator.common.settings.DeviceKey; +import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.cryptomator.cryptolib.common.P384KeyPair; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.UserInteractionLock; @@ -44,10 +45,12 @@ public class RegisterDeviceController implements FxController { @FXML public void browse() { - var deviceKey = BaseEncoding.base64Url().omitPadding().encode(keyPair.getPublic().getEncoded()); - var deviceId = "desktop-app"; // TODO use actual device id - var hash = computeVerificationHash(deviceId + deviceKey + verificationCode); - var url = hubConfig.deviceRegistrationUrl + "?device_key=" + deviceKey + "&device_id=" + deviceId + "&verification_hash=" + hash; + var deviceKey = keyPair.getPublic().getEncoded(); + var encodedKey = BaseEncoding.base64Url().omitPadding().encode(deviceKey); + var hashedKey = MessageDigestSupplier.SHA256.get().digest(deviceKey); + var deviceId = BaseEncoding.base16().encode(hashedKey); + var hash = computeVerificationHash(deviceId + encodedKey + verificationCode); + var url = hubConfig.deviceRegistrationUrl + "?device_key=" + encodedKey + "&device_id=" + deviceId + "&verification_hash=" + hash; application.getHostServices().showDocument(url); } From 346ce67bc4b89ff2c4cbeaf91419d4327a1bc5a1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 24 Aug 2021 14:43:26 +0200 Subject: [PATCH 23/55] add "unauthorized device" scene --- .../org/cryptomator/ui/common/FxmlFile.java | 1 + .../keyloading/hub/HubKeyLoadingModule.java | 11 +++++ .../keyloading/hub/ReceiveKeyController.java | 6 ++- .../hub/UnauthorizedDeviceController.java | 42 +++++++++++++++++++ .../fxml/hub_unauthorized_device.fxml | 40 ++++++++++++++++++ 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java create mode 100644 src/main/resources/fxml/hub_unauthorized_device.fxml diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 8d96dd216..3588b0fe4 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -17,6 +17,7 @@ public enum FxmlFile { HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), // HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), // HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), // + HUB_UNAUTHORIZED_DEVICE("/fxml/hub_unauthorized_device.fxml"), // LOCK_FORCED("/fxml/lock_forced.fxml"), // LOCK_FAILED("/fxml/lock_failed.fxml"), // MAIN_WINDOW("/fxml/main_window.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java index f9ade2585..7ec46cd7a 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java @@ -98,6 +98,13 @@ public abstract class HubKeyLoadingModule { return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_DEVICE); } + @Provides + @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) + @KeyLoadingScoped + static Scene provideHubUnauthorizedDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE); + } + @Binds @IntoMap @FxControllerKey(AuthFlowController.class) @@ -120,4 +127,8 @@ public abstract class HubKeyLoadingModule { @FxControllerKey(RegisterDeviceController.class) abstract FxController bindRegisterDeviceController(RegisterDeviceController controller); + @Binds + @IntoMap + @FxControllerKey(UnauthorizedDeviceController.class) + abstract FxController bindUnauthorizedDeviceController(UnauthorizedDeviceController controller); } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java index d14b59ced..5c8d488fd 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java @@ -48,19 +48,21 @@ public class ReceiveKeyController implements FxController { private final AtomicReference eciesParamsRef; private final UserInteractionLock result; private final Lazy registerDeviceScene; + private final Lazy unauthorizedScene; private final ErrorComponent.Builder errorComponent; private final URI vaultBaseUri; private final HttpClient httpClient; @Inject - public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, DeviceKey deviceKey, @Named("bearerToken") AtomicReference tokenRef, AtomicReference eciesParamsRef, UserInteractionLock result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, ErrorComponent.Builder errorComponent) { + public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, DeviceKey deviceKey, @Named("bearerToken") AtomicReference tokenRef, AtomicReference eciesParamsRef, UserInteractionLock result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, ErrorComponent.Builder errorComponent) { this.window = window; this.keyPair = Objects.requireNonNull(deviceKey.get()); this.bearerToken = Objects.requireNonNull(tokenRef.get()); this.eciesParamsRef = eciesParamsRef; this.result = result; this.registerDeviceScene = registerDeviceScene; + this.unauthorizedScene = unauthorizedScene; this.errorComponent = errorComponent; this.vaultBaseUri = getVaultBaseUri(vault); this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); @@ -115,7 +117,7 @@ public class ReceiveKeyController implements FxController { } private void accessNotGranted() { - LOG.warn("unauthorized device"); // TODO + window.setScene(unauthorizedScene.get()); } private void retrievalFailed(Throwable cause) { diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java new file mode 100644 index 000000000..a915f4016 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java @@ -0,0 +1,42 @@ +package org.cryptomator.ui.keyloading.hub; + +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.UserInteractionLock; +import org.cryptomator.ui.keyloading.KeyLoading; +import org.cryptomator.ui.keyloading.KeyLoadingScoped; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.fxml.FXML; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; + +@KeyLoadingScoped +public class UnauthorizedDeviceController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(UnauthorizedDeviceController.class); + + private final Stage window; + private final UserInteractionLock result; + + @Inject + public UnauthorizedDeviceController(@KeyLoading Stage window, UserInteractionLock result) { + this.window = window; + this.result = result; + this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); + } + + @FXML + public void close() { + window.close(); + } + + private void windowClosed(WindowEvent windowEvent) { + // if not already interacted, mark this workflow as cancelled: + if (result.awaitingInteraction().get()) { + LOG.debug("Authorization cancelled. Device not authorized."); + result.interacted(HubKeyLoadingModule.HubLoadingResult.CANCELLED); + } + } +} diff --git a/src/main/resources/fxml/hub_unauthorized_device.fxml b/src/main/resources/fxml/hub_unauthorized_device.fxml new file mode 100644 index 000000000..9538d19ef --- /dev/null +++ b/src/main/resources/fxml/hub_unauthorized_device.fxml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 34e4383c1ea6004cc82030460a18826f1d3688d4 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 24 Nov 2021 14:46:21 +0100 Subject: [PATCH 25/55] adjust release build to support hub features --- .github/workflows/release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 03d913c91..ea7d5fd42 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -121,6 +121,7 @@ jobs: --java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\"" --java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" --java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.p12Path=\"~/.config/Cryptomator/key.p12\"" --java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" --java-options "-Dcryptomator.showTrayIcon=false" --java-options "-Dcryptomator.buildNumber=\"appimage-${{ needs.metadata.outputs.revNum }}\"" @@ -134,6 +135,7 @@ jobs: --java-options "-Dcryptomator.pluginDir=\"~/AppData/Roaming/Cryptomator/Plugins\"" --java-options "-Dcryptomator.settingsPath=\"~/AppData/Roaming/Cryptomator/settings.json\"" --java-options "-Dcryptomator.ipcSocketPath=\"~/AppData/Roaming/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.p12Path=\"~/AppData/Roaming/Cryptomator/key.p12\"" --java-options "-Dcryptomator.keychainPath=\"~/AppData/Roaming/Cryptomator/keychain.json\"" --java-options "-Dcryptomator.mountPointsDir=\"~/Cryptomator\"" --java-options "-Dcryptomator.showTrayIcon=true" @@ -150,6 +152,7 @@ jobs: --java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/Cryptomator/Plugins\"" --java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\"" --java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.p12Path=\"~/Library/Application Support/Cryptomator/key.p12\"" --java-options "-Dcryptomator.showTrayIcon=true" --java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.metadata.outputs.revNum }}\"" --mac-package-identifier org.cryptomator @@ -170,7 +173,7 @@ jobs: --verbose --output runtime --module-path "${JAVA_HOME}/jmods" - --add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility + --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility --no-header-files --no-man-pages --strip-debug From 0110e5bedd0ed7fbfe785d82e256df5401039fe7 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 9 Dec 2021 14:02:39 +0100 Subject: [PATCH 26/55] added JWE decryption helper --- pom.xml | 7 ++- src/main/java/module-info.java | 1 + .../ui/keyloading/hub/JWEHelper.java | 55 ++++++++++++++++++ src/main/resources/license/THIRD-PARTY.txt | 4 +- .../ui/keyloading/hub/JWEHelperTest.java | 56 +++++++++++++++++++ 5 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java create mode 100644 src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java diff --git a/pom.xml b/pom.xml index 3ae31ef15..d52701afb 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ com.github.serceman,com.github.jnr,org.ow2.asm,net.java.dev.jna,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh - 2.1.0-beta2 + 2.1.0-beta3 2.2.0 1.0.0 1.0.0 @@ -157,6 +157,11 @@ java-jwt ${jwt.version}
+ + com.nimbusds + nimbus-jose-jwt + 9.15.2 + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 920c3bdc8..6627f5528 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -35,6 +35,7 @@ module org.cryptomator.desktop { requires static javax.inject; /* ugly dagger/guava crap */ requires logback.classic; requires logback.core; + requires com.nimbusds.jose.jwt; uses AutoStartProvider; uses KeychainAccessProvider; diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java new file mode 100644 index 000000000..2c2b9baa4 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java @@ -0,0 +1,55 @@ +package org.cryptomator.ui.keyloading.hub; + +import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWEObject; +import com.nimbusds.jose.crypto.ECDHDecrypter; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.interfaces.ECPrivateKey; +import java.util.Arrays; + +class JWEHelper { + + private static final Logger LOG = LoggerFactory.getLogger(JWEHelper.class); + private static final String JWE_PAYLOAD_MASTERKEY_FIELD = "key"; + + private JWEHelper(){} + + public static Masterkey decrypt(JWEObject jwe, ECPrivateKey privateKey) throws MasterkeyLoadingFailedException { + try { + jwe.decrypt(new ECDHDecrypter(privateKey)); + return readKey(jwe); + } catch (JOSEException e) { + LOG.warn("Failed to decrypt JWE: {}", jwe); + throw new MasterkeyLoadingFailedException("Failed to decrypt JWE", e); + } + } + + private static Masterkey readKey(JWEObject jwe) throws MasterkeyLoadingFailedException { + Preconditions.checkArgument(jwe.getState() == JWEObject.State.DECRYPTED); + var fields = jwe.getPayload().toJSONObject(); + if (fields == null) { + LOG.error("Expected JWE payload to be JSON: {}", jwe.getPayload()); + throw new MasterkeyLoadingFailedException("Expected JWE payload to be JSON"); + } + var keyBytes = new byte[0]; + try { + if (fields.get(JWE_PAYLOAD_MASTERKEY_FIELD) instanceof String key) { + keyBytes = BaseEncoding.base64().decode(key); + return new Masterkey(keyBytes); + } else { + throw new IllegalArgumentException("JWE payload doesn't contain field " + JWE_PAYLOAD_MASTERKEY_FIELD); + } + } catch (IllegalArgumentException e) { + LOG.error("Unexpected JWE payload: {}", jwe.getPayload()); + throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e); + } finally { + Arrays.fill(keyBytes, (byte) 0x00); + } + } +} diff --git a/src/main/resources/license/THIRD-PARTY.txt b/src/main/resources/license/THIRD-PARTY.txt index e58486ddd..5344f58e8 100644 --- a/src/main/resources/license/THIRD-PARTY.txt +++ b/src/main/resources/license/THIRD-PARTY.txt @@ -11,16 +11,18 @@ 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 43 third-party dependencies under the following licenses: +Cryptomator uses 45 third-party dependencies under the following licenses: Apache License v2.0: - jffi (com.github.jnr:jffi:1.3.5 - 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) - jnr-constants (com.github.jnr:jnr-constants:0.10.2 - http://github.com/jnr/jnr-constants) - jnr-ffi (com.github.jnr:jnr-ffi:2.2.7 - http://github.com/jnr/jnr-ffi) + - JCIP Annotations under Apache License (com.github.stephenc.jcip:jcip-annotations:1.0-1 - http://stephenc.github.com/jcip-annotations) - Gson (com.google.code.gson:gson:2.8.8 - https://github.com/google/gson/gson) - Dagger (com.google.dagger:dagger:2.39 - https://github.com/google/dagger) - Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess) - Guava: Google Core Libraries for Java (com.google.guava:guava:31.0-jre - https://github.com/google/guava) + - Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:9.15.2 - https://bitbucket.org/connect2id/nimbus-jose-jwt) - Apache Commons CLI (commons-cli:commons-cli:1.4 - http://commons.apache.org/proper/commons-cli/) - javax.inject (javax.inject:javax.inject:1 - http://code.google.com/p/atinject/) - Apache Commons Lang (org.apache.commons:commons-lang3:3.12.0 - https://commons.apache.org/proper/commons-lang/) diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java new file mode 100644 index 000000000..3d495e8c1 --- /dev/null +++ b/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java @@ -0,0 +1,56 @@ +package org.cryptomator.ui.keyloading.hub; + +import com.nimbusds.jose.JWEObject; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; +import org.cryptomator.cryptolib.common.P384KeyPair; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Base64; + +public class JWEHelperTest { + + private static final String JWE = "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52Nm02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD-kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA"; + private static final String PRIV_KEY = "ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ"; + private static final String PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERxQR+NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0aq6uqmUy4jUhuxnKxsv59A6JeK7Unn+mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu"; + + @Test + public void testDecrypt() throws ParseException, InvalidKeySpecException { + var jwe = JWEObject.parse(JWE); + var keyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))); + + var masterkey = JWEHelper.decrypt(jwe, keyPair.getPrivate()); + + var expectedEncKey = new byte[32]; + var expectedMacKey = new byte[32]; + Arrays.fill(expectedEncKey, (byte) 0x55); + Arrays.fill(expectedMacKey, (byte) 0x77); + Assertions.assertArrayEquals(expectedEncKey, masterkey.getEncKey().getEncoded()); + Assertions.assertArrayEquals(expectedMacKey, masterkey.getMacKey().getEncoded()); + } + + @ParameterizedTest + @ValueSource(strings = { + "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImdodGR3VnNoUU8wRGFBdjVBOXBiZ1NCTW0yYzZKWVF4dkloR3p6RVdQTncxczZZcEFYeTRQTjBXRFJUWExtQ2wiLCJ5IjoiN3Rncm1Gd016NGl0ZmVQNzBndkpLcjRSaGdjdENCMEJHZjZjWE9WZ2M0bjVXMWQ4dFgxZ1RQakdrczNVSm1zUiJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..x6JWRGSojUJUJYpp.5BRuzcaV.lLIhGH7Wz0n_iTBAubDFZA", // wrong key + "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkM2bWhsNE5BTHhEdHMwUlFlNXlyZWxQVDQyOGhDVzJNeUNYS3EwdUI0TDFMdnpXRHhVaVk3YTdZcEhJakJXcVoiLCJ5IjoiakM2dWc1NE9tbmdpNE9jUk1hdkNrczJpcFpXQjdkUmotR3QzOFhPSDRwZ2tpQ0lybWNlUnFxTnU3Z0c3Qk1yOSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..HNJJghL-SvERFz2v.N0z8YwFg.rYw29iX4i8XujdM4P4KKWg", // payload is not json + "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6InB3R05vcXRnY093MkJ6RDVmSnpBWDJvMzUwSWNsY3A5cFdVTHZ5VDRqRWVCRWdCc3hhTVJXQ1ZyNlJMVUVXVlMiLCJ5IjoiZ2lIVEE5MlF3VU5lbmg1OFV1bWFfb09BX3hnYmFDVWFXSlRnb3Z4WjU4R212TnN4eUlQRElLSm9WV1h5X0R6OSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..jDbzdI7d67_cUjGD.01BPnMq_tQ.aG_uFA6FYqoPS64QAJ4VBQ", // json payload doesn't contain "key" + "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkJyYm9UQkl5Y0NDUEdJQlBUekU2RjBnbTRzRjRCamZPN1I0a2x0aWlCaThKZkxxcVdXNVdUSVBLN01yMXV5QVUiLCJ5IjoiNUpGVUI0WVJiYjM2RUZpN2Y0TUxMcFFyZXd2UV9Tc3dKNHRVbFd1a2c1ZU04X1ZyM2pkeml2QXI2WThRczVYbSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..QEq4Z2m6iwBx2ioS.IBo8TbKJTS4pug.61Z-agIIXgP8bX10O_yEMA", // json payload field "key" not a string + "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImNZdlVFZm9LYkJjenZySE5zQjUxOGpycUxPMGJDOW5lZjR4NzFFMUQ5dk95MXRqd1piZzV3cFI0OE5nU1RQdHgiLCJ5IjoiaWRJekhCWERzSzR2NTZEeU9yczJOcDZsSG1zb29fMXV0VTlzX3JNdVVkbkxuVXIzUXdLZkhYMWdaVXREM1RKayJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..0VZqu5ei9U3blGtq.eDvhU6drw7mIwvXu6Q.f05QnhI7JWG3IYHvexwdFQ" // json payload field "key" invalid base64 data + }) + public void testDecryptInvalid(String malformed) throws ParseException, InvalidKeySpecException { + var jwe = JWEObject.parse(malformed); + var keyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))); + + Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> { + JWEHelper.decrypt(jwe, keyPair.getPrivate()); + }); + } + +} \ No newline at end of file From fb580ff79d8063471c9023d5bddb63f4e016bf7b Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 9 Dec 2021 14:03:39 +0100 Subject: [PATCH 27/55] read masterkey from JWE --- .../ui/keyloading/hub/EciesParams.java | 14 ------------- .../keyloading/hub/HubKeyLoadingModule.java | 3 ++- .../keyloading/hub/HubKeyLoadingStrategy.java | 14 +++++-------- .../keyloading/hub/ReceiveKeyController.java | 21 +++++++------------ .../hub/RegisterDeviceController.java | 2 +- 5 files changed, 16 insertions(+), 38 deletions(-) delete mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/EciesParams.java diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/EciesParams.java b/src/main/java/org/cryptomator/ui/keyloading/hub/EciesParams.java deleted file mode 100644 index 8b0519906..000000000 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/EciesParams.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.cryptomator.ui.keyloading.hub; - -/** - * ECIES parameters required to decrypt the masterkey: - *
    - *
  • m Encrypted Masterkey (base64url-encoded ciphertext)
  • - *
  • epk Ephemeral Public Key (base64url-encoded SPKI format)
  • - *
- *

- * No separate tag required, since we use GCM for encryption. - */ -record EciesParams(String m, String epk) { - -} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java index 7c76bc229..bf2cb10d5 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java @@ -1,5 +1,6 @@ package org.cryptomator.ui.keyloading.hub; +import com.nimbusds.jose.JWEObject; import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -55,7 +56,7 @@ public abstract class HubKeyLoadingModule { @Provides @KeyLoadingScoped - static AtomicReference provideEciesParamsRef() { + static AtomicReference provideJweRef() { return new AtomicReference<>(); } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java index 83338ed06..2aadfb1d0 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java @@ -1,13 +1,11 @@ package org.cryptomator.ui.keyloading.hub; import com.google.common.base.Preconditions; +import com.nimbusds.jose.JWEObject; import dagger.Lazy; import org.cryptomator.common.settings.DeviceKey; -import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; -import org.cryptomator.cryptolib.common.Destroyables; -import org.cryptomator.cryptolib.common.MasterkeyHubAccess; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.UserInteractionLock; @@ -21,7 +19,6 @@ import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.Window; import java.net.URI; -import java.security.KeyPair; import java.util.concurrent.atomic.AtomicReference; @KeyLoading @@ -35,24 +32,23 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { private final Lazy authFlowScene; private final UserInteractionLock userInteraction; private final DeviceKey deviceKey; - private final AtomicReference eciesParams; + private final AtomicReference jweRef; @Inject - public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, UserInteractionLock userInteraction, DeviceKey deviceKey, AtomicReference eciesParams) { + public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, UserInteractionLock userInteraction, DeviceKey deviceKey, AtomicReference jweRef) { this.window = window; this.authFlowScene = authFlowScene; this.userInteraction = userInteraction; this.deviceKey = deviceKey; - this.eciesParams = eciesParams; + this.jweRef = jweRef; } @Override public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException { Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX)); try { - var keyPair = deviceKey.get(); return switch (auth()) { - case SUCCESS -> MasterkeyHubAccess.decryptMasterkey(keyPair.getPrivate(), eciesParams.get().m(), eciesParams.get().epk()); + case SUCCESS -> JWEHelper.decrypt(jweRef.get(), deviceKey.get().getPrivate()); case FAILED -> throw new MasterkeyLoadingFailedException("failed to load keypair"); case CANCELLED -> throw new UnlockCancelledException("User cancelled auth workflow"); }; diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java index e16df1994..7514730cd 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java @@ -1,7 +1,7 @@ package org.cryptomator.ui.keyloading.hub; -import com.google.common.base.Preconditions; import com.google.common.io.BaseEncoding; +import com.nimbusds.jose.JWEObject; import dagger.Lazy; import org.cryptomator.common.settings.DeviceKey; import org.cryptomator.common.vaults.Vault; @@ -32,6 +32,7 @@ import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.text.ParseException; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; @@ -45,7 +46,7 @@ public class ReceiveKeyController implements FxController { private final Stage window; private final P384KeyPair keyPair; private final String bearerToken; - private final AtomicReference eciesParamsRef; + private final AtomicReference jweRef; private final UserInteractionLock result; private final Lazy registerDeviceScene; private final Lazy unauthorizedScene; @@ -53,13 +54,12 @@ public class ReceiveKeyController implements FxController { private final URI vaultBaseUri; private final HttpClient httpClient; - @Inject - public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, DeviceKey deviceKey, @Named("bearerToken") AtomicReference tokenRef, AtomicReference eciesParamsRef, UserInteractionLock result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, ErrorComponent.Builder errorComponent) { + public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, DeviceKey deviceKey, @Named("bearerToken") AtomicReference tokenRef, AtomicReference jweRef, UserInteractionLock result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, ErrorComponent.Builder errorComponent) { this.window = window; this.keyPair = Objects.requireNonNull(deviceKey.get()); this.bearerToken = Objects.requireNonNull(tokenRef.get()); - this.eciesParamsRef = eciesParamsRef; + this.jweRef = jweRef; this.result = result; this.registerDeviceScene = registerDeviceScene; this.unauthorizedScene = unauthorizedScene; @@ -98,16 +98,11 @@ public class ReceiveKeyController implements FxController { private void retrievalSucceeded(HttpResponse response) { try { - var json = HttpHelper.parseBody(response); - Preconditions.checkArgument(json.isJsonObject()); - Preconditions.checkArgument(json.getAsJsonObject().has("device_specific_masterkey")); - Preconditions.checkArgument(json.getAsJsonObject().has("ephemeral_public_key")); - var m = json.getAsJsonObject().get("device_specific_masterkey").getAsString(); - var epk = json.getAsJsonObject().get("ephemeral_public_key").getAsString(); - eciesParamsRef.set(new EciesParams(m, epk)); + var string = HttpHelper.readBody(response); + jweRef.set(JWEObject.parse(string)); result.interacted(HubKeyLoadingModule.HubLoadingResult.SUCCESS); window.close(); - } catch (IOException | IllegalArgumentException e) { + } catch (ParseException | IOException e) { retrievalFailed(e); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java index c4e33dbf5..46349280b 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java @@ -50,7 +50,7 @@ public class RegisterDeviceController implements FxController { var hashedKey = MessageDigestSupplier.SHA256.get().digest(deviceKey); var deviceId = BaseEncoding.base16().encode(hashedKey); var hash = computeVerificationHash(deviceId + encodedKey + verificationCode); - var url = hubConfig.deviceRegistrationUrl + "?device_key=" + encodedKey + "&device_id=" + deviceId + "&verification_hash=" + hash; + var url = hubConfig.deviceRegistrationUrl + "&device_key=" + encodedKey + "&device_id=" + deviceId + "&verification_hash=" + hash; application.getHostServices().showDocument(url); } From 921dd8fe677428b0de450bd7f361ad375c4e4581 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Fri, 10 Dec 2021 13:42:37 +0100 Subject: [PATCH 28/55] Add query to redirection to provide more context in the frontend (#1973) Co-authored-by: Sebastian Stenzel --- .../ui/keyloading/hub/AuthFlow.java | 4 ++-- .../ui/keyloading/hub/AuthFlowContext.java | 5 +++++ .../ui/keyloading/hub/AuthFlowController.java | 6 +++-- .../ui/keyloading/hub/AuthFlowReceiver.java | 22 ++++++++++--------- .../ui/keyloading/hub/AuthFlowTask.java | 6 +++-- .../keyloading/hub/HubKeyLoadingModule.java | 15 +++++++++++-- .../keyloading/hub/ReceiveKeyController.java | 13 +++-------- .../hub/AuthFlowIntegrationTest.java | 8 +++---- 8 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowContext.java diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java index 11ef34301..a4a55e78c 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java @@ -60,8 +60,8 @@ class AuthFlow implements AutoCloseable { * @return An authorization flow * @throws Exception In case of any problems starting the server */ - public static AuthFlow init(HubConfig hubConfig) throws Exception { - var receiver = AuthFlowReceiver.start(hubConfig); + public static AuthFlow init(HubConfig hubConfig, AuthFlowContext authFlowContext) throws Exception { + var receiver = AuthFlowReceiver.start(hubConfig, authFlowContext); return new AuthFlow(receiver, hubConfig); } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowContext.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowContext.java new file mode 100644 index 000000000..3ee2b55eb --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowContext.java @@ -0,0 +1,5 @@ +package org.cryptomator.ui.keyloading.hub; + +record AuthFlowContext(String deviceId) { + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java index 436516fca..65c9a0df7 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java @@ -35,6 +35,7 @@ public class AuthFlowController implements FxController { private final Application application; private final Stage window; private final ExecutorService executor; + private final String deviceId; private final HubConfig hubConfig; private final AtomicReference tokenRef; private final UserInteractionLock result; @@ -45,10 +46,11 @@ public class AuthFlowController implements FxController { private AuthFlowTask task; @Inject - public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("bearerToken") AtomicReference tokenRef, UserInteractionLock result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy receiveKeyScene, ErrorComponent.Builder errorComponent) { + public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @Named("deviceId") String deviceId, HubConfig hubConfig, @Named("bearerToken") AtomicReference tokenRef, UserInteractionLock result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy receiveKeyScene, ErrorComponent.Builder errorComponent) { this.application = application; this.window = window; this.executor = executor; + this.deviceId = deviceId; this.hubConfig = hubConfig; this.tokenRef = tokenRef; this.result = result; @@ -62,7 +64,7 @@ public class AuthFlowController implements FxController { @FXML public void initialize() { assert task == null; - task = new AuthFlowTask(hubConfig, this::setAuthUri);; + task = new AuthFlowTask(hubConfig, new AuthFlowContext(deviceId), this::setAuthUri);; task.setOnFailed(this::authFailed); task.setOnSucceeded(this::authSucceeded); executor.submit(task); diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java index d2995b0f8..dc84c450c 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java @@ -30,20 +30,18 @@ class AuthFlowReceiver implements AutoCloseable { private final Server server; private final ServerConnector connector; private final CallbackServlet servlet; - private final HubConfig hubConfig; - private AuthFlowReceiver(Server server, ServerConnector connector, CallbackServlet servlet, HubConfig hubConfig) { + private AuthFlowReceiver(Server server, ServerConnector connector, CallbackServlet servlet) { this.server = server; this.connector = connector; this.servlet = servlet; - this.hubConfig = hubConfig; } - public static AuthFlowReceiver start(HubConfig hubConfig) throws Exception { + public static AuthFlowReceiver start(HubConfig hubConfig, AuthFlowContext authFlowContext) throws Exception { var server = new Server(); var context = new ServletContextHandler(); - var servlet = new CallbackServlet(hubConfig); + var servlet = new CallbackServlet(hubConfig, authFlowContext); context.addServlet(new ServletHolder(servlet), CALLBACK_PATH); var connector = new ServerConnector(server); @@ -52,7 +50,7 @@ class AuthFlowReceiver implements AutoCloseable { server.setConnectors(new Connector[]{connector}); server.setHandler(context); server.start(); - return new AuthFlowReceiver(server, connector, servlet, hubConfig); + return new AuthFlowReceiver(server, connector, servlet); } public String getRedirectUri() { @@ -68,15 +66,19 @@ class AuthFlowReceiver implements AutoCloseable { server.stop(); } - public static record Callback(String error, String code, String state){} + public static record Callback(String error, String code, String state) { + + } private static class CallbackServlet extends HttpServlet { private final BlockingQueue callback = new LinkedBlockingQueue<>(); private final HubConfig hubConfig; + private final AuthFlowContext authFlowContext; - public CallbackServlet(HubConfig hubConfig) { + public CallbackServlet(HubConfig hubConfig, AuthFlowContext authFlowContext) { this.hubConfig = hubConfig; + this.authFlowContext = authFlowContext; } @Override @@ -87,9 +89,9 @@ class AuthFlowReceiver implements AutoCloseable { res.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); if (error == null && code != null) { - res.setHeader("Location", hubConfig.authSuccessUrl); + res.setHeader("Location", hubConfig.authSuccessUrl + "&device=" + authFlowContext.deviceId()); } else { - res.setHeader("Location", hubConfig.authErrorUrl); + res.setHeader("Location", hubConfig.authErrorUrl + "&device=" + authFlowContext.deviceId()); } callback.add(new Callback(error, code, state)); diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java index 901c7acc7..be41df2dd 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java @@ -7,6 +7,7 @@ import java.util.function.Consumer; class AuthFlowTask extends Task { + private final AuthFlowContext authFlowContext; private final Consumer redirectUriConsumer; /** @@ -15,14 +16,15 @@ class AuthFlowTask extends Task { * @param hubConfig Configuration object holding parameters required by {@link AuthFlow} * @param redirectUriConsumer A callback invoked with the redirectUri, as soon as the server has started */ - public AuthFlowTask(HubConfig hubConfig, Consumer redirectUriConsumer) { + public AuthFlowTask(HubConfig hubConfig, AuthFlowContext authFlowContext, Consumer redirectUriConsumer) { this.hubConfig = hubConfig; + this.authFlowContext = authFlowContext; this.redirectUriConsumer = redirectUriConsumer; } @Override protected String call() throws Exception { - try (var authFlow = AuthFlow.init(hubConfig)) { + try (var authFlow = AuthFlow.init(hubConfig, authFlowContext)) { return authFlow.run(uri -> Platform.runLater(() -> redirectUriConsumer.accept(uri))); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java index bf2cb10d5..04faa1493 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java @@ -1,12 +1,15 @@ package org.cryptomator.ui.keyloading.hub; +import com.google.common.io.BaseEncoding; import com.nimbusds.jose.JWEObject; import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; import dagger.multibindings.StringKey; +import org.cryptomator.common.settings.DeviceKey; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; @@ -23,8 +26,7 @@ import javax.inject.Named; import javafx.scene.Scene; import java.io.IOException; import java.io.UncheckedIOException; -import java.net.URI; -import java.security.KeyPair; +import java.util.Objects; import java.util.ResourceBundle; import java.util.concurrent.atomic.AtomicReference; @@ -47,6 +49,15 @@ public abstract class HubKeyLoadingModule { } } + @Provides + @KeyLoadingScoped + @Named("deviceId") + static String provideDeviceId(DeviceKey deviceKey) { + var publicKey = Objects.requireNonNull(deviceKey.get()).getPublic().getEncoded(); + var hashedKey = MessageDigestSupplier.SHA256.get().digest(publicKey); + return BaseEncoding.base16().encode(hashedKey); + } + @Provides @Named("bearerToken") @KeyLoadingScoped diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java index 7514730cd..90efc6abc 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java @@ -1,12 +1,8 @@ package org.cryptomator.ui.keyloading.hub; -import com.google.common.io.BaseEncoding; import com.nimbusds.jose.JWEObject; import dagger.Lazy; -import org.cryptomator.common.settings.DeviceKey; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.cryptolib.common.MessageDigestSupplier; -import org.cryptomator.cryptolib.common.P384KeyPair; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; @@ -44,7 +40,7 @@ public class ReceiveKeyController implements FxController { private static final String SCHEME_PREFIX = "hub+"; private final Stage window; - private final P384KeyPair keyPair; + private final String deviceId; private final String bearerToken; private final AtomicReference jweRef; private final UserInteractionLock result; @@ -55,9 +51,9 @@ public class ReceiveKeyController implements FxController { private final HttpClient httpClient; @Inject - public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, DeviceKey deviceKey, @Named("bearerToken") AtomicReference tokenRef, AtomicReference jweRef, UserInteractionLock result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, ErrorComponent.Builder errorComponent) { + public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference tokenRef, AtomicReference jweRef, UserInteractionLock result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, ErrorComponent.Builder errorComponent) { this.window = window; - this.keyPair = Objects.requireNonNull(deviceKey.get()); + this.deviceId = deviceId; this.bearerToken = Objects.requireNonNull(tokenRef.get()); this.jweRef = jweRef; this.result = result; @@ -71,9 +67,6 @@ public class ReceiveKeyController implements FxController { @FXML public void initialize() { - var deviceKey = keyPair.getPublic().getEncoded(); - var hashedKey = MessageDigestSupplier.SHA256.get().digest(deviceKey); - var deviceId = BaseEncoding.base16().encode(hashedKey); var keyUri = appendPath(vaultBaseUri, "/keys/" + deviceId); var request = HttpRequest.newBuilder(keyUri) // .header("Authorization", "Bearer " + bearerToken) // diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java index 1c875365a..f2b9e9cd5 100644 --- a/src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java +++ b/src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java @@ -6,8 +6,6 @@ import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.URI; - public class AuthFlowIntegrationTest { static { @@ -23,10 +21,10 @@ public class AuthFlowIntegrationTest { hubConfig.authEndpoint = "http://localhost:8080/auth/realms/cryptomator/protocol/openid-connect/auth"; hubConfig.tokenEndpoint = "http://localhost:8080/auth/realms/cryptomator/protocol/openid-connect/token"; hubConfig.clientId = "cryptomator-hub"; - hubConfig.authSuccessUrl = "http://localhost:3000/#/unlock-success"; - hubConfig.authErrorUrl = "http://localhost:3000/#/unlock-error"; + hubConfig.authSuccessUrl = "http://localhost:3000/#/unlock-success?vault=vaultId"; + hubConfig.authErrorUrl = "http://localhost:3000/#/unlock-error?vault=vaultId"; - try (var authFlow = AuthFlow.init(hubConfig)) { + try (var authFlow = AuthFlow.init(hubConfig, new AuthFlowContext("deviceId"))) { var token = authFlow.run(uri -> { LOG.info("Visit {} to authenticate", uri); }); From ef281f810fbba4e5c52c2fd7f076f54c6848a0a5 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 14 Dec 2021 17:35:24 +0100 Subject: [PATCH 29/55] register device via rest api --- src/main/java/module-info.java | 2 +- .../ui/keyloading/hub/CreateDeviceDto.java | 9 ++ .../keyloading/hub/HubKeyLoadingStrategy.java | 1 + .../keyloading/hub/ReceiveKeyController.java | 23 ++--- .../hub/RegisterDeviceController.java | 86 ++++++++++++------- .../resources/fxml/hub_register_device.fxml | 24 +++--- 6 files changed, 91 insertions(+), 54 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 6627f5528..fe1da0f3e 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -45,6 +45,7 @@ module org.cryptomator.desktop { exports org.cryptomator.ui.keyloading.hub to com.fasterxml.jackson.databind; opens org.cryptomator.common.settings to com.google.gson; + opens org.cryptomator.ui.keyloading.hub to com.google.gson, javafx.fxml; opens org.cryptomator.common to javafx.fxml; opens org.cryptomator.common.vaults to javafx.fxml; @@ -55,7 +56,6 @@ module org.cryptomator.desktop { opens org.cryptomator.ui.forgetPassword to javafx.fxml; opens org.cryptomator.ui.fxapp to javafx.fxml; opens org.cryptomator.ui.health to javafx.fxml; - opens org.cryptomator.ui.keyloading.hub to javafx.fxml; opens org.cryptomator.ui.keyloading.masterkeyfile to javafx.fxml; opens org.cryptomator.ui.lock to javafx.fxml; opens org.cryptomator.ui.mainwindow to javafx.fxml; diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java b/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java new file mode 100644 index 000000000..71377a318 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java @@ -0,0 +1,9 @@ +package org.cryptomator.ui.keyloading.hub; + +class CreateDeviceDto { + + public String id; + public String name; + public String publicKey; + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java index 2aadfb1d0..8316f640a 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java @@ -19,6 +19,7 @@ import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.Window; import java.net.URI; +import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicReference; @KeyLoading diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java index 90efc6abc..5bc8bc419 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java @@ -10,6 +10,7 @@ import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.keyloading.KeyLoading; import org.cryptomator.ui.keyloading.KeyLoadingScoped; +import org.eclipse.jetty.io.RuntimeIOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,30 +74,31 @@ public class ReceiveKeyController implements FxController { .GET() // .build(); httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) // - .whenCompleteAsync(this::loadedExistingKey, Platform::runLater); + .thenAcceptAsync(this::loadedExistingKey, Platform::runLater) // + .exceptionallyAsync(this::retrievalFailed, Platform::runLater); } - private void loadedExistingKey(HttpResponse response, Throwable error) { - if (error != null) { - retrievalFailed(error); - } else { + private void loadedExistingKey(HttpResponse response) { + try { switch (response.statusCode()) { case 200 -> retrievalSucceeded(response); case 403 -> accessNotGranted(); case 404 -> needsDeviceRegistration(); - default -> retrievalFailed(new IOException("Unexpected response " + response.statusCode())); + default -> throw new IOException("Unexpected response " + response.statusCode()); } + } catch (IOException e) { + throw new RuntimeIOException(e); } } - private void retrievalSucceeded(HttpResponse response) { + private void retrievalSucceeded(HttpResponse response) throws IOException { try { var string = HttpHelper.readBody(response); jweRef.set(JWEObject.parse(string)); result.interacted(HubKeyLoadingModule.HubLoadingResult.SUCCESS); window.close(); - } catch (ParseException | IOException e) { - retrievalFailed(e); + } catch (ParseException e) { + throw new IOException("Failed to parse JWE", e); } } @@ -108,10 +110,11 @@ public class ReceiveKeyController implements FxController { window.setScene(unauthorizedScene.get()); } - private void retrievalFailed(Throwable cause) { + private Void retrievalFailed(Throwable cause) { result.interacted(HubKeyLoadingModule.HubLoadingResult.FAILED); LOG.error("Key retrieval failed", cause); errorComponent.cause(cause).window(window).build().showErrorScene(); + return null; } @FXML diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java index 46349280b..442fe2022 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java @@ -1,57 +1,92 @@ package org.cryptomator.ui.keyloading.hub; +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; import com.google.common.io.BaseEncoding; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import org.cryptomator.common.settings.DeviceKey; -import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.cryptomator.cryptolib.common.P384KeyPair; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.keyloading.KeyLoading; import org.cryptomator.ui.keyloading.KeyLoadingScoped; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javafx.application.Application; +import javax.inject.Named; +import javafx.application.Platform; import javafx.fxml.FXML; +import javafx.scene.control.TextField; import javafx.stage.Stage; import javafx.stage.WindowEvent; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; -import java.security.KeyPair; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.util.Objects; +import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; @KeyLoadingScoped public class RegisterDeviceController implements FxController { - private final Application application; + private static final Logger LOG = LoggerFactory.getLogger(RegisterDeviceController.class); + private static final Gson GSON = new GsonBuilder().setLenient().create(); + private final Stage window; private final HubConfig hubConfig; + private final String bearerToken; + private final String deviceId; private final P384KeyPair keyPair; private final UserInteractionLock result; - private final String verificationCode; + private final DecodedJWT jwt; + private final HttpClient httpClient; + + public TextField deviceNameField; @Inject - public RegisterDeviceController(Application application, SecureRandom csprng, @KeyLoading Stage window, HubConfig hubConfig, DeviceKey deviceKey, UserInteractionLock result) { - this.application = application; + public RegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, UserInteractionLock result, @Named("bearerToken") AtomicReference bearerToken) { this.window = window; this.hubConfig = hubConfig; + this.deviceId = deviceId; this.keyPair = Objects.requireNonNull(deviceKey.get()); this.result = result; + this.bearerToken = Objects.requireNonNull(bearerToken.get()); + this.jwt = JWT.decode(this.bearerToken); this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); - this.verificationCode = String.format("%06d", csprng.nextInt(1_000_000)); + this.httpClient = HttpClient.newBuilder().executor(executor).build(); } @FXML - public void browse() { + public void register() { + var keyUri = URI.create("http://localhost:9090/devices/" + deviceId); // TODO lol hubConfig.deviceRegistrationUrl var deviceKey = keyPair.getPublic().getEncoded(); - var encodedKey = BaseEncoding.base64Url().omitPadding().encode(deviceKey); - var hashedKey = MessageDigestSupplier.SHA256.get().digest(deviceKey); - var deviceId = BaseEncoding.base16().encode(hashedKey); - var hash = computeVerificationHash(deviceId + encodedKey + verificationCode); - var url = hubConfig.deviceRegistrationUrl + "&device_key=" + encodedKey + "&device_id=" + deviceId + "&verification_hash=" + hash; - application.getHostServices().showDocument(url); + var dto = new CreateDeviceDto(); + dto.id = deviceId; + dto.name = deviceNameField.getText(); + dto.publicKey = BaseEncoding.base64Url().omitPadding().encode(deviceKey); + var json = GSON.toJson(dto); // TODO: do we want to keep GSON? doesn't support records -.- + var request = HttpRequest.newBuilder(keyUri) // + .header("Authorization", "Bearer " + bearerToken) // + .header("Content-Type", "application/json").PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // + .build(); + httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()) // + .thenAcceptAsync(this::registrationSucceeded, Platform::runLater) // + .exceptionallyAsync(this::registrationFailed, Platform::runLater); + } + + private void registrationSucceeded(HttpResponse voidHttpResponse) { + LOG.info("Registered!"); + } + + private Void registrationFailed(Throwable cause) { + result.interacted(HubKeyLoadingModule.HubLoadingResult.FAILED); + LOG.error("Key retrieval failed", cause); + // TODO errorComponent.cause(cause).window(window).build().showErrorScene(); + return null; } @FXML @@ -66,20 +101,11 @@ public class RegisterDeviceController implements FxController { } } - private static String computeVerificationHash(String input) { - try { - var digest = MessageDigest.getInstance("SHA-256"); - digest.update(StandardCharsets.UTF_8.encode(input)); - return BaseEncoding.base64Url().omitPadding().encode(digest.digest()); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Every implementation of the Java platform is required to support SHA-256."); - } - } - /* Getter */ - public String getVerificationCode() { - return verificationCode; + public String getUserName() { + return jwt.getClaim("email").asString(); } + } diff --git a/src/main/resources/fxml/hub_register_device.fxml b/src/main/resources/fxml/hub_register_device.fxml index edf7249e6..2fd5ceaa5 100644 --- a/src/main/resources/fxml/hub_register_device.fxml +++ b/src/main/resources/fxml/hub_register_device.fxml @@ -1,16 +1,15 @@ - + + - - - @@ -42,11 +44,7 @@ + diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index a7f59b63d..4b8db7eda 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -136,6 +136,7 @@ hub.receive.description=Cryptomator is receiving and processing the response fro hub.register.message=Device name required hub.register.description=This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device. hub.register.nameLabel=Device Name +hub.register.occupiedMsg=Name already in use hub.register.registerBtn=Confirm ### Registration Success hub.registerSuccess.message=Device named From a7fc8d6fc4e695833d69dbfb553469c56ebfdcee Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Fri, 8 Jul 2022 12:03:38 +0200 Subject: [PATCH 54/55] hide alreadyExistingLabel when deviceName changed by user --- .../cryptomator/ui/keyloading/hub/RegisterDeviceController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java index 3f45c89e8..bd997d994 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java @@ -81,6 +81,7 @@ public class RegisterDeviceController implements FxController { public void initialize() { deviceNameField.setText(determineHostname()); + deviceNameField.textProperty().addListener(observable -> deviceNameAlreadyExists.set(false)); } private String determineHostname() { From 422077ac08cc289bf347289bdaa8f930cf835912 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 20 Jul 2022 17:01:29 +0200 Subject: [PATCH 55/55] adjust pom --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 43db1ff55..1aee92cdd 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,7 @@ 2.9.0 18.0.1 4.0.0 + 9.23 1.2.11 1.7.36 0.5.1 @@ -154,7 +155,7 @@ com.nimbusds nimbus-jose-jwt - 9.23 + ${nimbus-jose.version}