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

Commit 85a1e60

Browse files
committed
Merge branch 'release/1.0.0' into main
2 parents f0aa33b + c19c726 commit 85a1e60

33 files changed

+836
-265
lines changed

pom.xml

Lines changed: 9 additions & 2 deletions
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>0.3.0</version>
8+
<version>1.0.0</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>
@@ -42,6 +42,8 @@
4242
<properties>
4343
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
4444
<guava.version>29.0-jre</guava.version>
45+
<jwt.version>3.10.3</jwt.version>
46+
<cryptolib.version>1.4.0</cryptolib.version>
4547

4648
<okhttp.version>4.7.2</okhttp.version>
4749
<okhttp-digest.version>2.4</okhttp-digest.version>
@@ -85,10 +87,15 @@
8587
<artifactId>guava</artifactId>
8688
<version>${guava.version}</version>
8789
</dependency>
90+
<dependency>
91+
<groupId>com.auth0</groupId>
92+
<artifactId>java-jwt</artifactId>
93+
<version>${jwt.version}</version>
94+
</dependency>
8895
<dependency>
8996
<groupId>org.cryptomator</groupId>
9097
<artifactId>cryptolib</artifactId>
91-
<version>1.4.0-beta3</version>
98+
<version>${cryptolib.version}</version>
9299
</dependency>
93100

94101
<!-- Logging -->

src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@
1010
requires okhttp3;
1111
requires okhttp.digest;
1212
requires okio;
13+
requires java.jwt;
1314
}

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

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
package org.cryptomator.cloudaccess;
22

3+
import com.auth0.jwt.JWT;
4+
import com.auth0.jwt.algorithms.Algorithm;
5+
import com.auth0.jwt.exceptions.JWTVerificationException;
6+
import com.auth0.jwt.exceptions.SignatureVerificationException;
7+
import com.google.common.base.Preconditions;
38
import org.cryptomator.cloudaccess.api.CloudPath;
49
import org.cryptomator.cloudaccess.api.CloudProvider;
10+
import org.cryptomator.cloudaccess.api.ProgressListener;
511
import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException;
12+
import org.cryptomator.cloudaccess.api.exceptions.VaultKeyVerificationFailedException;
13+
import org.cryptomator.cloudaccess.api.exceptions.VaultVerificationFailedException;
14+
import org.cryptomator.cloudaccess.api.exceptions.VaultVersionVerificationFailedException;
615
import org.cryptomator.cloudaccess.localfs.LocalFsCloudProvider;
716
import org.cryptomator.cloudaccess.vaultformat8.VaultFormat8ProviderDecorator;
817
import org.cryptomator.cloudaccess.webdav.WebDavCloudProvider;
918
import org.cryptomator.cloudaccess.webdav.WebDavCredential;
1019
import org.cryptomator.cryptolib.Cryptors;
1120

21+
import java.io.IOException;
1222
import java.net.URL;
23+
import java.nio.charset.StandardCharsets;
1324
import java.nio.file.Path;
1425
import java.security.NoSuchAlgorithmException;
1526
import java.security.SecureRandom;
27+
import java.time.Duration;
1628

1729
public class CloudAccess {
1830

@@ -22,17 +34,30 @@ private CloudAccess() {
2234
/**
2335
* Decorates an existing CloudProvider by encrypting paths and file contents using Cryptomator's Vault Format 8.
2436
* Uses an externally managed masterkey, i.e. it will only validate the vault version but not parse any vault config.
37+
* <p>
38+
* This method might return with one of the following exceptions:
39+
* <ul>
40+
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.VaultVersionVerificationFailedException} If the version of the vault config isn't 8</li>
41+
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.VaultKeyVerificationFailedException} If <code>rawKey</code> doesn't match the key used to create the vault config</li>
42+
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.VaultVerificationFailedException} If the <code>ciphermode</code> in the vault config doesn't match SIV_GCM</li>
43+
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.CloudProviderException} If the general error occurred</li>
44+
* <li>{@link CloudProviderException} in case of generic I/O errors</li>
45+
* </ul>
2546
*
2647
* @param cloudProvider A CloudProvider providing access to a storage space on which to store ciphertext data
27-
* @param pathToVault Path that can be used within the given <code>cloudProvider</code> leading to the vault's root
48+
* @param pathToVault Path that can be used within the given <code>cloudProvider</code> leading to the vault's root
2849
* @param rawKey 512 bit key used for cryptographic operations
2950
* @return A cleartext view on the given CloudProvider
3051
*/
3152
public static CloudProvider vaultFormat8GCMCloudAccess(CloudProvider cloudProvider, CloudPath pathToVault, byte[] rawKey) {
53+
Preconditions.checkArgument(rawKey.length == 64, "masterkey needs to be 512 bit");
54+
3255
try {
3356
var csprng = SecureRandom.getInstanceStrong();
3457
var cryptor = Cryptors.version2(csprng).createFromRawKey(rawKey);
35-
// TODO validate vaultFormat.jwt before creating decorator
58+
59+
verifyVaultFormat8GCMConfig(cloudProvider, pathToVault, rawKey);
60+
3661
VaultFormat8ProviderDecorator provider = new VaultFormat8ProviderDecorator(cloudProvider, pathToVault.resolve("d"), cryptor);
3762
provider.initialize();
3863
return new MetadataCachingProviderDecorator(provider);
@@ -44,6 +69,32 @@ public static CloudProvider vaultFormat8GCMCloudAccess(CloudProvider cloudProvid
4469
}
4570
}
4671

72+
private static void verifyVaultFormat8GCMConfig(CloudProvider cloudProvider, CloudPath pathToVault, byte[] rawKey) {
73+
var vaultConfigPath = pathToVault.resolve("vaultconfig.jwt");
74+
var algorithm = Algorithm.HMAC256(rawKey);
75+
var verifier = JWT.require(algorithm)
76+
.withClaim("version", 8)
77+
.withClaim("ciphermode", "SIV_GCM")
78+
.build();
79+
80+
var read = cloudProvider.read(vaultConfigPath, ProgressListener.NO_PROGRESS_AWARE);
81+
try (var in = read.toCompletableFuture().join()) {
82+
var vaultConfigContents = in.readAllBytes();
83+
var token = new String(vaultConfigContents, StandardCharsets.US_ASCII);
84+
verifier.verify(token);
85+
} catch (SignatureVerificationException e) {
86+
throw new VaultKeyVerificationFailedException(e);
87+
} catch (JWTVerificationException e) {
88+
if (e.getMessage().equals("The Claim 'version' value doesn't match the required one.")) {
89+
throw new VaultVersionVerificationFailedException(e);
90+
} else {
91+
throw new VaultVerificationFailedException(e);
92+
}
93+
} catch (CloudProviderException | IOException e) {
94+
throw new CloudProviderException(e);
95+
}
96+
}
97+
4798
/**
4899
* Creates a new CloudProvider which provides access to the given URL via WebDAV.
49100
*
@@ -53,7 +104,6 @@ public static CloudProvider vaultFormat8GCMCloudAccess(CloudProvider cloudProvid
53104
* @return A cloud access provider that provides access to the given WebDAV URL
54105
*/
55106
public static CloudProvider toWebDAV(URL url, String username, CharSequence password) {
56-
// TODO can we pass though CharSequence to the auth mechanism?
57107
return WebDavCloudProvider.from(WebDavCredential.from(url, username, password.toString()));
58108
}
59109

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

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.cryptomator.cloudaccess.api.CloudPath;
88
import org.cryptomator.cloudaccess.api.CloudProvider;
99
import org.cryptomator.cloudaccess.api.ProgressListener;
10+
import org.cryptomator.cloudaccess.api.Quota;
1011
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
1112

1213
import java.io.InputStream;
@@ -18,21 +19,27 @@
1819

1920
public class MetadataCachingProviderDecorator implements CloudProvider {
2021

21-
final Cache<CloudPath, Optional<CloudItemMetadata>> metadataCache;
22+
private final static int DEFAULT_CACHE_TIMEOUT_SECONDS = 10;
23+
24+
final Cache<CloudPath, Optional<CloudItemMetadata>> itemMetadataCache;
25+
final Cache<CloudPath, Optional<Quota>> quotaCache;
2226
private final CloudProvider delegate;
2327

2428
public MetadataCachingProviderDecorator(CloudProvider delegate) {
25-
this(delegate, Duration.ofSeconds(10));
29+
this(delegate, Duration.ofSeconds( //
30+
Integer.getInteger("org.cryptomator.cloudaccess.metadatacachingprovider.timeoutSeconds", DEFAULT_CACHE_TIMEOUT_SECONDS)
31+
));
2632
}
2733

2834
public MetadataCachingProviderDecorator(CloudProvider delegate, Duration cacheEntryMaxAge) {
2935
this.delegate = delegate;
30-
this.metadataCache = CacheBuilder.newBuilder().expireAfterWrite(cacheEntryMaxAge).build();
36+
this.itemMetadataCache = CacheBuilder.newBuilder().expireAfterWrite(cacheEntryMaxAge).build();
37+
this.quotaCache = CacheBuilder.newBuilder().expireAfterWrite(cacheEntryMaxAge).build();
3138
}
3239

3340
@Override
3441
public CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node) {
35-
var cachedMetadata = metadataCache.getIfPresent(node);
42+
var cachedMetadata = itemMetadataCache.getIfPresent(node);
3643
if (cachedMetadata != null) {
3744
return cachedMetadata //
3845
.map(CompletableFuture::completedFuture) //
@@ -42,11 +49,33 @@ public CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node) {
4249
.whenComplete((metadata, exception) -> {
4350
if (exception == null) {
4451
assert metadata != null;
45-
metadataCache.put(node, Optional.of(metadata));
52+
itemMetadataCache.put(node, Optional.of(metadata));
53+
} else if (exception instanceof NotFoundException) {
54+
itemMetadataCache.put(node, Optional.empty());
55+
} else {
56+
itemMetadataCache.invalidate(node);
57+
}
58+
});
59+
}
60+
}
61+
62+
@Override
63+
public CompletionStage<Quota> quota(CloudPath folder) {
64+
var cachedMetadata = quotaCache.getIfPresent(folder);
65+
if (cachedMetadata != null) {
66+
return cachedMetadata //
67+
.map(CompletableFuture::completedFuture) //
68+
.orElseGet(() -> CompletableFuture.failedFuture(new NotFoundException()));
69+
} else {
70+
return delegate.quota(folder) //
71+
.whenComplete((quota, exception) -> {
72+
if (exception == null) {
73+
assert quota != null;
74+
quotaCache.put(folder, Optional.of(quota));
4675
} else if (exception instanceof NotFoundException) {
47-
metadataCache.put(node, Optional.empty());
76+
quotaCache.put(folder, Optional.empty());
4877
} else {
49-
metadataCache.invalidate(node);
78+
quotaCache.invalidate(folder);
5079
}
5180
});
5281
}
@@ -59,7 +88,7 @@ public CompletionStage<CloudItemList> list(CloudPath folder, Optional<String> pa
5988
evictIncludingDescendants(folder);
6089
if (exception == null) {
6190
assert cloudItemList != null;
62-
cloudItemList.getItems().forEach(metadata -> metadataCache.put(metadata.getPath(), Optional.of(metadata)));
91+
cloudItemList.getItems().forEach(metadata -> itemMetadataCache.put(metadata.getPath(), Optional.of(metadata)));
6392
}
6493
});
6594
}
@@ -69,7 +98,7 @@ public CompletionStage<InputStream> read(CloudPath file, ProgressListener progre
6998
return delegate.read(file, progressListener) //
7099
.whenComplete((metadata, exception) -> {
71100
if (exception != null) {
72-
metadataCache.invalidate(file);
101+
itemMetadataCache.invalidate(file);
73102
}
74103
});
75104
}
@@ -79,7 +108,7 @@ public CompletionStage<InputStream> read(CloudPath file, long offset, long count
79108
return delegate.read(file, offset, count, progressListener) //
80109
.whenComplete((inputStream, exception) -> {
81110
if (exception != null) {
82-
metadataCache.invalidate(file);
111+
itemMetadataCache.invalidate(file);
83112
}
84113
});
85114
}
@@ -89,7 +118,8 @@ public CompletionStage<Void> write(CloudPath file, boolean replace, InputStream
89118
return delegate.write(file, replace, data, size, lastModified, progressListener) //
90119
.whenComplete((nullReturn, exception) -> {
91120
if (exception != null) {
92-
metadataCache.invalidate(file);
121+
itemMetadataCache.invalidate(file);
122+
quotaCache.invalidateAll();
93123
}
94124
});
95125
}
@@ -98,7 +128,7 @@ public CompletionStage<Void> write(CloudPath file, boolean replace, InputStream
98128
public CompletionStage<CloudPath> createFolder(CloudPath folder) {
99129
return delegate.createFolder(folder) //
100130
.whenComplete((metadata, exception) -> {
101-
metadataCache.invalidate(folder);
131+
itemMetadataCache.invalidate(folder);
102132
});
103133
}
104134

@@ -107,22 +137,24 @@ public CompletionStage<Void> delete(CloudPath node) {
107137
return delegate.delete(node) //
108138
.whenComplete((nullReturn, exception) -> {
109139
evictIncludingDescendants(node);
140+
quotaCache.invalidateAll();
110141
});
111142
}
112143

113144
@Override
114145
public CompletionStage<CloudPath> move(CloudPath source, CloudPath target, boolean replace) {
115146
return delegate.move(source, target, replace) //
116147
.whenComplete((path, exception) -> {
117-
metadataCache.invalidate(source);
118-
metadataCache.invalidate(target);
148+
itemMetadataCache.invalidate(source);
149+
itemMetadataCache.invalidate(target);
150+
quotaCache.invalidateAll();
119151
});
120152
}
121153

122154
private void evictIncludingDescendants(CloudPath cleartextPath) {
123-
for (var path : metadataCache.asMap().keySet()) {
155+
for (var path : itemMetadataCache.asMap().keySet()) {
124156
if (path.startsWith(cleartextPath)) {
125-
metadataCache.invalidate(path);
157+
itemMetadataCache.invalidate(path);
126158
}
127159
}
128160
}

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ public interface CloudProvider {
3434
*/
3535
CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node);
3636

37+
/**
38+
* Fetches the available, used and or total quota for a folder
39+
* <p>
40+
* The returned CompletionStage might complete exceptionally with one of the following exceptions:
41+
* <ul>
42+
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.NotFoundException} If no item exists for the given path</li>
43+
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.QuotaNotAvailableException} If the quota could not be queried. </li>
44+
* <li>{@link CloudProviderException} in case of generic I/O errors</li>
45+
* </ul>
46+
*
47+
* @param folder The remote path of the folder, whose quota to fetch.
48+
* @return CompletionStage with the quota info for a folder. If the fetch fails, it completes exceptionally.
49+
*/
50+
CompletionStage<Quota> quota(CloudPath folder);
51+
3752
/**
3853
* Starts fetching the contents of a folder.
3954
* If the result's <code>CloudItemList</code> has a <code>nextPageToken</code>, calling this method again with the provided token will continue listing.
@@ -81,7 +96,7 @@ private CompletionStage<CloudItemList> listExhaustively(CloudPath folder, CloudI
8196
* The returned CompletionStage might complete exceptionally with the same exceptions as specified in {@link #read(CloudPath, long, long, ProgressListener)}.
8297
*
8398
* @param file A remote path referencing a file
84-
* @param progressListener TODO Future use
99+
* @param progressListener Future use
85100
* @return CompletionStage with an InputStream to read from. If accessing the file fails, it'll complete exceptionally.
86101
* @see #read(CloudPath, long, long, ProgressListener)
87102
*/
@@ -102,7 +117,7 @@ default CompletionStage<InputStream> read(CloudPath file, ProgressListener progr
102117
* @param file A remote path referencing a file
103118
* @param offset The first byte (inclusive) to read.
104119
* @param count The number of bytes requested. Can exceed the actual file length. Set to {@link Long#MAX_VALUE} to read till EOF.
105-
* @param progressListener TODO Future use
120+
* @param progressListener Future use
106121
* @return CompletionStage with an InputStream to read from. If accessing the file fails, it'll complete exceptionally. If the requested range cannot be fulfilled, an inputstream with 0 bytes is returned
107122
*/
108123
CompletionStage<InputStream> read(CloudPath file, long offset, long count, ProgressListener progressListener);
@@ -115,6 +130,7 @@ default CompletionStage<InputStream> read(CloudPath file, ProgressListener progr
115130
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.NotFoundException} If the parent directory of this file doesn't exist</li>
116131
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.TypeMismatchException} If the path points to a node that isn't a file</li>
117132
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.AlreadyExistsException} If a node with the given path already exists and <code>replace</code> is false</li>
133+
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.ParentFolderDoesNotExistException} If the parent folder of a node doesn't exists</li>
118134
* <li>{@link CloudProviderException} in case of generic I/O errors</li>
119135
* </ul>
120136
*
@@ -123,7 +139,7 @@ default CompletionStage<InputStream> read(CloudPath file, ProgressListener progr
123139
* @param data A data source from which to copy contents to the remote file
124140
* @param size The size of data
125141
* @param lastModified The lastModified which should be provided to the server
126-
* @param progressListener TODO Future use
142+
* @param progressListener Future use
127143
* @return CompletionStage that will be completed after writing all <code>data</code>.
128144
*/
129145
CompletionStage<Void> write(CloudPath file, boolean replace, InputStream data, long size, Optional<Instant> lastModified, ProgressListener progressListener);
@@ -185,6 +201,7 @@ default CompletionStage<CloudPath> createFolderIfNonExisting(CloudPath folder) {
185201
* <ul>
186202
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.NotFoundException} If no item exists for the given source path</li>
187203
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.AlreadyExistsException} If a node with the given target path already exists and <code>replace</code> is false</li>
204+
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.ParentFolderDoesNotExistException} If the parent folder of a node doesn't exists</li>
188205
* <li>{@link CloudProviderException} in case of generic I/O errors</li>
189206
* </ul>
190207
*

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

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)