1717
1818import ch .cyberduck .core .ListService ;
1919import ch .cyberduck .core .Path ;
20+ import ch .cyberduck .core .PathAttributes ;
21+ import ch .cyberduck .core .Permission ;
2022import ch .cyberduck .core .Session ;
23+ import ch .cyberduck .core .SimplePathPredicate ;
2124import ch .cyberduck .core .UrlProvider ;
2225import ch .cyberduck .core .cryptomator .features .*;
2326import ch .cyberduck .core .exception .BackgroundException ;
2629import ch .cyberduck .core .shared .DefaultTouchFeature ;
2730import ch .cyberduck .core .transfer .TransferStatus ;
2831
32+ import org .apache .logging .log4j .LogManager ;
33+ import org .apache .logging .log4j .Logger ;
34+ import org .cryptomator .cryptolib .api .AuthenticationFailedException ;
2935import org .cryptomator .cryptolib .api .Cryptor ;
3036import org .cryptomator .cryptolib .api .FileContentCryptor ;
3137import org .cryptomator .cryptolib .api .FileHeaderCryptor ;
3238
39+ import java .nio .charset .StandardCharsets ;
40+ import java .util .EnumSet ;
41+ import java .util .regex .Matcher ;
42+ import java .util .regex .Pattern ;
43+
44+ import com .google .common .io .BaseEncoding ;
45+
3346public abstract class AbstractVault implements Vault {
3447
48+ private static final Logger log = LogManager .getLogger (AbstractVault .class );
49+
3550 public static final int VAULT_VERSION_DEPRECATED = 6 ;
3651 public static final int VAULT_VERSION = PreferencesFactory .get ().getInteger ("cryptomator.vault.version" );
3752
53+ public static final String DIR_PREFIX = "0" ;
54+
55+ private static final Pattern BASE32_PATTERN = Pattern .compile ("^0?(([A-Z2-7]{8})*[A-Z2-7=]{8})" );
56+ private static final Pattern BASE64URL_PATTERN = Pattern .compile ("^([A-Za-z0-9_=-]+).c9r" );
57+
3858 public abstract Path getMasterkey ();
3959
4060 public abstract Path getConfig ();
4161
62+ public abstract Path gethHome ();
63+
4264 public abstract int getVersion ();
4365
4466 public abstract FileHeaderCryptor getFileHeaderCryptor ();
@@ -82,6 +104,26 @@ public long toCleartextSize(final long cleartextFileOffset, final long ciphertex
82104 }
83105 }
84106
107+ @ Override
108+ public State getState () {
109+ return this .isUnlocked () ? State .open : State .closed ;
110+ }
111+
112+ @ Override
113+ public long toCiphertextSize (final long cleartextFileOffset , final long cleartextFileSize ) {
114+ if (TransferStatus .UNKNOWN_LENGTH == cleartextFileSize ) {
115+ return TransferStatus .UNKNOWN_LENGTH ;
116+ }
117+ final int headerSize ;
118+ if (0L == cleartextFileOffset ) {
119+ headerSize = this .getCryptor ().fileHeaderCryptor ().headerSize ();
120+ }
121+ else {
122+ headerSize = 0 ;
123+ }
124+ return headerSize + this .getCryptor ().fileContentCryptor ().ciphertextSize (cleartextFileSize );
125+ }
126+
85127 @ Override
86128 public Path encrypt (Session <?> session , Path file ) throws BackgroundException {
87129 return this .encrypt (session , file , file .attributes ().getDirectoryId (), false );
@@ -92,12 +134,172 @@ public Path encrypt(Session<?> session, Path file, boolean metadata) throws Back
92134 return this .encrypt (session , file , file .attributes ().getDirectoryId (), metadata );
93135 }
94136
95- public abstract Path encrypt (Session <?> session , Path file , String directoryId , boolean metadata ) throws BackgroundException ;
137+ public Path encrypt (Session <?> session , Path file , String directoryId , boolean metadata ) throws BackgroundException {
138+ final Path encrypted ;
139+ if (file .isFile () || metadata ) {
140+ if (file .getType ().contains (Path .Type .vault )) {
141+ log .warn ("Skip file {} because it is marked as an internal vault path" , file );
142+ return file ;
143+ }
144+ if (new SimplePathPredicate (file ).test (this .gethHome ())) {
145+ log .warn ("Skip vault home {} because the root has no metadata file" , file );
146+ return file ;
147+ }
148+ final Path parent ;
149+ final String filename ;
150+ if (file .getType ().contains (Path .Type .encrypted )) {
151+ final Path decrypted = file .attributes ().getDecrypted ();
152+ parent = this .getDirectoryProvider ().toEncrypted (session , decrypted .getParent ().attributes ().getDirectoryId (), decrypted .getParent ());
153+ filename = this .getDirectoryProvider ().toEncrypted (session , parent .attributes ().getDirectoryId (), decrypted .getName (), decrypted .getType ());
154+ }
155+ else {
156+ parent = this .getDirectoryProvider ().toEncrypted (session , file .getParent ().attributes ().getDirectoryId (), file .getParent ());
157+ filename = this .getDirectoryProvider ().toEncrypted (session , parent .attributes ().getDirectoryId (), file .getName (), file .getType ());
158+ }
159+ final PathAttributes attributes = new PathAttributes (file .attributes ());
160+ attributes .setDirectoryId (null );
161+ if (!file .isFile () && !metadata ) {
162+ // The directory is different from the metadata file used to resolve the actual folder
163+ attributes .setVersionId (null );
164+ attributes .setFileId (null );
165+ }
166+ // Translate file size
167+ attributes .setSize (this .toCiphertextSize (0L , file .attributes ().getSize ()));
168+ final EnumSet <Path .Type > type = EnumSet .copyOf (file .getType ());
169+ if (metadata && this .getVersion () == VAULT_VERSION_DEPRECATED ) {
170+ type .remove (Path .Type .directory );
171+ type .add (Path .Type .file );
172+ }
173+ type .remove (Path .Type .decrypted );
174+ type .add (Path .Type .encrypted );
175+ encrypted = new Path (parent , filename , type , attributes );
176+ }
177+ else {
178+ if (file .getType ().contains (Path .Type .encrypted )) {
179+ log .warn ("Skip file {} because it is already marked as an encrypted path" , file );
180+ return file ;
181+ }
182+ if (file .getType ().contains (Path .Type .vault )) {
183+ return this .getDirectoryProvider ().toEncrypted (session , this .gethHome ().attributes ().getDirectoryId (), this .gethHome ());
184+ }
185+ encrypted = this .getDirectoryProvider ().toEncrypted (session , directoryId , file );
186+ }
187+ // Add reference to decrypted file
188+ if (!file .getType ().contains (Path .Type .encrypted )) {
189+ encrypted .attributes ().setDecrypted (file );
190+ }
191+ // Add reference for vault
192+ file .attributes ().setVault (this .gethHome ());
193+ encrypted .attributes ().setVault (this .gethHome ());
194+ return encrypted ;
195+ }
196+
197+ @ Override
198+ public Path decrypt (final Session <?> session , final Path file ) throws BackgroundException {
199+ if (file .getType ().contains (Path .Type .decrypted )) {
200+ log .warn ("Skip file {} because it is already marked as an decrypted path" , file );
201+ return file ;
202+ }
203+ if (file .getType ().contains (Path .Type .vault )) {
204+ log .warn ("Skip file {} because it is marked as an internal vault path" , file );
205+ return file ;
206+ }
207+ final Path inflated = this .inflate (session , file );
208+ final Pattern pattern = this .getVersion () == VAULT_VERSION_DEPRECATED ? BASE32_PATTERN : BASE64URL_PATTERN ;
209+ final Matcher m = pattern .matcher (inflated .getName ());
210+ if (m .matches ()) {
211+ final String ciphertext = m .group (1 );
212+ try {
213+ final String cleartextFilename = this .getFileNameCryptor ().decryptFilename (
214+ this .getVersion () == VAULT_VERSION_DEPRECATED ? BaseEncoding .base32 () : BaseEncoding .base64Url (),
215+ ciphertext , file .getParent ().attributes ().getDirectoryId ().getBytes (StandardCharsets .UTF_8 ));
216+ final PathAttributes attributes = new PathAttributes (file .attributes ());
217+ if (this .isDirectory (inflated )) {
218+ if (Permission .EMPTY != attributes .getPermission ()) {
219+ final Permission permission = new Permission (attributes .getPermission ());
220+ permission .setUser (permission .getUser ().or (Permission .Action .execute ));
221+ permission .setGroup (permission .getGroup ().or (Permission .Action .execute ));
222+ permission .setOther (permission .getOther ().or (Permission .Action .execute ));
223+ attributes .setPermission (permission );
224+ }
225+ // Reset size for folders
226+ attributes .setSize (-1L );
227+ attributes .setVersionId (null );
228+ attributes .setFileId (null );
229+ }
230+ else {
231+ // Translate file size
232+ attributes .setSize (this .toCleartextSize (0L , file .attributes ().getSize ()));
233+ }
234+ // Add reference to encrypted file
235+ attributes .setEncrypted (file );
236+ // Add reference for vault
237+ attributes .setVault (this .gethHome ());
238+ final EnumSet <Path .Type > type = EnumSet .copyOf (file .getType ());
239+ type .remove (this .isDirectory (inflated ) ? Path .Type .file : Path .Type .directory );
240+ type .add (this .isDirectory (inflated ) ? Path .Type .directory : Path .Type .file );
241+ type .remove (Path .Type .encrypted );
242+ type .add (Path .Type .decrypted );
243+ final Path decrypted = new Path (file .getParent ().attributes ().getDecrypted (), cleartextFilename , type , attributes );
244+ if (type .contains (Path .Type .symboliclink )) {
245+ decrypted .setSymlinkTarget (file .getSymlinkTarget ());
246+ }
247+ return decrypted ;
248+ }
249+ catch (AuthenticationFailedException e ) {
250+ throw new CryptoAuthenticationException (
251+ "Failure to decrypt due to an unauthentic ciphertext" , e );
252+ }
253+ }
254+ else {
255+ throw new CryptoFilenameMismatchException (
256+ String .format ("Failure to decrypt %s due to missing pattern match for %s" , inflated .getName (), pattern ));
257+ }
258+ }
259+
260+ private boolean isDirectory (final Path p ) {
261+ if (this .getVersion () == VAULT_VERSION_DEPRECATED ) {
262+ return p .getName ().startsWith (DIR_PREFIX );
263+ }
264+ return p .isDirectory ();
265+ }
266+
267+ private Path inflate (final Session <?> session , final Path file ) throws BackgroundException {
268+ final String fileName = file .getName ();
269+ if (this .getFilenameProvider ().isDeflated (fileName )) {
270+ final String filename = this .getFilenameProvider ().inflate (session , fileName );
271+ return new Path (file .getParent (), filename , EnumSet .of (Path .Type .file ), file .attributes ());
272+ }
273+ return file ;
274+ }
96275
97276 public synchronized boolean isUnlocked () {
98277 return this .getCryptor () != null ;
99278 }
100279
280+ @ Override
281+ public boolean contains (final Path file ) {
282+ if (this .isUnlocked ()) {
283+ return new SimplePathPredicate (file ).test (this .gethHome ()) || file .isChild (this .getHome ());
284+ }
285+ return false ;
286+ }
287+
288+ @ Override
289+ public synchronized void close () {
290+ if (this .isUnlocked ()) {
291+ if (this .getCryptor () != null ) {
292+ getCryptor ().destroy ();
293+ }
294+ if (this .getDirectoryProvider () != null ) {
295+ this .getDirectoryProvider ().destroy ();
296+ }
297+ if (this .getFilenameProvider () != null ) {
298+ this .getFilenameProvider ().destroy ();
299+ }
300+ }
301+ }
302+
101303 @ Override
102304 @ SuppressWarnings ("unchecked" )
103305 public <T > T getFeature (final Session <?> session , final Class <T > type , final T delegate ) {
0 commit comments