Skip to content

Commit 2dedb3b

Browse files
authored
Merge pull request #263 from cryptomator/feature/cipher-to-clear
Feature: Public method for decrypting ciphertext name.
2 parents 38062dd + ffede33 commit 2dedb3b

File tree

8 files changed

+362
-19
lines changed

8 files changed

+362
-19
lines changed

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

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

3+
import org.cryptomator.cryptofs.common.Constants;
4+
35
import java.io.IOException;
46
import java.nio.file.FileSystem;
57
import java.nio.file.Files;
@@ -41,6 +43,26 @@ public abstract class CryptoFileSystem extends FileSystem {
4143
*/
4244
public abstract Path getCiphertextPath(Path cleartextPath) throws IOException;
4345

46+
/**
47+
* Computes from a valid,encrypted node (file or folder) its cleartext name.
48+
* <p>
49+
* Due to the structure of a vault, an encrypted node is valid if:
50+
* <ul>
51+
* <li>the path points into the vault (duh!)</li>
52+
* <li>the "file" extension is {@value Constants#CRYPTOMATOR_FILE_SUFFIX} or {@value Constants#DEFLATED_FILE_SUFFIX}</li>
53+
* <li>the node name is at least {@value Constants#MIN_CIPHER_NAME_LENGTH} characters long</li>
54+
* <li>it is located at depth 4 from the vault storage root, i.e. d/AB/CDEFG...XYZ/validFile.c9r</li>
55+
* </ul>
56+
*
57+
* @param ciphertextNode path to the ciphertext file or directory
58+
* @return the cleartext name of the ciphertext file or directory
59+
* @throws java.nio.file.NoSuchFileException if the ciphertextFile does not exist
60+
* @throws IOException if an I/O error occurs reading the ciphertext files
61+
* @throws IllegalArgumentException if {@param ciphertextNode} is not a valid ciphertext content node of the vault
62+
* @throws UnsupportedOperationException if the directory containing the {@param ciphertextNode} does not have a {@value Constants#DIR_ID_BACKUP_FILE_NAME} file
63+
*/
64+
public abstract String getCleartextName(Path ciphertextNode) throws IOException, UnsupportedOperationException;
65+
4466
/**
4567
* Provides file system performance statistics.
4668
*

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
import org.cryptomator.cryptofs.common.DeletingFileVisitor;
1818
import org.cryptomator.cryptofs.common.FinallyUtil;
1919
import org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter;
20-
import org.cryptomator.cryptofs.dir.DirectoryStreamFilters;
2120
import org.cryptomator.cryptofs.dir.DirectoryStreamFactory;
21+
import org.cryptomator.cryptofs.dir.DirectoryStreamFilters;
2222
import org.cryptomator.cryptofs.fh.OpenCryptoFiles;
2323
import org.cryptomator.cryptolib.api.Cryptor;
2424

@@ -95,16 +95,17 @@ class CryptoFileSystemImpl extends CryptoFileSystem {
9595

9696
private final CryptoPath rootPath;
9797
private final CryptoPath emptyPath;
98+
private final FileNameDecryptor fileNameDecryptor;
9899

99100
private volatile boolean open = true;
100101

101102
@Inject
102-
public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems cryptoFileSystems, @PathToVault Path pathToVault, Cryptor cryptor,
103-
CryptoFileStore fileStore, CryptoFileSystemStats stats, CryptoPathMapper cryptoPathMapper, CryptoPathFactory cryptoPathFactory,
104-
PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider, DirectoryIdBackup dirIdBackup,
105-
AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider,
106-
OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag,
107-
CryptoFileSystemProperties fileSystemProperties) {
103+
public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems cryptoFileSystems, @PathToVault Path pathToVault, Cryptor cryptor, //
104+
CryptoFileStore fileStore, CryptoFileSystemStats stats, CryptoPathMapper cryptoPathMapper, CryptoPathFactory cryptoPathFactory, //
105+
PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider, DirectoryIdBackup dirIdBackup, //
106+
AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider, //
107+
OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag, //
108+
CryptoFileSystemProperties fileSystemProperties, FileNameDecryptor fileNameDecryptor) {
108109
this.provider = provider;
109110
this.cryptoFileSystems = cryptoFileSystems;
110111
this.pathToVault = pathToVault;
@@ -129,6 +130,7 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems
129130

130131
this.rootPath = cryptoPathFactory.rootFor(this);
131132
this.emptyPath = cryptoPathFactory.emptyFor(this);
133+
this.fileNameDecryptor = fileNameDecryptor;
132134
}
133135

134136
@Override
@@ -151,6 +153,11 @@ public Path getCiphertextPath(Path cleartextPath) throws IOException {
151153
}
152154
}
153155

156+
@Override
157+
public String getCleartextName(Path ciphertextNode) throws IOException, UnsupportedOperationException {
158+
return fileNameDecryptor.decryptFilename(ciphertextNode);
159+
}
160+
154161
@Override
155162
public CryptoFileSystemStats getStats() {
156163
return stats;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package org.cryptomator.cryptofs;
2+
3+
import com.google.common.annotations.VisibleForTesting;
4+
import com.google.common.io.BaseEncoding;
5+
import org.cryptomator.cryptofs.common.Constants;
6+
import org.cryptomator.cryptofs.common.StringUtils;
7+
import org.cryptomator.cryptolib.api.CryptoException;
8+
import org.cryptomator.cryptolib.api.Cryptor;
9+
import org.cryptomator.cryptolib.api.FileNameCryptor;
10+
11+
import javax.inject.Inject;
12+
import java.io.IOException;
13+
import java.nio.file.FileSystemException;
14+
import java.nio.file.NoSuchFileException;
15+
import java.nio.file.Path;
16+
import java.util.stream.Stream;
17+
18+
/**
19+
* @see CryptoFileSystem#getCleartextName(Path)
20+
*/
21+
@CryptoFileSystemScoped
22+
class FileNameDecryptor {
23+
24+
private final DirectoryIdBackup dirIdBackup;
25+
private final LongFileNameProvider longFileNameProvider;
26+
private final Path vaultPath;
27+
private final FileNameCryptor fileNameCryptor;
28+
29+
@Inject
30+
public FileNameDecryptor(@PathToVault Path vaultPath, Cryptor cryptor, DirectoryIdBackup dirIdBackup, LongFileNameProvider longFileNameProvider) {
31+
this.vaultPath = vaultPath;
32+
this.fileNameCryptor = cryptor.fileNameCryptor();
33+
this.dirIdBackup = dirIdBackup;
34+
this.longFileNameProvider = longFileNameProvider;
35+
}
36+
37+
public String decryptFilename(Path ciphertextNode) throws IOException, UnsupportedOperationException {
38+
validatePath(ciphertextNode.toAbsolutePath());
39+
return decryptFilenameInternal(ciphertextNode);
40+
}
41+
42+
@VisibleForTesting
43+
String decryptFilenameInternal(Path ciphertextNode) throws IOException, UnsupportedOperationException {
44+
byte[] dirId = null;
45+
try {
46+
dirId = dirIdBackup.read(ciphertextNode);
47+
} catch (NoSuchFileException e) {
48+
throw new UnsupportedOperationException("Directory does not have a " + Constants.DIR_ID_BACKUP_FILE_NAME + " file.");
49+
} catch (CryptoException | IllegalStateException e) {
50+
throw new FileSystemException(ciphertextNode.toString(), null, "Decryption of dirId backup file failed:" + e);
51+
}
52+
var fullCipherNodeName = ciphertextNode.getFileName().toString();
53+
var cipherNodeExtension = fullCipherNodeName.substring(fullCipherNodeName.length() - 4);
54+
55+
String actualEncryptedName = switch (cipherNodeExtension) {
56+
case Constants.CRYPTOMATOR_FILE_SUFFIX -> StringUtils.removeEnd(fullCipherNodeName, Constants.CRYPTOMATOR_FILE_SUFFIX);
57+
case Constants.DEFLATED_FILE_SUFFIX -> longFileNameProvider.inflate(ciphertextNode);
58+
default -> throw new IllegalStateException("SHOULD NOT REACH HERE");
59+
};
60+
try {
61+
return fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), actualEncryptedName, dirId);
62+
} catch (CryptoException e) {
63+
throw new FileSystemException(ciphertextNode.toString(), null, "Filname decryption failed:" + e);
64+
}
65+
}
66+
67+
@VisibleForTesting
68+
void validatePath(Path absolutePath) {
69+
if (!belongsToVault(absolutePath)) {
70+
throw new IllegalArgumentException("Node %s is not a part of vault %s".formatted(absolutePath, vaultPath));
71+
}
72+
if (!isAtCipherNodeLevel(absolutePath)) {
73+
throw new IllegalArgumentException("Node %s is not located at depth 4 from vault storage root".formatted(absolutePath));
74+
}
75+
if (!(hasCipherNodeExtension(absolutePath) && hasMinimumFileNameLength(absolutePath))) {
76+
throw new IllegalArgumentException("Node %s does not end with %s or %s or filename is shorter than %d characters.".formatted(absolutePath, Constants.CRYPTOMATOR_FILE_SUFFIX, Constants.DEFLATED_FILE_SUFFIX, Constants.MIN_CIPHER_NAME_LENGTH));
77+
}
78+
}
79+
80+
boolean hasCipherNodeExtension(Path p) {
81+
var name = p.getFileName();
82+
return name != null && Stream.of(Constants.CRYPTOMATOR_FILE_SUFFIX, Constants.DEFLATED_FILE_SUFFIX).anyMatch(name.toString()::endsWith);
83+
}
84+
85+
boolean isAtCipherNodeLevel(Path absolutPah) {
86+
if (!absolutPah.isAbsolute()) {
87+
throw new IllegalArgumentException("Path " + absolutPah + "must be absolute");
88+
}
89+
return absolutPah.subpath(vaultPath.getNameCount(), absolutPah.getNameCount()).getNameCount() == 4;
90+
}
91+
92+
boolean hasMinimumFileNameLength(Path p) {
93+
return p.getFileName().toString().length() >= Constants.MIN_CIPHER_NAME_LENGTH;
94+
}
95+
96+
boolean belongsToVault(Path p) {
97+
return p.startsWith(vaultPath);
98+
}
99+
}

src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public class CryptoFileSystemImplTest {
105105
private final CiphertextDirectoryDeleter ciphertextDirDeleter = mock(CiphertextDirectoryDeleter.class);
106106
private final ReadonlyFlag readonlyFlag = mock(ReadonlyFlag.class);
107107
private final CryptoFileSystemProperties fileSystemProperties = mock(CryptoFileSystemProperties.class);
108+
private final FileNameDecryptor filenameDecryptor = mock(FileNameDecryptor.class);
108109

109110
private final CryptoPath root = mock(CryptoPath.class);
110111
private final CryptoPath empty = mock(CryptoPath.class);
@@ -127,7 +128,7 @@ public void setup() {
127128
pathMatcherFactory, directoryStreamFactory, dirIdProvider, dirIdBackup, //
128129
fileAttributeProvider, fileAttributeByNameProvider, fileAttributeViewProvider, //
129130
openCryptoFiles, symlinks, finallyUtil, ciphertextDirDeleter, readonlyFlag, //
130-
fileSystemProperties);
131+
fileSystemProperties, filenameDecryptor);
131132
}
132133

133134
@Test

src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.cryptomator.cryptofs;
22

33
import org.cryptomator.cryptofs.common.Constants;
4+
import org.cryptomator.cryptofs.health.dirid.OrphanContentDirTest;
5+
import org.cryptomator.cryptofs.util.TestCryptoException;
46
import org.cryptomator.cryptolib.api.CryptoException;
57
import org.cryptomator.cryptolib.api.Cryptor;
68
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
@@ -103,17 +105,13 @@ public void contentLongerThan36Chars() throws IOException {
103105
@DisplayName("If the backup file cannot be decrypted, a CryptoException is thrown")
104106
public void invalidEncryptionThrowsCryptoException() throws IOException {
105107
var dirIdBackupSpy = spy(dirIdBackup);
106-
var expectedException = new MyCryptoException();
108+
var expectedException = new TestCryptoException();
107109
Mockito.when(dirIdBackupSpy.wrapDecryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(decChannel);
108110
Mockito.when(decChannel.read(Mockito.any())).thenThrow(expectedException);
109111
var actual = Assertions.assertThrows(CryptoException.class, () -> dirIdBackupSpy.read(contentPath));
110112
Assertions.assertEquals(expectedException, actual);
111113
}
112114

113-
static class MyCryptoException extends CryptoException {
114-
115-
}
116-
117115
@Test
118116
@DisplayName("IOException accessing the file is rethrown")
119117
public void ioException() throws IOException {

0 commit comments

Comments
 (0)