Skip to content

Commit 8c91b77

Browse files
author
Armin Schrenk
authored
Feature/shortened names check (#110)
adds new health check to the health service: ShortenedNamesCheck * checks all c9s directories * tests if name.c9s is present and regular file * compares c9s dir name with name.9cs content * 4 results in total: Obese name file, hash mismatch, missing and valid
1 parent 3aac52a commit 8c91b77

File tree

10 files changed

+441
-3
lines changed

10 files changed

+441
-3
lines changed

pom.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,10 @@
143143
<version>3.0.0-M5</version>
144144
<configuration>
145145
<!-- Allow reflection for Mockito, so it can properly mock non-public classes -->
146-
<argLine>--add-opens=org.cryptomator.cryptofs/org.cryptomator.cryptofs.health.dirid=ALL-UNNAMED --add-opens=org.cryptomator.cryptofs/org.cryptomator.cryptofs.health.type=ALL-UNNAMED</argLine>
147-
</configuration>
146+
<argLine>--add-opens=org.cryptomator.cryptofs/org.cryptomator.cryptofs.health.dirid=ALL-UNNAMED
147+
--add-opens=org.cryptomator.cryptofs/org.cryptomator.cryptofs.health.type=ALL-UNNAMED
148+
--add-opens=org.cryptomator.cryptofs/org.cryptomator.cryptofs.health.shortened=ALL-UNNAMED</argLine>
149+
</configuration>
148150
</plugin>
149151
<plugin>
150152
<groupId>org.apache.maven.plugins</groupId>

src/main/java/module-info.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import org.cryptomator.cryptofs.common.CiphertextFileType;
33
import org.cryptomator.cryptofs.health.api.HealthCheck;
44
import org.cryptomator.cryptofs.health.dirid.DirIdCheck;
5+
import org.cryptomator.cryptofs.health.shortened.ShortenedNamesCheck;
56
import org.cryptomator.cryptofs.health.type.CiphertextFileTypeCheck;
67

78
import java.nio.file.spi.FileSystemProvider;
@@ -27,6 +28,6 @@
2728

2829
uses HealthCheck;
2930

30-
provides HealthCheck with DirIdCheck, CiphertextFileTypeCheck;
31+
provides HealthCheck with DirIdCheck, CiphertextFileTypeCheck, ShortenedNamesCheck;
3132
provides FileSystemProvider with CryptoFileSystemProvider;
3233
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.cryptomator.cryptofs.health.shortened;
2+
3+
import org.cryptomator.cryptofs.VaultConfig;
4+
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
5+
import org.cryptomator.cryptolib.api.Cryptor;
6+
import org.cryptomator.cryptolib.api.Masterkey;
7+
8+
import java.io.IOException;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
12+
/**
13+
* A c9s directory where the name of the directory is not a Base64URL encoded SHA1-hash of the contents in {@value org.cryptomator.cryptofs.common.Constants#INFLATED_FILE_NAME}
14+
*/
15+
public class LongShortNamesMismatch implements DiagnosticResult {
16+
17+
final Path c9sDir;
18+
final String expectedShortName;
19+
20+
public LongShortNamesMismatch(Path c9sDir, String expectedShortName) {
21+
this.c9sDir = c9sDir;
22+
this.expectedShortName = expectedShortName;
23+
}
24+
25+
@Override
26+
public Severity getSeverity() {
27+
return Severity.WARN;
28+
}
29+
30+
@Override
31+
public String toString() {
32+
return String.format("Name of %s is not a base64url encoded SHA1 hash of String inside name.c9s.", c9sDir);
33+
}
34+
35+
// fix by renaming the parent to the content of name.c9s
36+
@Override
37+
public void fix(Path pathToVault, VaultConfig config, Masterkey masterkey, Cryptor cryptor) throws IOException {
38+
Files.move(c9sDir, c9sDir.resolveSibling(expectedShortName));
39+
}
40+
41+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.cryptomator.cryptofs.health.shortened;
2+
3+
import org.cryptomator.cryptofs.common.Constants;
4+
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
5+
6+
import java.nio.file.Path;
7+
8+
/**
9+
* A c9s directory with a missing long name.
10+
* <p>
11+
* A long name is missing if either
12+
* <ul>
13+
* <li> the file {@value org.cryptomator.cryptofs.common.Constants#INFLATED_FILE_NAME} does not exist</li>
14+
* <li> it is not a regular file</li>
15+
* </ul>
16+
*/
17+
public class MissingLongName implements DiagnosticResult {
18+
19+
final Path c9sDir;
20+
21+
public MissingLongName(Path c9sDir) {this.c9sDir = c9sDir;}
22+
23+
@Override
24+
public Severity getSeverity() {
25+
return Severity.CRITICAL;
26+
}
27+
28+
@Override
29+
public String toString() {
30+
return String.format("Shortened resource %s either misses %s or the file has invalid content.", c9sDir, Constants.INFLATED_FILE_NAME);
31+
}
32+
33+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.cryptomator.cryptofs.health.shortened;
2+
3+
import org.cryptomator.cryptofs.LongFileNameProvider;
4+
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
5+
6+
import java.nio.file.Path;
7+
8+
/**
9+
* A shortend file name file which exceeds the maximum size of {@value org.cryptomator.cryptofs.LongFileNameProvider#MAX_FILENAME_BUFFER_SIZE} bytes.
10+
*/
11+
public class ObeseNameFile implements DiagnosticResult {
12+
13+
final Path nameFile;
14+
final long size;
15+
16+
public ObeseNameFile(Path nameFile, long size) {
17+
this.nameFile = nameFile;
18+
this.size = size;
19+
}
20+
21+
@Override
22+
public Severity getSeverity() {
23+
return Severity.CRITICAL;
24+
}
25+
26+
@Override
27+
public String toString() {
28+
return String.format("Long filename file %s with size %d exceeds limit of %d for this type.", nameFile, size, LongFileNameProvider.MAX_FILENAME_BUFFER_SIZE);
29+
}
30+
31+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package org.cryptomator.cryptofs.health.shortened;
2+
3+
import com.google.common.io.BaseEncoding;
4+
import org.cryptomator.cryptofs.LongFileNameProvider;
5+
import org.cryptomator.cryptofs.VaultConfig;
6+
import org.cryptomator.cryptofs.common.Constants;
7+
import org.cryptomator.cryptofs.health.api.CheckFailed;
8+
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
9+
import org.cryptomator.cryptofs.health.api.HealthCheck;
10+
import org.cryptomator.cryptolib.api.Cryptor;
11+
import org.cryptomator.cryptolib.api.Masterkey;
12+
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
import java.io.IOException;
17+
import java.nio.file.FileVisitResult;
18+
import java.nio.file.Files;
19+
import java.nio.file.LinkOption;
20+
import java.nio.file.NoSuchFileException;
21+
import java.nio.file.Path;
22+
import java.nio.file.SimpleFileVisitor;
23+
import java.nio.file.attribute.BasicFileAttributeView;
24+
import java.nio.file.attribute.BasicFileAttributes;
25+
import java.util.Set;
26+
import java.util.function.Consumer;
27+
28+
import static java.nio.charset.StandardCharsets.UTF_8;
29+
import static org.cryptomator.cryptofs.common.Constants.DEFLATED_FILE_SUFFIX;
30+
import static org.cryptomator.cryptofs.common.Constants.INFLATED_FILE_NAME;
31+
32+
/**
33+
* Visits all c9s directories and checks if they all are valid shortened resource according the Cryptomator vault specification.
34+
*/
35+
public class ShortenedNamesCheck implements HealthCheck {
36+
37+
private static final Logger LOG = LoggerFactory.getLogger(ShortenedNamesCheck.class);
38+
private static final int MAX_TRAVERSAL_DEPTH = 3;
39+
private static final BaseEncoding BASE64URL = BaseEncoding.base64Url();
40+
41+
@Override
42+
public String name() {
43+
return "Shortened Names Check";
44+
}
45+
46+
@Override
47+
public void check(Path pathToVault, VaultConfig config, Masterkey masterkey, Cryptor cryptor, Consumer<DiagnosticResult> resultCollector) {
48+
49+
// scan vault structure:
50+
var dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME);
51+
var dirVisitor = new ShortenedNamesCheck.DirVisitor(resultCollector);
52+
try {
53+
Files.walkFileTree(dataDirPath, Set.of(), MAX_TRAVERSAL_DEPTH, dirVisitor);
54+
} catch (IOException e) {
55+
LOG.error("Traversal of data dir failed.", e);
56+
resultCollector.accept(new CheckFailed("Traversal of data dir failed. See log for details."));
57+
}
58+
}
59+
60+
// visible for testing
61+
static class DirVisitor extends SimpleFileVisitor<Path> {
62+
63+
private final Consumer<DiagnosticResult> resultCollector;
64+
65+
public DirVisitor(Consumer<DiagnosticResult> resultCollector) {
66+
this.resultCollector = resultCollector;
67+
}
68+
69+
@Override
70+
public FileVisitResult visitFile(Path dir, BasicFileAttributes attrs) throws IOException {
71+
var name = dir.getFileName().toString();
72+
if (attrs.isDirectory() && name.endsWith(Constants.DEFLATED_FILE_SUFFIX)) {
73+
checkShortenedName(dir);
74+
}
75+
return FileVisitResult.CONTINUE;
76+
}
77+
78+
// visible for testing
79+
void checkShortenedName(Path dir) throws IOException {
80+
Path nameFile = dir.resolve(INFLATED_FILE_NAME);
81+
82+
final BasicFileAttributes attrs;
83+
try {
84+
attrs = Files.getFileAttributeView(nameFile, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes();
85+
} catch (NoSuchFileException e) {
86+
resultCollector.accept(new MissingLongName(dir));
87+
return;
88+
}
89+
if (!attrs.isRegularFile()) {
90+
resultCollector.accept(new MissingLongName(dir));
91+
return;
92+
} else if (attrs.size() > LongFileNameProvider.MAX_FILENAME_BUFFER_SIZE) {
93+
resultCollector.accept(new ObeseNameFile(nameFile, attrs.size()));
94+
return;
95+
}
96+
97+
var longName = Files.readString(nameFile, UTF_8);
98+
var expectedShortName = deflate(longName);
99+
if (!dir.getFileName().toString().equals(expectedShortName)) {
100+
resultCollector.accept(new LongShortNamesMismatch(dir, expectedShortName));
101+
} else {
102+
resultCollector.accept(new ValidShortenedFile(dir));
103+
}
104+
}
105+
106+
//visible for testing
107+
String deflate(String longFileName) {
108+
byte[] longFileNameBytes = longFileName.getBytes(UTF_8);
109+
byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFileNameBytes);
110+
return BASE64URL.encode(hash) + DEFLATED_FILE_SUFFIX;
111+
}
112+
113+
}
114+
115+
116+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.cryptomator.cryptofs.health.shortened;
2+
3+
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
4+
5+
import java.nio.file.Path;
6+
7+
/**
8+
* A valid shortened resource according to the Cryptomator vault specification.
9+
*/
10+
public class ValidShortenedFile implements DiagnosticResult {
11+
12+
final Path c9sDir;
13+
14+
public ValidShortenedFile(Path c9sDir) {this.c9sDir = c9sDir;}
15+
16+
@Override
17+
public Severity getSeverity() {
18+
return Severity.GOOD;
19+
}
20+
21+
@Override
22+
public String toString() {
23+
return String.format("Found valid shortened resource at %s.", c9sDir);
24+
}
25+
26+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
org.cryptomator.cryptofs.health.dirid.DirIdCheck
22
org.cryptomator.cryptofs.health.type.CiphertextFileTypeCheck
3+
org.cryptomator.cryptofs.health.shortened.ShortenedNamesCheck
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package org.cryptomator.cryptofs.health.shortened;
2+
3+
import org.cryptomator.cryptofs.VaultConfig;
4+
import org.cryptomator.cryptolib.api.Cryptor;
5+
import org.cryptomator.cryptolib.api.Masterkey;
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.junit.jupiter.api.DisplayName;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.io.TempDir;
10+
import org.mockito.Mockito;
11+
12+
import java.io.IOException;
13+
import java.nio.file.Files;
14+
import java.nio.file.Path;
15+
16+
public class LongShortNamesMismatchTest {
17+
18+
@TempDir
19+
public Path pathToVault;
20+
21+
private LongShortNamesMismatch result;
22+
private Path dataDir;
23+
private Path cipherDir;
24+
25+
@BeforeEach
26+
public void init() throws IOException {
27+
dataDir = pathToVault.resolve("d");
28+
cipherDir = dataDir.resolve("00/0000");
29+
Files.createDirectories(cipherDir);
30+
}
31+
32+
@Test
33+
@DisplayName("a successful fix only renames the c9s directory")
34+
public void testSuccessfulFixRenamesResource() throws IOException {
35+
//prepare
36+
Path c9sDir = cipherDir.resolve("foo==.c9s");
37+
result = new LongShortNamesMismatch(c9sDir, "bar==.c9s");
38+
39+
Files.createDirectory(c9sDir);
40+
41+
//execute
42+
result.fix(pathToVault, Mockito.mock(VaultConfig.class), Mockito.mock(Masterkey.class), Mockito.mock(Cryptor.class));
43+
44+
//evaluate
45+
Path expectedC9sDir = c9sDir.resolveSibling("bar==.c9s");
46+
Files.exists(expectedC9sDir);
47+
Files.notExists(c9sDir);
48+
}
49+
50+
}

0 commit comments

Comments
 (0)