11package org .cryptomator .cryptofs .health .dirid ;
22
3+ import com .google .common .io .BaseEncoding ;
4+ import org .cryptomator .cryptofs .CryptoPathMapper ;
5+ import org .cryptomator .cryptofs .VaultConfig ;
6+ import org .cryptomator .cryptofs .common .CiphertextFileType ;
7+ import org .cryptomator .cryptofs .common .Constants ;
38import org .cryptomator .cryptofs .health .api .DiagnosticResult ;
9+ import org .cryptomator .cryptolib .api .Cryptor ;
10+ import org .cryptomator .cryptolib .api .FileNameCryptor ;
11+ import org .cryptomator .cryptolib .api .Masterkey ;
412
13+ import java .io .IOException ;
14+ import java .nio .ByteBuffer ;
15+ import java .nio .charset .StandardCharsets ;
16+ import java .nio .file .FileAlreadyExistsException ;
17+ import java .nio .file .Files ;
18+ import java .nio .file .LinkOption ;
519import java .nio .file .Path ;
20+ import java .nio .file .StandardOpenOption ;
21+ import java .security .MessageDigest ;
22+ import java .security .NoSuchAlgorithmException ;
623import java .util .Map ;
24+ import java .util .UUID ;
25+ import java .util .concurrent .atomic .AtomicInteger ;
726
827import static org .cryptomator .cryptofs .health .api .CommonDetailKeys .ENCRYPTED_PATH ;
928
1231 */
1332public class OrphanDir implements DiagnosticResult {
1433
34+ private static final String FILE_PREFIX = "file" ;
35+ private static final String DIR_PREFIX = "directory" ;
36+ private static final String SYMLINK_PREFIX = "symlink" ;
37+ private static final String LONG_NAME_SUFFIX_BASE = "_withVeryLongName" ;
38+
1539 final Path dir ;
1640
1741 OrphanDir (Path dir ) {
@@ -33,5 +57,127 @@ public Map<String, String> details() {
3357 return Map .of (ENCRYPTED_PATH , dir .toString ());
3458 }
3559
36- // fix: create new dirId inside of L+F dir and rename existing dir accordingly.
60+ @ Override
61+ public void fix (Path pathToVault , VaultConfig config , Masterkey masterkey , Cryptor cryptor ) throws IOException {
62+ var sha1 = getSha1MessageDigest ();
63+ String runId = Integer .toString ((short ) UUID .randomUUID ().getMostSignificantBits (), 32 );
64+ Path dataDir = pathToVault .resolve (Constants .DATA_DIR_NAME );
65+ Path orphanedDir = dataDir .resolve (this .dir );
66+ String orphanDirIdHash = dir .getParent ().getFileName ().toString () + dir .getFileName ().toString ();
67+
68+ Path recoveryDir = prepareRecoveryDir (pathToVault , cryptor .fileNameCryptor ());
69+ if (recoveryDir .toAbsolutePath ().equals (orphanedDir .toAbsolutePath ())) {
70+ return ; //recovery dir was orphaned, already recovered by prepare method
71+ }
72+
73+ var stepParentDir = prepareStepParent (dataDir , recoveryDir , cryptor .fileNameCryptor (), orphanDirIdHash );
74+ AtomicInteger fileCounter = new AtomicInteger (1 );
75+ AtomicInteger dirCounter = new AtomicInteger (1 );
76+ AtomicInteger symlinkCounter = new AtomicInteger (1 );
77+ String longNameSuffix = createClearnameToBeShortened (config .getShorteningThreshold ());
78+ try (var orphanedContentStream = Files .newDirectoryStream (orphanedDir )) {
79+ for (Path orphanedResource : orphanedContentStream ) {
80+ //@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 ;
86+ //@formatter:on
87+ adoptOrphanedResource (orphanedResource , newClearName , stepParentDir , cryptor .fileNameCryptor (), longNameSuffix , sha1 );
88+ }
89+ }
90+ Files .delete (orphanedDir );
91+ }
92+
93+ //visible for testing
94+ Path prepareRecoveryDir (Path pathToVault , FileNameCryptor cryptor ) throws IOException {
95+ Path dataDir = pathToVault .resolve (Constants .DATA_DIR_NAME );
96+ String rootDirHash = cryptor .hashDirectoryId (Constants .ROOT_DIR_ID );
97+ Path vaultCipherRootPath = dataDir .resolve (rootDirHash .substring (0 , 2 )).resolve (rootDirHash .substring (2 )).toAbsolutePath ();
98+
99+ //check if recovery dir exists and has unique recovery id
100+ String cipherRecoveryDirName = convertClearToCiphertext (cryptor , Constants .RECOVERY_DIR_NAME , Constants .ROOT_DIR_ID );
101+ Path cipherRecoveryDirFile = vaultCipherRootPath .resolve (cipherRecoveryDirName + "/" + Constants .DIR_FILE_NAME );
102+ if (Files .notExists (cipherRecoveryDirFile , LinkOption .NOFOLLOW_LINKS )) {
103+ Files .createDirectories (cipherRecoveryDirFile .getParent ());
104+ Files .writeString (cipherRecoveryDirFile , Constants .RECOVERY_DIR_ID , StandardCharsets .UTF_8 , StandardOpenOption .CREATE_NEW , StandardOpenOption .WRITE );
105+ } else {
106+ String uuid = Files .readString (cipherRecoveryDirFile , StandardCharsets .UTF_8 );
107+ if (!Constants .RECOVERY_DIR_ID .equals (uuid )) {
108+ throw new FileAlreadyExistsException ("Directory /" + Constants .RECOVERY_DIR_NAME + " already exists, but with wrong directory id." );
109+ }
110+ }
111+ String recoveryDirHash = cryptor .hashDirectoryId (Constants .RECOVERY_DIR_ID );
112+ Path cipherRecoveryDir = dataDir .resolve (recoveryDirHash .substring (0 , 2 )).resolve (recoveryDirHash .substring (2 )).toAbsolutePath ();
113+ Files .createDirectories (cipherRecoveryDir );
114+
115+ return cipherRecoveryDir ;
116+ }
117+
118+ // visible for testing
119+ CryptoPathMapper .CiphertextDirectory prepareStepParent (Path dataDir , Path cipherRecoveryDir , FileNameCryptor cryptor , String clearStepParentDirName ) throws IOException {
120+ //create "step-parent" directory to move orphaned files to
121+ String cipherStepParentDirName = convertClearToCiphertext (cryptor , clearStepParentDirName , Constants .RECOVERY_DIR_ID );
122+ Path cipherStepParentDirFile = cipherRecoveryDir .resolve (cipherStepParentDirName + "/" + Constants .DIR_FILE_NAME );
123+ final String stepParentUUID ;
124+ if (Files .exists (cipherStepParentDirFile , LinkOption .NOFOLLOW_LINKS )) {
125+ stepParentUUID = Files .readString (cipherStepParentDirFile , StandardCharsets .UTF_8 );
126+ } else {
127+ Files .createDirectories (cipherStepParentDirFile .getParent ());
128+ stepParentUUID = UUID .randomUUID ().toString ();
129+ Files .writeString (cipherStepParentDirFile , stepParentUUID , StandardCharsets .UTF_8 , StandardOpenOption .CREATE_NEW , StandardOpenOption .WRITE );
130+ }
131+ String stepParentDirHash = cryptor .hashDirectoryId (stepParentUUID );
132+ Path stepParentDir = dataDir .resolve (stepParentDirHash .substring (0 , 2 )).resolve (stepParentDirHash .substring (2 )).toAbsolutePath ();
133+ Files .createDirectories (stepParentDir );
134+ return new CryptoPathMapper .CiphertextDirectory (stepParentUUID , stepParentDir );
135+ }
136+
137+ // 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 );
141+ var deflatedName = BaseEncoding .base64Url ().encode (sha1 .digest (newCipherName .getBytes (StandardCharsets .UTF_8 ))) + Constants .DEFLATED_FILE_SUFFIX ;
142+ Path targetPath = stepParentDir .path .resolve (deflatedName );
143+ Files .move (oldCipherPath , targetPath );
144+
145+ //adjust name.c9s
146+ try (var fc = Files .newByteChannel (targetPath .resolve (Constants .INFLATED_FILE_NAME ), StandardOpenOption .WRITE , StandardOpenOption .CREATE , StandardOpenOption .TRUNCATE_EXISTING )) {
147+ fc .write (ByteBuffer .wrap (newCipherName .getBytes (StandardCharsets .UTF_8 )));
148+ }
149+ } else {
150+ var newCipherName = convertClearToCiphertext (cryptor , newClearname , stepParentDir .dirId );
151+ Path targetPath = stepParentDir .path .resolve (newCipherName );
152+ Files .move (oldCipherPath , targetPath );
153+ }
154+ }
155+
156+ private static String createClearnameToBeShortened (int threshold ) {
157+ int neededLength = (threshold - 4 ) / 4 * 3 - 16 ;
158+ return LONG_NAME_SUFFIX_BASE .repeat ((neededLength % LONG_NAME_SUFFIX_BASE .length ()) + 1 );
159+ }
160+
161+ private static String convertClearToCiphertext (FileNameCryptor cryptor , String clearTextName , String dirId ) {
162+ return cryptor .encryptFilename (BaseEncoding .base64Url (), clearTextName , dirId .getBytes (StandardCharsets .UTF_8 )) + Constants .CRYPTOMATOR_FILE_SUFFIX ;
163+ }
164+
165+ private static CiphertextFileType determineCiphertextFileType (Path ciphertextPath ) {
166+ if (Files .exists (ciphertextPath .resolve (Constants .DIR_FILE_NAME ), LinkOption .NOFOLLOW_LINKS )) {
167+ return CiphertextFileType .DIRECTORY ;
168+ } else if (Files .exists (ciphertextPath .resolve (Constants .SYMLINK_FILE_NAME ), LinkOption .NOFOLLOW_LINKS )) {
169+ return CiphertextFileType .SYMLINK ;
170+ } else {
171+ return CiphertextFileType .FILE ;
172+ }
173+ }
174+
175+ private static MessageDigest getSha1MessageDigest () {
176+ try {
177+ return MessageDigest .getInstance ("SHA1" );
178+ } catch (NoSuchAlgorithmException e ) {
179+ throw new IllegalStateException ("Every JVM needs to provide a SHA1 implementation." );
180+ }
181+ }
182+
37183}
0 commit comments