Skip to content

Commit 8f64e2c

Browse files
authored
Merge pull request #127 from cryptomator/feature/126-use-dirid-file-in-orphan-dir-fix
Use dirid file in orphan dir fix
2 parents 024c15d + 0238e67 commit 8f64e2c

File tree

4 files changed

+365
-71
lines changed

4 files changed

+365
-71
lines changed

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,22 @@ public DirectoryIdBackup(Cryptor cryptor) {
3636
public void execute(CryptoPathMapper.CiphertextDirectory ciphertextDirectory) throws IOException {
3737
try (var channel = Files.newByteChannel(ciphertextDirectory.path.resolve(Constants.DIR_ID_FILE), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); //
3838
var encryptingChannel = wrapEncryptionAround(channel, cryptor)) {
39-
encryptingChannel.write(ByteBuffer.wrap(ciphertextDirectory.dirId.getBytes(StandardCharsets.UTF_8)));
39+
encryptingChannel.write(ByteBuffer.wrap(ciphertextDirectory.dirId.getBytes(StandardCharsets.US_ASCII)));
4040
}
4141
}
4242

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+
4355
static EncryptingWritableByteChannel wrapEncryptionAround(ByteChannel channel, Cryptor cryptor) {
4456
return new EncryptingWritableByteChannel(channel, cryptor);
4557
}

src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanDir.java

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22

33
import com.google.common.io.BaseEncoding;
44
import org.cryptomator.cryptofs.CryptoPathMapper;
5+
import org.cryptomator.cryptofs.DirectoryIdBackup;
56
import org.cryptomator.cryptofs.VaultConfig;
67
import org.cryptomator.cryptofs.common.CiphertextFileType;
78
import org.cryptomator.cryptofs.common.Constants;
89
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
10+
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
911
import org.cryptomator.cryptolib.api.Cryptor;
1012
import org.cryptomator.cryptolib.api.FileNameCryptor;
1113
import org.cryptomator.cryptolib.api.Masterkey;
14+
import org.cryptomator.cryptolib.common.ByteBuffers;
15+
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
16+
import org.slf4j.Logger;
17+
import org.slf4j.LoggerFactory;
1218

1319
import java.io.IOException;
1420
import java.nio.ByteBuffer;
21+
import java.nio.channels.ByteChannel;
1522
import java.nio.charset.StandardCharsets;
1623
import java.nio.file.FileAlreadyExistsException;
1724
import java.nio.file.Files;
@@ -21,6 +28,7 @@
2128
import java.security.MessageDigest;
2229
import java.security.NoSuchAlgorithmException;
2330
import java.util.Map;
31+
import java.util.Optional;
2432
import java.util.UUID;
2533
import java.util.concurrent.atomic.AtomicInteger;
2634

@@ -31,6 +39,8 @@
3139
*/
3240
public class OrphanDir implements DiagnosticResult {
3341

42+
private static final Logger LOG = LoggerFactory.getLogger(OrphanDir.class);
43+
3444
private static final String FILE_PREFIX = "file";
3545
private static final String DIR_PREFIX = "directory";
3646
private static final String SYMLINK_PREFIX = "symlink";
@@ -70,23 +80,36 @@ public void fix(Path pathToVault, VaultConfig config, Masterkey masterkey, Crypt
7080
return; //recovery dir was orphaned, already recovered by prepare method
7181
}
7282

73-
var stepParentDir = prepareStepParent(dataDir, recoveryDir, cryptor.fileNameCryptor(), orphanDirIdHash);
83+
var stepParentDir = prepareStepParent(dataDir, recoveryDir, cryptor, orphanDirIdHash);
7484
AtomicInteger fileCounter = new AtomicInteger(1);
7585
AtomicInteger dirCounter = new AtomicInteger(1);
7686
AtomicInteger symlinkCounter = new AtomicInteger(1);
7787
String longNameSuffix = createClearnameToBeShortened(config.getShorteningThreshold());
78-
try (var orphanedContentStream = Files.newDirectoryStream(orphanedDir)) {
88+
Optional<String> dirId = retrieveDirId(orphanedDir, cryptor);
89+
90+
try (var orphanedContentStream = Files.newDirectoryStream(orphanedDir, p -> !Constants.DIR_ID_FILE.equals(p.getFileName().toString()))) {
7991
for (Path orphanedResource : orphanedContentStream) {
92+
boolean isShortened = orphanedResource.toString().endsWith(Constants.DEFLATED_FILE_SUFFIX);
8093
//@formatter:off
81-
var newClearName = switch (determineCiphertextFileType(orphanedResource)) {
82-
case FILE -> FILE_PREFIX + fileCounter.getAndIncrement();
83-
case DIRECTORY -> DIR_PREFIX + dirCounter.getAndIncrement();
84-
case SYMLINK -> SYMLINK_PREFIX + symlinkCounter.getAndIncrement();
85-
} + "_" + runId;
94+
var newClearName = dirId.map(id -> {
95+
try {
96+
return decryptFileName(orphanedResource, isShortened, id, cryptor.fileNameCryptor());
97+
} catch (IOException | AuthenticationFailedException e) {
98+
LOG.warn("Unable to read and decrypt (long) file name of {}:", orphanedResource, e);
99+
return null;
100+
}})
101+
.orElseGet(() ->
102+
switch (determineCiphertextFileType(orphanedResource)) {
103+
case FILE -> FILE_PREFIX + fileCounter.getAndIncrement();
104+
case DIRECTORY -> DIR_PREFIX + dirCounter.getAndIncrement();
105+
case SYMLINK -> SYMLINK_PREFIX + symlinkCounter.getAndIncrement();
106+
} + "_" + runId + (isShortened ? longNameSuffix : ""));
86107
//@formatter:on
87-
adoptOrphanedResource(orphanedResource, newClearName, stepParentDir, cryptor.fileNameCryptor(), longNameSuffix, sha1);
108+
adoptOrphanedResource(orphanedResource, newClearName, isShortened, stepParentDir, cryptor.fileNameCryptor(), sha1);
88109
}
89110
}
111+
112+
Files.deleteIfExists(orphanedDir.resolve(Constants.DIR_ID_FILE));
90113
Files.delete(orphanedDir);
91114
}
92115

@@ -97,7 +120,7 @@ Path prepareRecoveryDir(Path pathToVault, FileNameCryptor cryptor) throws IOExce
97120
Path vaultCipherRootPath = dataDir.resolve(rootDirHash.substring(0, 2)).resolve(rootDirHash.substring(2)).toAbsolutePath();
98121

99122
//check if recovery dir exists and has unique recovery id
100-
String cipherRecoveryDirName = convertClearToCiphertext(cryptor, Constants.RECOVERY_DIR_NAME, Constants.ROOT_DIR_ID);
123+
String cipherRecoveryDirName = encrypt(cryptor, Constants.RECOVERY_DIR_NAME, Constants.ROOT_DIR_ID);
101124
Path cipherRecoveryDirFile = vaultCipherRootPath.resolve(cipherRecoveryDirName + "/" + Constants.DIR_FILE_NAME);
102125
if (Files.notExists(cipherRecoveryDirFile, LinkOption.NOFOLLOW_LINKS)) {
103126
Files.createDirectories(cipherRecoveryDirFile.getParent());
@@ -116,9 +139,9 @@ Path prepareRecoveryDir(Path pathToVault, FileNameCryptor cryptor) throws IOExce
116139
}
117140

118141
// visible for testing
119-
CryptoPathMapper.CiphertextDirectory prepareStepParent(Path dataDir, Path cipherRecoveryDir, FileNameCryptor cryptor, String clearStepParentDirName) throws IOException {
142+
CryptoPathMapper.CiphertextDirectory prepareStepParent(Path dataDir, Path cipherRecoveryDir, Cryptor cryptor, String clearStepParentDirName) throws IOException {
120143
//create "step-parent" directory to move orphaned files to
121-
String cipherStepParentDirName = convertClearToCiphertext(cryptor, clearStepParentDirName, Constants.RECOVERY_DIR_ID);
144+
String cipherStepParentDirName = encrypt(cryptor.fileNameCryptor(), clearStepParentDirName, Constants.RECOVERY_DIR_ID);
122145
Path cipherStepParentDirFile = cipherRecoveryDir.resolve(cipherStepParentDirName + "/" + Constants.DIR_FILE_NAME);
123146
final String stepParentUUID;
124147
if (Files.exists(cipherStepParentDirFile, LinkOption.NOFOLLOW_LINKS)) {
@@ -128,16 +151,66 @@ CryptoPathMapper.CiphertextDirectory prepareStepParent(Path dataDir, Path cipher
128151
stepParentUUID = UUID.randomUUID().toString();
129152
Files.writeString(cipherStepParentDirFile, stepParentUUID, StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
130153
}
131-
String stepParentDirHash = cryptor.hashDirectoryId(stepParentUUID);
154+
String stepParentDirHash = cryptor.fileNameCryptor().hashDirectoryId(stepParentUUID);
132155
Path stepParentDir = dataDir.resolve(stepParentDirHash.substring(0, 2)).resolve(stepParentDirHash.substring(2)).toAbsolutePath();
133156
Files.createDirectories(stepParentDir);
134-
return new CryptoPathMapper.CiphertextDirectory(stepParentUUID, stepParentDir);
157+
var stepParentCipherDir = new CryptoPathMapper.CiphertextDirectory(stepParentUUID, stepParentDir);
158+
//only if it does not exist
159+
try {
160+
DirectoryIdBackup.backupManually(cryptor, stepParentCipherDir);
161+
} catch (FileAlreadyExistsException e) {
162+
// already exists due to a previous recovery attempt
163+
}
164+
return stepParentCipherDir;
165+
}
166+
167+
//visible for testing
168+
Optional<String> retrieveDirId(Path orphanedDir, Cryptor cryptor) {
169+
var dirIdFile = orphanedDir.resolve(Constants.DIR_ID_FILE);
170+
var dirIdBuffer = ByteBuffer.allocate(36); //a dir id contains at most 36 ascii chars
171+
172+
try (var channel = Files.newByteChannel(dirIdFile, StandardOpenOption.READ); //
173+
var decryptingChannel = createDecryptingReadableByteChannel(channel, cryptor)) {
174+
ByteBuffers.fill(decryptingChannel, dirIdBuffer);
175+
dirIdBuffer.flip();
176+
} catch (IOException e) {
177+
LOG.info("Unable to read dirIdFile of {}.", orphanedDir, e);
178+
return Optional.empty();
179+
}
180+
181+
var allegedDirId = StandardCharsets.US_ASCII.decode(dirIdBuffer).toString();
182+
183+
var dirIdHash = orphanedDir.getParent().getFileName().toString() + orphanedDir.getFileName().toString();
184+
if (dirIdHash.equals(cryptor.fileNameCryptor().hashDirectoryId(allegedDirId))) {
185+
return Optional.of(allegedDirId);
186+
} else {
187+
LOG.info("Hash of read directory id {} does not match actual cipher dir hash {}.", allegedDirId, dirIdHash);
188+
return Optional.empty();
189+
}
190+
}
191+
192+
//exists and visible for testability
193+
DecryptingReadableByteChannel createDecryptingReadableByteChannel(ByteChannel channel, Cryptor cryptor) {
194+
return new DecryptingReadableByteChannel(channel, cryptor, true);
195+
}
196+
197+
//visible for testing
198+
String decryptFileName(Path orphanedResource, boolean isShortened, String dirId, FileNameCryptor cryptor) throws IOException, AuthenticationFailedException {
199+
final String filenameWithExtension;
200+
if (isShortened) {
201+
filenameWithExtension = Files.readString(orphanedResource.resolve(Constants.INFLATED_FILE_NAME));
202+
} else {
203+
filenameWithExtension = orphanedResource.getFileName().toString();
204+
}
205+
206+
final String filename = filenameWithExtension.substring(0, filenameWithExtension.length() - Constants.CRYPTOMATOR_FILE_SUFFIX.length());
207+
return cryptor.decryptFilename(BaseEncoding.base64Url(), filename, dirId.getBytes(StandardCharsets.UTF_8));
135208
}
136209

137210
// visible for testing
138-
void adoptOrphanedResource(Path oldCipherPath, String newClearname, CryptoPathMapper.CiphertextDirectory stepParentDir, FileNameCryptor cryptor, String longNameSuffix, MessageDigest sha1) throws IOException {
139-
if (oldCipherPath.toString().endsWith(Constants.DEFLATED_FILE_SUFFIX)) {
140-
var newCipherName = convertClearToCiphertext(cryptor, newClearname + longNameSuffix, stepParentDir.dirId);
211+
void adoptOrphanedResource(Path oldCipherPath, String newClearName, boolean isShortened, CryptoPathMapper.CiphertextDirectory stepParentDir, FileNameCryptor cryptor, MessageDigest sha1) throws IOException {
212+
var newCipherName = encrypt(cryptor, newClearName, stepParentDir.dirId);
213+
if (isShortened) {
141214
var deflatedName = BaseEncoding.base64Url().encode(sha1.digest(newCipherName.getBytes(StandardCharsets.UTF_8))) + Constants.DEFLATED_FILE_SUFFIX;
142215
Path targetPath = stepParentDir.path.resolve(deflatedName);
143216
Files.move(oldCipherPath, targetPath);
@@ -147,7 +220,6 @@ void adoptOrphanedResource(Path oldCipherPath, String newClearname, CryptoPathMa
147220
fc.write(ByteBuffer.wrap(newCipherName.getBytes(StandardCharsets.UTF_8)));
148221
}
149222
} else {
150-
var newCipherName = convertClearToCiphertext(cryptor, newClearname, stepParentDir.dirId);
151223
Path targetPath = stepParentDir.path.resolve(newCipherName);
152224
Files.move(oldCipherPath, targetPath);
153225
}
@@ -158,7 +230,7 @@ private static String createClearnameToBeShortened(int threshold) {
158230
return LONG_NAME_SUFFIX_BASE.repeat((neededLength % LONG_NAME_SUFFIX_BASE.length()) + 1);
159231
}
160232

161-
private static String convertClearToCiphertext(FileNameCryptor cryptor, String clearTextName, String dirId) {
233+
private static String encrypt(FileNameCryptor cryptor, String clearTextName, String dirId) {
162234
return cryptor.encryptFilename(BaseEncoding.base64Url(), clearTextName, dirId.getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX;
163235
}
164236

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public void testIdFileCreated() throws IOException {
5353
@Test
5454
public void testContentIsWritten() throws IOException {
5555
Mockito.when(encChannel.write(Mockito.any())).thenReturn(0);
56-
var expectedWrittenContent = ByteBuffer.wrap(dirId.getBytes(StandardCharsets.UTF_8));
56+
var expectedWrittenContent = ByteBuffer.wrap(dirId.getBytes(StandardCharsets.US_ASCII));
5757

5858
try (MockedStatic<DirectoryIdBackup> backupMock = Mockito.mockStatic(DirectoryIdBackup.class)) {
5959
backupMock.when(() -> DirectoryIdBackup.wrapEncryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(encChannel);

0 commit comments

Comments
 (0)