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