22
33import  com .google .common .io .BaseEncoding ;
44import  org .cryptomator .cryptofs .CryptoPathMapper ;
5+ import  org .cryptomator .cryptofs .DirectoryIdBackup ;
56import  org .cryptomator .cryptofs .VaultConfig ;
67import  org .cryptomator .cryptofs .common .CiphertextFileType ;
78import  org .cryptomator .cryptofs .common .Constants ;
89import  org .cryptomator .cryptofs .health .api .DiagnosticResult ;
10+ import  org .cryptomator .cryptolib .api .AuthenticationFailedException ;
911import  org .cryptomator .cryptolib .api .Cryptor ;
1012import  org .cryptomator .cryptolib .api .FileNameCryptor ;
1113import  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
1319import  java .io .IOException ;
1420import  java .nio .ByteBuffer ;
21+ import  java .nio .channels .ByteChannel ;
1522import  java .nio .charset .StandardCharsets ;
1623import  java .nio .file .FileAlreadyExistsException ;
1724import  java .nio .file .Files ;
2128import  java .security .MessageDigest ;
2229import  java .security .NoSuchAlgorithmException ;
2330import  java .util .Map ;
31+ import  java .util .Optional ;
2432import  java .util .UUID ;
2533import  java .util .concurrent .atomic .AtomicInteger ;
2634
3139 */ 
3240public  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
0 commit comments