Skip to content

Commit 43da42b

Browse files
committed
Merge branch 'develop' into release/2.9.0
2 parents b3bf06c + 815ed6b commit 43da42b

12 files changed

+136
-36
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.cryptomator.cryptofs;
2+
3+
import com.google.common.io.BaseEncoding;
4+
5+
import java.nio.file.Path;
6+
7+
public class CiphertextPathValidations {
8+
9+
10+
private CiphertextPathValidations() {}
11+
12+
public static boolean isCiphertextContentDir(Path p) {
13+
var twoCharDir = p.getParent();
14+
if (twoCharDir == null) {
15+
return false;
16+
}
17+
var testString = twoCharDir.getFileName().toString() + p.getFileName().toString();
18+
return testString.length() == 32 && BaseEncoding.base32().canDecode(testString);
19+
}
20+
21+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package org.cryptomator.cryptofs;
22

3+
import jakarta.inject.Inject;
34
import org.cryptomator.cryptofs.common.Constants;
45
import org.cryptomator.cryptolib.api.CryptoException;
56
import org.cryptomator.cryptolib.api.Cryptor;
67
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
78
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
89

9-
import jakarta.inject.Inject;
1010
import java.io.IOException;
1111
import java.nio.ByteBuffer;
1212
import java.nio.channels.ByteChannel;
@@ -65,6 +65,9 @@ public static void write(Cryptor cryptor, CiphertextDirectory ciphertextDirector
6565
* @throws IllegalStateException if the directory id exceeds {@value Constants#MAX_DIR_ID_LENGTH} chars
6666
*/
6767
public byte[] read(Path ciphertextContentDir) throws IOException, CryptoException, IllegalStateException {
68+
if (!CiphertextPathValidations.isCiphertextContentDir(ciphertextContentDir)) {
69+
throw new IllegalArgumentException("Directory %s is not a ciphertext content dir".formatted(ciphertextContentDir));
70+
}
6871
var dirIdBackupFile = getBackupFilePath(ciphertextContentDir);
6972
var dirIdBuffer = ByteBuffer.allocate(Constants.MAX_DIR_ID_LENGTH + 1); //a dir id contains at most 36 ascii chars, we add for security checks one more
7073

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public String decryptFilename(Path ciphertextNode) throws IOException, Unsupport
4343
String decryptFilenameInternal(Path ciphertextNode) throws IOException, UnsupportedOperationException {
4444
byte[] dirId = null;
4545
try {
46-
dirId = dirIdBackup.read(ciphertextNode);
46+
dirId = dirIdBackup.read(ciphertextNode.getParent());
4747
} catch (NoSuchFileException e) {
4848
throw new UnsupportedOperationException("Directory does not have a " + Constants.DIR_ID_BACKUP_FILE_NAME + " file.");
4949
} catch (CryptoException | IllegalStateException e) {
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
package org.cryptomator.cryptofs.event;
22

33
import java.nio.file.Path;
4+
import java.time.Instant;
45

56
/**
67
* Emitted, if a dir.c9r file is empty or exceeds 1000 Bytes.
78
*
9+
* @param timestamp timestamp of event appearance
810
* @param ciphertextPath path to the broken dir.c9r file
911
*/
10-
public record BrokenDirFileEvent(Path ciphertextPath) implements FilesystemEvent {
12+
public record BrokenDirFileEvent(Instant timestamp, Path ciphertextPath) implements FilesystemEvent {
1113

14+
public BrokenDirFileEvent(Path ciphertextPath) {
15+
this(Instant.now(), ciphertextPath);
16+
}
17+
18+
@Override
19+
public Instant getTimestamp() {
20+
return timestamp;
21+
}
1222
}
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
package org.cryptomator.cryptofs.event;
22

33
import java.nio.file.Path;
4+
import java.time.Instant;
45

56
/**
67
* Emitted, if a path within the cryptographic filesystem is accessed, but the directory representing it is missing identification files.
78
*
9+
* @param timestamp timestamp of event appearance
810
* @param cleartextPath path within the cryptographic filesystem
911
* @param ciphertextPath path of the incomplete, encrypted directory
10-
*
1112
* @see org.cryptomator.cryptofs.health.type.UnknownType
1213
*/
13-
public record BrokenFileNodeEvent(Path cleartextPath, Path ciphertextPath) implements FilesystemEvent {
14+
public record BrokenFileNodeEvent(Instant timestamp, Path cleartextPath, Path ciphertextPath) implements FilesystemEvent {
15+
16+
public BrokenFileNodeEvent(Path cleartextPath, Path ciphertextPath) {
17+
this(Instant.now(), cleartextPath, ciphertextPath);
18+
}
1419

20+
@Override
21+
public Instant getTimestamp() {
22+
return timestamp;
23+
}
1524
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
package org.cryptomator.cryptofs.event;
22

33
import java.nio.file.Path;
4+
import java.time.Instant;
45

56
/**
67
* Emitted, if the conflict resolution inside an encrypted directory failed
78
*
9+
* @param timestamp timestamp of event appearance
810
* @param canonicalCleartextPath path of the canonical file within the cryptographic filesystem
911
* @param conflictingCiphertextPath path of the encrypted, conflicting file
1012
* @param reason exception, why the resolution failed
1113
*/
12-
public record ConflictResolutionFailedEvent(Path canonicalCleartextPath, Path conflictingCiphertextPath, Exception reason) implements FilesystemEvent {
14+
public record ConflictResolutionFailedEvent(Instant timestamp, Path canonicalCleartextPath, Path conflictingCiphertextPath, Exception reason) implements FilesystemEvent {
1315

16+
public ConflictResolutionFailedEvent(Path canonicalCleartextPath, Path conflictingCiphertextPath, Exception reason) {
17+
this(Instant.now(), canonicalCleartextPath, conflictingCiphertextPath, reason);
18+
}
19+
20+
@Override
21+
public Instant getTimestamp() {
22+
return timestamp;
23+
}
1424
}

src/main/java/org/cryptomator/cryptofs/event/ConflictResolvedEvent.java

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

33
import java.nio.file.Path;
4+
import java.time.Instant;
45

56
/**
67
* Emitted, if a conflict inside an encrypted directory was resolved.
@@ -10,11 +11,20 @@
1011
* The file <i>with the suffix</i> is called <b>conflicting</b>
1112
* On successful conflict resolution the conflicting file is renamed to the <b>resolved</b> file
1213
*
14+
* @param timestamp timestamp of event appearance
1315
* @param canonicalCleartextPath path of the canonical file within the cryptographic filesystem
1416
* @param conflictingCiphertextPath path of the encrypted, conflicting file
1517
* @param resolvedCleartextPath path of the resolved file within the cryptographic filesystem
1618
* @param resolvedCiphertextPath path of the resolved, encrypted file
1719
*/
18-
public record ConflictResolvedEvent(Path canonicalCleartextPath, Path conflictingCiphertextPath, Path resolvedCleartextPath, Path resolvedCiphertextPath) implements FilesystemEvent {
20+
public record ConflictResolvedEvent(Instant timestamp, Path canonicalCleartextPath, Path conflictingCiphertextPath, Path resolvedCleartextPath, Path resolvedCiphertextPath) implements FilesystemEvent {
1921

22+
public ConflictResolvedEvent(Path canonicalCleartextPath, Path conflictingCiphertextPath, Path resolvedCleartextPath, Path resolvedCiphertextPath) {
23+
this(Instant.now(), canonicalCleartextPath, conflictingCiphertextPath, resolvedCleartextPath, resolvedCiphertextPath);
24+
}
25+
26+
@Override
27+
public Instant getTimestamp() {
28+
return timestamp;
29+
}
2030
}
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
package org.cryptomator.cryptofs.event;
22

33
import java.nio.file.Path;
4+
import java.time.Instant;
45

56
/**
67
* Emitted, if a decryption operation fails.
78
*
9+
* @param timestamp timestamp of event appearance
810
* @param ciphertextPath path to the encrypted resource
911
* @param e thrown exception
1012
*/
11-
public record DecryptionFailedEvent(Path ciphertextPath, Exception e) implements FilesystemEvent {
13+
public record DecryptionFailedEvent(Instant timestamp, Path ciphertextPath, Exception e) implements FilesystemEvent {
14+
15+
public DecryptionFailedEvent(Path ciphertextPath, Exception e) {
16+
this(Instant.now(), ciphertextPath, e);
17+
}
18+
19+
@Override
20+
public Instant getTimestamp() {
21+
return timestamp;
22+
}
1223
}

src/main/java/org/cryptomator/cryptofs/event/FilesystemEvent.java

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

3+
import java.time.Instant;
34
import java.util.function.Consumer;
45

56
/**
@@ -11,11 +12,11 @@
1112
* {@code
1213
* FilesystemEvent fse;
1314
* switch (fse) {
14-
* case DecryptionFailedEvent e -> //do stuff
1515
* case ConflictResolvedEvent e -> //do other stuff
16-
* //other cases
16+
* case DecryptionFailedEvent(Instant timestamp, Path ciphertext, Exception ex) -> //do stuff
17+
* //... other cases
1718
* }
18-
* if( fse instanceof DecryptionFailedEvent dfe) {
19+
* if( fse instanceof DecryptionFailedEvent(Instant timestamp, Path ciphertext, Exception ex) {
1920
* //do more stuff
2021
* }
2122
* }.
@@ -24,4 +25,10 @@
2425
*/
2526
public sealed interface FilesystemEvent permits BrokenDirFileEvent, BrokenFileNodeEvent, ConflictResolutionFailedEvent, ConflictResolvedEvent, DecryptionFailedEvent {
2627

28+
/**
29+
* Gets the timestamp when the event occurred.
30+
*
31+
* @return the event timestamp as an {@link Instant}
32+
*/
33+
Instant getTimestamp();
2734
}

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

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

3+
import com.google.common.io.BaseEncoding;
34
import org.cryptomator.cryptofs.common.Constants;
4-
import org.cryptomator.cryptofs.health.dirid.OrphanContentDirTest;
55
import org.cryptomator.cryptofs.util.TestCryptoException;
66
import org.cryptomator.cryptolib.api.CryptoException;
77
import org.cryptomator.cryptolib.api.Cryptor;
@@ -28,7 +28,7 @@
2828
public class DirectoryIdBackupTest {
2929

3030
@TempDir
31-
Path contentPath;
31+
Path testDir;
3232

3333
private String dirId = "12345678";
3434
private Cryptor cryptor;
@@ -50,7 +50,7 @@ public class Write {
5050

5151
@BeforeEach
5252
public void beforeEachWriteTest() {
53-
ciphertextDirectoryObject = new CiphertextDirectory(dirId, contentPath);
53+
ciphertextDirectoryObject = new CiphertextDirectory(dirId, testDir);
5454
encChannel = Mockito.mock(EncryptingWritableByteChannel.class);
5555
}
5656

@@ -62,7 +62,7 @@ public void testIdFileCreated() throws IOException {
6262

6363
dirIdBackupSpy.write(ciphertextDirectoryObject);
6464

65-
Assertions.assertTrue(Files.exists(contentPath.resolve(Constants.DIR_ID_BACKUP_FILE_NAME)));
65+
Assertions.assertTrue(Files.exists(testDir.resolve(Constants.DIR_ID_BACKUP_FILE_NAME)));
6666
}
6767

6868
@Test
@@ -83,22 +83,34 @@ public void testContentIsWritten() throws IOException {
8383
public class Read {
8484

8585
private DecryptingReadableByteChannel decChannel;
86+
private Path cipherContentDir;
8687

8788
@BeforeEach
8889
public void beforeEachRead() throws IOException {
89-
var backupFile = contentPath.resolve(Constants.DIR_ID_BACKUP_FILE_NAME);
90+
var dirNames = BaseEncoding.base32().encode(new byte [20]); //a directory id hash is due to SHA1 always 20 bytes long
91+
var twoCharDir = testDir.resolve(dirNames.substring(0,2));
92+
cipherContentDir = twoCharDir.resolve(dirNames.substring(2));
93+
var backupFile = cipherContentDir.resolve(Constants.DIR_ID_BACKUP_FILE_NAME);
94+
Files.createDirectories(cipherContentDir);
9095
Files.writeString(backupFile, dirId, StandardCharsets.US_ASCII, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
9196

9297
decChannel = mock(DecryptingReadableByteChannel.class);
9398
}
9499

100+
@Test
101+
@DisplayName("If the given path is not a cipherContentDir, throw IllegalArgumentException")
102+
public void wrongPath() throws IOException {
103+
var dirIdBackupSpy = spy(dirIdBackup);
104+
Assertions.assertThrows(IllegalArgumentException.class, () -> dirIdBackupSpy.read(testDir));
105+
}
106+
95107
@Test
96108
@DisplayName("If the directory id is longer than 36 characters, throw IllegalStateException")
97109
public void contentLongerThan36Chars() throws IOException {
98110
var dirIdBackupSpy = spy(dirIdBackup);
99111
Mockito.when(dirIdBackupSpy.wrapDecryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(decChannel);
100112
Mockito.when(decChannel.read(Mockito.any())).thenReturn(Constants.MAX_DIR_ID_LENGTH + 1);
101-
Assertions.assertThrows(IllegalStateException.class, () -> dirIdBackupSpy.read(contentPath));
113+
Assertions.assertThrows(IllegalStateException.class, () -> dirIdBackupSpy.read(cipherContentDir));
102114
}
103115

104116
@Test
@@ -108,7 +120,7 @@ public void invalidEncryptionThrowsCryptoException() throws IOException {
108120
var expectedException = new TestCryptoException();
109121
Mockito.when(dirIdBackupSpy.wrapDecryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(decChannel);
110122
Mockito.when(decChannel.read(Mockito.any())).thenThrow(expectedException);
111-
var actual = Assertions.assertThrows(CryptoException.class, () -> dirIdBackupSpy.read(contentPath));
123+
var actual = Assertions.assertThrows(CryptoException.class, () -> dirIdBackupSpy.read(cipherContentDir));
112124
Assertions.assertEquals(expectedException, actual);
113125
}
114126

@@ -119,7 +131,7 @@ public void ioException() throws IOException {
119131
var expectedException = new IOException("my oh my");
120132
Mockito.when(dirIdBackupSpy.wrapDecryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(decChannel);
121133
Mockito.when(decChannel.read(Mockito.any())).thenThrow(expectedException);
122-
var actual = Assertions.assertThrows(IOException.class, () -> dirIdBackupSpy.read(contentPath));
134+
var actual = Assertions.assertThrows(IOException.class, () -> dirIdBackupSpy.read(cipherContentDir));
123135
Assertions.assertEquals(expectedException, actual);
124136
}
125137

@@ -136,9 +148,10 @@ public void success() throws IOException {
136148
return expectedArray.length;
137149
}).when(decChannel).read(Mockito.any());
138150

139-
var readDirId = dirIdBackupSpy.read(contentPath);
151+
var readDirId = dirIdBackupSpy.read(cipherContentDir);
140152
Assertions.assertArrayEquals(expectedArray, readDirId);
141153
}
154+
142155
}
143156

144157

0 commit comments

Comments
 (0)