diff --git a/azure/pom.xml b/azure/pom.xml
index 05bf0335c00..abcc4487d47 100644
--- a/azure/pom.xml
+++ b/azure/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
azure
diff --git a/backblaze/pom.xml b/backblaze/pom.xml
index 60913e04f18..f394300555b 100644
--- a/backblaze/pom.xml
+++ b/backblaze/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
backblaze
jar
diff --git a/binding/pom.xml b/binding/pom.xml
index c512d6dc0f7..b4fbbcfe611 100644
--- a/binding/pom.xml
+++ b/binding/pom.xml
@@ -19,7 +19,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
binding
jar
diff --git a/bonjour/dll/pom.xml b/bonjour/dll/pom.xml
index 125939ec011..900bb962ae3 100644
--- a/bonjour/dll/pom.xml
+++ b/bonjour/dll/pom.xml
@@ -5,7 +5,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
Cyberduck.Bonjour
pom
diff --git a/bonjour/native/pom.xml b/bonjour/native/pom.xml
index 14d952b3ce9..3e552fb5f6b 100644
--- a/bonjour/native/pom.xml
+++ b/bonjour/native/pom.xml
@@ -5,7 +5,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
Cyberduck.Bonjour.Native
pom
diff --git a/bonjour/pom.xml b/bonjour/pom.xml
index 948a4ad272a..3aaeb862396 100644
--- a/bonjour/pom.xml
+++ b/bonjour/pom.xml
@@ -18,7 +18,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
4.0.0
diff --git a/box/pom.xml b/box/pom.xml
index f4a2e48acd9..c9f8834eb12 100644
--- a/box/pom.xml
+++ b/box/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
box
diff --git a/brick/pom.xml b/brick/pom.xml
index 7450f3f5b9f..b53c5c2e1ae 100644
--- a/brick/pom.xml
+++ b/brick/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
brick
jar
diff --git a/cli/dll/pom.xml b/cli/dll/pom.xml
index aae3208a140..34f9ddd9202 100644
--- a/cli/dll/pom.xml
+++ b/cli/dll/pom.xml
@@ -20,7 +20,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
Cyberduck.Cli
pom
diff --git a/cli/linux/pom.xml b/cli/linux/pom.xml
index 494b39c0a57..935cd9ab572 100644
--- a/cli/linux/pom.xml
+++ b/cli/linux/pom.xml
@@ -20,7 +20,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
cli-linux
Cyberduck CLI Linux
diff --git a/cli/osx/pom.xml b/cli/osx/pom.xml
index fe4aa09eea9..37983296958 100644
--- a/cli/osx/pom.xml
+++ b/cli/osx/pom.xml
@@ -20,7 +20,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
cli-osx
Cyberduck CLI Mac
diff --git a/cli/pom.xml b/cli/pom.xml
index 002c8163e0c..ef0a1e4bac9 100644
--- a/cli/pom.xml
+++ b/cli/pom.xml
@@ -19,7 +19,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
cli
diff --git a/cli/windows/pom.xml b/cli/windows/pom.xml
index 59550580c54..c964e88d18e 100644
--- a/cli/windows/pom.xml
+++ b/cli/windows/pom.xml
@@ -20,7 +20,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
cli-windows
Cyberduck CLI Windows
diff --git a/core/dll/pom.xml b/core/dll/pom.xml
index f0d2e563603..b123d18f24a 100644
--- a/core/dll/pom.xml
+++ b/core/dll/pom.xml
@@ -5,7 +5,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
Cyberduck.Core
pom
diff --git a/core/dylib/pom.xml b/core/dylib/pom.xml
index 98821a4589f..77701573cea 100644
--- a/core/dylib/pom.xml
+++ b/core/dylib/pom.xml
@@ -5,7 +5,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
libcore
diff --git a/core/native/pom.xml b/core/native/pom.xml
index 35449ff8a7b..5ef67bbbc5c 100644
--- a/core/native/pom.xml
+++ b/core/native/pom.xml
@@ -5,7 +5,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
Cyberduck.Core.Native
pom
diff --git a/core/native/refresh/pom.xml b/core/native/refresh/pom.xml
index 1a9401dabee..ee129e7bd97 100644
--- a/core/native/refresh/pom.xml
+++ b/core/native/refresh/pom.xml
@@ -5,7 +5,7 @@
ch.cyberduck
Cyberduck.Core.Native
../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
Cyberduck.Core.Refresh
pom
diff --git a/core/pom.xml b/core/pom.xml
index 6f891e500e1..e38097a5de7 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
core
jar
diff --git a/core/src/main/java/ch/cyberduck/core/PathAttributes.java b/core/src/main/java/ch/cyberduck/core/PathAttributes.java
index d94ed8880c6..62eba587522 100644
--- a/core/src/main/java/ch/cyberduck/core/PathAttributes.java
+++ b/core/src/main/java/ch/cyberduck/core/PathAttributes.java
@@ -158,7 +158,7 @@ public class PathAttributes extends Attributes implements Serializable {
/**
* Unique identifier for cryptomator
*/
- private String directoryId;
+ private byte[] directoryId;
private Map custom = Collections.emptyMap();
@@ -526,11 +526,11 @@ public PathAttributes withLockId(final String lockId) {
return this;
}
- public String getDirectoryId() {
+ public byte[] getDirectoryId() {
return directoryId;
}
- public void setDirectoryId(final String directoryId) {
+ public void setDirectoryId(final byte[] directoryId) {
this.directoryId = directoryId;
}
diff --git a/cryptomator/dll/pom.xml b/cryptomator/dll/pom.xml
index e789d1b5e3f..37e27551d2d 100644
--- a/cryptomator/dll/pom.xml
+++ b/cryptomator/dll/pom.xml
@@ -20,7 +20,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
Cyberduck.Cryptomator
pom
diff --git a/cryptomator/pom.xml b/cryptomator/pom.xml
index 8875a0ac066..d78c32f369f 100644
--- a/cryptomator/pom.xml
+++ b/cryptomator/pom.xml
@@ -19,13 +19,13 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
cryptomator
jar
- 2.1.2.1
+ 2.3.0.uvfdraft-SNAPSHOT
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/AbstractVault.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/AbstractVault.java
new file mode 100644
index 00000000000..9ee926cdf7c
--- /dev/null
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/AbstractVault.java
@@ -0,0 +1,426 @@
+package ch.cyberduck.core.cryptomator;
+
+/*
+ * Copyright (c) 2002-2025 iterate GmbH. All rights reserved.
+ * https://cyberduck.io/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+import ch.cyberduck.core.ListService;
+import ch.cyberduck.core.Path;
+import ch.cyberduck.core.PathAttributes;
+import ch.cyberduck.core.Permission;
+import ch.cyberduck.core.Session;
+import ch.cyberduck.core.SimplePathPredicate;
+import ch.cyberduck.core.UrlProvider;
+import ch.cyberduck.core.cryptomator.features.*;
+import ch.cyberduck.core.exception.BackgroundException;
+import ch.cyberduck.core.features.*;
+import ch.cyberduck.core.preferences.PreferencesFactory;
+import ch.cyberduck.core.shared.DefaultTouchFeature;
+import ch.cyberduck.core.transfer.TransferStatus;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.FileContentCryptor;
+import org.cryptomator.cryptolib.api.FileHeaderCryptor;
+
+import java.util.EnumSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.google.common.io.BaseEncoding;
+
+public abstract class AbstractVault implements Vault {
+
+ private static final Logger log = LogManager.getLogger(AbstractVault.class);
+
+ public static final int VAULT_VERSION_DEPRECATED = 6;
+ public static final int VAULT_VERSION = PreferencesFactory.get().getInteger("cryptomator.vault.version");
+
+ public static final String DIR_PREFIX = "0";
+
+ private static final Pattern BASE32_PATTERN = Pattern.compile("^0?(([A-Z2-7]{8})*[A-Z2-7=]{8})");
+
+ public abstract Path getMasterkey();
+
+ public abstract Path getConfig();
+
+ public abstract int getVersion();
+
+ public abstract FileHeaderCryptor getFileHeaderCryptor();
+
+ public abstract FileContentCryptor getFileContentCryptor();
+
+ public abstract CryptorCache getFileNameCryptor();
+
+ public abstract CryptoFilename getFilenameProvider();
+
+ public abstract CryptoDirectory getDirectoryProvider();
+
+ public abstract Cryptor getCryptor();
+
+ public abstract int getNonceSize();
+
+ public int numberOfChunks(long cleartextFileSize) {
+ return (int) (cleartextFileSize / this.getFileContentCryptor().cleartextChunkSize() +
+ ((cleartextFileSize % this.getFileContentCryptor().cleartextChunkSize() > 0) ? 1 : 0));
+ }
+
+ public long toCleartextSize(final long cleartextFileOffset, final long ciphertextFileSize) throws CryptoInvalidFilesizeException {
+ if(TransferStatus.UNKNOWN_LENGTH == ciphertextFileSize) {
+ return TransferStatus.UNKNOWN_LENGTH;
+ }
+ final int headerSize;
+ if(0L == cleartextFileOffset) {
+ headerSize = this.getFileHeaderCryptor().headerSize();
+ }
+ else {
+ headerSize = 0;
+ }
+ try {
+ return this.getFileContentCryptor().cleartextSize(ciphertextFileSize - headerSize);
+ }
+ catch(AssertionError e) {
+ throw new CryptoInvalidFilesizeException(String.format("Encrypted file size must be at least %d bytes", headerSize));
+ }
+ catch(IllegalArgumentException e) {
+ throw new CryptoInvalidFilesizeException(String.format("Invalid file size. %s", e.getMessage()));
+ }
+ }
+
+ @Override
+ public State getState() {
+ return this.isUnlocked() ? State.open : State.closed;
+ }
+
+ @Override
+ public long toCiphertextSize(final long cleartextFileOffset, final long cleartextFileSize) {
+ if(TransferStatus.UNKNOWN_LENGTH == cleartextFileSize) {
+ return TransferStatus.UNKNOWN_LENGTH;
+ }
+ final int headerSize;
+ if(0L == cleartextFileOffset) {
+ headerSize = this.getCryptor().fileHeaderCryptor().headerSize();
+ }
+ else {
+ headerSize = 0;
+ }
+ return headerSize + this.getCryptor().fileContentCryptor().ciphertextSize(cleartextFileSize);
+ }
+
+ @Override
+ public Path encrypt(Session> session, Path file) throws BackgroundException {
+ return this.encrypt(session, file, file.attributes().getDirectoryId(), false);
+ }
+
+ @Override
+ public Path encrypt(Session> session, Path file, boolean metadata) throws BackgroundException {
+ return this.encrypt(session, file, file.attributes().getDirectoryId(), metadata);
+ }
+
+ public Path encrypt(Session> session, Path file, byte[] directoryId, boolean metadata) throws BackgroundException {
+ final Path encrypted;
+ if(file.isFile() || metadata) {
+ if(file.getType().contains(Path.Type.vault)) {
+ log.warn("Skip file {} because it is marked as an internal vault path", file);
+ return file;
+ }
+ if(new SimplePathPredicate(file).test(this.getHome())) {
+ log.warn("Skip vault home {} because the root has no metadata file", file);
+ return file;
+ }
+ final Path parent;
+ final String filename;
+ if(file.getType().contains(Path.Type.encrypted)) {
+ final Path decrypted = file.attributes().getDecrypted();
+ parent = this.getDirectoryProvider().toEncrypted(session, decrypted.getParent().attributes().getDirectoryId(), decrypted.getParent());
+ filename = this.getDirectoryProvider().toEncrypted(session, parent.attributes().getDirectoryId(), decrypted.getName(), decrypted.getType());
+ }
+ else {
+ parent = this.getDirectoryProvider().toEncrypted(session, file.getParent().attributes().getDirectoryId(), file.getParent());
+ filename = this.getDirectoryProvider().toEncrypted(session, parent.attributes().getDirectoryId(), file.getName(), file.getType());
+ }
+ final PathAttributes attributes = new PathAttributes(file.attributes());
+ attributes.setDirectoryId(null);
+ if(!file.isFile() && !metadata) {
+ // The directory is different from the metadata file used to resolve the actual folder
+ attributes.setVersionId(null);
+ attributes.setFileId(null);
+ }
+ // Translate file size
+ attributes.setSize(this.toCiphertextSize(0L, file.attributes().getSize()));
+ final EnumSet type = EnumSet.copyOf(file.getType());
+ if(metadata && this.getVersion() == VAULT_VERSION_DEPRECATED) {
+ type.remove(Path.Type.directory);
+ type.add(Path.Type.file);
+ }
+ type.remove(Path.Type.decrypted);
+ type.add(Path.Type.encrypted);
+ encrypted = new Path(parent, filename, type, attributes);
+ }
+ else {
+ if(file.getType().contains(Path.Type.encrypted)) {
+ log.warn("Skip file {} because it is already marked as an encrypted path", file);
+ return file;
+ }
+ if(file.getType().contains(Path.Type.vault)) {
+ return this.getDirectoryProvider().toEncrypted(session, this.getHome().attributes().getDirectoryId(), this.getHome());
+ }
+ encrypted = this.getDirectoryProvider().toEncrypted(session, directoryId, file);
+ }
+ // Add reference to decrypted file
+ if(!file.getType().contains(Path.Type.encrypted)) {
+ encrypted.attributes().setDecrypted(file);
+ }
+ // Add reference for vault
+ file.attributes().setVault(this.getHome());
+ encrypted.attributes().setVault(this.getHome());
+ return encrypted;
+ }
+
+ @Override
+ public Path decrypt(final Session> session, final Path file) throws BackgroundException {
+ if(file.getType().contains(Path.Type.decrypted)) {
+ log.warn("Skip file {} because it is already marked as an decrypted path", file);
+ return file;
+ }
+ if(file.getType().contains(Path.Type.vault)) {
+ log.warn("Skip file {} because it is marked as an internal vault path", file);
+ return file;
+ }
+ final Path inflated = this.inflate(session, file);
+ final Pattern pattern = this.getVersion() == VAULT_VERSION_DEPRECATED ? BASE32_PATTERN : this.getBase64URLPattern();
+ final Matcher m = pattern.matcher(inflated.getName());
+ if(m.matches()) {
+ final String ciphertext = m.group(1);
+ try {
+ final String cleartextFilename = this.getFileNameCryptor().decryptFilename(
+ this.getVersion() == VAULT_VERSION_DEPRECATED ? BaseEncoding.base32() : BaseEncoding.base64Url(),
+ ciphertext, file.getParent().attributes().getDirectoryId());
+ final PathAttributes attributes = new PathAttributes(file.attributes());
+ if(this.isDirectory(inflated)) {
+ if(Permission.EMPTY != attributes.getPermission()) {
+ final Permission permission = new Permission(attributes.getPermission());
+ permission.setUser(permission.getUser().or(Permission.Action.execute));
+ permission.setGroup(permission.getGroup().or(Permission.Action.execute));
+ permission.setOther(permission.getOther().or(Permission.Action.execute));
+ attributes.setPermission(permission);
+ }
+ // Reset size for folders
+ attributes.setSize(-1L);
+ attributes.setVersionId(null);
+ attributes.setFileId(null);
+ }
+ else {
+ // Translate file size
+ attributes.setSize(this.toCleartextSize(0L, file.attributes().getSize()));
+ }
+ // Add reference to encrypted file
+ attributes.setEncrypted(file);
+ // Add reference for vault
+ attributes.setVault(this.getHome());
+ final EnumSet type = EnumSet.copyOf(file.getType());
+ type.remove(this.isDirectory(inflated) ? Path.Type.file : Path.Type.directory);
+ type.add(this.isDirectory(inflated) ? Path.Type.directory : Path.Type.file);
+ type.remove(Path.Type.encrypted);
+ type.add(Path.Type.decrypted);
+ final Path decrypted = new Path(file.getParent().attributes().getDecrypted(), cleartextFilename, type, attributes);
+ if(type.contains(Path.Type.symboliclink)) {
+ decrypted.setSymlinkTarget(file.getSymlinkTarget());
+ }
+ return decrypted;
+ }
+ catch(AuthenticationFailedException e) {
+ throw new CryptoAuthenticationException(
+ "Failure to decrypt due to an unauthentic ciphertext", e);
+ }
+ }
+ else {
+ throw new CryptoFilenameMismatchException(
+ String.format("Failure to decrypt %s due to missing pattern match for %s", inflated.getName(), pattern));
+ }
+ }
+
+ private boolean isDirectory(final Path p) {
+ if(this.getVersion() == VAULT_VERSION_DEPRECATED) {
+ return p.getName().startsWith(DIR_PREFIX);
+ }
+ return p.isDirectory();
+ }
+
+ private Path inflate(final Session> session, final Path file) throws BackgroundException {
+ final String fileName = file.getName();
+ if(this.getFilenameProvider().isDeflated(fileName)) {
+ final String filename = this.getFilenameProvider().inflate(session, fileName);
+ return new Path(file.getParent(), filename, EnumSet.of(Path.Type.file), file.attributes());
+ }
+ return file;
+ }
+
+ public synchronized boolean isUnlocked() {
+ return this.getCryptor() != null;
+ }
+
+ @Override
+ public boolean contains(final Path file) {
+ if(this.isUnlocked()) {
+ return new SimplePathPredicate(file).test(this.getHome()) || file.isChild(this.getHome());
+ }
+ return false;
+ }
+
+ public abstract String getRegularFileExtension();
+
+ public abstract String getDirectoryMetadataFilename();
+
+ public abstract String getBackupDirectoryMetadataFilename();
+
+ public abstract Pattern getBase64URLPattern();
+
+ public abstract byte[] getRootDirId();
+
+ @Override
+ public synchronized void close() {
+ if(this.isUnlocked()) {
+ if(this.getCryptor() != null) {
+ getCryptor().destroy();
+ }
+ if(this.getDirectoryProvider() != null) {
+ this.getDirectoryProvider().destroy();
+ }
+ if(this.getFilenameProvider() != null) {
+ this.getFilenameProvider().destroy();
+ }
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T getFeature(final Session> session, final Class type, final T delegate) {
+ if(this.isUnlocked()) {
+ if(type == ListService.class) {
+ return (T) new CryptoListService(session, (ListService) delegate, this);
+ }
+ if(type == Touch.class) {
+ // Use default touch feature because touch with remote implementation will not add encrypted file header
+ return (T) new CryptoTouchFeature(session, new DefaultTouchFeature(session._getFeature(Write.class)), session._getFeature(Write.class), this);
+ }
+ if(type == Directory.class) {
+ return (T) (this.getVersion() == VAULT_VERSION_DEPRECATED ?
+ new CryptoDirectoryV6Feature(session, (Directory) delegate, session._getFeature(Write.class), this) :
+ new CryptoDirectoryV7Feature(session, (Directory) delegate, session._getFeature(Write.class), this)
+ );
+ }
+ if(type == Upload.class) {
+ return (T) new CryptoUploadFeature(session, (Upload) delegate, session._getFeature(Write.class), this);
+ }
+ if(type == Download.class) {
+ return (T) new CryptoDownloadFeature(session, (Download) delegate, session._getFeature(Read.class), this);
+ }
+ if(type == Read.class) {
+ return (T) new CryptoReadFeature(session, (Read) delegate, this);
+ }
+ if(type == Write.class) {
+ return (T) new CryptoWriteFeature(session, (Write) delegate, this);
+ }
+ if(type == MultipartWrite.class) {
+ return (T) new CryptoMultipartWriteFeature(session, (Write) delegate, this);
+ }
+ if(type == Move.class) {
+ return (T) (this.getVersion() == VAULT_VERSION_DEPRECATED ?
+ new CryptoMoveV6Feature(session, (Move) delegate, this) :
+ new CryptoMoveV7Feature(session, (Move) delegate, this));
+
+ }
+ if(type == AttributesFinder.class) {
+ return (T) new CryptoAttributesFeature(session, (AttributesFinder) delegate, this);
+ }
+ if(type == Find.class) {
+ return (T) new CryptoFindFeature(session, (Find) delegate, this);
+ }
+ if(type == UrlProvider.class) {
+ return (T) new CryptoUrlProvider(session, (UrlProvider) delegate, this);
+ }
+ if(type == FileIdProvider.class) {
+ return (T) new CryptoFileIdProvider(session, (FileIdProvider) delegate, this);
+ }
+ if(type == VersionIdProvider.class) {
+ return (T) new CryptoVersionIdProvider(session, (VersionIdProvider) delegate, this);
+ }
+ if(type == Delete.class) {
+ return (T) (this.getVersion() == VAULT_VERSION_DEPRECATED ?
+ new CryptoDeleteV6Feature(session, (Delete) delegate, this) :
+ new CryptoDeleteV7Feature(session, (Delete) delegate, this));
+ }
+ if(type == Trash.class) {
+ return (T) (this.getVersion() == VAULT_VERSION_DEPRECATED ?
+ new CryptoDeleteV6Feature(session, (Delete) delegate, this) :
+ new CryptoDeleteV7Feature(session, (Delete) delegate, this));
+ }
+ if(type == Symlink.class) {
+ return (T) new CryptoSymlinkFeature(session, (Symlink) delegate, this);
+ }
+ if(type == Headers.class) {
+ return (T) new CryptoHeadersFeature(session, (Headers) delegate, this);
+ }
+ if(type == Compress.class) {
+ return (T) new CryptoCompressFeature(session, (Compress) delegate, this);
+ }
+ if(type == Bulk.class) {
+ return (T) new CryptoBulkFeature(session, (Bulk) delegate, session._getFeature(Delete.class), this);
+ }
+ if(type == UnixPermission.class) {
+ return (T) new CryptoUnixPermission(session, (UnixPermission) delegate, this);
+ }
+ if(type == AclPermission.class) {
+ return (T) new CryptoAclPermission(session, (AclPermission) delegate, this);
+ }
+ if(type == Copy.class) {
+ return (T) new CryptoCopyFeature(session, (Copy) delegate, this);
+ }
+ if(type == Timestamp.class) {
+ return (T) new CryptoTimestampFeature(session, (Timestamp) delegate, this);
+ }
+ if(type == Encryption.class) {
+ return (T) new CryptoEncryptionFeature(session, (Encryption) delegate, this);
+ }
+ if(type == Lifecycle.class) {
+ return (T) new CryptoLifecycleFeature(session, (Lifecycle) delegate, this);
+ }
+ if(type == Location.class) {
+ return (T) new CryptoLocationFeature(session, (Location) delegate, this);
+ }
+ if(type == Lock.class) {
+ return (T) new CryptoLockFeature(session, (Lock) delegate, this);
+ }
+ if(type == Logging.class) {
+ return (T) new CryptoLoggingFeature(session, (Logging) delegate, this);
+ }
+ if(type == Redundancy.class) {
+ return (T) new CryptoRedundancyFeature(session, (Redundancy) delegate, this);
+ }
+ if(type == Search.class) {
+ return (T) new CryptoSearchFeature(session, (Search) delegate, this);
+ }
+ if(type == TransferAcceleration.class) {
+ return (T) new CryptoTransferAccelerationFeature<>(session, (TransferAcceleration) delegate, this);
+ }
+ if(type == Versioning.class) {
+ return (T) new CryptoVersioningFeature(session, (Versioning) delegate, this);
+ }
+ }
+ return delegate;
+ }
+}
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/ContentReader.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/ContentReader.java
index 1a768dd32d0..d07d450ff43 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/ContentReader.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/ContentReader.java
@@ -21,10 +21,12 @@
import ch.cyberduck.core.Session;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.Read;
+import ch.cyberduck.core.io.StreamCopier;
import ch.cyberduck.core.transfer.TransferStatus;
import org.apache.commons.io.IOUtils;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -49,6 +51,19 @@ public String read(final Path file) throws BackgroundException {
}
}
+ public byte[] readBytes(final Path file) throws BackgroundException {
+ final Read read = session._getFeature(Read.class);
+ final TransferStatus status = new TransferStatus().withLength(file.attributes().getSize());
+ try (final InputStream in = read.read(file, status, new DisabledConnectionCallback())) {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new StreamCopier(status, status).transfer(in, out);
+ return out.toByteArray();
+ }
+ catch(IOException e) {
+ throw new DefaultIOExceptionMappingService().map(e);
+ }
+ }
+
public Reader getReader(final Path file) throws BackgroundException {
final Read read = session._getFeature(Read.class);
return new InputStreamReader(read.read(file, new TransferStatus().withLength(file.attributes().getSize()), new DisabledConnectionCallback()), StandardCharsets.UTF_8);
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoAclPermission.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoAclPermission.java
index eb7c1799ef2..19fe06f5047 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoAclPermission.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoAclPermission.java
@@ -22,16 +22,15 @@
import ch.cyberduck.core.features.AclPermission;
import ch.cyberduck.core.transfer.TransferStatus;
-import java.util.EnumSet;
import java.util.List;
public class CryptoAclPermission implements AclPermission {
private final Session> session;
private final AclPermission delegate;
- private final CryptoVault cryptomator;
+ private final AbstractVault cryptomator;
- public CryptoAclPermission(final Session> session, final AclPermission delegate, final CryptoVault cryptomator) {
+ public CryptoAclPermission(final Session> session, final AclPermission delegate, final AbstractVault cryptomator) {
this.session = session;
this.delegate = delegate;
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoDirectory.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoDirectory.java
index 88ccb32e736..2fdfa62c617 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoDirectory.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoDirectory.java
@@ -32,7 +32,7 @@ public interface CryptoDirectory {
* @param type File type
* @return Encrypted filename
*/
- String toEncrypted(Session> session, String directoryId, String filename, EnumSet type) throws BackgroundException;
+ String toEncrypted(Session> session, byte[] directoryId, String filename, EnumSet type) throws BackgroundException;
/**
* Get encrypted reference for clear text directory path.
@@ -41,7 +41,7 @@ public interface CryptoDirectory {
* @param directoryId Directory ID or null to read directory id from metadata file
* @param directory Clear text
*/
- Path toEncrypted(Session> session, String directoryId, Path directory) throws BackgroundException;
+ Path toEncrypted(Session> session, byte[] directoryId, Path directory) throws BackgroundException;
/**
* Remove from cache
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoTransferStatus.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoTransferStatus.java
index fc5e06559f8..7bdd884b96e 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoTransferStatus.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoTransferStatus.java
@@ -27,9 +27,9 @@
public class CryptoTransferStatus extends ProxyTransferStatus implements StreamCancelation, StreamProgress {
private static final Logger log = LogManager.getLogger(CryptoTransferStatus.class);
- private final CryptoVault vault;
+ private final AbstractVault vault;
- public CryptoTransferStatus(final CryptoVault vault, final TransferStatus proxy) {
+ public CryptoTransferStatus(final AbstractVault vault, final TransferStatus proxy) {
super(proxy);
this.vault = vault;
this.withLength(vault.toCiphertextSize(proxy.getOffset(), proxy.getLength()))
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoVault.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoVault.java
index 42535c125eb..9bbbc524b3a 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoVault.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptoVault.java
@@ -15,8 +15,19 @@
* GNU General Public License for more details.
*/
-import ch.cyberduck.core.*;
-import ch.cyberduck.core.cryptomator.features.*;
+import ch.cyberduck.core.Credentials;
+import ch.cyberduck.core.DescriptiveUrl;
+import ch.cyberduck.core.Host;
+import ch.cyberduck.core.LocaleFactory;
+import ch.cyberduck.core.LoginOptions;
+import ch.cyberduck.core.PasswordCallback;
+import ch.cyberduck.core.PasswordStore;
+import ch.cyberduck.core.PasswordStoreFactory;
+import ch.cyberduck.core.Path;
+import ch.cyberduck.core.PathAttributes;
+import ch.cyberduck.core.Session;
+import ch.cyberduck.core.SimplePathPredicate;
+import ch.cyberduck.core.UUIDRandomStringService;
import ch.cyberduck.core.cryptomator.impl.CryptoDirectoryV6Provider;
import ch.cyberduck.core.cryptomator.impl.CryptoDirectoryV7Provider;
import ch.cyberduck.core.cryptomator.impl.CryptoFilenameV6Provider;
@@ -26,10 +37,10 @@
import ch.cyberduck.core.exception.LocalAccessDeniedException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.exception.NotfoundException;
-import ch.cyberduck.core.features.*;
+import ch.cyberduck.core.features.Directory;
+import ch.cyberduck.core.features.Encryption;
import ch.cyberduck.core.preferences.Preferences;
import ch.cyberduck.core.preferences.PreferencesFactory;
-import ch.cyberduck.core.shared.DefaultTouchFeature;
import ch.cyberduck.core.shared.DefaultUrlProvider;
import ch.cyberduck.core.transfer.TransferStatus;
import ch.cyberduck.core.vault.DefaultVaultRegistry;
@@ -38,13 +49,13 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.FileContentCryptor;
import org.cryptomator.cryptolib.api.FileHeaderCryptor;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.MasterkeyFile;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
@@ -58,7 +69,6 @@
import java.text.MessageFormat;
import java.util.EnumSet;
import java.util.Objects;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.auth0.jwt.JWT;
@@ -68,7 +78,6 @@
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
-import com.google.common.io.BaseEncoding;
import com.google.gson.JsonParseException;
import static ch.cyberduck.core.vault.DefaultVaultRegistry.DEFAULT_VAULTCONFIG_FILE_NAME;
@@ -76,22 +85,23 @@
/**
* Cryptomator vault implementation
*/
-public class CryptoVault implements Vault {
+public class CryptoVault extends AbstractVault {
private static final Logger log = LogManager.getLogger(CryptoVault.class);
- public static final int VAULT_VERSION_DEPRECATED = 6;
- public static final int VAULT_VERSION = PreferencesFactory.get().getInteger("cryptomator.vault.version");
public static final byte[] VAULT_PEPPER = PreferencesFactory.get().getProperty("cryptomator.vault.pepper").getBytes(StandardCharsets.UTF_8);
- public static final String DIR_PREFIX = "0";
-
- private static final Pattern BASE32_PATTERN = Pattern.compile("^0?(([A-Z2-7]{8})*[A-Z2-7=]{8})");
- private static final Pattern BASE64URL_PATTERN = Pattern.compile("^([A-Za-z0-9_=-]+).c9r");
-
private static final String JSON_KEY_VAULTVERSION = "format";
private static final String JSON_KEY_CIPHERCONFIG = "cipherCombo";
private static final String JSON_KEY_SHORTENING_THRESHOLD = "shorteningThreshold";
+ private static final String REGULAR_FILE_EXTENSION = ".c9r";
+ private static final String FILENAME_DIRECTORYID = "dir";
+ private static final String DIRECTORY_METADATA_FILENAME = String.format("%s%s", FILENAME_DIRECTORYID, REGULAR_FILE_EXTENSION);
+ private static final String BACKUP_FILENAME_DIRECTORYID = "dirid";
+ private static final String BACKUP_DIRECTORY_METADATA_FILENAME = String.format("%s%s", BACKUP_FILENAME_DIRECTORYID, REGULAR_FILE_EXTENSION);
+
+ private static final Pattern BASE64URL_PATTERN = Pattern.compile("^([A-Za-z0-9_=-]+)" + REGULAR_FILE_EXTENSION);
+
/**
* Root of vault directory
*/
@@ -121,6 +131,7 @@ public CryptoVault(final Path home, final String masterkey, final String config,
this.home = home;
this.masterkey = new Path(home, masterkey, EnumSet.of(Path.Type.file, Path.Type.vault));
this.config = new Path(home, config, EnumSet.of(Path.Type.file, Path.Type.vault));
+
this.pepper = pepper;
// New vault home with vault flag set for internal use
final EnumSet type = EnumSet.copyOf(home.getType());
@@ -150,7 +161,7 @@ public synchronized Path create(final Session> session, final String region, f
}
final String passphrase = credentials.getPassword();
final ByteArrayOutputStream mkArray = new ByteArrayOutputStream();
- final Masterkey mk = Masterkey.generate(FastSecureRandomProvider.get().provide());
+ final PerpetualMasterkey mk = Masterkey.generate(FastSecureRandomProvider.get().provide());
final MasterkeyFileAccess access = new MasterkeyFileAccess(pepper, FastSecureRandomProvider.get().provide());
final MasterkeyFile masterkeyFile;
try {
@@ -235,7 +246,6 @@ private VaultConfig readVaultConfig(final Session> session) throws BackgroundE
}
}
-
public static VaultConfig parseVaultConfigFromJWT(final String token) {
final DecodedJWT decoded = JWT.decode(token);
return new VaultConfig(
@@ -300,18 +310,7 @@ public void unlock(final VaultConfig vaultConfig, final String passphrase, final
@Override
public synchronized void close() {
- if(this.isUnlocked()) {
- log.info("Close vault with cryptor {}", cryptor);
- if(cryptor != null) {
- cryptor.destroy();
- }
- if(directoryProvider != null) {
- directoryProvider.destroy();
- }
- if(filenameProvider != null) {
- filenameProvider.destroy();
- }
- }
+ super.close();
cryptor = null;
fileNameCryptor = null;
}
@@ -325,24 +324,19 @@ protected CryptoFilename createFilenameProvider(final VaultConfig vaultConfig) {
}
}
- protected CryptoDirectory createDirectoryProvider(final VaultConfig vaultConfig) {
+ protected CryptoDirectory createDirectoryProvider(final VaultConfig vaultConfig, final CryptoFilename filenameProvider, final CryptorCache filenameCryptor) {
switch(vaultConfig.version) {
case VAULT_VERSION_DEPRECATED:
- return new CryptoDirectoryV6Provider(vault, this);
+ return new CryptoDirectoryV6Provider(vault, filenameProvider, filenameCryptor);
default:
- return new CryptoDirectoryV7Provider(vault, this);
+ return new CryptoDirectoryV7Provider(this, filenameProvider, filenameCryptor);
}
}
protected void open(final VaultConfig vaultConfig, final CharSequence passphrase) throws BackgroundException {
- this.open(vaultConfig, passphrase, this.createFilenameProvider(vaultConfig), this.createDirectoryProvider(vaultConfig));
- }
-
- protected void open(final VaultConfig vaultConfig, final CharSequence passphrase,
- final CryptoFilename filenameProvider, final CryptoDirectory directoryProvider) throws BackgroundException {
try {
- final Masterkey masterKey = this.getMasterKey(vaultConfig.getMkfile(), passphrase);
- this.open(vaultConfig, masterKey, filenameProvider, directoryProvider);
+ final PerpetualMasterkey masterKey = this.getMasterKey(vaultConfig.getMkfile(), passphrase);
+ this.open(vaultConfig, masterKey);
}
catch(IllegalArgumentException | IOException e) {
throw new VaultException("Failure reading key file", e);
@@ -352,391 +346,102 @@ protected void open(final VaultConfig vaultConfig, final CharSequence passphrase
}
}
- protected void open(final VaultConfig vaultConfig, final Masterkey masterKey) throws BackgroundException {
- this.open(vaultConfig, masterKey, this.createFilenameProvider(vaultConfig), this.createDirectoryProvider(vaultConfig));
- }
-
- protected void open(final VaultConfig vaultConfig, final Masterkey masterKey,
- final CryptoFilename filenameProvider, final CryptoDirectory directoryProvider) throws BackgroundException {
+ protected void open(final VaultConfig vaultConfig, final PerpetualMasterkey masterKey) throws BackgroundException {
this.vaultVersion = vaultConfig.version;
final CryptorProvider provider = CryptorProvider.forScheme(vaultConfig.getCipherCombo());
log.debug("Initialized crypto provider {}", provider);
vaultConfig.verify(masterKey.getEncoded(), VAULT_VERSION);
this.cryptor = provider.provide(masterKey, FastSecureRandomProvider.get().provide());
this.fileNameCryptor = new CryptorCache(cryptor.fileNameCryptor());
- this.filenameProvider = filenameProvider;
- this.directoryProvider = directoryProvider;
+ this.filenameProvider = this.createFilenameProvider(vaultConfig);
+ this.directoryProvider = this.createDirectoryProvider(vaultConfig, this.filenameProvider, this.fileNameCryptor);
this.nonceSize = vaultConfig.getNonceSize();
}
- private Masterkey getMasterKey(final MasterkeyFile mkFile, final CharSequence passphrase) throws IOException {
+ private PerpetualMasterkey getMasterKey(final MasterkeyFile mkFile, final CharSequence passphrase) throws IOException {
final StringWriter writer = new StringWriter();
mkFile.write(writer);
return new MasterkeyFileAccess(pepper, FastSecureRandomProvider.get().provide()).load(
new ByteArrayInputStream(writer.getBuffer().toString().getBytes(StandardCharsets.UTF_8)), passphrase);
}
- public synchronized boolean isUnlocked() {
- return cryptor != null;
- }
-
- @Override
- public State getState() {
- return this.isUnlocked() ? State.open : State.closed;
- }
-
- @Override
- public boolean contains(final Path file) {
- if(this.isUnlocked()) {
- return new SimplePathPredicate(file).test(home) || file.isChild(home);
- }
- return false;
- }
-
- @Override
- public Path encrypt(final Session> session, final Path file) throws BackgroundException {
- return this.encrypt(session, file, file.attributes().getDirectoryId(), false);
- }
-
- @Override
- public Path encrypt(final Session> session, final Path file, boolean metadata) throws BackgroundException {
- return this.encrypt(session, file, file.attributes().getDirectoryId(), metadata);
- }
-
- public Path encrypt(final Session> session, final Path file, final String directoryId, boolean metadata) throws BackgroundException {
- final Path encrypted;
- if(file.isFile() || metadata) {
- if(file.getType().contains(Path.Type.vault)) {
- log.warn("Skip file {} because it is marked as an internal vault path", file);
- return file;
- }
- if(new SimplePathPredicate(file).test(home)) {
- log.warn("Skip vault home {} because the root has no metadata file", file);
- return file;
- }
- final Path parent;
- final String filename;
- if(file.getType().contains(Path.Type.encrypted)) {
- final Path decrypted = file.attributes().getDecrypted();
- parent = directoryProvider.toEncrypted(session, decrypted.getParent().attributes().getDirectoryId(), decrypted.getParent());
- filename = directoryProvider.toEncrypted(session, parent.attributes().getDirectoryId(), decrypted.getName(), decrypted.getType());
- }
- else {
- parent = directoryProvider.toEncrypted(session, file.getParent().attributes().getDirectoryId(), file.getParent());
- filename = directoryProvider.toEncrypted(session, parent.attributes().getDirectoryId(), file.getName(), file.getType());
- }
- final PathAttributes attributes = new PathAttributes(file.attributes());
- attributes.setDirectoryId(null);
- if(!file.isFile() && !metadata) {
- // The directory is different from the metadata file used to resolve the actual folder
- attributes.setVersionId(null);
- attributes.setFileId(null);
- }
- // Translate file size
- attributes.setSize(this.toCiphertextSize(0L, file.attributes().getSize()));
- final EnumSet type = EnumSet.copyOf(file.getType());
- if(metadata && vaultVersion == VAULT_VERSION_DEPRECATED) {
- type.remove(Path.Type.directory);
- type.add(Path.Type.file);
- }
- type.remove(Path.Type.decrypted);
- type.add(Path.Type.encrypted);
- encrypted = new Path(parent, filename, type, attributes);
- }
- else {
- if(file.getType().contains(Path.Type.encrypted)) {
- log.warn("Skip file {} because it is already marked as an encrypted path", file);
- return file;
- }
- if(file.getType().contains(Path.Type.vault)) {
- return directoryProvider.toEncrypted(session, home.attributes().getDirectoryId(), home);
- }
- encrypted = directoryProvider.toEncrypted(session, directoryId, file);
- }
- // Add reference to decrypted file
- if(!file.getType().contains(Path.Type.encrypted)) {
- encrypted.attributes().setDecrypted(file);
- }
- // Add reference for vault
- file.attributes().setVault(home);
- encrypted.attributes().setVault(home);
- return encrypted;
- }
-
- @Override
- public Path decrypt(final Session> session, final Path file) throws BackgroundException {
- if(file.getType().contains(Path.Type.decrypted)) {
- log.warn("Skip file {} because it is already marked as an decrypted path", file);
- return file;
- }
- if(file.getType().contains(Path.Type.vault)) {
- log.warn("Skip file {} because it is marked as an internal vault path", file);
- return file;
- }
- final Path inflated = this.inflate(session, file);
- final Pattern pattern = vaultVersion == VAULT_VERSION_DEPRECATED ? BASE32_PATTERN : BASE64URL_PATTERN;
- final Matcher m = pattern.matcher(inflated.getName());
- if(m.matches()) {
- final String ciphertext = m.group(1);
- try {
- final String cleartextFilename = fileNameCryptor.decryptFilename(
- vaultVersion == VAULT_VERSION_DEPRECATED ? BaseEncoding.base32() : BaseEncoding.base64Url(),
- ciphertext, file.getParent().attributes().getDirectoryId().getBytes(StandardCharsets.UTF_8));
- final PathAttributes attributes = new PathAttributes(file.attributes());
- if(this.isDirectory(inflated)) {
- if(Permission.EMPTY != attributes.getPermission()) {
- final Permission permission = new Permission(attributes.getPermission());
- permission.setUser(permission.getUser().or(Permission.Action.execute));
- permission.setGroup(permission.getGroup().or(Permission.Action.execute));
- permission.setOther(permission.getOther().or(Permission.Action.execute));
- attributes.setPermission(permission);
- }
- // Reset size for folders
- attributes.setSize(-1L);
- attributes.setVersionId(null);
- attributes.setFileId(null);
- }
- else {
- // Translate file size
- attributes.setSize(this.toCleartextSize(0L, file.attributes().getSize()));
- }
- // Add reference to encrypted file
- attributes.setEncrypted(file);
- // Add reference for vault
- attributes.setVault(home);
- final EnumSet type = EnumSet.copyOf(file.getType());
- type.remove(this.isDirectory(inflated) ? Path.Type.file : Path.Type.directory);
- type.add(this.isDirectory(inflated) ? Path.Type.directory : Path.Type.file);
- type.remove(Path.Type.encrypted);
- type.add(Path.Type.decrypted);
- final Path decrypted = new Path(file.getParent().attributes().getDecrypted(), cleartextFilename, type, attributes);
- if(type.contains(Path.Type.symboliclink)) {
- decrypted.setSymlinkTarget(file.getSymlinkTarget());
- }
- return decrypted;
- }
- catch(AuthenticationFailedException e) {
- throw new CryptoAuthenticationException(
- "Failure to decrypt due to an unauthentic ciphertext", e);
- }
- }
- else {
- throw new CryptoFilenameMismatchException(
- String.format("Failure to decrypt %s due to missing pattern match for %s", inflated.getName(), pattern));
- }
- }
-
- private boolean isDirectory(final Path p) {
- if(vaultVersion == VAULT_VERSION_DEPRECATED) {
- return p.getName().startsWith(DIR_PREFIX);
- }
- return p.isDirectory();
- }
-
- @Override
- public long toCiphertextSize(final long cleartextFileOffset, final long cleartextFileSize) {
- if(TransferStatus.UNKNOWN_LENGTH == cleartextFileSize) {
- return TransferStatus.UNKNOWN_LENGTH;
- }
- final int headerSize;
- if(0L == cleartextFileOffset) {
- headerSize = cryptor.fileHeaderCryptor().headerSize();
- }
- else {
- headerSize = 0;
- }
- return headerSize + cryptor.fileContentCryptor().ciphertextSize(cleartextFileSize);
- }
-
- @Override
- public long toCleartextSize(final long cleartextFileOffset, final long ciphertextFileSize) throws CryptoInvalidFilesizeException {
- if(TransferStatus.UNKNOWN_LENGTH == ciphertextFileSize) {
- return TransferStatus.UNKNOWN_LENGTH;
- }
- final int headerSize;
- if(0L == cleartextFileOffset) {
- headerSize = cryptor.fileHeaderCryptor().headerSize();
- }
- else {
- headerSize = 0;
- }
- try {
- return cryptor.fileContentCryptor().cleartextSize(ciphertextFileSize - headerSize);
- }
- catch(AssertionError e) {
- throw new CryptoInvalidFilesizeException(String.format("Encrypted file size must be at least %d bytes", headerSize));
- }
- catch(IllegalArgumentException e) {
- throw new CryptoInvalidFilesizeException(String.format("Invalid file size. %s", e.getMessage()));
- }
- }
-
- private Path inflate(final Session> session, final Path file) throws BackgroundException {
- final String fileName = file.getName();
- if(filenameProvider.isDeflated(fileName)) {
- final String filename = filenameProvider.inflate(session, fileName);
- return new Path(file.getParent(), filename, EnumSet.of(Path.Type.file), file.attributes());
- }
- return file;
- }
-
public Path getHome() {
return home;
}
+ @Override
public Path getMasterkey() {
return masterkey;
}
+ @Override
public Path getConfig() {
return config;
}
+ @Override
+ public int getVersion() {
+ return vaultVersion;
+ }
+
+ @Override
public FileHeaderCryptor getFileHeaderCryptor() {
return cryptor.fileHeaderCryptor();
}
+ @Override
public FileContentCryptor getFileContentCryptor() {
return cryptor.fileContentCryptor();
}
+ @Override
public CryptorCache getFileNameCryptor() {
return fileNameCryptor;
}
+ @Override
public CryptoFilename getFilenameProvider() {
return filenameProvider;
}
+ @Override
public CryptoDirectory getDirectoryProvider() {
return directoryProvider;
}
+ @Override
+ public Cryptor getCryptor() {
+ return cryptor;
+ }
+
+ @Override
public int getNonceSize() {
return nonceSize;
}
- public int numberOfChunks(final long cleartextFileSize) {
- return (int) (cleartextFileSize / cryptor.fileContentCryptor().cleartextChunkSize() +
- ((cleartextFileSize % cryptor.fileContentCryptor().cleartextChunkSize() > 0) ? 1 : 0));
+ @Override
+ public String getRegularFileExtension() {
+ return REGULAR_FILE_EXTENSION;
}
@Override
- @SuppressWarnings("unchecked")
- public T getFeature(final Session> session, final Class type, final T delegate) {
- if(this.isUnlocked()) {
- if(type == ListService.class) {
- return (T) new CryptoListService(session, (ListService) delegate, this);
- }
- if(type == Touch.class) {
- // Use default touch feature because touch with remote implementation will not add encrypted file header
- return (T) new CryptoTouchFeature(session, new DefaultTouchFeature(session._getFeature(Write.class)), session._getFeature(Write.class), this);
- }
- if(type == Directory.class) {
- return (T) (vaultVersion == VAULT_VERSION_DEPRECATED ?
- new CryptoDirectoryV6Feature(session, (Directory) delegate, session._getFeature(Write.class), this) :
- new CryptoDirectoryV7Feature(session, (Directory) delegate, session._getFeature(Write.class), this)
- );
- }
- if(type == Upload.class) {
- return (T) new CryptoUploadFeature(session, (Upload) delegate, session._getFeature(Write.class), this);
- }
- if(type == Download.class) {
- return (T) new CryptoDownloadFeature(session, (Download) delegate, session._getFeature(Read.class), this);
- }
- if(type == Read.class) {
- return (T) new CryptoReadFeature(session, (Read) delegate, this);
- }
- if(type == Write.class) {
- return (T) new CryptoWriteFeature(session, (Write) delegate, this);
- }
- if(type == MultipartWrite.class) {
- return (T) new CryptoMultipartWriteFeature(session, (Write) delegate, this);
- }
- if(type == Move.class) {
- return (T) (vaultVersion == VAULT_VERSION_DEPRECATED ?
- new CryptoMoveV6Feature(session, (Move) delegate, this) :
- new CryptoMoveV7Feature(session, (Move) delegate, this));
+ public String getDirectoryMetadataFilename() {
+ return DIRECTORY_METADATA_FILENAME;
+ }
- }
- if(type == AttributesFinder.class) {
- return (T) new CryptoAttributesFeature(session, (AttributesFinder) delegate, this);
- }
- if(type == Find.class) {
- return (T) new CryptoFindFeature(session, (Find) delegate, this);
- }
- if(type == UrlProvider.class) {
- return (T) new CryptoUrlProvider(session, (UrlProvider) delegate, this);
- }
- if(type == FileIdProvider.class) {
- return (T) new CryptoFileIdProvider(session, (FileIdProvider) delegate, this);
- }
- if(type == VersionIdProvider.class) {
- return (T) new CryptoVersionIdProvider(session, (VersionIdProvider) delegate, this);
- }
- if(type == Delete.class) {
- return (T) (vaultVersion == VAULT_VERSION_DEPRECATED ?
- new CryptoDeleteV6Feature(session, (Delete) delegate, this) :
- new CryptoDeleteV7Feature(session, (Delete) delegate, this));
- }
- if(type == Trash.class) {
- return (T) (vaultVersion == VAULT_VERSION_DEPRECATED ?
- new CryptoDeleteV6Feature(session, (Delete) delegate, this) :
- new CryptoDeleteV7Feature(session, (Delete) delegate, this));
- }
- if(type == Symlink.class) {
- return (T) new CryptoSymlinkFeature(session, (Symlink) delegate, this);
- }
- if(type == Headers.class) {
- return (T) new CryptoHeadersFeature(session, (Headers) delegate, this);
- }
- if(type == Compress.class) {
- return (T) new CryptoCompressFeature(session, (Compress) delegate, this);
- }
- if(type == Bulk.class) {
- return (T) new CryptoBulkFeature(session, (Bulk) delegate, session._getFeature(Delete.class), this);
- }
- if(type == UnixPermission.class) {
- return (T) new CryptoUnixPermission(session, (UnixPermission) delegate, this);
- }
- if(type == AclPermission.class) {
- return (T) new CryptoAclPermission(session, (AclPermission) delegate, this);
- }
- if(type == Copy.class) {
- return (T) new CryptoCopyFeature(session, (Copy) delegate, this);
- }
- if(type == Timestamp.class) {
- return (T) new CryptoTimestampFeature(session, (Timestamp) delegate, this);
- }
- if(type == Encryption.class) {
- return (T) new CryptoEncryptionFeature(session, (Encryption) delegate, this);
- }
- if(type == Lifecycle.class) {
- return (T) new CryptoLifecycleFeature(session, (Lifecycle) delegate, this);
- }
- if(type == Location.class) {
- return (T) new CryptoLocationFeature(session, (Location) delegate, this);
- }
- if(type == Lock.class) {
- return (T) new CryptoLockFeature(session, (Lock) delegate, this);
- }
- if(type == Logging.class) {
- return (T) new CryptoLoggingFeature(session, (Logging) delegate, this);
- }
- if(type == Redundancy.class) {
- return (T) new CryptoRedundancyFeature(session, (Redundancy) delegate, this);
- }
- if(type == Search.class) {
- return (T) new CryptoSearchFeature(session, (Search) delegate, this);
- }
- if(type == TransferAcceleration.class) {
- return (T) new CryptoTransferAccelerationFeature<>(session, (TransferAcceleration) delegate, this);
- }
- if(type == Versioning.class) {
- return (T) new CryptoVersioningFeature(session, (Versioning) delegate, this);
- }
- }
- return delegate;
+ @Override
+ public String getBackupDirectoryMetadataFilename() {
+ return BACKUP_DIRECTORY_METADATA_FILENAME;
+ }
+
+ @Override
+ public Pattern getBase64URLPattern() {
+ return BASE64URL_PATTERN;
+ }
+
+ @Override
+ public byte[] getRootDirId() {
+ return CryptoDirectoryV6Provider.ROOT_DIR_ID;
}
@Override
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptorCache.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptorCache.java
index f62cf502146..96bc1893bbf 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptorCache.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/CryptorCache.java
@@ -20,6 +20,7 @@
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileNameCryptor;
+import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Objects;
@@ -29,7 +30,7 @@ public class CryptorCache {
public static final BaseEncoding BASE32 = BaseEncoding.base32();
- private final LRUCache directoryIdCache = LRUCache.build(250);
+ private final LRUCache directoryIdCache = LRUCache.build(250);
private final LRUCache decryptCache = LRUCache.build(5000);
private final LRUCache encryptCache = LRUCache.build(5000);
@@ -39,11 +40,12 @@ public CryptorCache(final FileNameCryptor impl) {
this.impl = impl;
}
- public String hashDirectoryId(final String cleartextDirectoryId) {
- if(!directoryIdCache.contains(cleartextDirectoryId)) {
- directoryIdCache.put(cleartextDirectoryId, impl.hashDirectoryId(cleartextDirectoryId));
+ public String hashDirectoryId(final byte[] cleartextDirectoryId) {
+ final ByteBuffer wrap = ByteBuffer.wrap(cleartextDirectoryId);
+ if(!directoryIdCache.contains(wrap)) {
+ directoryIdCache.put(wrap, impl.hashDirectoryId(cleartextDirectoryId));
}
- return directoryIdCache.get(cleartextDirectoryId);
+ return directoryIdCache.get(wrap);
}
public String encryptFilename(final BaseEncoding encoding, final String cleartextName, final byte[] associatedData) {
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/UVFVault.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/UVFVault.java
new file mode 100644
index 00000000000..342484d0f86
--- /dev/null
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/UVFVault.java
@@ -0,0 +1,390 @@
+package ch.cyberduck.core.cryptomator;
+
+/*
+ * Copyright (c) 2002-2025 iterate GmbH. All rights reserved.
+ * https://cyberduck.io/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+import ch.cyberduck.core.LocaleFactory;
+import ch.cyberduck.core.LoginOptions;
+import ch.cyberduck.core.PasswordCallback;
+import ch.cyberduck.core.Path;
+import ch.cyberduck.core.PathAttributes;
+import ch.cyberduck.core.Permission;
+import ch.cyberduck.core.Session;
+import ch.cyberduck.core.SimplePathPredicate;
+import ch.cyberduck.core.cryptomator.impl.CryptoDirectoryUVFProvider;
+import ch.cyberduck.core.cryptomator.impl.CryptoFilenameV7Provider;
+import ch.cyberduck.core.cryptomator.random.FastSecureRandomProvider;
+import ch.cyberduck.core.exception.BackgroundException;
+import ch.cyberduck.core.vault.VaultCredentials;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.FileContentCryptor;
+import org.cryptomator.cryptolib.api.FileHeaderCryptor;
+import org.cryptomator.cryptolib.api.UVFMasterkey;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.text.MessageFormat;
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.google.common.io.BaseEncoding;
+
+public class UVFVault extends AbstractVault {
+
+ private static final Logger log = LogManager.getLogger(UVFVault.class);
+
+ private static final String REGULAR_FILE_EXTENSION = ".uvf";
+ private static final String FILENAME_DIRECTORYID = "dir";
+ private static final String DIRECTORY_METADATA_FILENAME = String.format("%s%s", FILENAME_DIRECTORYID, REGULAR_FILE_EXTENSION);
+ private static final String BACKUP_FILENAME_DIRECTORYID = "dirid";
+ private static final String BACKUP_DIRECTORY_METADATA_FILENAME = String.format("%s%s", BACKUP_FILENAME_DIRECTORYID, REGULAR_FILE_EXTENSION);
+
+ private static final Pattern BASE64URL_PATTERN = Pattern.compile("^([A-Za-z0-9_=-]+)" + REGULAR_FILE_EXTENSION);
+
+ // copied from AbstractVault
+ private static final Pattern BASE32_PATTERN = Pattern.compile("^0?(([A-Z2-7]{8})*[A-Z2-7=]{8})");
+
+ /**
+ * Root of vault directory
+ */
+ private final Path home;
+
+ private Cryptor cryptor;
+ private CryptorCache fileNameCryptor;
+ private CryptoFilename filenameProvider;
+ private CryptoDirectory directoryProvider;
+
+ private int nonceSize;
+ private byte[] rootDirId;
+
+ public UVFVault(final Path home) {
+ this.home = home;
+ }
+
+ @Override
+ public Path create(final Session> session, final String region, final VaultCredentials credentials) throws BackgroundException {
+ throw new UnsupportedOperationException();
+ }
+
+ // load -> unlock -> open
+ @Override
+ public UVFVault load(final Session> session, final PasswordCallback prompt) throws BackgroundException {
+ final UVFMasterkey masterKey = UVFMasterkey.fromDecryptedPayload(prompt.prompt(session.getHost(),
+ LocaleFactory.localizedString("Unlock Vault", "Cryptomator"),
+ MessageFormat.format(LocaleFactory.localizedString("Provide your passphrase to unlock the Cryptomator Vault {0}", "Cryptomator"), home.getName()),
+ new LoginOptions()
+ .save(false)
+ .user(false)
+ .anonymous(false)
+ .icon("cryptomator.tiff")
+ .passwordPlaceholder(LocaleFactory.localizedString("Passphrase", "Cryptomator"))).getPassword());
+ final CryptorProvider provider = CryptorProvider.forScheme(CryptorProvider.Scheme.UVF_DRAFT);
+ log.debug("Initialized crypto provider {}", provider);
+ this.cryptor = provider.provide(masterKey, FastSecureRandomProvider.get().provide());
+ this.fileNameCryptor = new CryptorCache(cryptor.fileNameCryptor(masterKey.firstRevision())); // TODO revision eventually depends on location - safe?
+ this.filenameProvider = new CryptoFilenameV7Provider(Integer.MAX_VALUE);
+ this.directoryProvider = new CryptoDirectoryUVFProvider(this, filenameProvider, fileNameCryptor);
+ this.nonceSize = 12;
+ this.rootDirId = masterKey.rootDirId();
+ return this;
+ }
+
+ @Override
+ public Path decrypt(final Session> session, final Path file) throws BackgroundException {
+ if(file.getType().contains(Path.Type.decrypted)) {
+ log.warn("Skip file {} because it is already marked as an defcrypted path", file);
+ return file;
+ }
+ if(file.getType().contains(Path.Type.vault)) {
+ log.warn("Skip file {} because it is marked as an internal vault path", file);
+ return file;
+ }
+ final Path inflated = this.inflate(session, file);
+ final Pattern pattern = this.getVersion() == VAULT_VERSION_DEPRECATED ? BASE32_PATTERN : this.getBase64URLPattern();
+ final Matcher m = pattern.matcher(inflated.getName());
+ if(m.matches()) {
+ final String ciphertext = m.group(1);
+ try {
+ final CryptorCache effectivefileNameCryptor;
+ // / diff to AbstractVault.decrypt
+ final int revision = loadRevision(session, file);
+ effectivefileNameCryptor = new CryptorCache(this.getCryptor().fileNameCryptor(revision));
+ // \
+ final String cleartextFilename = effectivefileNameCryptor.decryptFilename(
+ this.getVersion() == VAULT_VERSION_DEPRECATED ? BaseEncoding.base32() : BaseEncoding.base64Url(),
+ ciphertext, file.getParent().attributes().getDirectoryId());
+ final PathAttributes attributes = new PathAttributes(file.attributes());
+ if(this.isDirectory(inflated)) {
+ if(Permission.EMPTY != attributes.getPermission()) {
+ final Permission permission = new Permission(attributes.getPermission());
+ permission.setUser(permission.getUser().or(Permission.Action.execute));
+ permission.setGroup(permission.getGroup().or(Permission.Action.execute));
+ permission.setOther(permission.getOther().or(Permission.Action.execute));
+ attributes.setPermission(permission);
+ }
+ // Reset size for folders
+ attributes.setSize(-1L);
+ attributes.setVersionId(null);
+ attributes.setFileId(null);
+ }
+ else {
+ // Translate file size
+ attributes.setSize(this.toCleartextSize(0L, file.attributes().getSize()));
+ }
+ // Add reference to encrypted file
+ attributes.setEncrypted(file);
+ // Add reference for vault
+ attributes.setVault(this.getHome());
+ final EnumSet type = EnumSet.copyOf(file.getType());
+ type.remove(this.isDirectory(inflated) ? Path.Type.file : Path.Type.directory);
+ type.add(this.isDirectory(inflated) ? Path.Type.directory : Path.Type.file);
+ type.remove(Path.Type.encrypted);
+ type.add(Path.Type.decrypted);
+ final Path decrypted = new Path(file.getParent().attributes().getDecrypted(), cleartextFilename, type, attributes);
+ if(type.contains(Path.Type.symboliclink)) {
+ decrypted.setSymlinkTarget(file.getSymlinkTarget());
+ }
+ return decrypted;
+ }
+ catch(AuthenticationFailedException e) {
+ throw new CryptoAuthenticationException(
+ "Failure to decrypt due to an unauthentic ciphertext", e);
+ }
+ }
+ else {
+ throw new CryptoFilenameMismatchException(
+ String.format("Failure to decrypt %s due to missing pattern match for %s", inflated.getName(), pattern));
+ }
+ }
+
+ public Path encrypt(Session> session, Path file, byte[] directoryId, boolean metadata) throws BackgroundException {
+ final Path encrypted;
+ if(file.isFile() || metadata) {
+ if(file.getType().contains(Path.Type.vault)) {
+ log.warn("Skip file {} because it is marked as an internal vault path", file);
+ return file;
+ }
+ if(new SimplePathPredicate(file).test(this.getHome())) {
+ log.warn("Skip vault home {} because the root has no metadata file", file);
+ return file;
+ }
+ final Path parent;
+ final String filename;
+ if(file.getType().contains(Path.Type.encrypted)) {
+ final Path decrypted = file.attributes().getDecrypted();
+ parent = this.getDirectoryProvider().toEncrypted(session, decrypted.getParent().attributes().getDirectoryId(), decrypted.getParent());
+ filename = this.getDirectoryProvider().toEncrypted(session, parent.attributes().getDirectoryId(), decrypted.getName(), decrypted.getType());
+ }
+ else {
+ parent = this.getDirectoryProvider().toEncrypted(session, file.getParent().attributes().getDirectoryId(), file.getParent());
+ // / diff to AbstractVault.encrypt
+ String filenameO = this.getDirectoryProvider().toEncrypted(session, parent.attributes().getDirectoryId(), file.getName(), file.getType());
+ filename = ((CryptoDirectoryUVFProvider) this.getDirectoryProvider()).toEncrypted(session, file.getParent(), file.getName());
+ // \ diff to AbstractVault.decrypt
+ }
+ final PathAttributes attributes = new PathAttributes(file.attributes());
+ attributes.setDirectoryId(null);
+ if(!file.isFile() && !metadata) {
+ // The directory is different from the metadata file used to resolve the actual folder
+ attributes.setVersionId(null);
+ attributes.setFileId(null);
+ }
+ // Translate file size
+ attributes.setSize(this.toCiphertextSize(0L, file.attributes().getSize()));
+ final EnumSet type = EnumSet.copyOf(file.getType());
+ if(metadata && this.getVersion() == VAULT_VERSION_DEPRECATED) {
+ type.remove(Path.Type.directory);
+ type.add(Path.Type.file);
+ }
+ type.remove(Path.Type.decrypted);
+ type.add(Path.Type.encrypted);
+ encrypted = new Path(parent, filename, type, attributes);
+ }
+ else {
+ if(file.getType().contains(Path.Type.encrypted)) {
+ log.warn("Skip file {} because it is already marked as an encrypted path", file);
+ return file;
+ }
+ if(file.getType().contains(Path.Type.vault)) {
+ return this.getDirectoryProvider().toEncrypted(session, this.getHome().attributes().getDirectoryId(), this.getHome());
+ }
+ encrypted = this.getDirectoryProvider().toEncrypted(session, directoryId, file);
+ }
+ // Add reference to decrypted file
+ if(!file.getType().contains(Path.Type.encrypted)) {
+ encrypted.attributes().setDecrypted(file);
+ }
+ // Add reference for vault
+ file.attributes().setVault(this.getHome());
+ encrypted.attributes().setVault(this.getHome());
+ return encrypted;
+ }
+
+ private int loadRevision(final Session> session, final Path directory) throws BackgroundException {
+ // Read directory id from file
+ log.debug("Read directory ID from {}", directory);
+ final Path metadataFile = new Path(directory.getParent(), this.getDirectoryMetadataFilename(), EnumSet.of(Path.Type.file, Path.Type.encrypted));
+ final byte[] ciphertext = new ContentReader(session).readBytes(metadataFile);
+ // https://github.com/encryption-alliance/unified-vault-format/blob/develop/file%20name%20encryption/AES-SIV-512-B64URL.md#format-of-diruvf-and-symlinkuvf
+ // TODO can we not use org.cryptomator.cryptolib.v3.DirectoryContentCryptorImpl.decryptDirectoryMetadata()? DirectoryMetadataImpl is not visible and DirectoryMetadata is empty interface, so we cannot access dirId attribute.
+ if(ciphertext.length != 128) {
+ throw new IllegalArgumentException("Invalid dir.uvf length: " + ciphertext.length);
+ }
+ int headerSize = this.getCryptor().fileHeaderCryptor().headerSize();
+ ByteBuffer buffer = ByteBuffer.wrap(ciphertext);
+ ByteBuffer headerBuf = buffer.duplicate();
+ headerBuf.position(4).limit(headerSize);
+ return headerBuf.order(ByteOrder.BIG_ENDIAN).getInt();
+ }
+
+
+ // copied from AbstractVault
+ private boolean isDirectory(final Path p) {
+ if(this.getVersion() == VAULT_VERSION_DEPRECATED) {
+ return p.getName().startsWith(DIR_PREFIX);
+ }
+ return p.isDirectory();
+ }
+
+ // copied from AbstractVault
+ private Path inflate(final Session> session, final Path file) throws BackgroundException {
+ final String fileName = file.getName();
+ if(this.getFilenameProvider().isDeflated(fileName)) {
+ final String filename = this.getFilenameProvider().inflate(session, fileName);
+ return new Path(file.getParent(), filename, EnumSet.of(Path.Type.file), file.attributes());
+ }
+ return file;
+ }
+
+ @Override
+ public synchronized void close() {
+ super.close();
+ cryptor.destroy();
+ }
+
+ @Override
+ public Path getMasterkey() {
+ //TODO: implement
+ return null;
+ }
+
+ @Override
+ public Path getConfig() {
+ //TODO: implement
+ return null;
+ }
+
+ @Override
+ public Path getHome() {
+ return home;
+ }
+
+ @Override
+ public FileHeaderCryptor getFileHeaderCryptor() {
+ return cryptor.fileHeaderCryptor();
+ }
+
+ @Override
+ public FileContentCryptor getFileContentCryptor() {
+ return cryptor.fileContentCryptor();
+ }
+
+ @Override
+ public CryptorCache getFileNameCryptor() {
+ return fileNameCryptor;
+ }
+
+ @Override
+ public CryptoFilename getFilenameProvider() {
+ return filenameProvider;
+ }
+
+ @Override
+ public CryptoDirectory getDirectoryProvider() {
+ return directoryProvider;
+ }
+
+ @Override
+ public Cryptor getCryptor() {
+ return cryptor;
+ }
+
+ @Override
+ public int getNonceSize() {
+ return nonceSize;
+ }
+
+ @Override
+ public int getVersion() {
+ return VAULT_VERSION;
+ }
+
+ @Override
+ public String getRegularFileExtension() {
+ return REGULAR_FILE_EXTENSION;
+ }
+
+ @Override
+ public String getDirectoryMetadataFilename() {
+ return DIRECTORY_METADATA_FILENAME;
+ }
+
+ @Override
+ public String getBackupDirectoryMetadataFilename() {
+ return BACKUP_DIRECTORY_METADATA_FILENAME;
+ }
+
+ @Override
+ public Pattern getBase64URLPattern() {
+ return BASE64URL_PATTERN;
+ }
+
+ public byte[] getRootDirId() {
+ return rootDirId;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if(this == o) {
+ return true;
+ }
+ if(!(o instanceof UVFVault)) {
+ return false;
+ }
+ final UVFVault that = (UVFVault) o;
+ return new SimplePathPredicate(home).test(that.home);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(new SimplePathPredicate(home));
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("UVFVault{");
+ sb.append("home=").append(home);
+ sb.append(", cryptor=").append(cryptor);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoBulkFeature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoBulkFeature.java
index c16210fc644..aa6357876b2 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoBulkFeature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoBulkFeature.java
@@ -21,7 +21,7 @@
import ch.cyberduck.core.RandomStringService;
import ch.cyberduck.core.Session;
import ch.cyberduck.core.UUIDRandomStringService;
-import ch.cyberduck.core.cryptomator.CryptoVault;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.random.RandomNonceGenerator;
import ch.cyberduck.core.cryptomator.random.RotatingNonceGenerator;
import ch.cyberduck.core.exception.BackgroundException;
@@ -34,6 +34,7 @@
import org.cryptomator.cryptolib.api.FileHeader;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
@@ -46,9 +47,9 @@ public class CryptoBulkFeature implements Bulk {
private final Session> session;
private final Bulk delegate;
- private final CryptoVault cryptomator;
+ private final AbstractVault cryptomator;
- public CryptoBulkFeature(final Session> session, final Bulk delegate, final Delete delete, final CryptoVault cryptomator) {
+ public CryptoBulkFeature(final Session> session, final Bulk delegate, final Delete delete, final AbstractVault cryptomator) {
this.session = session;
this.delegate = delegate.withDelete(cryptomator.getFeature(session, Delete.class, delete));
this.cryptomator = cryptomator;
@@ -84,7 +85,7 @@ public int compare(final Map.Entry o1, final Map.E
switch(type) {
case upload:
// Preset directory ID for new folders to avert lookup with not found failure in directory ID provider
- final String directoryId = random.random();
+ final byte[] directoryId = random.random().getBytes(StandardCharsets.US_ASCII);
encrypted.put(new TransferItem(cryptomator.encrypt(session, file, directoryId, false), local), status);
break;
default:
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoChecksumCompute.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoChecksumCompute.java
index 51d4dc0c635..683b614fbda 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoChecksumCompute.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoChecksumCompute.java
@@ -15,8 +15,8 @@
* GNU General Public License for more details.
*/
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.CryptoOutputStream;
-import ch.cyberduck.core.cryptomator.CryptoVault;
import ch.cyberduck.core.cryptomator.random.RandomNonceGenerator;
import ch.cyberduck.core.cryptomator.random.RotatingNonceGenerator;
import ch.cyberduck.core.exception.BackgroundException;
@@ -55,11 +55,11 @@
public class CryptoChecksumCompute extends AbstractChecksumCompute {
private static final Logger log = LogManager.getLogger(CryptoChecksumCompute.class);
- private final CryptoVault cryptomator;
+ private final AbstractVault cryptomator;
private final ChecksumCompute delegate;
- public CryptoChecksumCompute(final ChecksumCompute delegate, final CryptoVault vault) {
- this.cryptomator = vault;
+ public CryptoChecksumCompute(final ChecksumCompute delegate, final AbstractVault CryptoVaultInterface) {
+ this.cryptomator = CryptoVaultInterface;
this.delegate = delegate;
}
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoCopyFeature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoCopyFeature.java
index dbff373f7a8..f9276c2b702 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoCopyFeature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoCopyFeature.java
@@ -19,7 +19,7 @@
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
import ch.cyberduck.core.Session;
-import ch.cyberduck.core.cryptomator.CryptoVault;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.random.RandomNonceGenerator;
import ch.cyberduck.core.cryptomator.random.RotatingNonceGenerator;
import ch.cyberduck.core.exception.BackgroundException;
@@ -37,11 +37,11 @@ public class CryptoCopyFeature implements Copy {
private final Session> session;
private final Copy proxy;
- private final CryptoVault vault;
+ private final AbstractVault vault;
private Session> target;
- public CryptoCopyFeature(final Session> session, final Copy proxy, final CryptoVault vault) {
+ public CryptoCopyFeature(final Session> session, final Copy proxy, final AbstractVault vault) {
this.session = session;
this.target = session;
this.proxy = proxy;
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDeleteV6Feature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDeleteV6Feature.java
index 01a3ce06cf9..6f3f1deb849 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDeleteV6Feature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDeleteV6Feature.java
@@ -20,8 +20,8 @@
import ch.cyberduck.core.PasswordCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.Session;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.CryptoFilename;
-import ch.cyberduck.core.cryptomator.CryptoVault;
import ch.cyberduck.core.exception.AccessDeniedException;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.NotfoundException;
@@ -44,10 +44,10 @@ public class CryptoDeleteV6Feature implements Delete, Trash {
private final Session> session;
private final Delete proxy;
- private final CryptoVault vault;
+ private final AbstractVault vault;
private final CryptoFilename filenameProvider;
- public CryptoDeleteV6Feature(final Session> session, final Delete proxy, final CryptoVault vault) {
+ public CryptoDeleteV6Feature(final Session> session, final Delete proxy, final AbstractVault vault) {
this.session = session;
this.proxy = proxy;
this.vault = vault;
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDeleteV7Feature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDeleteV7Feature.java
index 8d8570ef479..b4ffb1e212c 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDeleteV7Feature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDeleteV7Feature.java
@@ -15,14 +15,14 @@
* GNU General Public License for more details.
*/
+import ch.cyberduck.core.AbstractPath;
import ch.cyberduck.core.DisabledListProgressListener;
import ch.cyberduck.core.ListService;
import ch.cyberduck.core.PasswordCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.Session;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.CryptoFilename;
-import ch.cyberduck.core.cryptomator.CryptoVault;
-import ch.cyberduck.core.cryptomator.impl.CryptoDirectoryV7Provider;
import ch.cyberduck.core.exception.AccessDeniedException;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.NotfoundException;
@@ -45,10 +45,10 @@ public class CryptoDeleteV7Feature implements Delete, Trash {
private final Session> session;
private final Delete proxy;
- private final CryptoVault vault;
+ private final AbstractVault vault;
private final CryptoFilename filenameProvider;
- public CryptoDeleteV7Feature(final Session> session, final Delete proxy, final CryptoVault vault) {
+ public CryptoDeleteV7Feature(final Session> session, final Delete proxy, final AbstractVault vault) {
this.session = session;
this.proxy = proxy;
this.vault = vault;
@@ -62,8 +62,8 @@ public void delete(final Map files, final PasswordCallback
if(!f.equals(vault.getHome())) {
final Path encrypt = vault.encrypt(session, f);
if(f.isDirectory()) {
- final Path backup = new Path(encrypt, CryptoDirectoryV7Provider.BACKUP_DIRECTORY_METADATAFILE,
- EnumSet.of(Path.Type.file));
+ final Path backup = new Path(encrypt, vault.getBackupDirectoryMetadataFilename(),
+ EnumSet.of(AbstractPath.Type.file));
try {
log.debug("Deleting directory id backup file {}", backup);
proxy.delete(Collections.singletonList(backup), prompt, callback);
@@ -87,7 +87,7 @@ public void delete(final Map files, final PasswordCallback
}
final Path metadata = vault.encrypt(session, f, true);
if(f.isDirectory()) {
- final Path metadataFile = new Path(metadata, CryptoDirectoryV7Provider.DIRECTORY_METADATAFILE, EnumSet.of(Path.Type.file));
+ final Path metadataFile = new Path(metadata, vault.getDirectoryMetadataFilename(), EnumSet.of(Path.Type.file));
log.debug("Add metadata file {}", metadataFile);
metadataFiles.add(metadataFile);
metadataFiles.add(metadata);
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDirectoryV6Feature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDirectoryV6Feature.java
index f2eb8650ff8..b9112a2e95d 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDirectoryV6Feature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDirectoryV6Feature.java
@@ -19,8 +19,8 @@
import ch.cyberduck.core.RandomStringService;
import ch.cyberduck.core.Session;
import ch.cyberduck.core.UUIDRandomStringService;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.ContentWriter;
-import ch.cyberduck.core.cryptomator.CryptoVault;
import ch.cyberduck.core.cryptomator.random.RandomNonceGenerator;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.Directory;
@@ -40,11 +40,11 @@ public class CryptoDirectoryV6Feature implements Directory {
private final Session> session;
private final Write writer;
private final Directory delegate;
- private final CryptoVault vault;
+ private final AbstractVault vault;
private final RandomStringService random = new UUIDRandomStringService();
public CryptoDirectoryV6Feature(final Session> session, final Directory delegate,
- final Write writer, final CryptoVault cryptomator) {
+ final Write writer, final AbstractVault cryptomator) {
this.session = session;
this.writer = writer;
this.delegate = delegate;
@@ -53,12 +53,12 @@ public CryptoDirectoryV6Feature(final Session> session, final Directory
@Override
public Path mkdir(final Path folder, final TransferStatus status) throws BackgroundException {
- final Path encrypt = vault.encrypt(session, folder, random.random(), false);
- final String directoryId = encrypt.attributes().getDirectoryId();
+ final Path encrypt = vault.encrypt(session, folder, random.random().getBytes(StandardCharsets.US_ASCII), false);
+ final byte[] directoryId = encrypt.attributes().getDirectoryId();
// Create metadata file for directory
final Path directoryMetadataFile = vault.encrypt(session, folder, true);
log.debug("Write metadata {} for folder {}", directoryMetadataFile, folder);
- new ContentWriter(session).write(directoryMetadataFile, directoryId.getBytes(StandardCharsets.UTF_8));
+ new ContentWriter(session).write(directoryMetadataFile, directoryId);
final Path intermediate = encrypt.getParent();
if(!session._getFeature(Find.class).find(intermediate)) {
session._getFeature(Directory.class).mkdir(intermediate, new TransferStatus().withRegion(status.getRegion()));
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDirectoryV7Feature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDirectoryV7Feature.java
index ebf3e295402..b48598107f6 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDirectoryV7Feature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDirectoryV7Feature.java
@@ -19,9 +19,8 @@
import ch.cyberduck.core.RandomStringService;
import ch.cyberduck.core.Session;
import ch.cyberduck.core.UUIDRandomStringService;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.ContentWriter;
-import ch.cyberduck.core.cryptomator.CryptoVault;
-import ch.cyberduck.core.cryptomator.impl.CryptoDirectoryV7Provider;
import ch.cyberduck.core.cryptomator.random.RandomNonceGenerator;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.Directory;
@@ -42,29 +41,29 @@ public class CryptoDirectoryV7Feature implements Directory {
private final Session> session;
private final Write writer;
private final Directory delegate;
- private final CryptoVault vault;
+ private final AbstractVault vault;
private final RandomStringService random = new UUIDRandomStringService();
public CryptoDirectoryV7Feature(final Session> session, final Directory delegate,
- final Write writer, final CryptoVault cryptomator) {
+ final Write writer, final AbstractVault vault) {
this.session = session;
this.writer = writer;
this.delegate = delegate;
- this.vault = cryptomator;
+ this.vault = vault;
}
@Override
public Path mkdir(final Path folder, final TransferStatus status) throws BackgroundException {
- final Path encrypt = vault.encrypt(session, folder, random.random(), false);
- final String directoryId = encrypt.attributes().getDirectoryId();
+ final Path encrypt = vault.encrypt(session, folder, random.random().getBytes(StandardCharsets.US_ASCII), false);
+ final byte[] directoryId = encrypt.attributes().getDirectoryId();
// Create metadata file for directory
final Path directoryMetadataFolder = session._getFeature(Directory.class).mkdir(vault.encrypt(session, folder, true),
new TransferStatus().withRegion(status.getRegion()));
final Path directoryMetadataFile = new Path(directoryMetadataFolder,
- CryptoDirectoryV7Provider.DIRECTORY_METADATAFILE,
+ vault.getDirectoryMetadataFilename(),
EnumSet.of(Path.Type.file));
log.debug("Write metadata {} for folder {}", directoryMetadataFile, folder);
- new ContentWriter(session).write(directoryMetadataFile, directoryId.getBytes(StandardCharsets.UTF_8));
+ new ContentWriter(session).write(directoryMetadataFile, directoryId);
final Path intermediate = encrypt.getParent();
if(!session._getFeature(Find.class).find(intermediate)) {
session._getFeature(Directory.class).mkdir(intermediate, new TransferStatus().withRegion(status.getRegion()));
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDownloadFeature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDownloadFeature.java
index dc803e4c911..a9ce5863e45 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDownloadFeature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoDownloadFeature.java
@@ -19,12 +19,11 @@
import ch.cyberduck.core.Local;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.Session;
-import ch.cyberduck.core.cryptomator.CryptoVault;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.NotfoundException;
import ch.cyberduck.core.features.Download;
import ch.cyberduck.core.features.Read;
-import ch.cyberduck.core.features.Vault;
import ch.cyberduck.core.io.BandwidthThrottle;
import ch.cyberduck.core.io.StreamListener;
import ch.cyberduck.core.transfer.TransferStatus;
@@ -33,9 +32,9 @@ public class CryptoDownloadFeature implements Download {
private final Session> session;
private final Download proxy;
- private final Vault vault;
+ private final AbstractVault vault;
- public CryptoDownloadFeature(final Session> session, final Download proxy, final Read reader, final CryptoVault vault) {
+ public CryptoDownloadFeature(final Session> session, final Download proxy, final Read reader, final AbstractVault vault) {
this.session = session;
this.proxy = proxy.withReader(new CryptoReadFeature(session, reader, vault));
this.vault = vault;
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoEncryptionFeature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoEncryptionFeature.java
index 9f30a97b1be..6dc07f6edf2 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoEncryptionFeature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoEncryptionFeature.java
@@ -18,7 +18,6 @@
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.Session;
-import ch.cyberduck.core.cryptomator.CryptoVault;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.Encryption;
import ch.cyberduck.core.features.Vault;
@@ -31,7 +30,7 @@ public class CryptoEncryptionFeature implements Encryption {
private final Encryption delegate;
private final Vault vault;
- public CryptoEncryptionFeature(final Session> session, final Encryption delegate, final CryptoVault vault) {
+ public CryptoEncryptionFeature(final Session> session, final Encryption delegate, final Vault vault) {
this.session = session;
this.delegate = delegate;
this.vault = vault;
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMoveV6Feature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMoveV6Feature.java
index e43afc0965d..e6d80abba7c 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMoveV6Feature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMoveV6Feature.java
@@ -19,7 +19,7 @@
import ch.cyberduck.core.LocaleFactory;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.Session;
-import ch.cyberduck.core.cryptomator.CryptoVault;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.InvalidFilenameException;
import ch.cyberduck.core.features.Delete;
@@ -34,9 +34,9 @@ public class CryptoMoveV6Feature implements Move {
private final Session> session;
private final Move proxy;
- private final CryptoVault vault;
+ private final AbstractVault vault;
- public CryptoMoveV6Feature(final Session> session, final Move delegate, final CryptoVault cryptomator) {
+ public CryptoMoveV6Feature(final Session> session, final Move delegate, final AbstractVault cryptomator) {
this.session = session;
this.proxy = delegate;
this.vault = cryptomator;
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMoveV7Feature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMoveV7Feature.java
index a0bd1c8eb7f..ea1c2fd5659 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMoveV7Feature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMoveV7Feature.java
@@ -19,8 +19,7 @@
import ch.cyberduck.core.LocaleFactory;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.Session;
-import ch.cyberduck.core.cryptomator.CryptoVault;
-import ch.cyberduck.core.cryptomator.impl.CryptoDirectoryV7Provider;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.InvalidFilenameException;
import ch.cyberduck.core.features.Delete;
@@ -35,12 +34,12 @@ public class CryptoMoveV7Feature implements Move {
private final Session> session;
private final Move proxy;
- private final CryptoVault vault;
+ private final AbstractVault vault;
- public CryptoMoveV7Feature(final Session> session, final Move delegate, final CryptoVault cryptomator) {
+ public CryptoMoveV7Feature(final Session> session, final Move delegate, final AbstractVault vault) {
this.session = session;
this.proxy = delegate;
- this.vault = cryptomator;
+ this.vault = vault;
}
@Override
@@ -51,8 +50,8 @@ public Path move(final Path file, final Path renamed, final TransferStatus statu
final Path target = proxy.move(sourceEncrypted, targetEncrypted, status, callback, connectionCallback);
if(file.isDirectory()) {
if(!proxy.isRecursive(file, renamed)) {
- proxy.move(new Path(sourceEncrypted, CryptoDirectoryV7Provider.DIRECTORY_METADATAFILE, EnumSet.of(Path.Type.file)),
- new Path(targetEncrypted, CryptoDirectoryV7Provider.DIRECTORY_METADATAFILE, EnumSet.of(Path.Type.file)),
+ proxy.move(new Path(sourceEncrypted, vault.getDirectoryMetadataFilename(), EnumSet.of(Path.Type.file)),
+ new Path(targetEncrypted, vault.getBackupDirectoryMetadataFilename(), EnumSet.of(Path.Type.file)),
new TransferStatus(status), callback, connectionCallback);
}
vault.getDirectoryProvider().delete(file);
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMultipartWriteFeature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMultipartWriteFeature.java
index 4943f838a28..f5d6de84717 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMultipartWriteFeature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoMultipartWriteFeature.java
@@ -16,18 +16,18 @@
*/
import ch.cyberduck.core.Session;
-import ch.cyberduck.core.cryptomator.CryptoVault;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.features.AttributesFinder;
import ch.cyberduck.core.features.Find;
import ch.cyberduck.core.features.MultipartWrite;
import ch.cyberduck.core.features.Write;
public class CryptoMultipartWriteFeature extends CryptoWriteFeature implements MultipartWrite {
- public CryptoMultipartWriteFeature(final Session> session, final Write delegate, final CryptoVault vault) {
+ public CryptoMultipartWriteFeature(final Session> session, final Write delegate, final AbstractVault vault) {
super(session, delegate, vault);
}
- public CryptoMultipartWriteFeature(final Session> session, final Write delegate, final Find finder, final AttributesFinder attributes, final CryptoVault vault) {
+ public CryptoMultipartWriteFeature(final Session> session, final Write delegate, final Find finder, final AttributesFinder attributes, final AbstractVault vault) {
super(session, delegate, vault);
}
}
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoReadFeature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoReadFeature.java
index 758f6ee2db0..27d84106f53 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoReadFeature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoReadFeature.java
@@ -19,8 +19,8 @@
import ch.cyberduck.core.DefaultIOExceptionMappingService;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.Session;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.CryptoInputStream;
-import ch.cyberduck.core.cryptomator.CryptoVault;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.Read;
import ch.cyberduck.core.transfer.TransferStatus;
@@ -36,9 +36,9 @@ public class CryptoReadFeature implements Read {
private final Session> session;
private final Read proxy;
- private final CryptoVault vault;
+ private final AbstractVault vault;
- public CryptoReadFeature(final Session> session, final Read proxy, final CryptoVault vault) {
+ public CryptoReadFeature(final Session> session, final Read proxy, final AbstractVault vault) {
this.session = session;
this.proxy = proxy;
this.vault = vault;
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoTimestampFeature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoTimestampFeature.java
index 9db63f0ae3d..437a3c0697e 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoTimestampFeature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoTimestampFeature.java
@@ -17,8 +17,8 @@
import ch.cyberduck.core.Path;
import ch.cyberduck.core.Session;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.CryptoTransferStatus;
-import ch.cyberduck.core.cryptomator.CryptoVault;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.Timestamp;
import ch.cyberduck.core.transfer.TransferStatus;
@@ -27,9 +27,9 @@ public class CryptoTimestampFeature implements Timestamp {
private final Session> session;
private final Timestamp proxy;
- private final CryptoVault vault;
+ private final AbstractVault vault;
- public CryptoTimestampFeature(final Session> session, final Timestamp proxy, final CryptoVault vault) {
+ public CryptoTimestampFeature(final Session> session, final Timestamp proxy, final AbstractVault vault) {
this.session = session;
this.proxy = proxy;
this.vault = vault;
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoTouchFeature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoTouchFeature.java
index 238adb433b9..fafef01dc35 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoTouchFeature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoTouchFeature.java
@@ -19,7 +19,7 @@
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
import ch.cyberduck.core.Session;
-import ch.cyberduck.core.cryptomator.CryptoVault;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.random.RandomNonceGenerator;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.InvalidFilenameException;
@@ -35,9 +35,9 @@ public class CryptoTouchFeature implements Touch {
private final Session> session;
private final Touch proxy;
- private final CryptoVault vault;
+ private final AbstractVault vault;
- public CryptoTouchFeature(final Session> session, final Touch proxy, final Write writer, final CryptoVault cryptomator) {
+ public CryptoTouchFeature(final Session> session, final Touch proxy, final Write writer, final AbstractVault cryptomator) {
this.session = session;
this.proxy = proxy.withWriter(new CryptoWriteFeature<>(session, writer, cryptomator));
this.vault = cryptomator;
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoUploadFeature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoUploadFeature.java
index 4c677786be3..98047737d2c 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoUploadFeature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoUploadFeature.java
@@ -20,8 +20,8 @@
import ch.cyberduck.core.Path;
import ch.cyberduck.core.ProgressListener;
import ch.cyberduck.core.Session;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.CryptoTransferStatus;
-import ch.cyberduck.core.cryptomator.CryptoVault;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.Upload;
import ch.cyberduck.core.features.Write;
@@ -33,9 +33,9 @@ public class CryptoUploadFeature implements Upload {
private final Session> session;
private final Upload proxy;
- private final CryptoVault vault;
+ private final AbstractVault vault;
- public CryptoUploadFeature(final Session> session, final Upload delegate, final Write writer, final CryptoVault vault) {
+ public CryptoUploadFeature(final Session> session, final Upload delegate, final Write writer, final AbstractVault vault) {
this.session = session;
this.proxy = delegate.withWriter(new CryptoWriteFeature<>(session, writer, vault));
this.vault = vault;
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoWriteFeature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoWriteFeature.java
index 7a8ea6f211b..7142146e0e1 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoWriteFeature.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoWriteFeature.java
@@ -19,9 +19,9 @@
import ch.cyberduck.core.DefaultIOExceptionMappingService;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.Session;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.CryptoOutputStream;
import ch.cyberduck.core.cryptomator.CryptoTransferStatus;
-import ch.cyberduck.core.cryptomator.CryptoVault;
import ch.cyberduck.core.cryptomator.random.RandomNonceGenerator;
import ch.cyberduck.core.cryptomator.random.RotatingNonceGenerator;
import ch.cyberduck.core.exception.BackgroundException;
@@ -42,9 +42,9 @@ public class CryptoWriteFeature implements Write {
private final Session> session;
private final Write proxy;
- private final CryptoVault vault;
+ private final AbstractVault vault;
- public CryptoWriteFeature(final Session> session, final Write proxy, final CryptoVault vault) {
+ public CryptoWriteFeature(final Session> session, final Write proxy, final AbstractVault vault) {
this.session = session;
this.proxy = proxy;
this.vault = vault;
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryUVFProvider.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryUVFProvider.java
new file mode 100644
index 00000000000..6f42e9e3b5d
--- /dev/null
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryUVFProvider.java
@@ -0,0 +1,177 @@
+package ch.cyberduck.core.cryptomator.impl;
+
+/*
+ * Copyright (c) 2002-2025 iterate GmbH. All rights reserved.
+ * https://cyberduck.io/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+import ch.cyberduck.core.Path;
+import ch.cyberduck.core.PathAttributes;
+import ch.cyberduck.core.RandomStringService;
+import ch.cyberduck.core.Session;
+import ch.cyberduck.core.SimplePathPredicate;
+import ch.cyberduck.core.UUIDRandomStringService;
+import ch.cyberduck.core.cryptomator.AbstractVault;
+import ch.cyberduck.core.cryptomator.ContentReader;
+import ch.cyberduck.core.cryptomator.CryptoFilename;
+import ch.cyberduck.core.cryptomator.CryptorCache;
+import ch.cyberduck.core.exception.BackgroundException;
+import ch.cyberduck.core.exception.NotfoundException;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.cryptomator.cryptolib.api.FileHeader;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.EnumSet;
+
+import com.google.common.io.BaseEncoding;
+
+public class CryptoDirectoryUVFProvider extends CryptoDirectoryV7Provider {
+ private static final Logger log = LogManager.getLogger(CryptoDirectoryUVFProvider.class);
+
+ private final Path home;
+ private final AbstractVault vault;
+
+ private final RandomStringService random
+ = new UUIDRandomStringService();
+ private final Path dataRoot;
+ private final CryptorCache filenameCryptor;
+ private final CryptoFilename filenameProvider;
+
+ public CryptoDirectoryUVFProvider(final AbstractVault vault, final CryptoFilename filenameProvider, final CryptorCache filenameCryptor) {
+ super(vault, filenameProvider, filenameCryptor);
+ this.filenameCryptor = filenameCryptor;
+ this.filenameProvider = filenameProvider;
+ this.home = vault.getHome();
+ this.vault = vault;
+ this.dataRoot = new Path(home, "d", home.getType());
+ }
+
+ @Override
+ protected byte[] toDirectoryId(final Session> session, final Path directory, final byte[] directoryId) throws BackgroundException {
+ if(new SimplePathPredicate(home).test(directory)) {
+ return vault.getRootDirId();
+ }
+ return super.toDirectoryId(session, directory, directoryId);
+ }
+
+ // interface mismatch: we need parent path to get dirId and revision from dir.uvf
+ public String toEncrypted(final Session> session, final Path parent, final String filename) throws BackgroundException {
+ if(new SimplePathPredicate(home).test(parent)) {
+ final String ciphertextName = filenameCryptor.encryptFilename(BaseEncoding.base64Url(), filename, vault.getRootDirId()) + vault.getRegularFileExtension();
+ log.debug("Encrypted filename {} to {}", filename, ciphertextName);
+ return filenameProvider.deflate(session, ciphertextName);
+
+ }
+ final byte[] directoryId = load(session, parent);
+ final String ciphertextName = vault.getCryptor().fileNameCryptor(loadRevision(session, parent)).encryptFilename(BaseEncoding.base64Url(), filename, directoryId) + vault.getRegularFileExtension();
+ log.debug("Encrypted filename {} to {}", filename, ciphertextName);
+ return filenameProvider.deflate(session, ciphertextName);
+ }
+
+ @Override
+ public Path toEncrypted(final Session> session, final byte[] directoryId, final Path directory) throws BackgroundException {
+ if(!directory.isDirectory()) {
+ throw new NotfoundException(directory.getAbsolute());
+ }
+ if(new SimplePathPredicate(directory).test(home) || directory.isChild(home)) {
+ final PathAttributes attributes = new PathAttributes(directory.attributes());
+ // The root of the vault is a different target directory and file ids always correspond to the metadata file
+ attributes.withVersionId(null);
+ attributes.withFileId(null);
+ // Remember random directory id for use in vault
+ final byte[] id = this.toDirectoryId(session, directory, directoryId);
+ log.debug("Use directory ID '{}' for folder {}", id, directory);
+ attributes.setDirectoryId(id);
+ attributes.setDecrypted(directory);
+ final String directoryIdHash;
+ if(new SimplePathPredicate(home).test(directory)) {
+ // TODO hard-coded to initial seed in UVFVault
+ directoryIdHash = filenameCryptor.hashDirectoryId(id);
+ }
+ else {
+ directoryIdHash = vault.getCryptor().fileNameCryptor(loadRevision(session, directory)).hashDirectoryId(id);
+ }
+ // Intermediate directory
+ final Path intermediate = new Path(dataRoot, directoryIdHash.substring(0, 2), dataRoot.getType());
+ // Add encrypted type
+ final EnumSet type = EnumSet.copyOf(directory.getType());
+ type.add(Path.Type.encrypted);
+ type.remove(Path.Type.decrypted);
+ return new Path(intermediate, directoryIdHash.substring(2), type, attributes);
+ }
+ throw new NotfoundException(directory.getAbsolute());
+ }
+
+ protected byte[] load(final Session> session, final Path directory) throws BackgroundException {
+ if(new SimplePathPredicate(home).test(directory)) {
+ return vault.getRootDirId();
+ }
+ final Path parent = this.toEncrypted(session, directory.getParent().attributes().getDirectoryId(), directory.getParent());
+ final String cleartextName = directory.getName();
+ final String ciphertextName = this.toEncrypted(session, parent.attributes().getDirectoryId(), cleartextName, EnumSet.of(Path.Type.directory));
+ final Path metadataParent = new Path(parent, ciphertextName, EnumSet.of(Path.Type.directory));
+ // Read directory id from file
+ try {
+ log.debug("Read directory ID for folder {} from {}", directory, ciphertextName);
+ final Path metadataFile = new Path(metadataParent, vault.getDirectoryMetadataFilename(), EnumSet.of(Path.Type.file, Path.Type.encrypted));
+ final byte[] ciphertext = new ContentReader(session).readBytes(metadataFile);
+ // https://github.com/encryption-alliance/unified-vault-format/blob/develop/file%20name%20encryption/AES-SIV-512-B64URL.md#format-of-diruvf-and-symlinkuvf
+ // TODO can we not use org.cryptomator.cryptolib.v3.DirectoryContentCryptorImpl.decryptDirectoryMetadata()? DirectoryMetadataImpl is not visible and DirectoryMetadata is empty interface, so we cannot access dirId attribute.
+ if(ciphertext.length != 128) {
+ throw new IllegalArgumentException("Invalid dir.uvf length: " + ciphertext.length);
+ }
+ int headerSize = vault.getCryptor().fileHeaderCryptor().headerSize();
+ ByteBuffer buffer = ByteBuffer.wrap(ciphertext);
+ ByteBuffer headerBuf = buffer.duplicate();
+ headerBuf.position(0).limit(headerSize);
+ ByteBuffer contentBuf = buffer.duplicate();
+ contentBuf.position(headerSize);
+
+ FileHeader header = vault.getCryptor().fileHeaderCryptor(loadRevision(session, directory)).decryptHeader(headerBuf);
+ ByteBuffer plaintext = vault.getCryptor().fileContentCryptor().decryptChunk(contentBuf, 0, header, true);
+ assert plaintext.remaining() == 32;
+ byte[] dirId = new byte[32];
+ plaintext.get(dirId);
+ return dirId;
+ }
+ catch(NotfoundException e) {
+ log.warn("Missing directory ID for folder {}", directory);
+ return random.random().getBytes(StandardCharsets.US_ASCII);
+ }
+ }
+
+ protected int loadRevision(final Session> session, final Path directory) throws BackgroundException {
+ final Path parent = this.toEncrypted(session, directory.getParent().attributes().getDirectoryId(), directory.getParent());
+ final String cleartextName = directory.getName();
+ final String ciphertextName = this.toEncrypted(session, parent.attributes().getDirectoryId(), cleartextName, EnumSet.of(Path.Type.directory));
+ final Path metadataParent = new Path(parent, ciphertextName, EnumSet.of(Path.Type.directory));
+ // Read directory id from file
+ log.debug("Read directory ID for folder {} from {}", directory, ciphertextName);
+ final Path metadataFile = new Path(metadataParent, vault.getDirectoryMetadataFilename(), EnumSet.of(Path.Type.file, Path.Type.encrypted));
+ final byte[] ciphertext = new ContentReader(session).readBytes(metadataFile);
+ // https://github.com/encryption-alliance/unified-vault-format/blob/develop/file%20name%20encryption/AES-SIV-512-B64URL.md#format-of-diruvf-and-symlinkuvf
+ // TODO can we not use org.cryptomator.cryptolib.v3.DirectoryContentCryptorImpl.decryptDirectoryMetadata()? DirectoryMetadataImpl is not visible and DirectoryMetadata is empty interface, so we cannot access dirId attribute.
+ if(ciphertext.length != 128) {
+ throw new IllegalArgumentException("Invalid dir.uvf length: " + ciphertext.length);
+ }
+ int headerSize = vault.getCryptor().fileHeaderCryptor().headerSize();
+ ByteBuffer buffer = ByteBuffer.wrap(ciphertext);
+ ByteBuffer headerBuf = buffer.duplicate();
+ headerBuf.position(4).limit(headerSize);
+ return headerBuf.order(ByteOrder.BIG_ENDIAN).getInt();
+ }
+}
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV6Provider.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV6Provider.java
index 8efb36c5f69..19ff6c0198b 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV6Provider.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV6Provider.java
@@ -25,17 +25,19 @@
import ch.cyberduck.core.cache.LRUCache;
import ch.cyberduck.core.cryptomator.ContentReader;
import ch.cyberduck.core.cryptomator.CryptoDirectory;
+import ch.cyberduck.core.cryptomator.CryptoFilename;
import ch.cyberduck.core.cryptomator.CryptoVault;
import ch.cyberduck.core.cryptomator.CryptorCache;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.NotfoundException;
import ch.cyberduck.core.preferences.PreferencesFactory;
-import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.ArrayUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
import java.util.EnumSet;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -44,36 +46,39 @@ public class CryptoDirectoryV6Provider implements CryptoDirectory {
private static final Logger log = LogManager.getLogger(CryptoDirectoryV6Provider.class);
private static final String DATA_DIR_NAME = "d";
- private static final String ROOT_DIR_ID = StringUtils.EMPTY;
private final Path dataRoot;
private final Path home;
- private final CryptoVault cryptomator;
+ private final CryptoFilename filenameProvider;
+ private final CryptorCache filenameCryptor;
private final RandomStringService random
- = new UUIDRandomStringService();
+ = new UUIDRandomStringService();
private final Lock lock = new ReentrantLock();
- private final LRUCache, String> cache = LRUCache.build(
- PreferencesFactory.get().getInteger("cryptomator.cache.size"));
+ private final LRUCache, byte[]> cache = LRUCache.build(
+ PreferencesFactory.get().getInteger("cryptomator.cache.size"));
- public CryptoDirectoryV6Provider(final Path vault, final CryptoVault cryptomator) {
+ public static final byte[] ROOT_DIR_ID = new byte[0];
+
+ public CryptoDirectoryV6Provider(final Path vault, final CryptoFilename filenameProvider, final CryptorCache filenameCryptor) {
this.home = vault;
this.dataRoot = new Path(vault, DATA_DIR_NAME, vault.getType());
- this.cryptomator = cryptomator;
+ this.filenameProvider = filenameProvider;
+ this.filenameCryptor = filenameCryptor;
}
@Override
- public String toEncrypted(final Session> session, final String directoryId, final String filename, final EnumSet type) throws BackgroundException {
+ public String toEncrypted(final Session> session, final byte[] directoryId, final String filename, final EnumSet type) throws BackgroundException {
final String prefix = type.contains(Path.Type.directory) ? CryptoVault.DIR_PREFIX : "";
- final String ciphertextName = prefix + cryptomator.getFileNameCryptor().encryptFilename(CryptorCache.BASE32, filename, directoryId.getBytes(StandardCharsets.UTF_8));
+ final String ciphertextName = prefix + filenameCryptor.encryptFilename(CryptorCache.BASE32, filename, directoryId);
log.debug("Encrypted filename {} to {}", filename, ciphertextName);
- return cryptomator.getFilenameProvider().deflate(session, ciphertextName);
+ return filenameProvider.deflate(session, ciphertextName);
}
@Override
- public Path toEncrypted(final Session> session, final String directoryId, final Path directory) throws BackgroundException {
+ public Path toEncrypted(final Session> session, final byte[] directoryId, final Path directory) throws BackgroundException {
if(!directory.isDirectory()) {
throw new NotfoundException(directory.getAbsolute());
}
@@ -83,11 +88,11 @@ public Path toEncrypted(final Session> session, final String directoryId, fina
attributes.withVersionId(null);
attributes.withFileId(null);
// Remember random directory id for use in vault
- final String id = this.toDirectoryId(session, directory, directoryId);
+ final byte[] id = this.toDirectoryId(session, directory, directoryId);
log.debug("Use directory ID '{}' for folder {}", id, directory);
attributes.setDirectoryId(id);
attributes.setDecrypted(directory);
- final String directoryIdHash = cryptomator.getFileNameCryptor().hashDirectoryId(id);
+ final String directoryIdHash = filenameCryptor.hashDirectoryId(id);
// Intermediate directory
final Path intermediate = new Path(dataRoot, directoryIdHash.substring(0, 2), dataRoot.getType());
// Add encrypted type
@@ -99,18 +104,18 @@ public Path toEncrypted(final Session> session, final String directoryId, fina
throw new NotfoundException(directory.getAbsolute());
}
- private String toDirectoryId(final Session> session, final Path directory, final String directoryId) throws BackgroundException {
+ protected byte[] toDirectoryId(final Session> session, final Path directory, final byte[] directoryId) throws BackgroundException {
if(new SimplePathPredicate(home).test(directory)) {
return ROOT_DIR_ID;
}
- if(StringUtils.isBlank(directoryId)) {
+ if(ArrayUtils.isEmpty(directoryId)) {
if(cache.contains(new SimplePathPredicate(directory))) {
return cache.get(new SimplePathPredicate(directory));
}
try {
log.debug("Acquire lock for {}", directory);
lock.lock();
- final String id = this.load(session, directory);
+ final byte[] id = this.load(session, directory);
cache.put(new SimplePathPredicate(directory), id);
return id;
}
@@ -122,15 +127,15 @@ private String toDirectoryId(final Session> session, final Path directory, fin
cache.put(new SimplePathPredicate(directory), directoryId);
}
else {
- final String existing = cache.get(new SimplePathPredicate(directory));
- if(!existing.equals(directoryId)) {
+ final byte[] existing = cache.get(new SimplePathPredicate(directory));
+ if(!Arrays.equals(existing, directoryId)) {
log.warn("Do not override already cached id {} with {}", existing, directoryId);
}
}
return cache.get(new SimplePathPredicate(directory));
}
- protected String load(final Session> session, final Path directory) throws BackgroundException {
+ protected byte[] load(final Session> session, final Path directory) throws BackgroundException {
final Path parent = this.toEncrypted(session, directory.getParent().attributes().getDirectoryId(), directory.getParent());
final String cleartextName = directory.getName();
final String ciphertextName = this.toEncrypted(session, parent.attributes().getDirectoryId(), cleartextName, EnumSet.of(Path.Type.directory));
@@ -138,11 +143,11 @@ protected String load(final Session> session, final Path directory) throws Bac
try {
log.debug("Read directory ID for folder {} from {}", directory, ciphertextName);
final Path metadataFile = new Path(parent, ciphertextName, EnumSet.of(Path.Type.file, Path.Type.encrypted));
- return new ContentReader(session).read(metadataFile);
+ return new ContentReader(session).readBytes(metadataFile);
}
catch(NotfoundException e) {
log.warn("Missing directory ID for folder {}", directory);
- return random.random();
+ return random.random().getBytes(StandardCharsets.US_ASCII);
}
}
diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV7Provider.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV7Provider.java
index 28b4bb6211d..97ca6161b5f 100644
--- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV7Provider.java
+++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV7Provider.java
@@ -19,8 +19,10 @@
import ch.cyberduck.core.RandomStringService;
import ch.cyberduck.core.Session;
import ch.cyberduck.core.UUIDRandomStringService;
+import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.ContentReader;
-import ch.cyberduck.core.cryptomator.CryptoVault;
+import ch.cyberduck.core.cryptomator.CryptoFilename;
+import ch.cyberduck.core.cryptomator.CryptorCache;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.NotfoundException;
@@ -35,31 +37,28 @@
public class CryptoDirectoryV7Provider extends CryptoDirectoryV6Provider {
private static final Logger log = LogManager.getLogger(CryptoDirectoryV7Provider.class);
- public static final String EXTENSION_REGULAR = ".c9r";
- public static final String FILENAME_DIRECTORYID = "dir";
- public static final String DIRECTORY_METADATAFILE = String.format("%s%s", FILENAME_DIRECTORYID, EXTENSION_REGULAR);
- public static final String BACKUP_FILENAME_DIRECTORYID = "dirid";
- public static final String BACKUP_DIRECTORY_METADATAFILE = String.format("%s%s", BACKUP_FILENAME_DIRECTORYID, EXTENSION_REGULAR);
-
- private final CryptoVault cryptomator;
+ private final CryptoFilename filenameProvider;
+ private final CryptorCache filenameCryptor;
+ private final AbstractVault vault;
private final RandomStringService random
- = new UUIDRandomStringService();
+ = new UUIDRandomStringService();
- public CryptoDirectoryV7Provider(final Path vault, final CryptoVault cryptomator) {
- super(vault, cryptomator);
- this.cryptomator = cryptomator;
+ public CryptoDirectoryV7Provider(final AbstractVault vault, final CryptoFilename filenameProvider, final CryptorCache filenameCryptor) {
+ super(vault.getHome(), filenameProvider, filenameCryptor);
+ this.filenameProvider = filenameProvider;
+ this.filenameCryptor = filenameCryptor;
+ this.vault = vault;
}
@Override
- public String toEncrypted(final Session> session, final String directoryId, final String filename, final EnumSet type) throws BackgroundException {
- final String ciphertextName = cryptomator.getFileNameCryptor().encryptFilename(BaseEncoding.base64Url(),
- filename, directoryId.getBytes(StandardCharsets.UTF_8)) + EXTENSION_REGULAR;
+ public String toEncrypted(final Session> session, final byte[] directoryId, final String filename, final EnumSet type) throws BackgroundException {
+ final String ciphertextName = filenameCryptor.encryptFilename(BaseEncoding.base64Url(), filename, directoryId) + vault.getRegularFileExtension();
log.debug("Encrypted filename {} to {}", filename, ciphertextName);
- return cryptomator.getFilenameProvider().deflate(session, ciphertextName);
+ return filenameProvider.deflate(session, ciphertextName);
}
- protected String load(final Session> session, final Path directory) throws BackgroundException {
+ protected byte[] load(final Session> session, final Path directory) throws BackgroundException {
final Path parent = this.toEncrypted(session, directory.getParent().attributes().getDirectoryId(), directory.getParent());
final String cleartextName = directory.getName();
final String ciphertextName = this.toEncrypted(session, parent.attributes().getDirectoryId(), cleartextName, EnumSet.of(Path.Type.directory));
@@ -67,12 +66,12 @@ protected String load(final Session> session, final Path directory) throws Bac
// Read directory id from file
try {
log.debug("Read directory ID for folder {} from {}", directory, ciphertextName);
- final Path metadataFile = new Path(metadataParent, CryptoDirectoryV7Provider.DIRECTORY_METADATAFILE, EnumSet.of(Path.Type.file, Path.Type.encrypted));
- return new ContentReader(session).read(metadataFile);
+ final Path metadataFile = new Path(metadataParent, vault.getDirectoryMetadataFilename(), EnumSet.of(Path.Type.file, Path.Type.encrypted));
+ return new ContentReader(session).readBytes(metadataFile);
}
catch(NotfoundException e) {
log.warn("Missing directory ID for folder {}", directory);
- return random.random();
+ return random.random().getBytes(StandardCharsets.US_ASCII);
}
}
}
diff --git a/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/CryptoVaultTest.java b/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/CryptoVaultTest.java
index 4bcfc9c8d12..59f21686881 100644
--- a/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/CryptoVaultTest.java
+++ b/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/CryptoVaultTest.java
@@ -41,7 +41,7 @@
import org.apache.commons.io.IOUtils;
import org.cryptomator.cryptolib.api.CryptorProvider;
-import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.MasterkeyFile;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.junit.Test;
@@ -563,7 +563,7 @@ public static String createJWT(final String masterkeyCryptomator,
final MasterkeyFile mkFile = MasterkeyFile.read(new StringReader(masterkeyCryptomator));
final StringWriter writer = new StringWriter();
mkFile.write(writer);
- final Masterkey masterkey = new MasterkeyFileAccess(PreferencesFactory.get().getProperty("cryptomator.vault.pepper").getBytes(StandardCharsets.UTF_8),
+ final PerpetualMasterkey masterkey = new MasterkeyFileAccess(PreferencesFactory.get().getProperty("cryptomator.vault.pepper").getBytes(StandardCharsets.UTF_8),
FastSecureRandomProvider.get().provide()).load(new ByteArrayInputStream(writer.getBuffer().toString().getBytes(StandardCharsets.UTF_8)), passphrase);
final Algorithm algorithm = Algorithm.HMAC256(masterkey.getEncoded());
return JWT.create()
diff --git a/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/CryptorCacheTest.java b/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/CryptorCacheTest.java
index f88038fc29e..94deb11d3d5 100644
--- a/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/CryptorCacheTest.java
+++ b/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/CryptorCacheTest.java
@@ -19,10 +19,11 @@
import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.junit.Test;
+import java.nio.charset.StandardCharsets;
+
import com.google.common.io.BaseEncoding;
import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
public class CryptorCacheTest {
@@ -31,10 +32,10 @@ public class CryptorCacheTest {
public void TestHashDirectoryId() {
final FileNameCryptor mock = mock(FileNameCryptor.class);
final CryptorCache cryptor = new CryptorCache(mock);
- when(mock.hashDirectoryId(anyString())).thenReturn("hashed");
- assertEquals("hashed", cryptor.hashDirectoryId("id"));
- assertEquals("hashed", cryptor.hashDirectoryId("id"));
- verify(mock, times(1)).hashDirectoryId(anyString());
+ when(mock.hashDirectoryId(any(byte[].class))).thenReturn("hashed");
+ assertEquals("hashed", cryptor.hashDirectoryId("id".getBytes(StandardCharsets.US_ASCII)));
+ assertEquals("hashed", cryptor.hashDirectoryId("id".getBytes(StandardCharsets.US_ASCII)));
+ verify(mock, times(1)).hashDirectoryId(any(byte[].class));
verifyNoMoreInteractions(mock);
}
diff --git a/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/features/CryptoBulkFeatureTest.java b/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/features/CryptoBulkFeatureTest.java
index 52f12e31f97..6499bae0cb2 100644
--- a/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/features/CryptoBulkFeatureTest.java
+++ b/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/features/CryptoBulkFeatureTest.java
@@ -105,7 +105,7 @@ public boolean test(final TransferItem item) {
return item.remote.isDirectory();
}
}).findFirst().get().remote;
- final String directoryId = encryptedDirectory.attributes().getDirectoryId();
+ final byte[] directoryId = encryptedDirectory.attributes().getDirectoryId();
assertNotNull(directoryId);
for(TransferItem file : pre.keySet().stream().filter(new Predicate() {
@Override
diff --git a/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV6ProviderTest.java b/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV6ProviderTest.java
index a2f78fa324e..f4b09439953 100644
--- a/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV6ProviderTest.java
+++ b/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV6ProviderTest.java
@@ -15,27 +15,20 @@
* GNU General Public License for more details.
*/
-import ch.cyberduck.core.ConnectionCallback;
-import ch.cyberduck.core.Credentials;
-import ch.cyberduck.core.DisabledPasswordCallback;
import ch.cyberduck.core.Host;
-import ch.cyberduck.core.LoginOptions;
import ch.cyberduck.core.NullSession;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.TestProtocol;
import ch.cyberduck.core.cryptomator.CryptoDirectory;
-import ch.cyberduck.core.cryptomator.CryptoVault;
-import ch.cyberduck.core.exception.BackgroundException;
+import ch.cyberduck.core.cryptomator.CryptorCache;
import ch.cyberduck.core.exception.NotfoundException;
-import ch.cyberduck.core.features.Read;
-import ch.cyberduck.core.transfer.TransferStatus;
-import ch.cyberduck.core.vault.VaultCredentials;
-import org.apache.commons.io.IOUtils;
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.junit.Test;
-import java.io.InputStream;
-import java.nio.charset.Charset;
+import java.security.SecureRandom;
import java.util.EnumSet;
import static org.junit.Assert.assertEquals;
@@ -46,62 +39,31 @@ public class CryptoDirectoryV6ProviderTest {
@Test(expected = NotfoundException.class)
public void testToEncryptedInvalidArgument() throws Exception {
final Path home = new Path("/vault", EnumSet.of(Path.Type.directory));
- final CryptoVault vault = new CryptoVault(home);
- final CryptoDirectory provider = new CryptoDirectoryV6Provider(home, vault);
+ final CryptorProvider crypto = CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_CTRMAC);
+ final SecureRandom random = new SecureRandom();
+ final Cryptor cryptor = crypto.provide(PerpetualMasterkey.generate(random), random);
+ final CryptoDirectory provider = new CryptoDirectoryV6Provider(home, new CryptoFilenameV6Provider(home), new CryptorCache(cryptor.fileNameCryptor()));
provider.toEncrypted(new NullSession(new Host(new TestProtocol())), null, new Path("/vault/f", EnumSet.of(Path.Type.file)));
}
@Test(expected = NotfoundException.class)
public void testToEncryptedInvalidPath() throws Exception {
final Path home = new Path("/vault", EnumSet.of(Path.Type.directory));
- final CryptoVault vault = new CryptoVault(home);
- final CryptoDirectory provider = new CryptoDirectoryV6Provider(home, vault);
+ final CryptorProvider crypto = CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_CTRMAC);
+ final SecureRandom random = new SecureRandom();
+ final Cryptor cryptor = crypto.provide(PerpetualMasterkey.generate(random), random);
+ final CryptoDirectory provider = new CryptoDirectoryV6Provider(home, new CryptoFilenameV6Provider(home), new CryptorCache(cryptor.fileNameCryptor()));
provider.toEncrypted(new NullSession(new Host(new TestProtocol())), null, new Path("/", EnumSet.of(Path.Type.directory)));
}
@Test
public void testToEncryptedDirectory() throws Exception {
final Path home = new Path("/vault", EnumSet.of(Path.Type.directory));
- final NullSession session = new NullSession(new Host(new TestProtocol())) {
- @Override
- @SuppressWarnings("unchecked")
- public T _getFeature(final Class type) {
- if(type == Read.class) {
- return (T) new Read() {
- @Override
- public InputStream read(final Path file, final TransferStatus status, final ConnectionCallback callback) throws BackgroundException {
- final String masterKey = "{\n" +
- " \"scryptSalt\": \"NrC7QGG/ouc=\",\n" +
- " \"scryptCostParam\": 16384,\n" +
- " \"scryptBlockSize\": 8,\n" +
- " \"primaryMasterKey\": \"Q7pGo1l0jmZssoQh9rXFPKJE9NIXvPbL+HcnVSR9CHdkeR8AwgFtcw==\",\n" +
- " \"hmacMasterKey\": \"xzBqT4/7uEcQbhHFLC0YmMy4ykVKbuvJEA46p1Xm25mJNuTc20nCbw==\",\n" +
- " \"versionMac\": \"hlNr3dz/CmuVajhaiGyCem9lcVIUjDfSMLhjppcXOrM=\",\n" +
- " \"version\": 5\n" +
- "}";
- if("masterkey.cryptomator".equals(file.getName())) {
- return IOUtils.toInputStream(masterKey, Charset.defaultCharset());
- }
- throw new NotfoundException(String.format("%s not found", file.getName()));
- }
-
- @Override
- public boolean offset(final Path file) {
- return false;
- }
- };
- }
- return super._getFeature(type);
- }
- };
- final CryptoVault vault = new CryptoVault(home);
- vault.load(session, new DisabledPasswordCallback() {
- @Override
- public Credentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) {
- return new VaultCredentials("vault");
- }
- });
- final CryptoDirectory provider = new CryptoDirectoryV6Provider(home, vault);
+ final NullSession session = new NullSession(new Host(new TestProtocol()));
+ final CryptorProvider crypto = CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_CTRMAC);
+ final SecureRandom csprng = new SecureRandom();
+ final Cryptor cryptor = crypto.provide(PerpetualMasterkey.generate(csprng), csprng);
+ final CryptoDirectory provider = new CryptoDirectoryV6Provider(home, new CryptoFilenameV6Provider(home), new CryptorCache(cryptor.fileNameCryptor()));
assertNotNull(provider.toEncrypted(session, null, home));
final Path f = new Path("/vault/f", EnumSet.of(Path.Type.directory));
assertNotNull(provider.toEncrypted(session, null, f));
diff --git a/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV7ProviderTest.java b/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV7ProviderTest.java
index ee372bed4d0..dedc79f25e0 100644
--- a/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV7ProviderTest.java
+++ b/cryptomator/src/test/java/ch/cyberduck/core/cryptomator/impl/CryptoDirectoryV7ProviderTest.java
@@ -15,32 +15,23 @@
* GNU General Public License for more details.
*/
-import ch.cyberduck.core.ConnectionCallback;
-import ch.cyberduck.core.Credentials;
-import ch.cyberduck.core.DisabledPasswordCallback;
import ch.cyberduck.core.Host;
-import ch.cyberduck.core.LoginOptions;
import ch.cyberduck.core.NullSession;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.TestProtocol;
import ch.cyberduck.core.cryptomator.CryptoDirectory;
import ch.cyberduck.core.cryptomator.CryptoVault;
-import ch.cyberduck.core.exception.BackgroundException;
+import ch.cyberduck.core.cryptomator.CryptorCache;
import ch.cyberduck.core.exception.NotfoundException;
-import ch.cyberduck.core.features.Read;
-import ch.cyberduck.core.transfer.TransferStatus;
-import ch.cyberduck.core.vault.VaultCredentials;
-import org.apache.commons.io.IOUtils;
+import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.junit.Test;
-import java.io.InputStream;
-import java.nio.charset.Charset;
+import java.security.SecureRandom;
import java.util.EnumSet;
-import static ch.cyberduck.core.cryptomator.CryptoVault.VAULT_VERSION;
-import static ch.cyberduck.core.cryptomator.CryptoVaultTest.createJWT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -49,65 +40,31 @@ public class CryptoDirectoryV7ProviderTest {
@Test(expected = NotfoundException.class)
public void testToEncryptedInvalidArgument() throws Exception {
final Path home = new Path("/vault", EnumSet.of(Path.Type.directory));
- final CryptoVault vault = new CryptoVault(home);
- final CryptoDirectory provider = new CryptoDirectoryV7Provider(home, vault);
+ final CryptorProvider crypto = CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_GCM);
+ final SecureRandom random = new SecureRandom();
+ final Cryptor cryptor = crypto.provide(PerpetualMasterkey.generate(random), random);
+ final CryptoDirectory provider = new CryptoDirectoryV7Provider(new CryptoVault(home), new CryptoFilenameV7Provider(), new CryptorCache(cryptor.fileNameCryptor()));
provider.toEncrypted(new NullSession(new Host(new TestProtocol())), null, new Path("/vault/f", EnumSet.of(Path.Type.file)));
}
@Test(expected = NotfoundException.class)
public void testToEncryptedInvalidPath() throws Exception {
final Path home = new Path("/vault", EnumSet.of(Path.Type.directory));
- final CryptoVault vault = new CryptoVault(home);
- final CryptoDirectory provider = new CryptoDirectoryV7Provider(home, vault);
+ final CryptorProvider crypto = CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_GCM);
+ final SecureRandom random = new SecureRandom();
+ final Cryptor cryptor = crypto.provide(PerpetualMasterkey.generate(random), random);
+ final CryptoDirectory provider = new CryptoDirectoryV7Provider(new CryptoVault(home), new CryptoFilenameV7Provider(), new CryptorCache(cryptor.fileNameCryptor()));
provider.toEncrypted(new NullSession(new Host(new TestProtocol())), null, new Path("/", EnumSet.of(Path.Type.directory)));
}
@Test
public void testToEncryptedDirectory() throws Exception {
final Path home = new Path("/vault", EnumSet.of(Path.Type.directory));
- final NullSession session = new NullSession(new Host(new TestProtocol())) {
- @Override
- @SuppressWarnings("unchecked")
- public T _getFeature(final Class type) {
- if(type == Read.class) {
- return (T) new Read() {
- @Override
- public InputStream read(final Path file, final TransferStatus status, final ConnectionCallback callback) throws BackgroundException {
- final String masterKey = "{\n" +
- " \"scryptSalt\": \"NrC7QGG/ouc=\",\n" +
- " \"scryptCostParam\": 16384,\n" +
- " \"scryptBlockSize\": 8,\n" +
- " \"primaryMasterKey\": \"Q7pGo1l0jmZssoQh9rXFPKJE9NIXvPbL+HcnVSR9CHdkeR8AwgFtcw==\",\n" +
- " \"hmacMasterKey\": \"xzBqT4/7uEcQbhHFLC0YmMy4ykVKbuvJEA46p1Xm25mJNuTc20nCbw==\",\n" +
- " \"versionMac\": \"hlNr3dz/CmuVajhaiGyCem9lcVIUjDfSMLhjppcXOrM=\",\n" +
- " \"version\": 8\n" +
- "}";
- if("masterkey.cryptomator".equals(file.getName())) {
- return IOUtils.toInputStream(masterKey, Charset.defaultCharset());
- }
- if("vault.cryptomator".equals(file.getName())) {
- return IOUtils.toInputStream(createJWT(masterKey, VAULT_VERSION, CryptorProvider.Scheme.SIV_GCM, "vault"), Charset.defaultCharset());
- }
- throw new NotfoundException(String.format("%s not found", file.getName()));
- }
-
- @Override
- public boolean offset(final Path file) {
- return false;
- }
- };
- }
- return super._getFeature(type);
- }
- };
- final CryptoVault vault = new CryptoVault(home);
- vault.load(session, new DisabledPasswordCallback() {
- @Override
- public Credentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) {
- return new VaultCredentials("vault");
- }
- });
- final CryptoDirectory provider = new CryptoDirectoryV7Provider(home, vault);
+ final NullSession session = new NullSession(new Host(new TestProtocol()));
+ final CryptorProvider crypto = CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_GCM);
+ final SecureRandom random = new SecureRandom();
+ final Cryptor cryptor = crypto.provide(PerpetualMasterkey.generate(random), random);
+ final CryptoDirectory provider = new CryptoDirectoryV7Provider(new CryptoVault(home), new CryptoFilenameV7Provider(), new CryptorCache(cryptor.fileNameCryptor()));
assertNotNull(provider.toEncrypted(session, null, home));
final Path f = new Path("/vault/f", EnumSet.of(Path.Type.directory));
assertNotNull(provider.toEncrypted(session, null, f));
diff --git a/ctera/pom.xml b/ctera/pom.xml
index e5a6ab2f541..b748ef0d652 100644
--- a/ctera/pom.xml
+++ b/ctera/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
ctera
jar
diff --git a/deepbox/pom.xml b/deepbox/pom.xml
index e369ff2157a..a023c70bc83 100644
--- a/deepbox/pom.xml
+++ b/deepbox/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
deepbox
diff --git a/defaults/pom.xml b/defaults/pom.xml
index 1d720376643..bba2d526004 100644
--- a/defaults/pom.xml
+++ b/defaults/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
defaults
diff --git a/dracoon/pom.xml b/dracoon/pom.xml
index 4a926163534..8b59a9c294a 100644
--- a/dracoon/pom.xml
+++ b/dracoon/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
dracoon
diff --git a/dropbox/pom.xml b/dropbox/pom.xml
index 204619ec257..dba5a6e487d 100644
--- a/dropbox/pom.xml
+++ b/dropbox/pom.xml
@@ -18,7 +18,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
4.0.0
dropbox
diff --git a/eue/pom.xml b/eue/pom.xml
index 0d2b0de5af5..7af86755c97 100644
--- a/eue/pom.xml
+++ b/eue/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
eue
jar
diff --git a/freenet/pom.xml b/freenet/pom.xml
index 47dede28b16..1cc67515406 100644
--- a/freenet/pom.xml
+++ b/freenet/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
freenet
jar
diff --git a/ftp/pom.xml b/ftp/pom.xml
index 61648eba10c..301af31d099 100644
--- a/ftp/pom.xml
+++ b/ftp/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
ftp
jar
diff --git a/googledrive/pom.xml b/googledrive/pom.xml
index a7b55a5872a..71631c38bb0 100644
--- a/googledrive/pom.xml
+++ b/googledrive/pom.xml
@@ -18,7 +18,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
4.0.0
googledrive
diff --git a/googlestorage/pom.xml b/googlestorage/pom.xml
index 73f4aa5cfdd..0bab1508262 100644
--- a/googlestorage/pom.xml
+++ b/googlestorage/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
googlestorage
diff --git a/hubic/pom.xml b/hubic/pom.xml
index b86233f24c2..31886ccc631 100644
--- a/hubic/pom.xml
+++ b/hubic/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
hubic
jar
diff --git a/i18n/pom.xml b/i18n/pom.xml
index 5a9746338ae..89a43779f38 100644
--- a/i18n/pom.xml
+++ b/i18n/pom.xml
@@ -18,7 +18,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
4.0.0
jar
diff --git a/importer/dll/pom.xml b/importer/dll/pom.xml
index dcfe952d3db..cc71cd7c948 100644
--- a/importer/dll/pom.xml
+++ b/importer/dll/pom.xml
@@ -5,7 +5,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
Cyberduck.Importer
pom
diff --git a/importer/pom.xml b/importer/pom.xml
index ce4f4ecf68c..c7ed91f9a8b 100644
--- a/importer/pom.xml
+++ b/importer/pom.xml
@@ -18,7 +18,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
4.0.0
diff --git a/irods/pom.xml b/irods/pom.xml
index 0b465e748c4..dc9f5b27044 100644
--- a/irods/pom.xml
+++ b/irods/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
irods
jar
diff --git a/jersey/pom.xml b/jersey/pom.xml
index 06ecfec237a..e5decbbe847 100644
--- a/jersey/pom.xml
+++ b/jersey/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
jersey
diff --git a/manta/pom.xml b/manta/pom.xml
index e642aef743d..47aa8e4aeb2 100644
--- a/manta/pom.xml
+++ b/manta/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
manta
diff --git a/nextcloud/pom.xml b/nextcloud/pom.xml
index 67678153000..8a9eb0a1c8e 100644
--- a/nextcloud/pom.xml
+++ b/nextcloud/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
nextcloud
jar
diff --git a/nio/pom.xml b/nio/pom.xml
index c732d62f15f..ad3f251fbae 100644
--- a/nio/pom.xml
+++ b/nio/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
nio
jar
diff --git a/oauth/pom.xml b/oauth/pom.xml
index 7a1673edf22..d946b3de8c1 100644
--- a/oauth/pom.xml
+++ b/oauth/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
oauth
diff --git a/onedrive/pom.xml b/onedrive/pom.xml
index 3379dc830bd..3334136eef3 100644
--- a/onedrive/pom.xml
+++ b/onedrive/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
onedrive
diff --git a/openstack/pom.xml b/openstack/pom.xml
index 4647dfa94e4..8bc8394475a 100644
--- a/openstack/pom.xml
+++ b/openstack/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
openstack
jar
diff --git a/osx/pom.xml b/osx/pom.xml
index dd01834ba62..5a80921e45f 100644
--- a/osx/pom.xml
+++ b/osx/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
osx
jar
diff --git a/owncloud/pom.xml b/owncloud/pom.xml
index 056b5072ccf..8a79b524c00 100644
--- a/owncloud/pom.xml
+++ b/owncloud/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
owncloud
jar
diff --git a/pom.xml b/pom.xml
index 7b8cf014906..60f63d696fc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
parent
Cyberduck
pom
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
defaults
@@ -94,6 +94,7 @@
8u312b07
0.9.1
ch.cyberduck.test.IntegrationTest
+ ch.cyberduck.core.cryptomator.SFTPCryptomatorInteroperabilityTest
@@ -555,6 +556,9 @@
${project.build.directory}
${surefire.group.excluded}
+
+ ${surefire.exclude}
+
false
diff --git a/profiles/pom.xml b/profiles/pom.xml
index f76ad6b0e31..60d176cb1f4 100644
--- a/profiles/pom.xml
+++ b/profiles/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
profiles
jar
diff --git a/protocols/dll/pom.xml b/protocols/dll/pom.xml
index 1f3243aedbf..0437b3bbf4b 100644
--- a/protocols/dll/pom.xml
+++ b/protocols/dll/pom.xml
@@ -5,7 +5,7 @@
ch.cyberduck
parent
../../pom.xml
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
Cyberduck.Protocols
pom
diff --git a/protocols/pom.xml b/protocols/pom.xml
index a72a303d7ef..65112a833c1 100644
--- a/protocols/pom.xml
+++ b/protocols/pom.xml
@@ -18,7 +18,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
4.0.0
pom
diff --git a/s3/pom.xml b/s3/pom.xml
index da2dd9bb35e..9fb9738e7a7 100644
--- a/s3/pom.xml
+++ b/s3/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
s3
jar
diff --git a/s3/src/test/java/ch/cyberduck/core/cryptomator/UVFIntegrationTest.java b/s3/src/test/java/ch/cyberduck/core/cryptomator/UVFIntegrationTest.java
new file mode 100644
index 00000000000..fa67774f1b4
--- /dev/null
+++ b/s3/src/test/java/ch/cyberduck/core/cryptomator/UVFIntegrationTest.java
@@ -0,0 +1,280 @@
+package ch.cyberduck.core.cryptomator;
+
+/*
+ * Copyright (c) 2002-2025 iterate GmbH. All rights reserved.
+ * https://cyberduck.io/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+import ch.cyberduck.core.*;
+import ch.cyberduck.core.exception.BackgroundException;
+import ch.cyberduck.core.features.AttributesFinder;
+import ch.cyberduck.core.features.Bulk;
+import ch.cyberduck.core.features.Delete;
+import ch.cyberduck.core.features.Move;
+import ch.cyberduck.core.features.Read;
+import ch.cyberduck.core.features.Write;
+import ch.cyberduck.core.io.StatusOutputStream;
+import ch.cyberduck.core.proxy.ProxyFactory;
+import ch.cyberduck.core.s3.S3BucketCreateService;
+import ch.cyberduck.core.s3.S3Protocol;
+import ch.cyberduck.core.s3.S3Session;
+import ch.cyberduck.core.shared.DefaultPathHomeFeature;
+import ch.cyberduck.core.sts.AbstractAssumeRoleWithWebIdentityTest;
+import ch.cyberduck.core.transfer.Transfer;
+import ch.cyberduck.core.transfer.TransferItem;
+import ch.cyberduck.core.transfer.TransferStatus;
+import ch.cyberduck.core.vault.DefaultVaultRegistry;
+import ch.cyberduck.core.vault.VaultRegistry;
+import ch.cyberduck.core.worker.DeleteWorker;
+import ch.cyberduck.test.TestcontainerTest;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.RandomUtils;
+import org.cryptomator.cryptolib.api.UVFMasterkey;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.testcontainers.containers.ComposeContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.AbstractMap;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test {@link UVFVault} implementation against test data from
+ * org.cryptomator.cryptolib.v3.UVFIntegrationTest
+ */
+@Category(TestcontainerTest.class)
+public class UVFIntegrationTest {
+
+ private static final ComposeContainer container = new ComposeContainer(
+ new File(AbstractAssumeRoleWithWebIdentityTest.class.getResource("/uvf/docker-compose.yml").getFile()))
+ .withPull(false)
+// .withLocalCompose(true)
+ .withEnv(
+ Stream.of(
+ new AbstractMap.SimpleImmutableEntry<>("MINIO_PORT", "9000"),
+ new AbstractMap.SimpleImmutableEntry<>("MINIO_CONSOLE_PORT", "9001")
+ ).collect(Collectors.toMap(AbstractMap.SimpleImmutableEntry::getKey, AbstractMap.SimpleImmutableEntry::getValue)))
+ .withExposedService("minio-1", 9000, Wait.forListeningPort());
+
+
+ @Test
+ public void listMinio() throws BackgroundException, IOException {
+ final String bucketName = "cyberduckbucket";
+
+ final Host bookmark = getMinIOBookmark();
+ final S3Session storage = getS3SessionForBookmark(bookmark);
+
+ final Path bucket = new Path(bucketName, EnumSet.of(AbstractPath.Type.directory));
+ new S3BucketCreateService(storage).create(bucket, "us-east-1");
+
+ final List files = Arrays.asList(
+ "/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/rExOms183v5evFwgIKiW0qvbsor1Hg==.uvf/dir.uvf", // -> /subir
+ "/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/dir.uvf", // -> /
+ "/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/GsMMTRvsuuP_6NjgRwopmWcuof-PyRQ=.uvf", // -> /foo.txt
+ "/d/6L/HPWBEU3OJP2EZUCP4CV3HHL47BXVEX/5qTOPMA1BouBRhz_G7qfmKety92geI4=.uvf", // -> /subdir/bar.txt
+ "/d/6L/HPWBEU3OJP2EZUCP4CV3HHL47BXVEX/dir.uvf" // /subdir
+ );
+ final String jwe = "{\n" +
+ " \"fileFormat\": \"AES-256-GCM-32k\",\n" +
+ " \"nameFormat\": \"AES-SIV-512-B64URL\",\n" +
+ " \"seeds\": {\n" +
+ " \"HDm38g\": \"ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs=\",\n" +
+ " \"gBryKw\": \"PiPoFgA5WUoziU9lZOGxNIu9egCI1CxKy3PurtWcAJ0=\",\n" +
+ " \"QBsJFg\": \"Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y=\"\n" +
+ " },\n" +
+ " \"initialSeed\": \"HDm38i\",\n" +
+ " \"latestSeed\": \"QBsJFo\",\n" +
+ " \"kdf\": \"HKDF-SHA512\",\n" +
+ " \"kdfSalt\": \"NIlr89R7FhochyP4yuXZmDqCnQ0dBB3UZ2D+6oiIjr8=\",\n" +
+ " \"org.example.customfield\": 42\n" +
+ "}";
+
+ try {
+ for(final String fi : files) {
+ final Path file = new Path("/" + bucketName + "/" + fi, EnumSet.of(AbstractPath.Type.file));
+ byte[] content = new byte[1000];
+ final int size;
+ try(final InputStream in = UVFIntegrationTest.class.getResourceAsStream("/uvf/first_vault" + fi)) {
+ size = in.read(content);
+ }
+ final TransferStatus transferStatus = new TransferStatus().withLength(size);
+ transferStatus.setChecksum(storage.getFeature(Write.class).checksum(file, transferStatus).compute(new ByteArrayInputStream(content), transferStatus));
+ storage.getFeature(Bulk.class).pre(Transfer.Type.upload, Collections.singletonMap(new TransferItem(file), transferStatus), new DisabledConnectionCallback());
+ final StatusOutputStream> out = storage.getFeature(Write.class).write(file, transferStatus, new DisabledConnectionCallback());
+ IOUtils.copyLarge(UVFIntegrationTest.class.getResourceAsStream("/uvf/first_vault" + fi), out);
+ out.close();
+ }
+
+ final VaultRegistry vaults = new DefaultVaultRegistry(new DisabledPasswordCallback());
+ bookmark.setDefaultPath("/" + bucketName);
+ final UVFVault vault = new UVFVault(new DefaultPathHomeFeature(bookmark).find());
+ vaults.add(vault.load(storage, new DisabledPasswordCallback() {
+ @Override
+ public Credentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) {
+ return new Credentials().withPassword(jwe);
+ }
+ }));
+ final PathAttributes attr = storage.getFeature(AttributesFinder.class).find(vault.getHome());
+ storage.withRegistry(vaults);
+ try(final UVFMasterkey masterKey = UVFMasterkey.fromDecryptedPayload(jwe)) {
+ assertArrayEquals(masterKey.rootDirId(), vault.getRootDirId());
+ }
+
+ final Path home = vault.getHome().withAttributes(attr).withType(EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.vault));
+ {
+ final AttributedList list = storage.getFeature(ListService.class).list(home, new DisabledListProgressListener());
+ assertEquals(
+ new HashSet<>(Arrays.asList(
+ new Path("/cyberduckbucket/subdir", EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.placeholder, AbstractPath.Type.decrypted)),
+ new Path("/cyberduckbucket/foo.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)))
+ ),
+ new HashSet<>(list.toList()));
+ assertEquals("Hello Foo", readFile(storage, new Path("/cyberduckbucket/foo.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted))));
+ }
+ {
+ final byte[] expected = writeRandomFile(storage, new Path("/cyberduckbucket/alice.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)), 57);
+ final AttributedList list = storage.getFeature(ListService.class).list(home, new DisabledListProgressListener());
+ assertEquals(
+ new HashSet<>(Arrays.asList(
+ new Path("/cyberduckbucket/subdir", EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.placeholder, AbstractPath.Type.decrypted)),
+ new Path("/cyberduckbucket/alice.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)),
+ new Path("/cyberduckbucket/foo.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)))),
+ new HashSet<>(list.toList()));
+
+ assertEquals(new String(expected), readFile(storage, new Path("/cyberduckbucket/alice.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted))));
+ }
+ {
+ final PathAttributes subdir = storage.getFeature(AttributesFinder.class).find(new Path("/cyberduckbucket/subdir", EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.placeholder, AbstractPath.Type.decrypted)));
+ final AttributedList list = storage.getFeature(ListService.class).list(new Path("/cyberduckbucket/subdir", EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.placeholder, AbstractPath.Type.decrypted)).withAttributes(subdir), new DisabledListProgressListener());
+ assertEquals(
+ new HashSet<>(Collections.singletonList(
+ new Path("/cyberduckbucket/subdir/bar.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)))
+ ),
+ new HashSet<>(list.toList()));
+ assertEquals("Hello Bar", readFile(storage, new Path("/cyberduckbucket/subdir/bar.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted))));
+ }
+ {
+ final byte[] expected = writeRandomFile(storage, new Path("/cyberduckbucket/subdir/alice.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)), 55);
+ final AttributedList list = storage.getFeature(ListService.class).list(new Path("/cyberduckbucket/subdir", EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.placeholder, AbstractPath.Type.decrypted)), new DisabledListProgressListener());
+ assertEquals(
+ new HashSet<>(Arrays.asList(
+ new Path("/cyberduckbucket/subdir/alice.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)),
+ new Path("/cyberduckbucket/subdir/bar.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)))),
+ new HashSet<>(list.toList()));
+
+ assertEquals(new String(expected), readFile(storage, new Path("/cyberduckbucket/subdir/alice.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted))));
+ }
+ {
+ storage.getFeature(Delete.class).delete(Collections.singletonList(new Path("/cyberduckbucket/subdir/bar.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted))), new DisabledPasswordCallback(), new Delete.DisabledCallback());
+ final AttributedList list = storage.getFeature(ListService.class).list(new Path("/cyberduckbucket/subdir", EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.placeholder, AbstractPath.Type.decrypted)), new DisabledListProgressListener());
+ assertEquals(1, list.size());
+ assertTrue(Arrays.toString(list.toArray()), list.contains(new Path("/cyberduckbucket/subdir/alice.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted))));
+ assertEquals(
+ new HashSet<>(Collections.singletonList(
+ new Path("/cyberduckbucket/subdir/alice.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted))
+ )),
+ new HashSet<>(list.toList()));
+ }
+ {
+ storage.getFeature(Move.class).move(
+ new Path("/cyberduckbucket/foo.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)),
+ new Path("/cyberduckbucket/subdir/Dave.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)),
+ new TransferStatus(), new Delete.DisabledCallback(), new DisabledConnectionCallback()
+ );
+
+ final AttributedList listSubDir = storage.getFeature(ListService.class).list(new Path("/cyberduckbucket/subdir", EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.placeholder, AbstractPath.Type.decrypted)), new DisabledListProgressListener());
+ assertEquals(
+ new HashSet<>(Arrays.asList(
+ new Path("/cyberduckbucket/subdir/alice.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)),
+ new Path("/cyberduckbucket/subdir/Dave.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)))
+ ),
+ new HashSet<>(listSubDir.toList()));
+ final AttributedList listHome = storage.getFeature(ListService.class).list(new Path("/cyberduckbucket/", EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.placeholder, AbstractPath.Type.decrypted)), new DisabledListProgressListener());
+ assertEquals(
+ new HashSet<>(Arrays.asList(
+ new Path("/cyberduckbucket/alice.txt", EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.decrypted)),
+ new Path("/cyberduckbucket/subdir", EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.placeholder, AbstractPath.Type.decrypted)))
+ ),
+ new HashSet<>(listHome.toList()));
+ }
+ }
+ finally {
+ storage.withRegistry(new DefaultVaultRegistry(new DisabledPasswordCallback()));
+ new DeleteWorker(new DisabledLoginCallback(),
+ storage.getFeature(ListService.class).list(bucket, new DisabledListProgressListener()).toList().stream()
+ .filter(f -> storage.getFeature(Delete.class).isSupported(f)).collect(Collectors.toList()),
+ new DisabledProgressListener()).run(storage);
+ storage.getFeature(Delete.class).delete(Collections.singletonList(bucket), new DisabledPasswordCallback(), new Delete.DisabledCallback());
+ }
+ }
+
+ private static @NotNull Host getMinIOBookmark() {
+ final Host bookmark = new Host(new S3Protocol() {
+ @Override
+ public Scheme getScheme() {
+ return Scheme.http;
+ }
+ }, "localhost", 9000).withCredentials(new Credentials("minioadmin", "minioadmin"));
+ bookmark.setProperty("s3.bucket.virtualhost.disable", "true");
+ bookmark.setDefaultPath("/");
+ return bookmark;
+ }
+
+ private static @NotNull S3Session getS3SessionForBookmark(final Host bookmark) throws BackgroundException {
+ final S3Session storage = new S3Session(bookmark);
+ storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback());
+ storage.login(new DisabledLoginCallback() {
+ @Override
+ public Credentials prompt(final Host bookmark, final String username, final String title, final String reason,
+ final LoginOptions options) {
+ return storage.getHost().getCredentials();
+ }
+ },
+ new DisabledCancelCallback());
+ return storage;
+ }
+
+ private static byte @NotNull [] writeRandomFile(final Session> session, final Path file, int size) throws BackgroundException, IOException {
+ final byte[] content = RandomUtils.nextBytes(size);
+ final TransferStatus transferStatus = new TransferStatus().withLength(content.length);
+ transferStatus.setChecksum(session.getFeature(Write.class).checksum(file, transferStatus).compute(new ByteArrayInputStream(content), transferStatus));
+ session.getFeature(Bulk.class).pre(Transfer.Type.upload, Collections.singletonMap(new TransferItem(file), transferStatus), new DisabledConnectionCallback());
+ final StatusOutputStream> out = session.getFeature(Write.class).write(file, transferStatus, new DisabledConnectionCallback());
+ IOUtils.copyLarge(new ByteArrayInputStream(content), out);
+ out.close();
+ return content;
+ }
+
+ private static String readFile(final Session> session, final Path foo) throws IOException, BackgroundException {
+ final byte[] buf = new byte[300];
+ final TransferStatus status = new TransferStatus();
+ try(final InputStream inputStream = session.getFeature(Read.class).read(foo, status, new DisabledConnectionCallback())) {
+ int l = inputStream.read(buf);
+ return new String(Arrays.copyOfRange(buf, 0, l));
+ }
+ }
+}
diff --git a/s3/src/test/resources/uvf/docker-compose.yml b/s3/src/test/resources/uvf/docker-compose.yml
new file mode 100644
index 00000000000..e739b4218cf
--- /dev/null
+++ b/s3/src/test/resources/uvf/docker-compose.yml
@@ -0,0 +1,44 @@
+version: '3'
+
+services:
+ minio:
+ hostname: minio
+ image: minio/minio:latest
+ restart: on-failure
+ ports:
+ - "${MINIO_PORT}:${MINIO_PORT}"
+ - "${MINIO_CONSOLE_PORT}:${MINIO_CONSOLE_PORT}"
+ environment:
+ MINIO_ROOT_USER: minioadmin
+ MINIO_ROOT_PASSWORD: minioadmin
+ healthcheck:
+ test: [ "CMD", "bash", "-c", "curl -v --fail 127.0.0.1:${MINIO_PORT}/minio/health/ready" ]
+ interval: 5s
+ timeout: 1s
+ retries: 5
+ command: server /data --console-address :9001
+# networks:
+# - testContainerNetwork
+#
+# minio_setup:
+# image: minio/mc:latest
+# depends_on:
+# minio:
+# condition: service_healthy
+# entrypoint: [ "/bin/sh","-c" ]
+# command:
+# - |
+# set -x
+# set -e
+# /usr/bin/mc config host add myminio http://minio:${MINIO_PORT} minioadmin minioadmin
+#
+# # if container is restarted, the bucket already exists...
+# /usr/bin/mc mb myminio/cyberduckbucket --with-versioning || true
+# /usr/bin/mc rm --recursive --force myminio/cyberduckbucket
+#
+# echo "createbuckets successful"
+# networks:
+# - testContainerNetwork
+#
+#networks:
+# testContainerNetwork:
\ No newline at end of file
diff --git a/s3/src/test/resources/uvf/first_vault/d/6L/HPWBEU3OJP2EZUCP4CV3HHL47BXVEX/5qTOPMA1BouBRhz_G7qfmKety92geI4=.uvf b/s3/src/test/resources/uvf/first_vault/d/6L/HPWBEU3OJP2EZUCP4CV3HHL47BXVEX/5qTOPMA1BouBRhz_G7qfmKety92geI4=.uvf
new file mode 100644
index 00000000000..9df3310ef3f
Binary files /dev/null and b/s3/src/test/resources/uvf/first_vault/d/6L/HPWBEU3OJP2EZUCP4CV3HHL47BXVEX/5qTOPMA1BouBRhz_G7qfmKety92geI4=.uvf differ
diff --git a/s3/src/test/resources/uvf/first_vault/d/6L/HPWBEU3OJP2EZUCP4CV3HHL47BXVEX/dir.uvf b/s3/src/test/resources/uvf/first_vault/d/6L/HPWBEU3OJP2EZUCP4CV3HHL47BXVEX/dir.uvf
new file mode 100644
index 00000000000..378fce0a7d2
Binary files /dev/null and b/s3/src/test/resources/uvf/first_vault/d/6L/HPWBEU3OJP2EZUCP4CV3HHL47BXVEX/dir.uvf differ
diff --git a/s3/src/test/resources/uvf/first_vault/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/GsMMTRvsuuP_6NjgRwopmWcuof-PyRQ=.uvf b/s3/src/test/resources/uvf/first_vault/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/GsMMTRvsuuP_6NjgRwopmWcuof-PyRQ=.uvf
new file mode 100644
index 00000000000..d7fdea3faa4
Binary files /dev/null and b/s3/src/test/resources/uvf/first_vault/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/GsMMTRvsuuP_6NjgRwopmWcuof-PyRQ=.uvf differ
diff --git a/s3/src/test/resources/uvf/first_vault/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/dir.uvf b/s3/src/test/resources/uvf/first_vault/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/dir.uvf
new file mode 100644
index 00000000000..d947abc9558
Binary files /dev/null and b/s3/src/test/resources/uvf/first_vault/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/dir.uvf differ
diff --git a/s3/src/test/resources/uvf/first_vault/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/rExOms183v5evFwgIKiW0qvbsor1Hg==.uvf/dir.uvf b/s3/src/test/resources/uvf/first_vault/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/rExOms183v5evFwgIKiW0qvbsor1Hg==.uvf/dir.uvf
new file mode 100644
index 00000000000..84ec139b177
Binary files /dev/null and b/s3/src/test/resources/uvf/first_vault/d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4/rExOms183v5evFwgIKiW0qvbsor1Hg==.uvf/dir.uvf differ
diff --git a/smb/pom.xml b/smb/pom.xml
index c190ed8cb5b..ec3d396cf59 100644
--- a/smb/pom.xml
+++ b/smb/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
smb
jar
diff --git a/spectra/pom.xml b/spectra/pom.xml
index b5ed51b2051..67c52aa609f 100644
--- a/spectra/pom.xml
+++ b/spectra/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
spectra
jar
diff --git a/ssh/pom.xml b/ssh/pom.xml
index 0964d315451..a577e3ef771 100644
--- a/ssh/pom.xml
+++ b/ssh/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
ssh
jar
diff --git a/ssh/src/test/java/ch/cyberduck/core/cryptomator/SFTPCryptomatorInteroperabilityTest.java b/ssh/src/test/java/ch/cyberduck/core/cryptomator/SFTPCryptomatorInteroperabilityTest.java
index 0dac54678ba..58a14ea9a7d 100644
--- a/ssh/src/test/java/ch/cyberduck/core/cryptomator/SFTPCryptomatorInteroperabilityTest.java
+++ b/ssh/src/test/java/ch/cyberduck/core/cryptomator/SFTPCryptomatorInteroperabilityTest.java
@@ -54,6 +54,7 @@
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.cryptolib.common.ReseedingSecureRandom;
import org.junit.After;
@@ -99,7 +100,7 @@ public void startSerer() throws Exception {
default:
csprng = FastSecureRandomProvider.get().provide();
}
- final Masterkey mk = Masterkey.generate(csprng);
+ final PerpetualMasterkey mk = Masterkey.generate(csprng);
final MasterkeyFileAccess mkAccess = new MasterkeyFileAccess(CryptoVault.VAULT_PEPPER, csprng);
final java.nio.file.Path mkPath = Paths.get(vault.toString(), DefaultVaultRegistry.DEFAULT_MASTERKEY_FILE_NAME);
mkAccess.persist(mk, mkPath, passphrase);
diff --git a/storegate/pom.xml b/storegate/pom.xml
index a26f91e85f6..ef86af8f413 100644
--- a/storegate/pom.xml
+++ b/storegate/pom.xml
@@ -19,7 +19,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
storegate
diff --git a/test/pom.xml b/test/pom.xml
index e4a6d2dbffa..474f80dd688 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
4.0.0
pom
diff --git a/tus/pom.xml b/tus/pom.xml
index 9742e3382bb..2d66e75cebb 100644
--- a/tus/pom.xml
+++ b/tus/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
tus
jar
diff --git a/webdav/pom.xml b/webdav/pom.xml
index 8afa76658f5..33daec6778c 100644
--- a/webdav/pom.xml
+++ b/webdav/pom.xml
@@ -18,7 +18,7 @@
ch.cyberduck
parent
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
webdav
jar
diff --git a/windows/pom.xml b/windows/pom.xml
index c380891d918..4da4e1234a8 100644
--- a/windows/pom.xml
+++ b/windows/pom.xml
@@ -18,7 +18,7 @@
parent
ch.cyberduck
- 9.1.3-SNAPSHOT
+ 9.1.3.uvfdraft-SNAPSHOT
4.0.0
Cyberduck.Native