From 8039d11033146dacb875fc9dc794bae5ce9d652b Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Apr 2021 13:58:42 +0200 Subject: [PATCH 1/6] updated slack notification channel [ci skip] --- .github/workflows/publish-github.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml index c74c504..f65726a 100644 --- a/.github/workflows/publish-github.yml +++ b/.github/workflows/publish-github.yml @@ -33,7 +33,7 @@ jobs: SLACK_USERNAME: 'Cryptobot' SLACK_ICON: SLACK_ICON_EMOJI: ':bot:' - SLACK_CHANNEL: 'cryptomator-desktop' + SLACK_CHANNEL: 'proj-clap' SLACK_TITLE: "Published ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}" SLACK_MESSAGE: "Ready to ." SLACK_FOOTER: From 2b3456cf95443757e42300332d92cc16b8cb5a3e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Apr 2021 15:09:24 +0200 Subject: [PATCH 2/6] added new convenience method --- .../cloudaccess/api/CloudProvider.java | 22 +++++++++++++ .../cloudaccess/api/CloudProviderTest.java | 31 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/main/java/org/cryptomator/cloudaccess/api/CloudProvider.java b/src/main/java/org/cryptomator/cloudaccess/api/CloudProvider.java index 32411a0..a3f2163 100644 --- a/src/main/java/org/cryptomator/cloudaccess/api/CloudProvider.java +++ b/src/main/java/org/cryptomator/cloudaccess/api/CloudProvider.java @@ -2,6 +2,7 @@ import org.cryptomator.cloudaccess.api.exceptions.AlreadyExistsException; import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException; +import org.cryptomator.cloudaccess.api.exceptions.NotFoundException; import java.io.InputStream; import java.time.Instant; @@ -34,6 +35,27 @@ public interface CloudProvider { */ CompletionStage itemMetadata(CloudPath node); + /** + * Convenience method to check whether the given node exists by attempting to fetch its metadata. + *

+ * The returned CompletionState might fail with a {@link CloudProviderException} in case of generic I/O errors. + * + * @param node The remote path of the file or folder, whose metadata to fetch. + * @return true if metadata is returned, false in case of a {@link org.cryptomator.cloudaccess.api.exceptions.NotFoundException} + * @since 1.1.3 + */ + default CompletionStage exists(CloudPath node) { + return itemMetadata(node).handle((result, exception) -> { + if (result != null) { + return CompletableFuture.completedFuture(true); + } else if (exception instanceof NotFoundException) { + return CompletableFuture.completedFuture(false); + } else { + return CompletableFuture.failedFuture(exception); + } + }).thenCompose(Function.identity()); + } + /** * Fetches the available, used and or total quota for a folder *

diff --git a/src/test/java/org/cryptomator/cloudaccess/api/CloudProviderTest.java b/src/test/java/org/cryptomator/cloudaccess/api/CloudProviderTest.java index 1bef3c9..bf85f4d 100644 --- a/src/test/java/org/cryptomator/cloudaccess/api/CloudProviderTest.java +++ b/src/test/java/org/cryptomator/cloudaccess/api/CloudProviderTest.java @@ -1,6 +1,7 @@ package org.cryptomator.cloudaccess.api; import org.cryptomator.cloudaccess.api.exceptions.AlreadyExistsException; +import org.cryptomator.cloudaccess.api.exceptions.NotFoundException; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; @@ -82,4 +83,34 @@ public void testCreateFolderIfNonExisting2() { Assertions.assertEquals(path, result); } + @Test + @DisplayName("exists() for existing node") + public void testExists1() { + var provider = Mockito.mock(CloudProvider.class); + var path = Mockito.mock(CloudPath.class, "/path/to/node"); + var metadata = Mockito.mock(CloudItemMetadata.class); + Mockito.when(provider.itemMetadata(path)).thenReturn(CompletableFuture.completedFuture(metadata)); + Mockito.when(provider.exists(Mockito.any())).thenCallRealMethod(); + + var futureResult = provider.exists(path); + var result = Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> futureResult.toCompletableFuture().get()); + + Assertions.assertTrue(result); + } + + @Test + @DisplayName("exists() for non-existing node") + public void testExists2() { + var provider = Mockito.mock(CloudProvider.class); + var path = Mockito.mock(CloudPath.class, "/path/to/node"); + var e = new NotFoundException("/path/to/node"); + Mockito.when(provider.itemMetadata(path)).thenReturn(CompletableFuture.failedFuture(e)); + Mockito.when(provider.exists(Mockito.any())).thenCallRealMethod(); + + var futureResult = provider.exists(path); + var result = Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> futureResult.toCompletableFuture().get()); + + Assertions.assertFalse(result); + } + } From 670817f548f81aa51c770cabb710f94826cb531e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Apr 2021 15:10:28 +0200 Subject: [PATCH 3/6] updated vault config jwt to reflect latest spec changes --- src/test/resources/vaultconfig.jwt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/vaultconfig.jwt b/src/test/resources/vaultconfig.jwt index 90743f9..e27a04c 100644 --- a/src/test/resources/vaultconfig.jwt +++ b/src/test/resources/vaultconfig.jwt @@ -1 +1 @@ -eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjaXBoZXJDb21ibyI6IlNJVl9HQ00iLCJmb3JtYXQiOjgsImp0aSI6IjExMTExMTExLTIyMjItMzMzMy00NDQ0LTU1NTU1NTU1NTU1NSJ9.3vSf-eTUoJU8AppBc_sn1TEiGhnUn3Ds_4qT9L0sQ6o \ No newline at end of file +eyJraWQiOiJjbGFwOjExMTExMTExLTIyMjItMzMzMy00NDQ0LTU1NTU1NTU1NTU1NSIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIxNDc0ODM2NDcsImp0aSI6IjExMTExMTExLTIyMjItMzMzMy00NDQ0LTU1NTU1NTU1NTU1NSIsImNpcGhlckNvbWJvIjoiU0lWX0dDTSJ9.M3VO9EXbGGAJIyfSbZwDg-NaKvprBY_NO1BupuvtiVU \ No newline at end of file From 412cd82b6e42c76a41b9ee99f055cbea9680d06b Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Apr 2021 15:11:59 +0200 Subject: [PATCH 4/6] restructured some tests --- .../VaultFormat8IntegrationTest.java | 77 ++++++++++++------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/src/test/java/org/cryptomator/cloudaccess/vaultformat8/VaultFormat8IntegrationTest.java b/src/test/java/org/cryptomator/cloudaccess/vaultformat8/VaultFormat8IntegrationTest.java index 9586199..9e61340 100644 --- a/src/test/java/org/cryptomator/cloudaccess/vaultformat8/VaultFormat8IntegrationTest.java +++ b/src/test/java/org/cryptomator/cloudaccess/vaultformat8/VaultFormat8IntegrationTest.java @@ -12,7 +12,10 @@ import org.cryptomator.cloudaccess.api.exceptions.VaultVerificationFailedException; import org.cryptomator.cloudaccess.api.exceptions.VaultVersionVerificationFailedException; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -31,47 +34,63 @@ public class VaultFormat8IntegrationTest { private static final Duration TIMEOUT = Duration.ofMillis(100); private CloudProvider localProvider; - private CloudProvider encryptedProvider; @BeforeEach public void setup(@TempDir Path tmpDir) throws IOException { this.localProvider = CloudAccess.toLocalFileSystem(tmpDir); - var in = getClass().getResourceAsStream("/vaultconfig.jwt"); - localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join(); - this.encryptedProvider = CloudAccess.vaultFormat8GCMCloudAccess(localProvider, CloudPath.of("/"), new byte[64]); } - @Test - public void testWriteThenReadFile() throws IOException { - var path = CloudPath.of("/file.txt"); - var content = new byte[100_000]; - new Random(42l).nextBytes(content); - - // write 100k - var futureMetadata = encryptedProvider.write(path, true, new ByteArrayInputStream(content), content.length, Optional.empty(), ProgressListener.NO_PROGRESS_AWARE); - Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureMetadata.toCompletableFuture().get()); - - // read all bytes - var futureInputStream1 = encryptedProvider.read(path, ProgressListener.NO_PROGRESS_AWARE); - var inputStream1 = Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureInputStream1.toCompletableFuture().get()); - Assertions.assertArrayEquals(content, inputStream1.readAllBytes()); - - // read partially - var futureInputStream2 = encryptedProvider.read(path, 2000, 15000, ProgressListener.NO_PROGRESS_AWARE); - var inputStream2 = Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureInputStream2.toCompletableFuture().get()); - Assertions.assertArrayEquals(Arrays.copyOfRange(content, 2000, 17000), inputStream2.readAllBytes()); + @Nested + @DisplayName("with valid /vaultconfig.jwt") + public class WithInitializedVaultConfig { + + private CloudProvider encryptedProvider; + + @BeforeEach + public void setup() throws IOException { + var in = getClass().getResourceAsStream("/vaultconfig.jwt"); + localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join(); + this.encryptedProvider = CloudAccess.vaultFormat8GCMCloudAccess(localProvider, CloudPath.of("/"), new byte[64]); + } + + @Test + @DisplayName("read and write through encryption decorator") + public void testWriteThenReadFile() throws IOException { + var path = CloudPath.of("/file.txt"); + var content = new byte[100_000]; + new Random(42l).nextBytes(content); + + // write 100k + var futureMetadata = encryptedProvider.write(path, true, new ByteArrayInputStream(content), content.length, Optional.empty(), ProgressListener.NO_PROGRESS_AWARE); + Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureMetadata.toCompletableFuture().get()); + + // read all bytes + var futureInputStream1 = encryptedProvider.read(path, ProgressListener.NO_PROGRESS_AWARE); + var inputStream1 = Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureInputStream1.toCompletableFuture().get()); + Assertions.assertArrayEquals(content, inputStream1.readAllBytes()); + + // read partially + var futureInputStream2 = encryptedProvider.read(path, 2000, 15000, ProgressListener.NO_PROGRESS_AWARE); + var inputStream2 = Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureInputStream2.toCompletableFuture().get()); + Assertions.assertArrayEquals(Arrays.copyOfRange(content, 2000, 17000), inputStream2.readAllBytes()); + } + } @Test + @DisplayName("init with missing /vaultconfig.jwt fails") public void testInstantiateFormat8GCMCloudAccessWithoutVaultConfigFile() { - localProvider.deleteFile(CloudPath.of("/vaultconfig.jwt")); + Assumptions.assumeFalse(localProvider.exists(CloudPath.of("/vaultconfig.jwt")).toCompletableFuture().join()); + var exception = Assertions.assertThrows(CloudProviderException.class, () -> CloudAccess.vaultFormat8GCMCloudAccess(localProvider, CloudPath.of("/"), new byte[64])); Assertions.assertTrue(exception.getCause() instanceof NotFoundException); } @Test + @DisplayName("init with wrong format") public void testInstantiateFormat8GCMCloudAccessWithWrongVaultVersion() { - localProvider.deleteFile(CloudPath.of("/vaultconfig.jwt")); + Assumptions.assumeFalse(localProvider.exists(CloudPath.of("/vaultconfig.jwt")).toCompletableFuture().join()); + byte[] masterkey = new byte[64]; Algorithm algorithm = Algorithm.HMAC256(masterkey); var token = JWT.create() @@ -86,8 +105,10 @@ public void testInstantiateFormat8GCMCloudAccessWithWrongVaultVersion() { } @Test + @DisplayName("init with invalid cipherCombo fails") public void testInstantiateFormat8GCMCloudAccessWithWrongCiphermode() { - localProvider.deleteFile(CloudPath.of("/vaultconfig.jwt")); + Assumptions.assumeFalse(localProvider.exists(CloudPath.of("/vaultconfig.jwt")).toCompletableFuture().join()); + byte[] masterkey = new byte[64]; Algorithm algorithm = Algorithm.HMAC256(masterkey); var token = JWT.create() @@ -102,8 +123,10 @@ public void testInstantiateFormat8GCMCloudAccessWithWrongCiphermode() { } @Test + @DisplayName("init with wrong key") public void testInstantiateFormat8GCMCloudAccessWithWrongKey() { - localProvider.deleteFile(CloudPath.of("/vaultconfig.jwt")); + Assumptions.assumeFalse(localProvider.exists(CloudPath.of("/vaultconfig.jwt")).toCompletableFuture().join()); + byte[] masterkey = new byte[64]; Arrays.fill(masterkey, (byte) 15); Algorithm algorithm = Algorithm.HMAC256(masterkey); From a6ecef11735b132c3875cd39e3b108d6cd4abf24 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Apr 2021 15:57:18 +0200 Subject: [PATCH 5/6] fail fast if encountering a shorteningThreshold other than MAXINT because c9s support is not currently implemented --- .../cryptomator/cloudaccess/CloudAccess.java | 1 + .../VaultFormat8IntegrationTest.java | 24 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/cloudaccess/CloudAccess.java b/src/main/java/org/cryptomator/cloudaccess/CloudAccess.java index 759ccd0..c16b6e3 100644 --- a/src/main/java/org/cryptomator/cloudaccess/CloudAccess.java +++ b/src/main/java/org/cryptomator/cloudaccess/CloudAccess.java @@ -75,6 +75,7 @@ private static void verifyVaultFormat8GCMConfig(CloudProvider cloudProvider, Clo var verifier = JWT.require(algorithm) .withClaim("format", 8) .withClaim("cipherCombo", "SIV_GCM") + .withClaim("shorteningThreshold", Integer.MAX_VALUE) // no shortening supported atm .build(); var read = cloudProvider.read(vaultConfigPath, ProgressListener.NO_PROGRESS_AWARE); diff --git a/src/test/java/org/cryptomator/cloudaccess/vaultformat8/VaultFormat8IntegrationTest.java b/src/test/java/org/cryptomator/cloudaccess/vaultformat8/VaultFormat8IntegrationTest.java index 9e61340..e7405f3 100644 --- a/src/test/java/org/cryptomator/cloudaccess/vaultformat8/VaultFormat8IntegrationTest.java +++ b/src/test/java/org/cryptomator/cloudaccess/vaultformat8/VaultFormat8IntegrationTest.java @@ -97,6 +97,7 @@ public void testInstantiateFormat8GCMCloudAccessWithWrongVaultVersion() { .withJWTId(UUID.randomUUID().toString()) .withClaim("format", 9) .withClaim("cipherCombo", "SIV_GCM") + .withClaim("shorteningThreshold", Integer.MAX_VALUE) .sign(algorithm); var in = new ByteArrayInputStream(token.getBytes(StandardCharsets.US_ASCII)); localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join(); @@ -115,6 +116,7 @@ public void testInstantiateFormat8GCMCloudAccessWithWrongCiphermode() { .withJWTId(UUID.randomUUID().toString()) .withClaim("format", 8) .withClaim("cipherCombo", "FOO") + .withClaim("shorteningThreshold", Integer.MAX_VALUE) .sign(algorithm); var in = new ByteArrayInputStream(token.getBytes(StandardCharsets.US_ASCII)); localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join(); @@ -133,7 +135,8 @@ public void testInstantiateFormat8GCMCloudAccessWithWrongKey() { var token = JWT.create() .withJWTId(UUID.randomUUID().toString()) .withClaim("format", 8) - .withClaim("cipherCombo", "FOO") + .withClaim("cipherCombo", "SIV_GCM") + .withClaim("shorteningThreshold", Integer.MAX_VALUE) .sign(algorithm); var in = new ByteArrayInputStream(token.getBytes(StandardCharsets.US_ASCII)); localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join(); @@ -141,4 +144,23 @@ public void testInstantiateFormat8GCMCloudAccessWithWrongKey() { Assertions.assertThrows(VaultKeyVerificationFailedException.class, () -> CloudAccess.vaultFormat8GCMCloudAccess(localProvider, CloudPath.of("/"), new byte[64])); } + @Test + @DisplayName("init with shorteningThreshold") + public void testInstantiateFormat8GCMCloudAccessWithShortening() { + Assumptions.assumeFalse(localProvider.exists(CloudPath.of("/vaultconfig.jwt")).toCompletableFuture().join()); + + byte[] masterkey = new byte[64]; + Algorithm algorithm = Algorithm.HMAC256(masterkey); + var token = JWT.create() + .withJWTId(UUID.randomUUID().toString()) + .withClaim("format", 8) + .withClaim("cipherCombo", "SIV_GCM") + .withClaim("shorteningThreshold", 42) + .sign(algorithm); + var in = new ByteArrayInputStream(token.getBytes(StandardCharsets.US_ASCII)); + localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join(); + + Assertions.assertThrows(VaultVerificationFailedException.class, () -> CloudAccess.vaultFormat8GCMCloudAccess(localProvider, CloudPath.of("/"), new byte[64])); + } + } From c6f1c81ab9e969ef22e53f6729c20d502b5998f1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Apr 2021 15:58:02 +0200 Subject: [PATCH 6/6] preparing 1.1.3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8cc604d..fe160b9 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.cryptomator cloud-access - 1.2.0-SNAPSHOT + 1.1.3 Cryptomator CloudAccess in Java CloudAccess is used in e.g. Cryptomator for Android to access different cloud providers.