Skip to content

Commit

Permalink
Merge branch 'release/1.1.3' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Apr 19, 2021
2 parents 363d728 + c6f1c81 commit 90f8aa7
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish-github.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/${{ github.repository }}/actions/workflows/publish-central.yml|deploy to Maven Central>."
SLACK_FOOTER:
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>cloud-access</artifactId>
<version>1.1.2</version>
<version>1.1.3</version>

<name>Cryptomator CloudAccess in Java</name>
<description>CloudAccess is used in e.g. Cryptomator for Android to access different cloud providers.</description>
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/cryptomator/cloudaccess/CloudAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/org/cryptomator/cloudaccess/api/CloudProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -34,6 +35,27 @@ public interface CloudProvider {
*/
CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node);

/**
* Convenience method to check whether the given node exists by attempting to fetch its metadata.
* <p>
* 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 <code>true</code> if metadata is returned, <code>false</code> in case of a {@link org.cryptomator.cloudaccess.api.exceptions.NotFoundException}
* @since 1.1.3
*/
default CompletionStage<Boolean> 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.<Boolean>failedFuture(exception);
}
}).thenCompose(Function.identity());
}

/**
* Fetches the available, used and or total quota for a folder
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -31,53 +34,70 @@ 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()
.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();
Expand All @@ -86,14 +106,17 @@ 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()
.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();
Expand All @@ -102,20 +125,42 @@ 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);
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();

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]));
}

}
2 changes: 1 addition & 1 deletion src/test/resources/vaultconfig.jwt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjaXBoZXJDb21ibyI6IlNJVl9HQ00iLCJmb3JtYXQiOjgsImp0aSI6IjExMTExMTExLTIyMjItMzMzMy00NDQ0LTU1NTU1NTU1NTU1NSJ9.3vSf-eTUoJU8AppBc_sn1TEiGhnUn3Ds_4qT9L0sQ6o
eyJraWQiOiJjbGFwOjExMTExMTExLTIyMjItMzMzMy00NDQ0LTU1NTU1NTU1NTU1NSIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIxNDc0ODM2NDcsImp0aSI6IjExMTExMTExLTIyMjItMzMzMy00NDQ0LTU1NTU1NTU1NTU1NSIsImNpcGhlckNvbWJvIjoiU0lWX0dDTSJ9.M3VO9EXbGGAJIyfSbZwDg-NaKvprBY_NO1BupuvtiVU

0 comments on commit 90f8aa7

Please sign in to comment.