Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.

Commit 90f8aa7

Browse files
Merge branch 'release/1.1.3' into main
2 parents 363d728 + c6f1c81 commit 90f8aa7

File tree

7 files changed

+130
-31
lines changed

7 files changed

+130
-31
lines changed

.github/workflows/publish-github.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
SLACK_USERNAME: 'Cryptobot'
3434
SLACK_ICON:
3535
SLACK_ICON_EMOJI: ':bot:'
36-
SLACK_CHANNEL: 'cryptomator-desktop'
36+
SLACK_CHANNEL: 'proj-clap'
3737
SLACK_TITLE: "Published ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}"
3838
SLACK_MESSAGE: "Ready to <https://github.com/${{ github.repository }}/actions/workflows/publish-central.yml|deploy to Maven Central>."
3939
SLACK_FOOTER:

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<modelVersion>4.0.0</modelVersion>
66
<groupId>org.cryptomator</groupId>
77
<artifactId>cloud-access</artifactId>
8-
<version>1.1.2</version>
8+
<version>1.1.3</version>
99

1010
<name>Cryptomator CloudAccess in Java</name>
1111
<description>CloudAccess is used in e.g. Cryptomator for Android to access different cloud providers.</description>

src/main/java/org/cryptomator/cloudaccess/CloudAccess.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ private static void verifyVaultFormat8GCMConfig(CloudProvider cloudProvider, Clo
7575
var verifier = JWT.require(algorithm)
7676
.withClaim("format", 8)
7777
.withClaim("cipherCombo", "SIV_GCM")
78+
.withClaim("shorteningThreshold", Integer.MAX_VALUE) // no shortening supported atm
7879
.build();
7980

8081
var read = cloudProvider.read(vaultConfigPath, ProgressListener.NO_PROGRESS_AWARE);

src/main/java/org/cryptomator/cloudaccess/api/CloudProvider.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.cryptomator.cloudaccess.api.exceptions.AlreadyExistsException;
44
import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException;
5+
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
56

67
import java.io.InputStream;
78
import java.time.Instant;
@@ -34,6 +35,27 @@ public interface CloudProvider {
3435
*/
3536
CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node);
3637

38+
/**
39+
* Convenience method to check whether the given node exists by attempting to fetch its metadata.
40+
* <p>
41+
* The returned CompletionState might fail with a {@link CloudProviderException} in case of generic I/O errors.
42+
*
43+
* @param node The remote path of the file or folder, whose metadata to fetch.
44+
* @return <code>true</code> if metadata is returned, <code>false</code> in case of a {@link org.cryptomator.cloudaccess.api.exceptions.NotFoundException}
45+
* @since 1.1.3
46+
*/
47+
default CompletionStage<Boolean> exists(CloudPath node) {
48+
return itemMetadata(node).handle((result, exception) -> {
49+
if (result != null) {
50+
return CompletableFuture.completedFuture(true);
51+
} else if (exception instanceof NotFoundException) {
52+
return CompletableFuture.completedFuture(false);
53+
} else {
54+
return CompletableFuture.<Boolean>failedFuture(exception);
55+
}
56+
}).thenCompose(Function.identity());
57+
}
58+
3759
/**
3860
* Fetches the available, used and or total quota for a folder
3961
* <p>

src/test/java/org/cryptomator/cloudaccess/api/CloudProviderTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.cryptomator.cloudaccess.api;
22

33
import org.cryptomator.cloudaccess.api.exceptions.AlreadyExistsException;
4+
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
45
import org.hamcrest.CoreMatchers;
56
import org.hamcrest.MatcherAssert;
67
import org.junit.jupiter.api.Assertions;
@@ -82,4 +83,34 @@ public void testCreateFolderIfNonExisting2() {
8283
Assertions.assertEquals(path, result);
8384
}
8485

86+
@Test
87+
@DisplayName("exists() for existing node")
88+
public void testExists1() {
89+
var provider = Mockito.mock(CloudProvider.class);
90+
var path = Mockito.mock(CloudPath.class, "/path/to/node");
91+
var metadata = Mockito.mock(CloudItemMetadata.class);
92+
Mockito.when(provider.itemMetadata(path)).thenReturn(CompletableFuture.completedFuture(metadata));
93+
Mockito.when(provider.exists(Mockito.any())).thenCallRealMethod();
94+
95+
var futureResult = provider.exists(path);
96+
var result = Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> futureResult.toCompletableFuture().get());
97+
98+
Assertions.assertTrue(result);
99+
}
100+
101+
@Test
102+
@DisplayName("exists() for non-existing node")
103+
public void testExists2() {
104+
var provider = Mockito.mock(CloudProvider.class);
105+
var path = Mockito.mock(CloudPath.class, "/path/to/node");
106+
var e = new NotFoundException("/path/to/node");
107+
Mockito.when(provider.itemMetadata(path)).thenReturn(CompletableFuture.failedFuture(e));
108+
Mockito.when(provider.exists(Mockito.any())).thenCallRealMethod();
109+
110+
var futureResult = provider.exists(path);
111+
var result = Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> futureResult.toCompletableFuture().get());
112+
113+
Assertions.assertFalse(result);
114+
}
115+
85116
}

src/test/java/org/cryptomator/cloudaccess/vaultformat8/VaultFormat8IntegrationTest.java

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
import org.cryptomator.cloudaccess.api.exceptions.VaultVerificationFailedException;
1313
import org.cryptomator.cloudaccess.api.exceptions.VaultVersionVerificationFailedException;
1414
import org.junit.jupiter.api.Assertions;
15+
import org.junit.jupiter.api.Assumptions;
1516
import org.junit.jupiter.api.BeforeEach;
17+
import org.junit.jupiter.api.DisplayName;
18+
import org.junit.jupiter.api.Nested;
1619
import org.junit.jupiter.api.Test;
1720
import org.junit.jupiter.api.io.TempDir;
1821

@@ -31,53 +34,70 @@ public class VaultFormat8IntegrationTest {
3134
private static final Duration TIMEOUT = Duration.ofMillis(100);
3235

3336
private CloudProvider localProvider;
34-
private CloudProvider encryptedProvider;
3537

3638
@BeforeEach
3739
public void setup(@TempDir Path tmpDir) throws IOException {
3840
this.localProvider = CloudAccess.toLocalFileSystem(tmpDir);
39-
var in = getClass().getResourceAsStream("/vaultconfig.jwt");
40-
localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join();
41-
this.encryptedProvider = CloudAccess.vaultFormat8GCMCloudAccess(localProvider, CloudPath.of("/"), new byte[64]);
4241
}
4342

44-
@Test
45-
public void testWriteThenReadFile() throws IOException {
46-
var path = CloudPath.of("/file.txt");
47-
var content = new byte[100_000];
48-
new Random(42l).nextBytes(content);
49-
50-
// write 100k
51-
var futureMetadata = encryptedProvider.write(path, true, new ByteArrayInputStream(content), content.length, Optional.empty(), ProgressListener.NO_PROGRESS_AWARE);
52-
Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureMetadata.toCompletableFuture().get());
53-
54-
// read all bytes
55-
var futureInputStream1 = encryptedProvider.read(path, ProgressListener.NO_PROGRESS_AWARE);
56-
var inputStream1 = Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureInputStream1.toCompletableFuture().get());
57-
Assertions.assertArrayEquals(content, inputStream1.readAllBytes());
58-
59-
// read partially
60-
var futureInputStream2 = encryptedProvider.read(path, 2000, 15000, ProgressListener.NO_PROGRESS_AWARE);
61-
var inputStream2 = Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureInputStream2.toCompletableFuture().get());
62-
Assertions.assertArrayEquals(Arrays.copyOfRange(content, 2000, 17000), inputStream2.readAllBytes());
43+
@Nested
44+
@DisplayName("with valid /vaultconfig.jwt")
45+
public class WithInitializedVaultConfig {
46+
47+
private CloudProvider encryptedProvider;
48+
49+
@BeforeEach
50+
public void setup() throws IOException {
51+
var in = getClass().getResourceAsStream("/vaultconfig.jwt");
52+
localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join();
53+
this.encryptedProvider = CloudAccess.vaultFormat8GCMCloudAccess(localProvider, CloudPath.of("/"), new byte[64]);
54+
}
55+
56+
@Test
57+
@DisplayName("read and write through encryption decorator")
58+
public void testWriteThenReadFile() throws IOException {
59+
var path = CloudPath.of("/file.txt");
60+
var content = new byte[100_000];
61+
new Random(42l).nextBytes(content);
62+
63+
// write 100k
64+
var futureMetadata = encryptedProvider.write(path, true, new ByteArrayInputStream(content), content.length, Optional.empty(), ProgressListener.NO_PROGRESS_AWARE);
65+
Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureMetadata.toCompletableFuture().get());
66+
67+
// read all bytes
68+
var futureInputStream1 = encryptedProvider.read(path, ProgressListener.NO_PROGRESS_AWARE);
69+
var inputStream1 = Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureInputStream1.toCompletableFuture().get());
70+
Assertions.assertArrayEquals(content, inputStream1.readAllBytes());
71+
72+
// read partially
73+
var futureInputStream2 = encryptedProvider.read(path, 2000, 15000, ProgressListener.NO_PROGRESS_AWARE);
74+
var inputStream2 = Assertions.assertTimeoutPreemptively(TIMEOUT, () -> futureInputStream2.toCompletableFuture().get());
75+
Assertions.assertArrayEquals(Arrays.copyOfRange(content, 2000, 17000), inputStream2.readAllBytes());
76+
}
77+
6378
}
6479

6580
@Test
81+
@DisplayName("init with missing /vaultconfig.jwt fails")
6682
public void testInstantiateFormat8GCMCloudAccessWithoutVaultConfigFile() {
67-
localProvider.deleteFile(CloudPath.of("/vaultconfig.jwt"));
83+
Assumptions.assumeFalse(localProvider.exists(CloudPath.of("/vaultconfig.jwt")).toCompletableFuture().join());
84+
6885
var exception = Assertions.assertThrows(CloudProviderException.class, () -> CloudAccess.vaultFormat8GCMCloudAccess(localProvider, CloudPath.of("/"), new byte[64]));
6986
Assertions.assertTrue(exception.getCause() instanceof NotFoundException);
7087
}
7188

7289
@Test
90+
@DisplayName("init with wrong format")
7391
public void testInstantiateFormat8GCMCloudAccessWithWrongVaultVersion() {
74-
localProvider.deleteFile(CloudPath.of("/vaultconfig.jwt"));
92+
Assumptions.assumeFalse(localProvider.exists(CloudPath.of("/vaultconfig.jwt")).toCompletableFuture().join());
93+
7594
byte[] masterkey = new byte[64];
7695
Algorithm algorithm = Algorithm.HMAC256(masterkey);
7796
var token = JWT.create()
7897
.withJWTId(UUID.randomUUID().toString())
7998
.withClaim("format", 9)
8099
.withClaim("cipherCombo", "SIV_GCM")
100+
.withClaim("shorteningThreshold", Integer.MAX_VALUE)
81101
.sign(algorithm);
82102
var in = new ByteArrayInputStream(token.getBytes(StandardCharsets.US_ASCII));
83103
localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join();
@@ -86,14 +106,17 @@ public void testInstantiateFormat8GCMCloudAccessWithWrongVaultVersion() {
86106
}
87107

88108
@Test
109+
@DisplayName("init with invalid cipherCombo fails")
89110
public void testInstantiateFormat8GCMCloudAccessWithWrongCiphermode() {
90-
localProvider.deleteFile(CloudPath.of("/vaultconfig.jwt"));
111+
Assumptions.assumeFalse(localProvider.exists(CloudPath.of("/vaultconfig.jwt")).toCompletableFuture().join());
112+
91113
byte[] masterkey = new byte[64];
92114
Algorithm algorithm = Algorithm.HMAC256(masterkey);
93115
var token = JWT.create()
94116
.withJWTId(UUID.randomUUID().toString())
95117
.withClaim("format", 8)
96118
.withClaim("cipherCombo", "FOO")
119+
.withClaim("shorteningThreshold", Integer.MAX_VALUE)
97120
.sign(algorithm);
98121
var in = new ByteArrayInputStream(token.getBytes(StandardCharsets.US_ASCII));
99122
localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join();
@@ -102,20 +125,42 @@ public void testInstantiateFormat8GCMCloudAccessWithWrongCiphermode() {
102125
}
103126

104127
@Test
128+
@DisplayName("init with wrong key")
105129
public void testInstantiateFormat8GCMCloudAccessWithWrongKey() {
106-
localProvider.deleteFile(CloudPath.of("/vaultconfig.jwt"));
130+
Assumptions.assumeFalse(localProvider.exists(CloudPath.of("/vaultconfig.jwt")).toCompletableFuture().join());
131+
107132
byte[] masterkey = new byte[64];
108133
Arrays.fill(masterkey, (byte) 15);
109134
Algorithm algorithm = Algorithm.HMAC256(masterkey);
110135
var token = JWT.create()
111136
.withJWTId(UUID.randomUUID().toString())
112137
.withClaim("format", 8)
113-
.withClaim("cipherCombo", "FOO")
138+
.withClaim("cipherCombo", "SIV_GCM")
139+
.withClaim("shorteningThreshold", Integer.MAX_VALUE)
114140
.sign(algorithm);
115141
var in = new ByteArrayInputStream(token.getBytes(StandardCharsets.US_ASCII));
116142
localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join();
117143

118144
Assertions.assertThrows(VaultKeyVerificationFailedException.class, () -> CloudAccess.vaultFormat8GCMCloudAccess(localProvider, CloudPath.of("/"), new byte[64]));
119145
}
120146

147+
@Test
148+
@DisplayName("init with shorteningThreshold")
149+
public void testInstantiateFormat8GCMCloudAccessWithShortening() {
150+
Assumptions.assumeFalse(localProvider.exists(CloudPath.of("/vaultconfig.jwt")).toCompletableFuture().join());
151+
152+
byte[] masterkey = new byte[64];
153+
Algorithm algorithm = Algorithm.HMAC256(masterkey);
154+
var token = JWT.create()
155+
.withJWTId(UUID.randomUUID().toString())
156+
.withClaim("format", 8)
157+
.withClaim("cipherCombo", "SIV_GCM")
158+
.withClaim("shorteningThreshold", 42)
159+
.sign(algorithm);
160+
var in = new ByteArrayInputStream(token.getBytes(StandardCharsets.US_ASCII));
161+
localProvider.write(CloudPath.of("/vaultconfig.jwt"), false, in, in.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join();
162+
163+
Assertions.assertThrows(VaultVerificationFailedException.class, () -> CloudAccess.vaultFormat8GCMCloudAccess(localProvider, CloudPath.of("/"), new byte[64]));
164+
}
165+
121166
}

src/test/resources/vaultconfig.jwt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjaXBoZXJDb21ibyI6IlNJVl9HQ00iLCJmb3JtYXQiOjgsImp0aSI6IjExMTExMTExLTIyMjItMzMzMy00NDQ0LTU1NTU1NTU1NTU1NSJ9.3vSf-eTUoJU8AppBc_sn1TEiGhnUn3Ds_4qT9L0sQ6o
1+
eyJraWQiOiJjbGFwOjExMTExMTExLTIyMjItMzMzMy00NDQ0LTU1NTU1NTU1NTU1NSIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIxNDc0ODM2NDcsImp0aSI6IjExMTExMTExLTIyMjItMzMzMy00NDQ0LTU1NTU1NTU1NTU1NSIsImNpcGhlckNvbWJvIjoiU0lWX0dDTSJ9.M3VO9EXbGGAJIyfSbZwDg-NaKvprBY_NO1BupuvtiVU

0 commit comments

Comments
 (0)