Skip to content

Commit c5432f8

Browse files
committed
Merge branch 'release/2.4.2'
2 parents 62fd056 + 0983ed5 commit c5432f8

16 files changed

+714
-102
lines changed

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<modelVersion>4.0.0</modelVersion>
33
<groupId>org.cryptomator</groupId>
44
<artifactId>cryptofs</artifactId>
5-
<version>2.4.1</version>
5+
<version>2.4.2</version>
66
<name>Cryptomator Crypto Filesystem</name>
77
<description>This library provides the Java filesystem provider used by Cryptomator.</description>
88
<url>https://github.com/cryptomator/cryptofs</url>

src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java

+39-8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.nio.file.DirectoryStream.Filter;
3636
import java.nio.file.FileAlreadyExistsException;
3737
import java.nio.file.FileStore;
38+
import java.nio.file.FileSystemException;
3839
import java.nio.file.Files;
3940
import java.nio.file.LinkOption;
4041
import java.nio.file.NoSuchFileException;
@@ -59,6 +60,7 @@
5960
import java.util.Collections;
6061
import java.util.EnumSet;
6162
import java.util.Map;
63+
import java.util.Optional;
6264
import java.util.Set;
6365
import java.util.stream.Collectors;
6466

@@ -68,7 +70,7 @@
6870

6971
@CryptoFileSystemScoped
7072
class CryptoFileSystemImpl extends CryptoFileSystem {
71-
73+
7274
private final CryptoFileSystemProvider provider;
7375
private final CryptoFileSystems cryptoFileSystems;
7476
private final Path pathToVault;
@@ -80,6 +82,7 @@ class CryptoFileSystemImpl extends CryptoFileSystem {
8082
private final PathMatcherFactory pathMatcherFactory;
8183
private final DirectoryStreamFactory directoryStreamFactory;
8284
private final DirectoryIdProvider dirIdProvider;
85+
private final DirectoryIdBackup dirIdBackup;
8386
private final AttributeProvider fileAttributeProvider;
8487
private final AttributeByNameProvider fileAttributeByNameProvider;
8588
private final AttributeViewProvider fileAttributeViewProvider;
@@ -98,7 +101,7 @@ class CryptoFileSystemImpl extends CryptoFileSystem {
98101
@Inject
99102
public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems cryptoFileSystems, @PathToVault Path pathToVault, Cryptor cryptor,
100103
CryptoFileStore fileStore, CryptoFileSystemStats stats, CryptoPathMapper cryptoPathMapper, CryptoPathFactory cryptoPathFactory,
101-
PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider,
104+
PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider, DirectoryIdBackup dirIdBackup,
102105
AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider,
103106
OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag,
104107
CryptoFileSystemProperties fileSystemProperties) {
@@ -113,6 +116,7 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems
113116
this.pathMatcherFactory = pathMatcherFactory;
114117
this.directoryStreamFactory = directoryStreamFactory;
115118
this.dirIdProvider = dirIdProvider;
119+
this.dirIdBackup = dirIdBackup;
116120
this.fileAttributeProvider = fileAttributeProvider;
117121
this.fileAttributeByNameProvider = fileAttributeByNameProvider;
118122
this.fileAttributeViewProvider = fileAttributeViewProvider;
@@ -235,13 +239,17 @@ <A extends BasicFileAttributes> A readAttributes(CryptoPath cleartextPath, Class
235239

236240
/**
237241
* @param cleartextPath the path to the file
238-
* @param type the Class object corresponding to the file attribute view
239-
* @param options future use
242+
* @param type the Class object corresponding to the file attribute view
243+
* @param options future use
240244
* @return a file attribute view of the specified type, or <code>null</code> if the attribute view type is not available
241245
* @see AttributeViewProvider#getAttributeView(CryptoPath, Class, LinkOption...)
242246
*/
243247
<V extends FileAttributeView> V getFileAttributeView(CryptoPath cleartextPath, Class<V> type, LinkOption... options) {
244-
return fileAttributeViewProvider.getAttributeView(cleartextPath, type, options);
248+
if (fileStore.supportsFileAttributeView(type)) {
249+
return fileAttributeViewProvider.getAttributeView(cleartextPath, type, options);
250+
} else {
251+
return null;
252+
}
245253
}
246254

247255
void checkAccess(CryptoPath cleartextPath, AccessMode... modes) throws IOException {
@@ -282,6 +290,10 @@ boolean isHidden(CryptoPath cleartextPath) throws IOException {
282290
void createDirectory(CryptoPath cleartextDir, FileAttribute<?>... attrs) throws IOException {
283291
readonlyFlag.assertWritable();
284292
assertCleartextNameLengthAllowed(cleartextDir);
293+
if (rootPath.equals(cleartextDir)) {
294+
throw new FileAlreadyExistsException(rootPath.toString());
295+
}
296+
285297
CryptoPath cleartextParentDir = cleartextDir.getParent();
286298
if (cleartextParentDir == null) {
287299
return;
@@ -302,6 +314,7 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute<?>... attrs) throws
302314
// create dir if and only if the dirFile has been created right now (not if it has been created before):
303315
try {
304316
Files.createDirectories(ciphertextDir.path);
317+
dirIdBackup.execute(ciphertextDir);
305318
ciphertextPath.persistLongFileName();
306319
} catch (IOException e) {
307320
// make sure there is no orphan dir file:
@@ -375,14 +388,17 @@ private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, Effecti
375388
stats.incrementAccessesRead();
376389
}
377390
return ch;
378-
} catch (Exception e){
391+
} catch (Exception e) {
379392
ch.close();
380393
throw e;
381394
}
382395
}
383396

384397
void delete(CryptoPath cleartextPath) throws IOException {
385398
readonlyFlag.assertWritable();
399+
if (rootPath.equals(cleartextPath)) {
400+
throw new FileSystemException("The filesystem root cannot be deleted.");
401+
}
386402
CiphertextFileType ciphertextFileType = cryptoPathMapper.getCiphertextFileType(cleartextPath);
387403
CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath);
388404
switch (ciphertextFileType) {
@@ -414,6 +430,11 @@ void copy(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption...
414430
if (cleartextSource.equals(cleartextTarget)) {
415431
return;
416432
}
433+
434+
if (rootPath.equals(cleartextTarget) && ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
435+
throw new FileSystemException("The filesystem root cannot be replaced.");
436+
}
437+
417438
CiphertextFileType ciphertextFileType = cryptoPathMapper.getCiphertextFileType(cleartextSource);
418439
if (!ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
419440
cryptoPathMapper.assertNonExisting(cleartextTarget);
@@ -508,6 +529,16 @@ private void copyAttributes(Path src, Path dst) throws IOException {
508529
void move(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... options) throws IOException {
509530
readonlyFlag.assertWritable();
510531
assertCleartextNameLengthAllowed(cleartextTarget);
532+
533+
if (rootPath.equals(cleartextSource)) {
534+
throw new FileSystemException("Filesystem root cannot be moved.");
535+
536+
}
537+
538+
if (rootPath.equals(cleartextTarget)) {
539+
throw new FileAlreadyExistsException(rootPath.toString());
540+
}
541+
511542
if (cleartextSource.equals(cleartextTarget)) {
512543
return;
513544
}
@@ -582,7 +613,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
582613
}
583614
Files.walkFileTree(ciphertextTarget.getRawPath(), DeletingFileVisitor.INSTANCE);
584615
}
585-
616+
586617
// no exceptions until this point, so MOVE:
587618
Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options);
588619
if (ciphertextTarget.isShortened()) {
@@ -621,7 +652,7 @@ CryptoPath getEmptyPath() {
621652
}
622653

623654
void assertCleartextNameLengthAllowed(CryptoPath cleartextPath) throws FileNameTooLongException {
624-
String filename = cleartextPath.getFileName().toString();
655+
String filename = Optional.ofNullable(cleartextPath.getFileName()).map(CryptoPath::toString).orElse(""); //fs root has no explicit name
625656
if (filename.length() > fileSystemProperties.maxCleartextNameLength()) {
626657
throw new FileNameTooLongException(cleartextPath.toString(), fileSystemProperties.maxCleartextNameLength());
627658
}

src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java

+5
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
6464
* Key identifying the name of the masterkey file located inside the vault directory.
6565
*
6666
* @since 1.1.0
67+
* @deprecated Replaced with {@link #PROPERTY_KEYLOADER external keyloader} API
6768
*/
69+
@Deprecated
6870
public static final String PROPERTY_MASTERKEY_FILENAME = "masterkeyFilename";
6971

7072
static final String DEFAULT_MASTERKEY_FILENAME = "masterkey.cryptomator";
@@ -128,6 +130,7 @@ String vaultConfigFilename() {
128130
return (String) get(PROPERTY_VAULTCONFIG_FILENAME);
129131
}
130132

133+
@Deprecated
131134
String masterkeyFilename() {
132135
return (String) get(PROPERTY_MASTERKEY_FILENAME);
133136
}
@@ -295,7 +298,9 @@ public Builder withVaultConfigFilename(String vaultConfigFilename) {
295298
* @param masterkeyFilename the filename of the json file containing configuration to decrypt the masterkey
296299
* @return this
297300
* @since 1.1.0
301+
* @deprecated Supply a {@link #withKeyLoader(MasterkeyLoader) keyloader} instead.
298302
*/
303+
@Deprecated
299304
public Builder withMasterkeyFilename(String masterkeyFilename) {
300305
this.masterkeyFilename = masterkeyFilename;
301306
return this;

src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
157157
} finally {
158158
Arrays.fill(rawKey, (byte) 0x00);
159159
}
160-
assert checkDirStructureForVault(pathToVault, properties.vaultConfigFilename(), properties.masterkeyFilename()) == DirStructure.VAULT;
160+
assert checkDirStructureForVault(pathToVault, properties.vaultConfigFilename(), null) == DirStructure.VAULT;
161161
}
162162

163163
/**

src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,10 @@ private String readVaultConfigFile(Path pathToVault, CryptoFileSystemProperties
112112
try {
113113
return Files.readString(vaultConfigFile, StandardCharsets.US_ASCII);
114114
} catch (NoSuchFileException e) {
115-
Path masterkeyPath = pathToVault.resolve(properties.masterkeyFilename());
116-
if (Files.exists(masterkeyPath)) {
117-
LOG.warn("Failed to read {}, but found {}}", vaultConfigFile, masterkeyPath);
115+
// TODO: remove this check and tell downstream users to check the vault dir structure before creating a CryptoFileSystemImpl
116+
@SuppressWarnings("deprecation") var masterkeyFilename = properties.masterkeyFilename();
117+
if (masterkeyFilename != null && Files.exists(pathToVault.resolve(masterkeyFilename))) {
118+
LOG.warn("Failed to read {}, but found {}}", vaultConfigFile, masterkeyFilename);
118119
throw new FileSystemNeedsMigrationException(pathToVault);
119120
} else {
120121
throw e;

src/main/java/org/cryptomator/cryptofs/DirStructure.java

+3-5
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,19 @@ public enum DirStructure {
4646
*
4747
* @param pathToVault A directory path
4848
* @param vaultConfigFilename Name of the vault config file
49-
* @param masterkeyFilename Name of the masterkey file
49+
* @param masterkeyFilename Name of the masterkey file (may be null to skip detection of legacy vaults)
5050
* @return enum indicating what this directory might be
5151
* @throws IOException if the provided path is not a directory, does not exist or cannot be read
5252
*/
5353
public static DirStructure checkDirStructure(Path pathToVault, String vaultConfigFilename, String masterkeyFilename) throws IOException {
5454
if(! Files.readAttributes(pathToVault, BasicFileAttributes.class).isDirectory()) {
5555
throw new NotDirectoryException(pathToVault.toString());
5656
}
57-
Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename);
58-
Path masterkeyPath = pathToVault.resolve(masterkeyFilename);
5957
Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME);
6058
if (Files.isDirectory(dataDirPath)) {
61-
if (Files.isReadable(vaultConfigPath)) {
59+
if (Files.isReadable(pathToVault.resolve(vaultConfigFilename))) {
6260
return VAULT;
63-
} else if (Files.isReadable(masterkeyPath)) {
61+
} else if (masterkeyFilename != null && Files.isReadable(pathToVault.resolve(masterkeyFilename))) {
6462
return MAYBE_LEGACY;
6563
}
6664
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.cryptomator.cryptofs;
2+
3+
import org.cryptomator.cryptofs.common.Constants;
4+
import org.cryptomator.cryptolib.api.Cryptor;
5+
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
6+
7+
import javax.inject.Inject;
8+
import java.io.IOException;
9+
import java.nio.ByteBuffer;
10+
import java.nio.channels.ByteChannel;
11+
import java.nio.charset.StandardCharsets;
12+
import java.nio.file.Files;
13+
import java.nio.file.StandardOpenOption;
14+
15+
/**
16+
* Single purpose class to backup the directory id of an encrypted directory when it is created.
17+
*/
18+
@CryptoFileSystemScoped
19+
public class DirectoryIdBackup {
20+
21+
private Cryptor cryptor;
22+
23+
@Inject
24+
public DirectoryIdBackup(Cryptor cryptor) {
25+
this.cryptor = cryptor;
26+
}
27+
28+
/**
29+
* Performs the backup operation for the given {@link CryptoPathMapper.CiphertextDirectory} object.
30+
* <p>
31+
* The directory id is written via an encrypting channel to the file {@link CryptoPathMapper.CiphertextDirectory#path}/{@value Constants#DIR_ID_FILE}.
32+
*
33+
* @param ciphertextDirectory The cipher dir object containing the dir id and the encrypted content root
34+
* @throws IOException if an IOException is raised during the write operation
35+
*/
36+
public void execute(CryptoPathMapper.CiphertextDirectory ciphertextDirectory) throws IOException {
37+
try (var channel = Files.newByteChannel(ciphertextDirectory.path.resolve(Constants.DIR_ID_FILE), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); //
38+
var encryptingChannel = wrapEncryptionAround(channel, cryptor)) {
39+
encryptingChannel.write(ByteBuffer.wrap(ciphertextDirectory.dirId.getBytes(StandardCharsets.US_ASCII)));
40+
}
41+
}
42+
43+
/**
44+
* Static method to explicitly backup the directory id for a specified ciphertext directory.
45+
*
46+
* @param cryptor The cryptor to be used
47+
* @param ciphertextDirectory A {@link org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory} for which the dirId should be back up'd.
48+
* @throws IOException when the dirId file already exists, or it cannot be written to.
49+
*/
50+
public static void backupManually(Cryptor cryptor, CryptoPathMapper.CiphertextDirectory ciphertextDirectory) throws IOException {
51+
new DirectoryIdBackup(cryptor).execute(ciphertextDirectory);
52+
}
53+
54+
55+
static EncryptingWritableByteChannel wrapEncryptionAround(ByteChannel channel, Cryptor cryptor) {
56+
return new EncryptingWritableByteChannel(channel, cryptor);
57+
}
58+
}

src/main/java/org/cryptomator/cryptofs/common/Constants.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ private Constants() {
2929
public static final int DEFAULT_SHORTENING_THRESHOLD = 220;
3030
public static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1
3131
public static final int MAX_DIR_FILE_LENGTH = 36; // UUIDv4: hex-encoded 16 byte int + 4 hyphens = 36 ASCII chars
32+
public static final int MIN_CIPHER_NAME_LENGTH = 26; //rounded up base64url encoded (16 bytes IV + 0 bytes empty string) + file suffix = 26 ASCII chars
3233

3334
public static final String SEPARATOR = "/";
34-
public static final String RECOVERY_DIR_NAME = "CRYPTOMATOR_RECOVERY";
35+
public static final String RECOVERY_DIR_NAME = "LOST+FOUND";
36+
public static final String DIR_ID_FILE = "dirid.c9r";
3537
}

src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java

+13-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.cryptomator.cryptofs.CryptoPath;
55
import org.cryptomator.cryptofs.CryptoPathMapper;
66
import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory;
7+
import org.cryptomator.cryptofs.common.Constants;
78

89
import javax.inject.Inject;
910
import java.io.IOException;
@@ -13,15 +14,14 @@
1314
import java.nio.file.Files;
1415
import java.nio.file.Path;
1516
import java.util.HashMap;
16-
import java.util.Iterator;
1717
import java.util.Map;
1818

1919
@CryptoFileSystemScoped
2020
public class DirectoryStreamFactory {
2121

2222
private final CryptoPathMapper cryptoPathMapper;
2323
private final DirectoryStreamComponent.Builder directoryStreamComponentBuilder; // sharing reusable builder via synchronized
24-
private final Map<CryptoDirectoryStream, DirectoryStream> streams = new HashMap<>();
24+
private final Map<CryptoDirectoryStream, DirectoryStream<Path>> streams = new HashMap<>();
2525

2626
private volatile boolean closed = false;
2727

@@ -36,7 +36,8 @@ public synchronized CryptoDirectoryStream newDirectoryStream(CryptoPath cleartex
3636
throw new ClosedFileSystemException();
3737
}
3838
CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir);
39-
DirectoryStream<Path> ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path);
39+
//TODO: use HealthCheck with warning and suggest fix to create one
40+
DirectoryStream<Path> ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path, this::matchesEncryptedContentPattern);
4041
CryptoDirectoryStream cleartextDirStream = directoryStreamComponentBuilder //
4142
.dirId(ciphertextDir.dirId) //
4243
.ciphertextDirectoryStream(ciphertextDirStream) //
@@ -49,12 +50,19 @@ public synchronized CryptoDirectoryStream newDirectoryStream(CryptoPath cleartex
4950
return cleartextDirStream;
5051
}
5152

53+
//visible for testing
54+
boolean matchesEncryptedContentPattern(Path path) {
55+
var tmp = path.getFileName().toString();
56+
return tmp.length() >= Constants.MIN_CIPHER_NAME_LENGTH //
57+
&& (tmp.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX) || tmp.endsWith(Constants.DEFLATED_FILE_SUFFIX));
58+
}
59+
5260
public synchronized void close() throws IOException {
5361
closed = true;
5462
IOException exception = new IOException("Close failed");
55-
Iterator<Map.Entry<CryptoDirectoryStream, DirectoryStream>> iter = streams.entrySet().iterator();
63+
var iter = streams.entrySet().iterator();
5664
while (iter.hasNext()) {
57-
Map.Entry<CryptoDirectoryStream, DirectoryStream> entry = iter.next();
65+
var entry = iter.next();
5866
iter.remove();
5967
try {
6068
entry.getKey().close();

0 commit comments

Comments
 (0)