1
1
package org .cryptomator .cryptofs .health .dirid ;
2
2
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 ;
3
8
import 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 ;
4
12
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 ;
5
19
import java .nio .file .Path ;
20
+ import java .nio .file .StandardOpenOption ;
21
+ import java .security .MessageDigest ;
22
+ import java .security .NoSuchAlgorithmException ;
6
23
import java .util .Map ;
24
+ import java .util .UUID ;
25
+ import java .util .concurrent .atomic .AtomicInteger ;
7
26
8
27
import static org .cryptomator .cryptofs .health .api .CommonDetailKeys .ENCRYPTED_PATH ;
9
28
12
31
*/
13
32
public class OrphanDir implements DiagnosticResult {
14
33
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
+
15
39
final Path dir ;
16
40
17
41
OrphanDir (Path dir ) {
@@ -33,5 +57,127 @@ public Map<String, String> details() {
33
57
return Map .of (ENCRYPTED_PATH , dir .toString ());
34
58
}
35
59
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
+
37
183
}
0 commit comments