Compare commits

...

197 Commits

Author SHA1 Message Date
Tobias Hagemann
1e18a11886 release 0.10.0
- fixes #92
- fixes #84
- improves #76
- improves #66 (fewer WebDAV requests needed for file creation)
- might solve a few problems of #68
2015-10-29 19:04:38 +01:00
Sebastian Stenzel
386059a238 enabled UI caching for most controls (references #76) 2015-10-29 12:49:45 +01:00
Sebastian Stenzel
b4ab09b3aa Using Java 8 LongAdder instead of AtomicLong for better performance during I/O analysis. 2015-10-29 11:46:48 +01:00
Sebastian Stenzel
289ac55ccd Deleted IORuntimeException in favour of UncheckedIOException 2015-10-29 11:26:28 +01:00
Sebastian Stenzel
b5160cddb9 improved recursive directory deletion 2015-10-29 10:24:11 +01:00
Sebastian Stenzel
1a81b3a781 several refactorings, especially concerning LOCK operations on windows 2015-10-28 22:44:59 +01:00
Tobias Hagemann
b6a5db5797 Merge pull request #95 from tuphamnguyen/patch-1
Updated README.md
2015-10-28 22:25:45 +01:00
Tu Pham Nguyen
aaf98c4fb9 Updated README.md
Minor spelling, capitalization, and grammar issues. Nothing serious :)
2015-10-28 16:03:51 -05:00
Sebastian Stenzel
55d1ffe703 improved smoothing, stricter IO impl during encryption 2015-10-28 17:55:28 +01:00
Sebastian Stenzel
5fefa3c6d4 exponential smoothing for I/O graph 2015-10-28 16:40:50 +01:00
Sebastian Stenzel
b404e52670 starting development of 0.10.0 [ci skip] 2015-10-16 13:54:56 +02:00
Sebastian Stenzel
44475fa3f1 Version 0.9.0 2015-10-16 13:52:03 +02:00
Sebastian Stenzel
f430f3c579 just fixes some warnings in eclipse [ci skip] 2015-10-16 13:24:52 +02:00
Sebastian Stenzel
3efa23987f reverted previous commit (fixing travis setting) 2015-10-15 17:29:55 +02:00
Sebastian Stenzel
1dce871354 trying out travis container-based builds 2015-10-15 17:27:58 +02:00
Sebastian Stenzel
d919c727cf trying out travis container-based builds 2015-10-15 17:24:45 +02:00
Sebastian Stenzel
b691e374eb fixes #74 2015-10-15 17:19:27 +02:00
Sebastian Stenzel
ca88e05849 Preparations for #74 2015-10-10 17:01:25 +02:00
Sebastian Stenzel
104c3b64f6 - fixes #75
- restored progress indicator, broken by previous commit
2015-10-04 22:01:01 +02:00
Sebastian Stenzel
1bef4e786d - Fixed "return" key in unlock view
- Fixed password field focus
- Don't show unlock error messages from one vault, when switching to another vault
- Hide advanced mount options by default (preparation for things like #74)
2015-10-04 15:38:41 +02:00
Sebastian Stenzel
c1f32105d8 last byte in request range inclusive (!!) 2015-10-03 14:07:41 +02:00
Sebastian Stenzel
09b4130c3e - fixed size obfuscation padding
- fixed behaviour when serving invalid content ranges, thus improving random access performance (thats why we created the 0.8.2 workaround)
- reduced loglevels of some frequent messages
2015-10-03 13:10:28 +02:00
Sebastian Stenzel
6d1e0fe609 loading settings only once 2015-10-03 12:07:24 +02:00
Sebastian Stenzel
e65c84ca1d Merge tag '0.8.2'
Conflicts:
	main/core/pom.xml
	main/crypto-aes/pom.xml
	main/crypto-api/pom.xml
	main/installer-debian/pom.xml
	main/installer-osx/pom.xml
	main/installer-win-portable/pom.xml
	main/installer-win/pom.xml
	main/pom.xml
	main/uber-jar/pom.xml
	main/ui/pom.xml
2015-10-02 17:38:40 +02:00
Sebastian Stenzel
095f60ec03 Update 0.8.2
Fixes slow range requests on OS X, as WebDAVFS sometimes processes responses only after closing the connection, even though "keep-alive" is requested.
2015-10-02 17:36:04 +02:00
Sebastian Stenzel
485df3aa71 fixes #65 2015-09-26 16:39:18 +02:00
Sebastian Stenzel
6b073c1499 small I/O tweaks 2015-08-25 10:55:46 +02:00
Sebastian Stenzel
71983cc3a8 - added copyright information for the OS X progress indicator
- fixed spinning direction (now clockwise)
- use default progress indicator style for win + tux
[ci skip]
2015-08-16 11:55:42 +02:00
Sebastian Stenzel
db2297d2f1 fixed unit test 2015-08-15 23:19:34 +02:00
Sebastian Stenzel
38ab167fa4 clear password field when switching vault before unlocking [ci skip] 2015-08-15 23:14:05 +02:00
Sebastian Stenzel
f87e8f55f1 CSS cleanup 2015-08-15 23:05:53 +02:00
Sebastian Stenzel
a4e6365e0b New UI style for Linux builds 2015-08-15 16:51:36 +02:00
Sebastian Stenzel
06034fd95b code cleanup 2015-08-15 12:07:29 +02:00
Sebastian Stenzel
571fee9524 simplified DI, where possible 2015-08-09 00:38:23 +02:00
Sebastian Stenzel
af9deffa6d moved from guice to dagger 2015-08-08 19:13:16 +02:00
Sebastian Stenzel
48b319ec99 started working on 0.9.0-SNAPSHOT 2015-08-08 19:12:44 +02:00
Sebastian Stenzel
9ea9cb6eb2 Removed JVM flag for windows builds, as it may prevent the application from starting. This is a hotfix release for Windows only. 2015-08-06 22:42:59 +02:00
Sebastian Stenzel
301ba9cdb7 Update .travis.yml 2015-08-04 20:29:00 +02:00
Sebastian Stenzel
740c4c2ba9 Update .travis.yml 2015-08-04 18:01:13 +02:00
Sebastian Stenzel
18e7dcd91f Beta 0.8.0 2015-08-04 17:44:19 +02:00
Sebastian Stenzel
95133152f9 using 1GB heap size on 32 bit windows systems, due to http://www.oracle.com/technetwork/java/hotspotfaq-138619.html#gc_heap_32bit 2015-08-04 17:38:42 +02:00
Sebastian Stenzel
4cd243e32a Build with unlimited strength JCE policy files on Travis 2015-08-04 15:23:50 +02:00
Sebastian Stenzel
f454f48248 updated travis configuration 2015-08-04 15:06:59 +02:00
Sebastian Stenzel
ad3801b223 - Allow up to 2GB RAM to be used by native packages.
- Copy dependency jars instead of bundling them (allows more restrictively licensed dependencies)
2015-08-03 22:49:55 +02:00
Sebastian Stenzel
3f946d1c82 Creating directories lazily now. This fixes an issue, where a "ui" folder appeared inside the vault, just because Windows Explorer queries the existence of "ui/SwDRM.dll". 2015-08-03 12:33:11 +02:00
Sebastian Stenzel
ecb178d5b2 simplified code 2015-08-02 15:13:56 +02:00
Sebastian Stenzel
ed7dc60f5e slow start, increasing number of blocks worked on per thread, reusing ciphers for multiple consecutive blocks 2015-08-02 00:37:26 +02:00
Sebastian Stenzel
6bbfacd794 small changes for easier debugging 2015-08-01 21:34:47 +02:00
Sebastian Stenzel
5a06d01ef5 moved to ByteBuffers 2015-07-31 10:56:34 +02:00
Sebastian Stenzel
aac9ead633 - mutlithreaded encryption
- moved to bytebuffer (experimental)
2015-07-27 22:25:24 +02:00
Sebastian Stenzel
cdcc1626ce multithreaded decryption using producer/consumer pattern 2015-07-26 22:22:50 +02:00
Sebastian Stenzel
738d2dfc34 first multithreaded decryption test (PoC quality) 2015-07-25 17:28:00 +02:00
Sebastian Stenzel
9771c6d1e7 don't decrypt file size obfucscation padding 2015-07-25 16:18:02 +02:00
Sebastian Stenzel
bc0a26b0ad building pre-release versions
(trying out https://github.com/travis-ci/dpl/issues/234#issuecomment-123309684)
2015-07-25 01:58:25 +02:00
Tobias Hagemann
7349ef754e Unsatisfiable content range in partial file requests 2015-07-25 01:56:06 +02:00
Sebastian Stenzel
e8e80f306b WebDAV range request refinements 2015-07-25 01:52:37 +02:00
Sebastian Stenzel
e1ce400bcd fixed wrong long-to-bytearray conversion 2015-07-14 15:20:50 +02:00
Sebastian Stenzel
8c4d5a9614 non-interchangable chunks by adding additional data to MACs 2015-07-14 14:34:17 +02:00
Sebastian Stenzel
93a87c86a4 sensitive header data is now encrypted using a single enc_cbc(concat(fileSize, fileKey)) instead of encrypting both data separately 2015-07-13 17:37:06 +02:00
Sebastian Stenzel
685e347524 New MAC authentication warning, preventing CCAs, but allowing to force-decrypt unauthentic files. 2015-07-09 17:16:43 +02:00
Sebastian Stenzel
9d2d847727 fixed build 2015-07-05 20:57:15 +02:00
Sebastian Stenzel
a00086ff2d - simplified range request handling
- correct handling of HTTP 416 responses
- moved unit test to apache httpclient (old version 3.1 due to jackrabbit's dependency)
2015-07-04 20:47:23 +02:00
Sebastian Stenzel
d76154c8d1 - reduced size of chunks, a MAC is calculated for (not final yet)
- faster range requests due to reduced chunk size, thus faster video playback start
- fixed range requests
- making file locks optional (if not supported by file system)
2015-07-03 19:30:49 +02:00
Sebastian Stenzel
bc76ab285d fixed unit test 2015-06-27 08:09:12 +02:00
Sebastian Stenzel
0d3a5b4e70 - increased vault version
- Showing "per vault" MAC authentication failure dialogs
2015-06-26 23:35:24 +02:00
Sebastian Stenzel
48f544ef91 - support for http range requests in new schema 2015-06-21 22:11:15 +02:00
Sebastian Stenzel
45cf87d089 new, more secure encryption scheme
- fixed flaw reported by Stan Drapkin (SecurityDriven.NET) reducing effective key size to 96 bit
- multiple file content MACs for 1MB blocks, preventing chosen ciphertext attacks, as authentication now happens before decryption
- allowing files bigger than 64GiB
2015-06-21 18:51:39 +02:00
Sebastian Stenzel
d7186bb2dd Merge tag '0.7.2' 2015-06-05 15:20:09 +02:00
Sebastian Stenzel
85f3487cf0 fixes #64 by attempting to mount the more common "localhost", which is more likely to be excluded from proxy autoconfiguration scripts. 2015-06-05 15:17:33 +02:00
Sebastian Stenzel
4a754d6a6c Beginning development of 0.8.0 2015-05-31 00:04:26 +02:00
Sebastian Stenzel
abf9920caf its getting late... 2015-05-30 22:29:59 +02:00
Sebastian Stenzel
dd2863da5b 0.7.1 (fixed debian build)
updated travis script (requires git release tags to be equal to maven version, so starting with this tag we drop the preceeding "v")
2015-05-30 22:22:50 +02:00
Sebastian Stenzel
d43396bcfb updated version 2015-05-30 21:31:26 +02:00
Sebastian Stenzel
b6383f49b1 logging to %appdata% on windows 2015-05-30 20:55:29 +02:00
Sebastian Stenzel
c5b241a68a cleanup 2015-05-30 20:40:08 +02:00
Sebastian Stenzel
00a39c80cb Merge branch 'windows-unc-path-mounter' 2015-05-30 20:39:23 +02:00
Sebastian Stenzel
8d8fe74d3a restored ability to open vaults with 128 bit keylength 2015-05-30 20:13:11 +02:00
Sebastian Stenzel
e767436f5d updated jackrabbit (fixing security issue, see https://issues.apache.org/jira/browse/JCR-3883) 2015-05-29 23:39:36 +02:00
Sebastian Stenzel
03cdf1fdc9 added metadata caching 2015-05-29 11:18:23 +02:00
Sebastian Stenzel
49646aae41 improved directory name caching (>95% hitrate now) 2015-05-29 10:47:50 +02:00
Sebastian Stenzel
f3aa636b8b windows mount/unmount improvements 2015-05-28 17:34:56 +02:00
Sebastian Stenzel
c73f18e3b8 using ipv6-literal instead of localhost and bypassing proxy for localhost (wtf anyway) again... 2015-05-25 17:32:55 +02:00
Sebastian Stenzel
5f40ce50e7 fixes #41 2015-05-25 16:43:41 +02:00
Sebastian Stenzel
744f9db958 fixes #52 2015-05-25 16:22:52 +02:00
Sebastian Stenzel
111ee99ae1 - fixed invalid path for windows logfiles
- yet another attempt to improve (i don't even dare to say fix) #41
2015-05-25 14:37:12 +02:00
Sebastian Stenzel
7d81ff3b43 Merge pull request #59 from MuscleRumble/master
Replaced tray icon with monochrome version
2015-05-24 23:19:00 +02:00
Tobias Hagemann
00a2c6c5ae Replaced tray icon with monochrome version 2015-05-24 23:16:54 +02:00
Sebastian Stenzel
587c45ee63 added a default logging location, if logPath property is not set. 2015-05-24 22:30:12 +02:00
Sebastian Stenzel
3d3cb7bb86 Writing logfiles now. 2015-05-24 21:51:37 +02:00
Sebastian Stenzel
0e3513e86d - locking file header during creation,
- suggesting range request for files > 32MiB only
2015-05-22 22:26:39 +02:00
Sebastian Stenzel
8845efb983 fixed infinite number of authentication jobs resulting in heavy cpu load 2015-05-22 22:04:32 +02:00
Sebastian Stenzel
88f81d2682 Merge branch 'webdav-directory-moving' 2015-05-21 18:50:56 +02:00
Sebastian Stenzel
58d500baaf Merge pull request #58 from flyingarg/master
fixes #57
Thank you very much, @flyingarg
2015-05-18 17:27:36 +02:00
Mohit Raju
103ea9047f updated method and paramternames to openMountWithWebdavUri 2015-05-18 16:13:25 +03:00
Mohit Raju
f4b07b9807 restructure openFMWithWebdavSchema 2015-05-18 12:40:19 +03:00
Mohit Raju
6a3b4d486d added contributor name 2015-05-18 11:50:35 +03:00
Mohit Raju
13bcde318b removing debug logs 2015-05-18 10:10:07 +03:00
Mohit Raju
242486c0b1 Allowing webdav schema name fallback 2015-05-17 16:57:22 +03:00
Sebastian Stenzel
ea9c8eee83 yet another refactoring session (functionality restored now) 2015-05-15 23:17:24 +02:00
Sebastian Stenzel
0d969432c2 some more flat hierarchy fixes 2015-05-15 18:13:34 +02:00
Sebastian Stenzel
be369b480b some more destruction... 2015-05-14 21:48:02 +02:00
Sebastian Stenzel
4cf872f916 directory moving 2015-05-14 07:37:56 +02:00
Sebastian Stenzel
3d3c36b66f Update README.md 2015-05-12 22:19:36 +02:00
Sebastian Stenzel
54c2afe3d1 os-specific installer modules 2015-05-11 00:37:31 +02:00
Sebastian Stenzel
3c71878b6b First attempt of adding a portable version for windows users. (Issue #48) 2015-05-10 17:23:57 +02:00
Sebastian Stenzel
f36a61df1c Merge pull request #54 from cryptomator/flatDirectoryStructure
Flat directory structure
2015-05-10 14:54:49 +02:00
Sebastian Stenzel
1642aa4688 fixes #49 2015-05-10 14:13:07 +02:00
Sebastian Stenzel
6f9b16a7dc fixes #53 2015-05-10 14:00:00 +02:00
Sebastian Stenzel
66ed9126de version check during masterkey decryption -> added option to go to download page of different version 2015-05-10 12:39:28 +02:00
Sebastian Stenzel
a07efc5209 Proper error handling for outdated vault formats 2015-05-05 17:29:51 +02:00
Sebastian Stenzel
bbeeb79812 reduced max file name size, locking metadata files before read/write. 2015-05-05 06:50:16 +02:00
Sebastian Stenzel
4d08e9d72b cleanup 2015-05-04 22:02:47 +02:00
Sebastian Stenzel
040f260bf0 authenticated file header 2015-05-04 21:31:41 +02:00
Sebastian Stenzel
cdf9c28a38 refactored directory structure, so windows (and OneDrive) can handle vaults better 2015-04-28 18:19:05 +02:00
Sebastian Stenzel
a6972f62f2 Merge pull request #51 from MuscleRumble/master
Fixed .cryptomator bundle extension registration in OS X
2015-04-17 15:01:34 +02:00
Tobias Hagemann
1db32470b1 Fixed .cryptomator bundle extension registration in OS X 2015-04-17 10:22:10 +02:00
Sebastian Stenzel
ed022412fe fixed travis build for untagged versions 2015-04-08 21:42:06 +02:00
Sebastian Stenzel
a2356b62c7 Updated travis configuration and paths to new GitHub repo 2015-04-08 21:32:57 +02:00
Sebastian Stenzel
9aa6117fb0 Fixes #47
References #41 (increased wait time before retrying)
2015-03-16 15:03:03 +01:00
Sebastian Stenzel
b9b85a58ac Increased Version to 0.7.0-SNAPSHOT 2015-03-14 22:10:51 +01:00
Sebastian Stenzel
9024465d6c Beta 0.6.0 2015-03-14 22:09:25 +01:00
Sebastian Stenzel
f22142a876 Improved unmounting (failing, if encrypted drive is still busy) 2015-03-14 21:58:52 +01:00
Sebastian Stenzel
652c4cbafb Using 96 bit of random data and a 32 bit counter (as specified in https://tools.ietf.org/html/rfc3686#section-4). Thus maximum file size supported by Cryptomator is 64GiB, but decreasing risk of IV collisions to 1 : 2^48 2015-03-14 21:58:06 +01:00
Sebastian Stenzel
188a13b202 - better handling of MAC auth fails, providing link to help page
- use random data as file size obfuscation padding
- fixed osx unmount error
- new attempt to close #41
2015-03-14 19:11:24 +01:00
Sebastian Stenzel
75c21b4c9b fixes #37 2015-03-14 12:37:28 +01:00
Sebastian Stenzel
c7ecd612c9 added update notification 2015-03-14 12:34:11 +01:00
Sebastian Stenzel
3f8f0b1fa7 Update README.md 2015-03-13 13:24:35 +01:00
Sebastian Stenzel
2b4b359adb Merge branch '0.5.3'
Conflicts:
	main/core/pom.xml
	main/crypto-aes/pom.xml
	main/crypto-api/pom.xml
	main/pom.xml
	main/ui/pom.xml
2015-03-12 19:51:20 +01:00
Sebastian Stenzel
0562a909f9 fixes #46 2015-03-12 19:26:20 +01:00
Sebastian Stenzel
c10d80de18 fixes #35 2015-03-12 19:10:43 +01:00
Sebastian Stenzel
05abea0508 Updated welcome screen 2015-03-12 09:40:59 +01:00
Sebastian Stenzel
d19ffc327b improved windows WebDAV mounting 2015-03-11 21:18:53 +01:00
Sebastian Stenzel
a042c14fb9 changed version number 2015-03-11 19:38:11 +01:00
Sebastian Stenzel
a4be81267e preparation for some windows fixes, that need to be done during installation. This allows files of up to 4GiB 2015-03-11 19:36:20 +01:00
Sebastian Stenzel
c1dd902a10 Async MAC authentication for HTTP range requests. Fixes #38 2015-03-09 16:32:59 +01:00
Sebastian Stenzel
0994e7bb39 Show warning dialog, if MAC check failed. 2015-03-09 09:56:25 +01:00
Sebastian Stenzel
1f3b91f187 add license and gvfs dependencies to .deb package 2015-03-07 02:37:30 +01:00
Sebastian Stenzel
e883a04577 Merge remote-tracking branch 'origin/master' into 0.5.2
Conflicts:
	main/core/pom.xml
	main/crypto-aes/pom.xml
	main/crypto-api/pom.xml
	main/pom.xml
	main/ui/pom.xml
2015-03-06 15:06:31 +01:00
Sebastian Stenzel
1dd8a28a9d Merge remote-tracking branch 'origin/master' into 0.5.2
Conflicts:
	main/core/pom.xml
	main/crypto-aes/pom.xml
	main/crypto-api/pom.xml
	main/pom.xml
	main/ui/pom.xml
2015-03-06 14:56:22 +01:00
Sebastian Stenzel
39df98ea3c Branch 0.5.2 for windows 2015-03-06 14:55:30 +01:00
Sebastian Stenzel
2849e39e85 on-the-fly MAC calculation for better performance (addresses issue #38)
we still need to add some kind of warning on the UI and create an async MAC checker for ranged requests
2015-03-01 22:23:42 +01:00
Sebastian Stenzel
9433c22d7f minor I/O improvements 2015-03-01 20:55:32 +01:00
Sebastian Stenzel
5bd38d31bf Merge branch '0.5.1'
Conflicts:
	main/core/pom.xml
	main/crypto-aes/pom.xml
	main/crypto-api/pom.xml
	main/pom.xml
	main/ui/pom.xml
2015-02-23 14:53:31 +01:00
Sebastian Stenzel
63f64fae03 Fixed performance implications due to slow /dev/random. Now seeding PRNG only once per Cryptor. Fixes #36 2015-02-23 14:51:52 +01:00
Sebastian Stenzel
e321994c35 Update README.md 2015-02-22 23:03:47 +01:00
Sebastian Stenzel
f86b27d62f Updated Version to 0.6.0-SNAPSHOT 2015-02-22 22:19:13 +01:00
Sebastian Stenzel
cba8bbefc5 Beta Version 0.5.0 2015-02-22 22:18:18 +01:00
Sebastian Stenzel
507e21f8a3 - fixes folder creation and automounting on Linux
- using IPv6 address for mounting on Windows only (hostnames on OS X and Linux)
2015-02-22 21:04:46 +01:00
Sebastian Stenzel
676cb10ef0 fixes automount on linux distributions, that do not accept the [::1] literal as localhost
fixes reset of Settings, if a Vault no longer exists upon Cryptomator startup
2015-02-22 18:01:13 +01:00
Sebastian Stenzel
3b3aa4107b fixes #33 2015-02-22 16:46:16 +01:00
Sebastian Stenzel
7edd303f2e Added change password functionality (fixes #20)
Moved controllers to new package
Small UI improvements
2015-02-22 16:10:17 +01:00
Sebastian Stenzel
ea3384d189 removed multi user functionality (see #21)
using fixed masterkey filename now
2015-02-22 15:15:43 +01:00
Sebastian Stenzel
b2be41e39b Refactorings 2015-02-22 14:25:48 +01:00
Sebastian Stenzel
f1d125bf8d reduced public interface complexity of Vault 2015-02-22 14:06:52 +01:00
Sebastian Stenzel
028f6ea824 WebDavMounter warmUp in background thread. 2015-02-22 13:52:28 +01:00
Sebastian Stenzel
30dc8eecb1 - Refactored WebDavMounter (using Guice)
- implemented warm start for windows mounts
2015-02-22 13:21:08 +01:00
Sebastian Stenzel
4d979c26f6 (hopefully) fixed NPE in FXMLLoader.
see http://stackoverflow.com/questions/26434758/npe-in-fxmlloader/26436265#26436265
2015-02-22 12:36:17 +01:00
Sebastian Stenzel
4776dbf603 Renamed volume icon 2015-02-22 12:18:42 +01:00
Sebastian Stenzel
0b5e4469b4 Update .travis.yml 2015-02-20 22:11:00 +01:00
Sebastian Stenzel
8ba89a3bf5 Injecting Cryptor using Guice 2015-02-20 21:30:33 +01:00
Sebastian Stenzel
b68cf71494 - always check HMAC before decryption
- separating AES and CMAC key during SIV mode
2015-02-20 19:47:45 +01:00
Sebastian Stenzel
5569ecbfc7 fixes #23 2015-02-19 19:50:03 +01:00
Sebastian Stenzel
19bc1ed569 using beginning of long filename instead of checksum 2015-02-19 18:54:31 +01:00
Sebastian Stenzel
5aaee7bbf6 - fixed xorend function
- SIV implementation now satisfies all official test vectors
2015-02-15 15:55:49 +01:00
Sebastian Stenzel
3187520797 - fixed special chars in folder names
- fixed IndexOutOfBoundsException
- removal of no longer existing vault directories (at runtime)
2015-02-15 00:48:03 +01:00
Sebastian Stenzel
bcee1e0d12 Filename padding no longer needed: This was done in order to prevent AES-CTR to switch to a stream mode on the last block, which would be highly exploitable. Now we're using SIV mode, which operates on whole blocks. 2015-02-14 19:21:08 +01:00
Sebastian Stenzel
9fdd2f339c - changed file name encryption to SIV mode
- vastly improved exception handling, if decryption of a path name fails
2015-02-14 18:55:33 +01:00
Sebastian Stenzel
ebdf37ed63 RFC 5297 AEAD_AES_SIV_CMAC_256 2015-02-14 18:20:17 +01:00
Sebastian Stenzel
09c26f5e86 Merge pull request #32 from Tillerino/injection
Dependency injection instead of static instances
2015-02-14 16:34:19 +01:00
Tillmann Gaida
def70c5891 Removed static resources in WebDavServer, FXThreads and Settings with
dependency injection. Replaced static references to MainApplication in
the context of closing resources with an injected DeferredCloser. Using
controller factory for dependency injection into FX controllers.
2015-02-14 14:11:55 +01:00
Sebastian Stenzel
11396b71e6 Merge pull request #31 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2015-02-14 12:45:10 +01:00
The Gitter Badger
05ec9b574e Added Gitter badge 2015-02-14 11:44:48 +00:00
Sebastian Stenzel
efac770915 allow adding *.cryptomator files to vault list 2015-02-13 21:22:26 +01:00
Sebastian Stenzel
f29bcc447c - fixed automount on windows 2015-02-13 21:05:16 +01:00
Sebastian Stenzel
5e0ebab587 refactored "add vault" functionality, which fixes #14
removed some dependencies
refactored Main/MainApplication, which fixes #16
2015-02-13 19:46:07 +01:00
Sebastian Stenzel
751dbe6b7e Merge pull request #30 from Tillerino/osxNames
Named mounting (only affects OSX atm)
2015-01-25 13:44:44 +01:00
Tillmann Gaida
a72f8ba8ab Added the new mount name to the web dav mounter interface. Under OSX, we
can now use the name, which fixes #5
2015-01-25 12:42:16 +01:00
Sebastian Stenzel
999285617d Merge pull request #28 from Tillerino/windowsNames
Pretty network drive names on Windows
2015-01-25 12:05:04 +01:00
Sebastian Stenzel
addf488b26 Merge pull request #29 from Tillerino/master
Merged. But we should investigate alternatives to axet's openFileHandler
2015-01-25 12:04:12 +01:00
Tillmann Gaida
cd5e878a26 Bugfix (magic file open handler broke context class loader for event
thread)
2015-01-23 16:25:54 +01:00
Tillmann Gaida
0a671aa9bc Addition of a name to the context path of the WebDAV servlet. The name
will then appear as the name of the network drive on Windows.
The name is "normalized" down to characters, which are certain to be
accepted. I added a field to the unlock controller, which normalizes the
name as you type.
2015-01-23 14:28:22 +01:00
Sebastian Stenzel
8cc445a12a New application icon by Thomas Pähler 2015-01-23 00:20:40 +01:00
Sebastian Stenzel
432beb2a17 - fixed #19 (again): vault-specific prefix is now handled by the servlet context instead of jackrabbit.
- simplified webdav locator, as workspaces and pathPrefixes are not relevant to jackrabbit any longer
2015-01-22 21:48:52 +01:00
Sebastian Stenzel
9fd271ad7b fixed NPE 2015-01-22 21:42:45 +01:00
Sebastian Stenzel
72b1ff78c3 Merge pull request #27 from Tillerino/master
Single Running Instance + Double-clicking folders/files shows in GUI
2015-01-21 20:07:51 +01:00
Tillmann Gaida
edfd264e47 Changes proposed by @totalvoidness in code review 2015-01-21 19:54:10 +01:00
Tillmann Gaida
0cfc3fb7f7 Prevents starting a second instance of the GUI and forwards
main-method-arguments to the running instance. Command line arguments
are treated by showing the corresponding folder in the GUI.

If an argument is a folder, it is shown directly. If an argument is a
.masterkey.json file, the parent directory is shown. If an argument does
not exist, but the folder can be created, the newly created folder is
shown.

It was necessary to move the main function away from the MainApplication
class because running the main method of a class, which extends the
javafx Application class, will start a non-daemon thread. This prevents
the VM from exiting naturally.

OSX needs its own mechanism, which is implemented in OS-specific code.
It is vital that the required handler is added in the main thread of the
application, not the Java FX thread, which is a bit awkward to
implement. Since it is possible to open .cryptomator packages on OSX,
this extension is now hidden in the folder list.
2015-01-21 17:35:25 +01:00
Sebastian Stenzel
ecf29a91b8 Update README.md 2015-01-18 15:35:35 +01:00
Sebastian Stenzel
38884c6dfd - added custom info.plist template for OS X native packages (references #14) kudos to @tillerino 2015-01-17 19:57:15 +01:00
Sebastian Stenzel
7813a11381 - pad filenames with NULL bytes (fixes #24) 2015-01-16 19:55:33 +01:00
Sebastian Stenzel
d774546bf8 - pad file contents to reach a multiple of 16 bytes (so AES/CTR always works on complete blocks) - references #24
- calculate MAC over complete ciphertext (including file length obfuscation trash data)
2015-01-16 19:50:57 +01:00
Sebastian Stenzel
0b64c7ce25 - Updated exception 2015-01-15 12:29:10 +01:00
Sebastian Stenzel
0aef60efc4 - Single Jetty instnace (fixes #19) 2015-01-15 12:27:10 +01:00
Sebastian Stenzel
f0fa4fcf3d Merge branch 'master' of https://github.com/totalvoidness/open-cloud-encryptor 2015-01-14 19:35:04 +01:00
Sebastian Stenzel
8bfdad38b9 - fixed timing attack on MAC (see http://codahale.com/a-lesson-in-timing-attacks/) 2015-01-14 19:34:36 +01:00
Sebastian Stenzel
19ea81f0e5 Update README.md 2015-01-13 13:57:38 +01:00
Sebastian Stenzel
5e6f343e68 - Updated version to 0.5.0-SNAPSHOT 2015-01-13 11:04:58 +01:00
167 changed files with 10976 additions and 7941 deletions

View File

@@ -1,4 +1,22 @@
language: java
jdk:
- oraclejdk8
script: mvn -fmain/pom.xml clean package
- oraclejdk8
before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip -o /tmp/policy.zip && sudo unzip -j -o /tmp/policy.zip *.jar -d `jdk_switcher home oraclejdk8`/jre/lib/security && rm /tmp/policy.zip"
script: mvn -fmain/pom.xml -Puber-jar clean package
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/7d429ab35361726e26f2
on_success: change
on_failure: always
on_start: false
deploy:
provider: releases
prerelease: true
api_key:
secure: ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk=
file: main/uber-jar/target/Cryptomator-$TRAVIS_TAG.jar
skip_cleanup: true
on:
repo: cryptomator/cryptomator
tags: true

View File

@@ -4,53 +4,14 @@ Copyright (c) 2014, Sebastian Stenzel
Cryptomator is licensed under the MIT license. The details can be found in the accompanying license file.
## Third party softwares
Cryptomator uses third party softwares that may be licensed under different licenses.
### AquaFX
The ProgressIndicator in ui/src/main/resource/css/mac_theme.css contains code from the AquaFX project.
### Jackson
Jackson is a high-performance, Free/Open Source JSON processing library.
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
been in development since 2007.
It is currently developed by a community of developers, as well as supported
commercially by FasterXML.com.
Copyright 2013 Claudine Zillmann (http://aquafx-project.com/)
**Licensing:** Jackson core and extension components may licensed under different licenses.
To find the details that apply to this artifact see the accompanying Apache 2.0 license file.
For more information, including possible other licensing options, contact
FasterXML.com (http://fasterxml.com).
**Credits:** A list of contributors may be found from CREDITS file, which is included
in some artifacts (usually source distributions); but is always available
from the source code management (SCM) system project uses.
### Jetty
Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
and Apache License v2.0 which accompanies this distribution.
The UnixCrypt.java code implements the one way cryptography used by
Unix systems for simple password protection. Copyright 1996 Aki Yoshida,
modified April 2001 by Iris Van den Broeke, Daniel Deville.
Permission to use, copy, modify and distribute UnixCrypt
for non-commercial or commercial purposes and without fee is
granted provided that the copyright notice appears in all copies.
### Jackrabbit WebDAV Library
Copyright 2004-2014 The Apache Software Foundation
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
Based on source code originally developed by Day Software (http://www.day.com/).
### Apache Jakarta HttpClient
Copyright 1999-2007 The Apache Software Foundation
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
Licensed under the accompanying BSD license file.
### Apache Commons Collections
Copyright 2001-2013 The Apache Software Foundation
@@ -83,6 +44,17 @@ Copyright (c) 2013, ControlsFX
Licensed under the accompanying BSD license file.
### Dagger 2
Copyright 2014 Google, Inc.
Copyright 2012 Square, Inc.
Licensed under the Apache License, Version 2.0
### Apache Jakarta HttpClient
Copyright 1999-2007 The Apache Software Foundation
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
### Apache Log4j
Copyright 1999-2012 Apache Software Foundation
@@ -90,7 +62,44 @@ This product includes software developed at The Apache Software Foundation (http
ResolverUtil.java Copyright 2005-2006 Tim Fennell
### Jackrabbit WebDAV Library
Copyright 2004-2014 The Apache Software Foundation
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
Based on source code originally developed by Day Software (http://www.day.com/).
### Jackson
Jackson is a high-performance, Free/Open Source JSON processing library.
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
been in development since 2007.
It is currently developed by a community of developers, as well as supported
commercially by FasterXML.com.
**Licensing:** Jackson core and extension components may licensed under different licenses.
To find the details that apply to this artifact see the accompanying Apache 2.0 license file.
For more information, including possible other licensing options, contact
FasterXML.com (http://fasterxml.com).
**Credits:** A list of contributors may be found from CREDITS file, which is included
in some artifacts (usually source distributions); but is always available
from the source code management (SCM) system project uses.
### Jetty
Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
and Apache License v2.0 which accompanies this distribution.
The UnixCrypt.java code implements the one way cryptography used by
Unix systems for simple password protection. Copyright 1996 Aki Yoshida,
modified April 2001 by Iris Van den Broeke, Daniel Deville.
Permission to use, copy, modify and distribute UnixCrypt
for non-commercial or commercial purposes and without fee is
granted provided that the copyright notice appears in all copies.
### JUnit
Copyright (c) 2000-2006, www.hamcrest.org
Licensed under the accompanying BSD license file.
Licensed under the accompanying BSD license file.

View File

@@ -1,47 +1,55 @@
Cryptomator
====================
Multiplatform transparent client-side encryption of your files in the cloud. You need Java 8 in order to run the application. Get the runtime environment here: http://www.oracle.com/technetwork/java/javase/downloads/index.html
[![Build Status](https://travis-ci.org/cryptomator/cryptomator.svg?branch=master)](https://travis-ci.org/cryptomator/cryptomator)
[![Join the chat at https://gitter.im/totalvoidness/cryptomator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Flattr Cryptomator](https://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=totalvoidness&url=https%3A%2F%2Fgithub.com%2Ftotalvoidness%2Fcryptomator&title=Cryptomator&language=en_GB&tags=github&category=software)
If you want to take a look at the current beta version, go ahead and download [Cryptomator.dmg](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.dmg), [Cryptomator.exe](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.exe) or [Cryptomator.jar](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.jar).
Multiplatform transparent client-side encryption of your files in the cloud.
If you want to take a look at the current beta version, go ahead and get your copy of cryptomator on [Cryptomator.org](https://cryptomator.org) or clone and build Cryptomator using Maven (instructions below).
## Features
- Totally transparent: Just work on the encrypted volume, as if it was an USB drive
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory
- Totally transparent: Just work on the encrypted volume, as if it was an USB flash drive
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory.
- In fact it works with any directory. You can use it to encrypt as many folders as you like
- AES encryption with up to 256 bit key length
- AES encryption with 256-bit key length
- Client-side. No accounts, no data shared with any online service
- Filenames get encrypted too
- No need to provide credentials for any 3rd party service
- Open Source means: No backdoors. Control is better than trust
- Use as many encrypted folders in your dropbox as you want. Each having individual passwords
- Use as many encrypted folders in your Dropbox as you want. Each having individual passwords
- No commercial interest, no government agency, no wasted taxpayers' money ;-)
## Security
- Default key length is 256 bit (falls back to 128 bit, if JCE isn't installed)
- Scrypt key generation
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
### Privacy
- 256 bit keys (unlimited strength policy bundled with native binaries - 128-bit elsewhere)
- Scrypt key derivation
- Cryptographically secure random numbers for salts, IVs and the master key of course
- Sensitive data is swiped from the heap asap
- Lightweight: Complexity kills security
- Lightweight: [Complexity kills security](https://www.schneier.com/essays/archives/1999/11/a_plea_for_simplicit.html)
## Consistency
- I/O operations are transactional and atomic, if the file systems supports it
- ~~Metadata is stored per-folder, so it's not a SPOF~~
- *NEW:* No Metadata at all. Encrypted files can be decrypted even on completely shuffled file systems (if their contents are undamaged).
### Consistency
- HMAC over file contents to recognize changed ciphertext before decryption
- I/O operations are transactional and atomic, if the file systems support it
- Each file contains all information needed for decryption (except for the key of course). No common metadata means no [SPOF](http://en.wikipedia.org/wiki/Single_point_of_failure)
## Dependencies
- Java 8
- see pom.xml ;-)
## Building
## TODO
#### Dependencies
* Java 8
* Maven 3
* Optional: OS-dependent build tools for native packaging
* Optional: JCE unlimited strength policy files (needed for 256-bit keys)
### UI
- Native L&F
- Drive icons in WebDAV volumes
- Change password functionality
- Better explanations on UI
#### Building on Debian-based OS
```bash
apt-get install oracle-java8-installer oracle-java8-unlimited-jce-policy fakeroot maven git
git clone https://github.com/cryptomator/cryptomator.git
cd cryptomator/main
git checkout 0.7.1
mvn clean install -Pdebian
```
## License
Distributed under the MIT X Consortium license license. See the LICENSE file for more info.
[![Build Status](https://travis-ci.org/totalvoidness/cryptomator.svg?branch=master)](https://travis-ci.org/totalvoidness/cryptomator)
Distributed under the MIT X Consortium license. See the LICENSE file for more info.

View File

@@ -12,16 +12,14 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.4.0</version>
<version>0.10.0</version>
</parent>
<artifactId>core</artifactId>
<name>Cryptomator core I/O module</name>
<name>Cryptomator WebDAV and I/O module</name>
<properties>
<jetty.version>9.2.5.v20141112</jetty.version>
<jackrabbit.version>2.9.0</jackrabbit.version>
<commons.transaction.version>1.2</commons.transaction.version>
<jta.version>1.1</jta.version>
<jetty.version>9.3.3.v20150827</jetty.version>
<jackrabbit.version>2.11.0</jackrabbit.version>
</properties>
<dependencies>
@@ -29,6 +27,11 @@
<groupId>org.cryptomator</groupId>
<artifactId>crypto-api</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>crypto-aes</artifactId>
<scope>test</scope>
</dependency>
<!-- Jetty (Servlet Container) -->
<dependency>
@@ -41,6 +44,11 @@
<artifactId>jetty-webapp</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<scope>test</scope>
</dependency>
<!-- Jackrabbit -->
<dependency>
@@ -49,6 +57,12 @@
<version>${jackrabbit.version}</version>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- I/O -->
<dependency>
<groupId>commons-io</groupId>
@@ -58,9 +72,11 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,83 +0,0 @@
package org.cryptomator.files;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.CryptorIOSupport;
public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements CryptorIOSupport {
private final Path rootDir;
private final Cryptor cryptor;
private final EncryptionDecider encryptionDecider;
private Path currentDir;
public EncryptingFileVisitor(Path rootDir, Cryptor cryptor, EncryptionDecider encryptionDecider) {
this.rootDir = rootDir;
this.cryptor = cryptor;
this.encryptionDecider = encryptionDecider;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (rootDir.equals(dir) || encryptionDecider.shouldEncrypt(dir)) {
this.currentDir = dir;
return FileVisitResult.CONTINUE;
} else {
return FileVisitResult.SKIP_SUBTREE;
}
}
@Override
public FileVisitResult visitFile(Path plaintextFile, BasicFileAttributes attrs) throws IOException {
if (encryptionDecider.shouldEncrypt(plaintextFile)) {
final String plaintextName = plaintextFile.getFileName().toString();
final String encryptedName = cryptor.encryptPath(plaintextName, '/', '/', this);
final Path encryptedPath = plaintextFile.resolveSibling(encryptedName);
final InputStream plaintextIn = Files.newInputStream(plaintextFile, StandardOpenOption.READ);
final SeekableByteChannel ciphertextOut = Files.newByteChannel(encryptedPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
cryptor.encryptFile(plaintextIn, ciphertextOut);
Files.delete(plaintextFile);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (encryptionDecider.shouldEncrypt(dir)) {
final String plaintext = dir.getFileName().toString();
final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this);
final Path newPath = dir.resolveSibling(encrypted);
Files.move(dir, newPath, StandardCopyOption.ATOMIC_MOVE);
}
return FileVisitResult.CONTINUE;
}
@Override
public void writePathSpecificMetadata(String metadataFile, byte[] encryptedMetadata) throws IOException {
final Path path = currentDir.resolve(metadataFile);
Files.write(path, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
}
@Override
public byte[] readPathSpecificMetadata(String metadataFile) throws IOException {
final Path path = currentDir.resolve(metadataFile);
return Files.readAllBytes(path);
}
/* callback */
public interface EncryptionDecider {
boolean shouldEncrypt(Path path);
}
}

View File

@@ -8,16 +8,25 @@
******************************************************************************/
package org.cryptomator.webdav;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.jackrabbit.WebDavServlet;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
@@ -26,45 +35,39 @@ import org.slf4j.LoggerFactory;
public final class WebDavServer {
private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class);
private static final String LOCALHOST = "::1";
private static final String LOCALHOST = SystemUtils.IS_OS_WINDOWS ? "::1" : "localhost";
private static final int MAX_PENDING_REQUESTS = 200;
private static final int MAX_THREADS = 200;
private static final int MIN_THREADS = 4;
private static final int THREAD_IDLE_SECONDS = 20;
private final Server server;
private int port;
private final ServerConnector localConnector;
private final ContextHandlerCollection servletCollection;
public WebDavServer() {
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
server = new Server(tp);
localConnector = new ServerConnector(server);
localConnector.setHost(LOCALHOST);
servletCollection = new ContextHandlerCollection();
if (SystemUtils.IS_OS_WINDOWS) {
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, "/", ServletContextHandler.NO_SESSIONS);
final ServletHolder servlet = new ServletHolder(WindowsSucksServlet.class);
servletContext.addServlet(servlet, "/");
}
server.setConnectors(new Connector[] {localConnector});
server.setHandler(servletCollection);
}
/**
* @param workDir Path of encrypted folder.
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
* @return <code>true</code> upon success
*/
public synchronized boolean start(final String workDir, final boolean checkFileIntegrity, final Cryptor cryptor) {
final ServerConnector connector = new ServerConnector(server);
connector.setHost(LOCALHOST);
final String contextPath = "/";
final String servletPathSpec = "/*";
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.addServlet(getWebDavServletHolder(workDir, contextPath, checkFileIntegrity, cryptor), servletPathSpec);
context.setContextPath(contextPath);
server.setHandler(context);
public synchronized void start() {
try {
server.setConnectors(new Connector[] {connector});
server.start();
port = connector.getLocalPort();
return true;
LOG.info("Cryptomator is running on port {}", getPort());
} catch (Exception ex) {
LOG.error("Server couldn't be started", ex);
return false;
throw new RuntimeException("Server couldn't be started", ex);
}
}
@@ -72,26 +75,100 @@ public final class WebDavServer {
return server.isRunning();
}
public synchronized boolean stop() {
public synchronized void stop() {
try {
server.stop();
port = 0;
} catch (Exception ex) {
LOG.error("Server couldn't be stopped", ex);
}
return server.isStopped();
}
private ServletHolder getWebDavServletHolder(final String workDir, final String contextPath, final boolean checkFileIntegrity, final Cryptor cryptor) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor));
/**
* @param workDir Path of encrypted folder.
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
* @param failingMacCollection A (observable, thread-safe) collection, to which the names of resources are written, whose MAC authentication fails.
* @param name The name of the folder. Must be non-empty and only contain any of _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
* @return servlet
*/
public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection, final String name) {
try {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("name empty");
}
if (!StringUtils.containsOnly(name, "_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")) {
throw new IllegalArgumentException("name contains illegal characters: " + name);
}
final URI uri = new URI(null, null, localConnector.getHost(), localConnector.getLocalPort(), "/" + UUID.randomUUID().toString() + "/" + name, null, null);
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, uri.getRawPath(), ServletContextHandler.SESSIONS);
final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor, failingMacCollection, whitelistedResourceCollection);
servletContext.addServlet(servlet, "/*");
servletCollection.mapContexts();
LOG.debug("{} available on http:{}", workDir, uri.getRawSchemeSpecificPart());
return new ServletLifeCycleAdapter(servletContext, uri);
} catch (URISyntaxException e) {
throw new IllegalStateException("Invalid hard-coded URI components.", e);
}
}
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection, whitelistedResourceCollection));
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
result.setInitParameter(WebDavServlet.CFG_HTTP_ROOT, contextPath);
result.setInitParameter(WebDavServlet.CFG_CHECK_FILE_INTEGRITY, Boolean.toString(checkFileIntegrity));
return result;
}
public int getPort() {
return port;
return localConnector.getLocalPort();
}
/**
* Exposes implementation-specific methods to other modules.
*/
public class ServletLifeCycleAdapter implements AutoCloseable {
private final LifeCycle lifecycle;
private final URI servletUri;
private ServletLifeCycleAdapter(LifeCycle lifecycle, URI servletUri) {
this.lifecycle = lifecycle;
this.servletUri = servletUri;
}
public boolean isRunning() {
return lifecycle.isRunning();
}
public boolean start() {
try {
lifecycle.start();
return true;
} catch (Exception e) {
LOG.error("Failed to start", e);
return false;
}
}
public boolean stop() {
try {
lifecycle.stop();
return true;
} catch (Exception e) {
LOG.error("Failed to stop", e);
return false;
}
}
public URI getServletUri() {
return servletUri;
}
@Override
public void close() throws Exception {
this.stop();
}
}
}

View File

@@ -0,0 +1,31 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Windows mount attempts will fail, if not all requests on parent paths of a WebDAV resource get served. This servlet will respond to any
* request with status code 200, if the requested resource doesn't match a different servlet.
*/
public class WindowsSucksServlet extends HttpServlet {
private static final long serialVersionUID = -515280795196074354L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(HttpServletResponse.SC_OK);
}
}

View File

@@ -0,0 +1,23 @@
package org.cryptomator.webdav.exceptions;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
public class DecryptFailedRuntimeException extends RuntimeException {
private static final long serialVersionUID = -2726689824823439865L;
public DecryptFailedRuntimeException(DecryptFailedException cause) {
super(cause);
}
@Override
public String getMessage() {
return getCause().getMessage();
}
@Override
public String getLocalizedMessage() {
return getCause().getLocalizedMessage();
}
}

View File

@@ -1,31 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.exceptions;
import java.io.IOException;
public class IORuntimeException extends RuntimeException {
private static final long serialVersionUID = -4713080133052143303L;
public IORuntimeException(IOException ioException) {
super(ioException);
}
@Override
public String getMessage() {
return getCause().getMessage();
}
@Override
public String getLocalizedMessage() {
return getCause().getLocalizedMessage();
}
}

View File

@@ -6,22 +6,23 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
@@ -35,9 +36,9 @@ import org.apache.jackrabbit.webdav.property.DavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
import org.apache.jackrabbit.webdav.property.DavPropertySet;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.PropEntry;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -45,22 +46,34 @@ abstract class AbstractEncryptedNode implements DavResource {
private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class);
private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
private static final String[] DAV_CREATIONDATE_PROPNAMES = {DavPropertyName.CREATIONDATE.getName(), "Win32CreationTime"};
private static final String[] DAV_MODIFIEDDATE_PROPNAMES = {DavPropertyName.GETLASTMODIFIED.getName(), "Win32LastModifiedTime"};
protected final DavResourceFactory factory;
protected final CryptoResourceFactory factory;
protected final DavResourceLocator locator;
protected final DavSession session;
protected final LockManager lockManager;
protected final Cryptor cryptor;
protected final Path filePath;
protected final DavPropertySet properties;
protected AbstractEncryptedNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
protected AbstractEncryptedNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath) {
this.factory = factory;
this.locator = locator;
this.session = session;
this.lockManager = lockManager;
this.cryptor = cryptor;
this.filePath = filePath;
this.properties = new DavPropertySet();
this.determineProperties();
if (filePath != null && Files.exists(filePath)) {
try {
final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
} catch (IOException e) {
LOG.error("Error determining metadata " + filePath.toString(), e);
}
}
}
@Override
@@ -75,8 +88,7 @@ abstract class AbstractEncryptedNode implements DavResource {
@Override
public boolean exists() {
final Path path = ResourcePathUtils.getPhysicalPath(this);
return Files.exists(path);
return Files.exists(filePath);
}
@Override
@@ -107,16 +119,13 @@ abstract class AbstractEncryptedNode implements DavResource {
@Override
public long getModificationTime() {
final Path path = ResourcePathUtils.getPhysicalPath(this);
try {
return Files.getLastModifiedTime(path).toMillis();
return Files.getLastModifiedTime(filePath).toMillis();
} catch (IOException e) {
return -1;
}
}
protected abstract void determineProperties();
@Override
public DavPropertyName[] getPropertyNames() {
return getProperties().getPropertyNames();
@@ -136,25 +145,27 @@ abstract class AbstractEncryptedNode implements DavResource {
public void setProperty(DavProperty<?> property) throws DavException {
getProperties().add(property);
LOG.info("Set property {}", property.getName());
try {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (DavPropertyName.CREATIONDATE.equals(property.getName()) && property.getValue() instanceof String) {
final String createDateStr = (String) property.getValue();
final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr);
final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
attrView.setTimes(null, null, createTime);
LOG.info("Updating Creation Date: {}", createTime.toString());
} else if (DavPropertyName.GETLASTMODIFIED.equals(property.getName()) && property.getValue() instanceof String) {
final String lastModifiedTimeStr = (String) property.getValue();
final FileTime lastModifiedTime = FileTimeUtils.fromRfc1123String(lastModifiedTimeStr);
final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
attrView.setTimes(lastModifiedTime, null, null);
LOG.info("Updating Last Modified Date: {}", lastModifiedTime.toString());
LOG.trace("Set property {}", property.getName());
final String namespacelessPropertyName = property.getName().getName();
if (Files.exists(filePath)) {
try {
if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
final String createDateStr = (String) property.getValue();
final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr);
final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
attrView.setTimes(null, null, createTime);
LOG.debug("Updating Creation Date: {}", createTime.toString());
} else if (Arrays.asList(DAV_MODIFIEDDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
final String lastModifiedTimeStr = (String) property.getValue();
final FileTime lastModifiedTime = FileTimeUtils.fromRfc1123String(lastModifiedTimeStr);
final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
attrView.setTimes(lastModifiedTime, null, null);
LOG.debug("Updating Last Modified Date: {}", lastModifiedTime.toString());
}
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
}
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
@@ -186,7 +197,7 @@ abstract class AbstractEncryptedNode implements DavResource {
return null;
}
final String parentResource = FilenameUtils.getPath(locator.getResourcePath());
final String parentResource = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(locator.getResourcePath()), "/");
final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
try {
return getFactory().createResource(parentLocator, session);
@@ -196,49 +207,37 @@ abstract class AbstractEncryptedNode implements DavResource {
}
@Override
public void move(DavResource dest) throws DavException {
final Path src = ResourcePathUtils.getPhysicalPath(this);
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
try {
// check for conflicts:
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
}
// move:
public final void move(DavResource dest) throws DavException {
if (dest instanceof AbstractEncryptedNode) {
try {
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
this.move((AbstractEncryptedNode) dest);
} catch (IOException e) {
LOG.error("Error moving file from " + this.getResourcePath() + " to " + dest.getResourcePath());
throw new UncheckedIOException(e);
}
} catch (IOException e) {
LOG.error("Error moving file from " + src.toString() + " to " + dst.toString());
throw new IORuntimeException(e);
} else {
throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName());
}
}
public abstract void move(AbstractEncryptedNode dest) throws DavException, IOException;
@Override
public void copy(DavResource dest, boolean shallow) throws DavException {
final Path src = ResourcePathUtils.getPhysicalPath(this);
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
try {
// check for conflicts:
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
}
// copy:
public final void copy(DavResource dest, boolean shallow) throws DavException {
if (dest instanceof AbstractEncryptedNode) {
try {
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
this.copy((AbstractEncryptedNode) dest, shallow);
} catch (IOException e) {
LOG.error("Error copying file from " + this.getResourcePath() + " to " + dest.getResourcePath());
throw new UncheckedIOException(e);
}
} catch (IOException e) {
LOG.error("Error copying file from " + src.toString() + " to " + dst.toString());
throw new IORuntimeException(e);
} else {
throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName());
}
}
public abstract void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException;
@Override
public boolean isLockable(Type type, Scope scope) {
return true;
@@ -257,7 +256,11 @@ abstract class AbstractEncryptedNode implements DavResource {
@Override
public ActiveLock[] getLocks() {
final ActiveLock exclusiveWriteLock = getLock(Type.WRITE, Scope.EXCLUSIVE);
return new ActiveLock[] {exclusiveWriteLock};
if (exclusiveWriteLock != null) {
return new ActiveLock[] {exclusiveWriteLock};
} else {
return new ActiveLock[0];
}
}
@Override
@@ -281,7 +284,7 @@ abstract class AbstractEncryptedNode implements DavResource {
}
@Override
public DavResourceFactory getFactory() {
public CryptoResourceFactory getFactory() {
return factory;
}

View File

@@ -1,24 +0,0 @@
package org.cryptomator.webdav.jackrabbit;
import java.util.Map;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.AbstractDualBidiMap;
import org.apache.commons.collections4.map.LRUMap;
final class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
BidiLRUMap(int maxSize) {
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
}
protected BidiLRUMap(final Map<K, V> normalMap, final Map<V, K> reverseMap, final BidiMap<V, K> inverseBidiMap) {
super(normalMap, reverseMap, inverseBidiMap);
}
@Override
protected BidiMap<V, K> createBidiMap(Map<V, K> normalMap, Map<K, V> reverseMap, BidiMap<K, V> inverseMap) {
return new BidiLRUMap<V, K>(normalMap, reverseMap, inverseMap);
}
}

View File

@@ -0,0 +1,127 @@
package org.cryptomator.webdav.jackrabbit;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.webdav.DavLocatorFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.util.EncodeUtil;
public class CleartextLocatorFactory implements DavLocatorFactory {
private final String pathPrefix;
public CleartextLocatorFactory(String pathPrefix) {
this.pathPrefix = StringUtils.removeEnd(pathPrefix, "/");
}
// resourcePath == repositoryPath. No encryption here.
@Override
public DavResourceLocator createResourceLocator(String prefix, String href) {
final String fullPrefix = StringUtils.removeEnd(prefix, "/");
final String relativeHref = StringUtils.removeStart(href, fullPrefix);
final String relativeCleartextPath = EncodeUtil.unescape(relativeHref);
assert relativeCleartextPath.startsWith("/");
return new CleartextLocator(relativeCleartextPath);
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
assert resourcePath.startsWith("/");
return new CleartextLocator(resourcePath);
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
assert path.startsWith("/");
return new CleartextLocator(path);
}
private class CleartextLocator implements DavResourceLocator {
private final String relativeCleartextPath;
private CleartextLocator(String relativeCleartextPath) {
this.relativeCleartextPath = StringUtils.prependIfMissing(FilenameUtils.normalizeNoEndSeparator(relativeCleartextPath, true), "/");
}
@Override
public String getPrefix() {
return pathPrefix;
}
@Override
public String getResourcePath() {
return relativeCleartextPath;
}
@Override
public String getWorkspacePath() {
return null;
}
@Override
public String getWorkspaceName() {
return null;
}
@Override
public boolean isSameWorkspace(DavResourceLocator locator) {
return false;
}
@Override
public boolean isSameWorkspace(String workspaceName) {
return false;
}
@Override
public String getHref(boolean isCollection) {
final String encodedResourcePath = EncodeUtil.escapePath(relativeCleartextPath);
if (isRootLocation()) {
return pathPrefix + "/";
} else if (isCollection) {
return pathPrefix + encodedResourcePath + "/";
} else {
return pathPrefix + encodedResourcePath;
}
}
@Override
public boolean isRootLocation() {
return "/".equals(relativeCleartextPath);
}
@Override
public DavLocatorFactory getFactory() {
return CleartextLocatorFactory.this;
}
@Override
public String getRepositoryPath() {
return relativeCleartextPath;
}
@Override
public String toString() {
return "Locator: " + relativeCleartextPath + " (Prefix: " + pathPrefix + ")";
}
@Override
public int hashCode() {
return relativeCleartextPath.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CleartextLocator) {
final CleartextLocator other = (CleartextLocator) obj;
return relativeCleartextPath == null && other.relativeCleartextPath == null || relativeCleartextPath.equals(other.relativeCleartextPath);
} else {
return false;
}
}
}
}

View File

@@ -0,0 +1,287 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.format.DateTimeParseException;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavMethods;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletRequest;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
import org.cryptomator.crypto.Cryptor;
import org.eclipse.jetty.http.HttpHeader;
public class CryptoResourceFactory implements DavResourceFactory, FileConstants {
private static final String RANGE_BYTE_PREFIX = "bytes=";
private static final char RANGE_SET_SEP = ',';
private static final char RANGE_SEP = '-';
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
private final Path dataRoot;
private final FilenameTranslator filenameTranslator;
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, String vaultRoot) {
Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot);
this.cryptor = cryptor;
this.cryptoWarningHandler = cryptoWarningHandler;
this.dataRoot = vaultRootPath.resolve("d");
this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
}
@Override
public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
if (locator.isRootLocation()) {
return createRootDirectory(locator, request.getDavSession());
}
try {
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
final String ifRangeHeader = request.getHeader(HttpHeader.IF_RANGE.asString());
if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
// DIRECTORY
return createDirectory(locator, request.getDavSession(), dirFilePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
// FILE RANGE
final Pair<String, String> requestRange = getRequestRange(rangeHeader);
response.setStatus(DavServletResponse.SC_PARTIAL_CONTENT);
return createFilePart(locator, request.getDavSession(), requestRange, filePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && !isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
// FULL FILE (if-range not fulfilled)
return createFile(locator, request.getDavSession(), filePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && !isRangeSatisfiable(rangeHeader)) {
// FULL FILE (unsatisfiable range)
response.setStatus(DavServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
final EncryptedFile file = createFile(locator, request.getDavSession(), filePath);
response.addHeader(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + file.getContentLength());
return file;
} else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
// FULL FILE (as requested)
return createFile(locator, request.getDavSession(), filePath);
}
} catch (NonExistingParentException e) {
// return non-existing
}
return createNonExisting(locator, request.getDavSession());
}
@Override
public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
if (locator.isRootLocation()) {
return createRootDirectory(locator, session);
}
try {
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
if (Files.exists(dirFilePath)) {
return createDirectory(locator, session, dirFilePath);
} else if (Files.exists(filePath)) {
return createFile(locator, session, filePath);
}
} catch (NonExistingParentException e) {
// return non-existing
}
return createNonExisting(locator, session);
}
DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException {
return createDirectory(locator, session, existingDirectoryFile);
}
DavResource createChildFileResource(DavResourceLocator locator, DavSession session, Path existingFile) throws DavException {
return createFile(locator, session, existingFile);
}
/**
* @return <code>true</code> if a partial response should be generated according to an If-Range precondition.
*/
private boolean isIfRangePreconditionFulfilled(String ifRangeHeader, Path filePath) throws DavException {
if (ifRangeHeader == null) {
// no header set -> fulfilled implicitly
return true;
} else {
try {
final FileTime expectedTime = FileTimeUtils.fromRfc1123String(ifRangeHeader);
final FileTime actualTime = Files.getLastModifiedTime(filePath);
return expectedTime.compareTo(actualTime) == 0;
} catch (DateTimeParseException e) {
throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Unsupported If-Range header: " + ifRangeHeader);
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
}
/**
* @return <code>true</code> if and only if exactly one byte range has been requested.
*/
private boolean isRangeSatisfiable(String rangeHeader) {
assert rangeHeader != null;
if (!rangeHeader.startsWith(RANGE_BYTE_PREFIX)) {
return false;
}
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, RANGE_BYTE_PREFIX);
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
if (byteRanges.length != 1) {
return false;
}
return true;
}
/**
* Processes the given range header field, if it is supported. Only headers containing a single byte range are supported.<br/>
* <code>
* bytes=100-200<br/>
* bytes=-500<br/>
* bytes=1000-
* </code>
*
* @return Tuple of left and right range.
* @throws DavException HTTP statuscode 400 for malformed requests.
* @throws IllegalArgumentException If the given rangeHeader is not satisfiable. Check with {@link #isRangeSatisfiable(String)} before.
*/
private Pair<String, String> getRequestRange(String rangeHeader) throws DavException {
assert rangeHeader != null;
if (!rangeHeader.startsWith(RANGE_BYTE_PREFIX)) {
throw new IllegalArgumentException("Unsatisfiable range. Should have generated 416 resonse.");
}
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, RANGE_BYTE_PREFIX);
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
if (byteRanges.length != 1) {
throw new IllegalArgumentException("Unsatisfiable range. Should have generated 416 resonse.");
}
final String byteRange = byteRanges[0];
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
throw new DavException(DavServletResponse.SC_BAD_REQUEST, "malformed range header: " + rangeHeader);
}
return new ImmutablePair<>(bytePos[0], bytePos[1]);
}
/**
* @return Absolute file path for a given cleartext file resourcePath.
* @throws NonExistingParentException If one ancestor of the encrypted path is missing
*/
Path getEncryptedFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
assert relativeCleartextPath.startsWith("/");
final String parentCleartextPath = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(relativeCleartextPath), "/");
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
try {
final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
return parent.resolve(encryptedFilename);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* @return Absolute file path for a given cleartext file resourcePath.
* @throws NonExistingParentException If one ancestor of the encrypted path is missing
*/
Path getEncryptedDirectoryFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
assert relativeCleartextPath.startsWith("/");
final String parentCleartextPath = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(relativeCleartextPath), "/");
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
try {
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
return parent.resolve(encryptedFilename);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* @param createNonExisting if <code>false</code>, a {@link NonExistingParentException} will be thrown for missing ancestors.
* @return Absolute directory path for a given cleartext directory resourcePath.
* @throws NonExistingParentException if one ancestor directory is missing.
*/
private Path getEncryptedDirectoryPath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
assert relativeCleartextPath.startsWith("/");
assert "/".equals(relativeCleartextPath) || !relativeCleartextPath.endsWith("/");
try {
final Path result;
if ("/".equals(relativeCleartextPath)) {
// root level
final String fixedRootDirectory = cryptor.encryptDirectoryPath("", FileSystems.getDefault().getSeparator());
result = dataRoot.resolve(fixedRootDirectory);
} else {
final String parentCleartextPath = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(relativeCleartextPath), "/");
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
final Path directoryFile = parent.resolve(encryptedFilename);
if (!createNonExisting && !Files.exists(directoryFile)) {
throw new NonExistingParentException();
}
final String directoryId = filenameTranslator.getDirectoryId(directoryFile, true);
final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
result = dataRoot.resolve(directory);
}
Files.createDirectories(result);
return result;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, Path filePath) {
return new EncryptedFilePart(this, locator, session, requestRange, lockManager, cryptor, cryptoWarningHandler, filePath);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session, Path filePath) {
return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
}
private EncryptedDir createRootDirectory(DavResourceLocator locator, DavSession session) throws DavException {
final Path rootFile = dataRoot.resolve(ROOT_FILE);
final Path rootDir = filenameTranslator.getEncryptedDirectoryPath("");
try {
// make sure, root dir always exists.
// create dir first (because it fails silently, if alreay existing)
Files.createDirectories(rootDir);
Files.createFile(rootFile);
} catch (FileAlreadyExistsException e) {
// no-op
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
}
return createDirectory(locator, session, dataRoot.resolve(ROOT_FILE));
}
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path filePath) {
return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, filePath);
}
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
return new NonExistingNode(this, locator, session, lockManager, cryptor);
}
static class NonExistingParentException extends Exception {
private static final long serialVersionUID = 4421121746624627094L;
}
}

View File

@@ -0,0 +1,26 @@
package org.cryptomator.webdav.jackrabbit;
import java.util.Collection;
class CryptoWarningHandler {
private final Collection<String> resourcesWithInvalidMac;
private final Collection<String> whitelistedResources;
public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac, Collection<String> whitelistedResources) {
this.resourcesWithInvalidMac = resourcesWithInvalidMac;
this.whitelistedResources = whitelistedResources;
}
public void macAuthFailed(String resourcePath) {
// collection might be a list, but we don't want duplicates:
if (!resourcesWithInvalidMac.contains(resourcePath)) {
resourcesWithInvalidMac.add(resourcePath);
}
}
public boolean ignoreMac(String resourcePath) {
return whitelistedResources.contains(resourcePath);
}
}

View File

@@ -1,118 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import org.apache.commons.collections4.BidiMap;
import org.apache.jackrabbit.webdav.AbstractLocatorFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.SensitiveDataSwipeListener;
class DavLocatorFactoryImpl extends AbstractLocatorFactory implements SensitiveDataSwipeListener, CryptorIOSupport {
private static final int MAX_CACHED_PATHS = 10000;
private final Path fsRoot;
private final Cryptor cryptor;
private final BidiMap<String, String> pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // <decryptedPath, encryptedPath>
DavLocatorFactoryImpl(String fsRoot, String httpRoot, Cryptor cryptor) {
super(httpRoot);
this.fsRoot = FileSystems.getDefault().getPath(fsRoot);
this.cryptor = cryptor;
cryptor.addSensitiveDataSwipeListener(this);
}
/**
* @return Encrypted absolute paths on the file system.
*/
@Override
protected String getRepositoryPath(String resourcePath, String wspPath) {
String encryptedPath = pathCache.get(resourcePath);
if (encryptedPath == null) {
encryptedPath = encryptRepositoryPath(resourcePath);
pathCache.put(resourcePath, encryptedPath);
}
return encryptedPath;
}
private String encryptRepositoryPath(String resourcePath) {
if (resourcePath == null) {
return fsRoot.toString();
}
final String encryptedRepoPath = cryptor.encryptPath(resourcePath, FileSystems.getDefault().getSeparator().charAt(0), '/', this);
return fsRoot.resolve(encryptedRepoPath).toString();
}
/**
* @return Decrypted path for use in URIs.
*/
@Override
protected String getResourcePath(String repositoryPath, String wspPath) {
String decryptedPath = pathCache.getKey(repositoryPath);
if (decryptedPath == null) {
decryptedPath = decryptResourcePath(repositoryPath);
pathCache.put(decryptedPath, repositoryPath);
}
return decryptedPath;
}
private String decryptResourcePath(String repositoryPath) {
final Path absRepoPath = FileSystems.getDefault().getPath(repositoryPath);
if (fsRoot.equals(absRepoPath)) {
return null;
} else {
final Path relativeRepositoryPath = fsRoot.relativize(absRepoPath);
final String resourcePath = cryptor.decryptPath(relativeRepositoryPath.toString(), FileSystems.getDefault().getSeparator().charAt(0), '/', this);
return resourcePath;
}
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
// we don't support workspaces
return super.createResourceLocator(prefix, "", path, isResourcePath);
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
// we don't support workspaces
return super.createResourceLocator(prefix, "", resourcePath);
}
@Override
public void swipeSensitiveData() {
pathCache.clear();
}
/* Cryptor I/O Support */
@Override
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException {
final Path metaDataFile = fsRoot.resolve(encryptedPath);
Files.write(metaDataFile, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
}
@Override
public byte[] readPathSpecificMetadata(String encryptedPath) throws IOException {
final Path metaDataFile = fsRoot.resolve(encryptedPath);
if (!Files.isReadable(metaDataFile)) {
return null;
} else {
return Files.readAllBytes(metaDataFile);
}
}
}

View File

@@ -1,90 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavMethods;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletRequest;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.jackrabbit.resources.EncryptedDir;
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFile;
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFilePart;
import org.cryptomator.webdav.jackrabbit.resources.NonExistingNode;
import org.cryptomator.webdav.jackrabbit.resources.ResourcePathUtils;
import org.eclipse.jetty.http.HttpHeader;
class DavResourceFactoryImpl implements DavResourceFactory {
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final boolean checkFileIntegrity;
DavResourceFactoryImpl(Cryptor cryptor, boolean checkFileIntegrity) {
this.cryptor = cryptor;
this.checkFileIntegrity = checkFileIntegrity;
}
@Override
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
final Path path = ResourcePathUtils.getPhysicalPath(locator);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (Files.isRegularFile(path) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
return createFilePart(locator, request.getDavSession(), request);
} else if (Files.isRegularFile(path) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
return createFile(locator, request.getDavSession());
} else if (Files.isDirectory(path) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
return createDirectory(locator, request.getDavSession());
} else {
return createNonExisting(locator, request.getDavSession());
}
}
@Override
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
final Path path = ResourcePathUtils.getPhysicalPath(locator);
if (Files.isRegularFile(path)) {
return createFile(locator, session);
} else if (Files.isDirectory(path)) {
return createDirectory(locator, session);
} else {
return createNonExisting(locator, session);
}
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, checkFileIntegrity);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
return new EncryptedFile(this, locator, session, lockManager, cryptor, checkFileIntegrity);
}
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session) {
return new EncryptedDir(this, locator, session, lockManager, cryptor);
}
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
return new NonExistingNode(this, locator, session, lockManager, cryptor);
}
}

View File

@@ -0,0 +1,418 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.LinkedTransferQueue;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.ActiveLock;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.ResourceType;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.webdav.exceptions.DavRuntimeException;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
private final FilenameTranslator filenameTranslator;
private String directoryId;
private Path directoryPath;
public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path filePath) {
super(factory, locator, session, lockManager, cryptor, filePath);
this.filenameTranslator = filenameTranslator;
properties.add(new ResourceType(ResourceType.COLLECTION));
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
}
/**
* @return Path or <code>null</code>, if directory does not yet exist.
*/
protected synchronized String getDirectoryId() {
if (directoryId == null) {
try {
directoryId = filenameTranslator.getDirectoryId(filePath, false);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
return directoryId;
}
/**
* @return Path or <code>null</code>, if directory does not yet exist.
*/
private synchronized Path getDirectoryPath() {
if (directoryPath == null) {
final String dirId = getDirectoryId();
if (dirId != null) {
directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
}
}
return directoryPath;
}
@Override
public boolean exists() {
return Files.exists(filePath) && Files.exists(getDirectoryPath());
}
@Override
public boolean isCollection() {
return true;
}
@Override
public long getModificationTime() {
try {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
return -1;
} else {
return Files.getLastModifiedTime(dirPath).toMillis();
}
} catch (IOException e) {
return -1;
}
}
@Override
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
if (resource instanceof AbstractEncryptedNode) {
addMember((AbstractEncryptedNode) resource, inputContext);
} else {
throw new IllegalArgumentException("Unsupported resource type: " + resource.getClass().getName());
}
}
private void addMember(AbstractEncryptedNode childResource, InputContext inputContext) throws DavException {
if (childResource.isCollection()) {
this.addMemberDir(childResource.getLocator(), inputContext);
} else {
this.addMemberFile(childResource.getLocator(), inputContext);
}
}
private void addMemberDir(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
try {
final String cleartextDirName = FilenameUtils.getName(childLocator.getResourcePath());
final String ciphertextDirName = filenameTranslator.getEncryptedDirFileName(cleartextDirName);
final Path dirFilePath = dirPath.resolve(ciphertextDirName);
final String directoryId = filenameTranslator.getDirectoryId(dirFilePath, true);
final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
Files.createDirectories(directoryPath);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
private void addMemberFile(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
try {
final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath());
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
final Path filePath = dirPath.resolve(ciphertextFilename);
final Path tmpFilePath = Files.createTempFile(dirPath, null, null);
// encrypt to tmp file:
try (final FileChannel c = FileChannel.open(tmpFilePath, StandardOpenOption.WRITE, StandardOpenOption.DSYNC)) {
cryptor.encryptFile(inputContext.getInputStream(), c);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (EncryptFailedException e) {
LOG.error("Encryption failed for unknown reasons.", e);
throw new IllegalStateException("Encryption failed for unknown reasons.", e);
} finally {
IOUtils.closeQuietly(inputContext.getInputStream());
}
// mv tmp to target file:
try {
Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING);
}
Files.setLastModifiedTime(filePath, FileTime.from(Instant.now()));
} catch (IOException e) {
LOG.error("Failed to create file.", e);
throw new UncheckedIOException(e);
}
}
@Override
public DavResourceIterator getMembers() {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavRuntimeException(new DavException(DavServletResponse.SC_NOT_FOUND));
}
try (final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dirPath, DIRECTORY_CONTENT_FILTER)) {
final List<DavResource> result = new ArrayList<>();
for (final Path childPath : directoryStream) {
try {
final String cleartextFilename = filenameTranslator.getCleartextFilename(childPath.getFileName().toString());
final String cleartextFilepath = locator.isRootLocation() ? '/' + cleartextFilename : locator.getResourcePath() + '/' + cleartextFilename;
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), cleartextFilepath);
final DavResource resource;
if (StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), DIR_EXT)) {
resource = factory.createChildDirectoryResource(childLocator, session, childPath);
} else {
assert StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), FILE_EXT);
resource = factory.createChildFileResource(childLocator, session, childPath);
}
if (resource.exists()) {
result.add(resource);
}
} catch (DecryptFailedException e) {
LOG.warn("Decryption of resource failed: " + childPath);
continue;
}
}
return new DavResourceIteratorImpl(result);
} catch (IOException e) {
LOG.error("Exception during getMembers.", e);
throw new UncheckedIOException(e);
} catch (DavException e) {
LOG.error("Exception during getMembers.", e);
throw new DavRuntimeException(e);
}
}
@Override
public void removeMember(DavResource member) throws DavException {
if (member instanceof AbstractEncryptedNode) {
removeMember((AbstractEncryptedNode) member);
} else {
throw new IllegalArgumentException("Unsupported resource type: " + member.getClass().getName());
}
}
private void removeMember(AbstractEncryptedNode member) throws DavException {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
// https://tools.ietf.org/html/rfc4918#section-9.6
// we must unlock anything we want to delete:
for (ActiveLock lock : member.getLocks()) {
member.unlock(lock.getToken());
}
// now we can delete the file or directory:
try {
final String cleartextFilename = FilenameUtils.getName(member.getResourcePath());
if (member instanceof EncryptedDir) {
final EncryptedDir subDir = (EncryptedDir) member;
deleteSubDirectory(subDir);
} else {
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
final Path memberPath = dirPath.resolve(ciphertextFilename);
Files.deleteIfExists(memberPath);
}
} catch (FileNotFoundException e) {
// no-op
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public void move(AbstractEncryptedNode dest) throws DavException, IOException {
// when moving a directory we only need to move the file (actual dir is ID-dependent and won't change)
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).materializeDirFilePath();
} else {
dstPath = dest.filePath;
}
// move:
Files.createDirectories(dstPath.getParent());
try {
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING);
}
}
@Override
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
final Path dstDirFilePath;
if (dest instanceof NonExistingNode) {
dstDirFilePath = ((NonExistingNode) dest).materializeDirFilePath();
} else {
dstDirFilePath = dest.filePath;
}
// copy dirFile:
final String srcDirId = getDirectoryId();
if (srcDirId == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
final String dstDirId = UUID.randomUUID().toString();
try (final FileChannel c = FileChannel.open(dstDirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
c.write(ByteBuffer.wrap(dstDirId.getBytes(StandardCharsets.UTF_8)));
}
// copy actual dir:
if (!shallow) {
copyDirectoryContents(srcDirId, dstDirId);
} else {
final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
Files.createDirectories(dstDirPath);
}
}
private void copyDirectoryContents(String srcDirId, String dstDirId) throws IOException {
final Path srcDirPath = filenameTranslator.getEncryptedDirectoryPath(srcDirId);
final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
Files.createDirectories(dstDirPath);
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(srcDirPath, DIRECTORY_CONTENT_FILTER);
for (final Path srcChildPath : directoryStream) {
final String childName = srcChildPath.getFileName().toString();
final Path dstChildPath = dstDirPath.resolve(childName);
if (StringUtils.endsWithIgnoreCase(childName, FILE_EXT)) {
try {
Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}
} else if (StringUtils.endsWithIgnoreCase(childName, DIR_EXT)) {
final String srcSubdirId = filenameTranslator.getDirectoryId(srcChildPath, false);
final String dstSubdirId = filenameTranslator.getDirectoryId(dstChildPath, true);
copyDirectoryContents(srcSubdirId, dstSubdirId);
}
}
}
@Override
public void spool(OutputContext outputContext) throws IOException {
// do nothing
}
/**
* Deletes a given directory recursively by resolving subdirectories using their directory files.
*/
private void deleteSubDirectory(final EncryptedDir subDir) throws IOException {
final Path subDirPath = subDir.getDirectoryPath();
filenameTranslator.uncacheDirectoryId(subDir.filePath);
Files.delete(subDir.filePath);
final LinkedTransferQueue<Path> queue = new LinkedTransferQueue<>();
queue.put(subDirPath);
Path dir;
while ((dir = queue.poll()) != null) {
if (Files.exists(dir)) {
Files.walkFileTree(dir, new RecursiveDirectoryDeletingVisitor(queue));
}
}
}
/**
* Deletes all files it visits and enqueues subdirectories into a given {@link Queue} for deletion, too.
*
* If its parent directory is empty after deleting, it will get deleted, too.
*/
private class RecursiveDirectoryDeletingVisitor extends SimpleFileVisitor<Path> {
private final Queue<Path> directories;
private RecursiveDirectoryDeletingVisitor(Queue<Path> directories) {
this.directories = directories;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
if (file.toString().endsWith(DIR_EXT)) {
final String directoryId = filenameTranslator.getDirectoryId(file, false);
final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
directories.add(directoryPath);
filenameTranslator.uncacheDirectoryId(file);
}
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
// first check, if we're the only remaining child:
boolean hasSiblings = false;
try (final DirectoryStream<Path> siblings = Files.newDirectoryStream(dir.getParent())) {
for (Path sibling : siblings) {
if (!dir.getFileName().equals(sibling.getFileName())) {
hasSiblings = true;
break;
}
}
}
// delete our current directory:
Files.delete(dir);
// if we have siblings, we still need our parent. Otherwise delete it, too:
if (!hasSiblings) {
Files.delete(dir.getParent());
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
LOG.error("Failed to delete file " + file.toString(), exc);
return FileVisitResult.TERMINATE;
}
}
}

View File

@@ -0,0 +1,154 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.EOFException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.channels.FileChannel;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
protected final CryptoWarningHandler cryptoWarningHandler;
protected final Long contentLength;
public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) {
super(factory, locator, session, lockManager, cryptor, filePath);
if (filePath == null) {
throw new IllegalArgumentException("filePath must not be null");
}
this.cryptoWarningHandler = cryptoWarningHandler;
Long contentLength = null;
if (Files.isRegularFile(filePath)) {
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
contentLength = cryptor.decryptedContentLength(c);
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
if (contentLength > RANGE_REQUEST_LOWER_LIMIT) {
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
}
} catch (OverlappingFileLockException e) {
// file header currently locked, report -1 for unknown size.
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, -1l));
} catch (MacAuthenticationFailedException e) {
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
// don't add content length DAV property
} catch (IOException e) {
LOG.error("Error reading filesize " + filePath.toString(), e);
throw new UncheckedIOException(e);
}
}
this.contentLength = contentLength;
}
public Long getContentLength() {
return contentLength;
}
@Override
public boolean isCollection() {
return false;
}
@Override
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
throw new UnsupportedOperationException("Can not add member to file.");
}
@Override
public DavResourceIterator getMembers() {
throw new UnsupportedOperationException("Can not list members of file.");
}
@Override
public void removeMember(DavResource member) throws DavException {
throw new UnsupportedOperationException("Can not remove member to file.");
}
@Override
public void spool(OutputContext outputContext) throws IOException {
if (Files.isRegularFile(filePath)) {
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
final Long contentLength = cryptor.decryptedContentLength(c);
if (contentLength != null) {
outputContext.setContentLength(contentLength);
}
if (outputContext.hasStream()) {
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
cryptor.decryptFile(c, outputContext.getOutputStream(), authenticate);
outputContext.getOutputStream().flush();
}
} catch (EOFException e) {
LOG.warn("Unexpected end of stream (possibly client hung up).");
}
}
}
@Override
public void move(AbstractEncryptedNode dest) throws DavException, IOException {
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).materializeFilePath();
} else {
dstPath = dest.filePath;
}
try {
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING);
}
}
@Override
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).materializeFilePath();
} else {
dstPath = dest.filePath;
}
try {
Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}
}
}

View File

@@ -0,0 +1,90 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.cryptomator.crypto.Cryptor;
import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Delivers only the requested range of bytes from a file.
*
* @see {@link https://tools.ietf.org/html/rfc7233#section-4}
*/
class EncryptedFilePart extends EncryptedFile {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
private final Pair<Long, Long> range;
public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, LockManager lockManager, Cryptor cryptor,
CryptoWarningHandler cryptoWarningHandler, Path filePath) {
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
try {
final Long lower = requestRange.getLeft().isEmpty() ? null : Long.valueOf(requestRange.getLeft());
final Long upper = requestRange.getRight().isEmpty() ? null : Long.valueOf(requestRange.getRight());
if (lower == null) {
range = new ImmutablePair<Long, Long>(contentLength - upper, contentLength - 1);
} else if (upper == null) {
range = new ImmutablePair<Long, Long>(lower, contentLength - 1);
} else {
range = new ImmutablePair<Long, Long>(lower, Math.min(upper, contentLength - 1));
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid byte range: " + requestRange, e);
}
}
@Override
public void spool(OutputContext outputContext) throws IOException {
assert Files.isRegularFile(filePath);
assert contentLength != null;
final Long rangeLength = range.getRight() - range.getLeft() + 1;
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
if (rangeLength <= 0 || range.getLeft() > contentLength - 1) {
// unsatisfiable content range:
outputContext.setContentLength(0);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength);
LOG.debug("Requested content range unsatisfiable: " + getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
return;
} else {
outputContext.setContentLength(rangeLength);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
}
assert range.getLeft() > 0;
assert range.getLeft() < contentLength;
assert range.getRight() < contentLength;
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ)) {
if (outputContext.hasStream()) {
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
cryptor.decryptRange(c, outputContext.getOutputStream(), range.getLeft(), rangeLength, authenticate);
outputContext.getOutputStream().flush();
}
} catch (EOFException e) {
if (LOG.isDebugEnabled()) {
LOG.trace("Unexpected end of stream during delivery of partial content (client hung up).");
}
}
}
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
return String.format("bytes %d-%d/%d", firstByte, lastByte, completeLength);
}
}

View File

@@ -0,0 +1,108 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
interface FileConstants {
/**
* Number of bytes in the file header.
*/
long FILE_HEADER_LENGTH = 104;
/**
* Allow range requests for files > 32MiB.
*/
long RANGE_REQUEST_LOWER_LIMIT = 32 * 1024 * 1024;
/**
* Maximum path length on some file systems or cloud storage providers is restricted.<br/>
* Parent folder path uses up to 58 chars (sha256 -&gt; 32 bytes base32 encoded to 56 bytes + two slashes). That in mind we don't want the total path to be longer than 255 chars.<br/>
* 128 chars would be enought for up to 80 plaintext chars. Also we need up to 9 chars for our file extension. So lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
*/
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 137;
/**
* Dummy file, on which file attributes can be stored for the root directory.
*/
String ROOT_FILE = "root";
/**
* For encrypted directory names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String DIR_EXT = ".dir";
/**
* For encrypted direcotry names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String LONG_DIR_EXT = ".lng.dir";
/**
* For encrypted file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String FILE_EXT = ".file";
/**
* For encrypted file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String LONG_FILE_EXT = ".lng.file";
/**
* Length of prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
*/
int LONG_NAME_PREFIX_LENGTH = 8;
/**
* Matches valid encrypted filenames (both normal and long filenames - see {@link #ENCRYPTED_FILENAME_LENGTH_LIMIT}).
*/
PathMatcher ENCRYPTED_FILE_MATCHER = new PathMatcher() {
private final Pattern BASIC_NAME_PATTERN = Pattern.compile("^[a-z2-7]+=*$", Pattern.CASE_INSENSITIVE);
private final Pattern LONG_NAME_PATTERN = Pattern.compile("^[a-z2-7]{8}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE);
@Override
public boolean matches(Path path) {
final String filename = path.getFileName().toString();
if (StringUtils.endsWithIgnoreCase(filename, LONG_FILE_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_FILE_EXT);
return LONG_NAME_PATTERN.matcher(basename).matches();
} else if (StringUtils.endsWithIgnoreCase(filename, FILE_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, FILE_EXT);
return BASIC_NAME_PATTERN.matcher(basename).matches();
} else if (StringUtils.endsWithIgnoreCase(filename, LONG_DIR_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_DIR_EXT);
return LONG_NAME_PATTERN.matcher(basename).matches();
} else if (StringUtils.endsWithIgnoreCase(filename, DIR_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, DIR_EXT);
return BASIC_NAME_PATTERN.matcher(basename).matches();
} else {
return false;
}
}
};
/**
* Filter to determine files of interest in encrypted directory. Based on {@link #ENCRYPTED_FILE_MATCHER}.
*/
Filter<Path> DIRECTORY_CONTENT_FILTER = new Filter<Path>() {
@Override
public boolean accept(Path entry) throws IOException {
return ENCRYPTED_FILE_MATCHER.matches(entry);
}
};
}

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.nio.file.attribute.FileTime;
import java.time.Instant;

View File

@@ -0,0 +1,234 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileTime;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.collections4.map.LRUMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
class FilenameTranslator implements FileConstants {
private static final int MAX_CACHED_DIRECTORY_IDS = 5000;
private static final int MAX_CACHED_METADATA_FILES = 1000;
private final Cryptor cryptor;
private final Path dataRoot;
private final Path metadataRoot;
private final ObjectMapper objectMapper = new ObjectMapper();
private final Map<Pair<Path, FileTime>, String> directoryIdCache = new LRUMap<>(MAX_CACHED_DIRECTORY_IDS); // <directoryFile, directoryId>
private final Map<Pair<Path, FileTime>, LongFilenameMetadata> metadataCache = new LRUMap<>(MAX_CACHED_METADATA_FILES); // <metadataFile, metadata>
public FilenameTranslator(Cryptor cryptor, Path vaultRoot) {
this.cryptor = cryptor;
this.dataRoot = vaultRoot.resolve("d");
this.metadataRoot = vaultRoot.resolve("m");
}
/* file and directory name en/decryption */
public String getDirectoryId(Path directoryFile, boolean createIfNonexisting) throws IOException {
try {
final Pair<Path, FileTime> key = ImmutablePair.of(directoryFile, Files.getLastModifiedTime(directoryFile));
String directoryId = directoryIdCache.get(key);
if (directoryId == null) {
directoryId = new String(readAllBytesAtomically(directoryFile), StandardCharsets.UTF_8);
directoryIdCache.put(key, directoryId);
}
return directoryId;
} catch (FileNotFoundException | NoSuchFileException e) {
if (createIfNonexisting) {
final String directoryId = UUID.randomUUID().toString();
writeAllBytesAtomically(directoryFile, directoryId.getBytes(StandardCharsets.UTF_8));
final Pair<Path, FileTime> key = ImmutablePair.of(directoryFile, Files.getLastModifiedTime(directoryFile));
directoryIdCache.put(key, directoryId);
return directoryId;
} else {
return null;
}
}
}
/**
* to be called when a directory gets deleted, so the corresponding directory id is not longer cached.
*/
public void uncacheDirectoryId(Path directoryFile) throws IOException {
final Pair<Path, FileTime> key = ImmutablePair.of(directoryFile, Files.getLastModifiedTime(directoryFile));
directoryIdCache.remove(key);
}
public Path getEncryptedDirectoryPath(String directoryId) {
final String encrypted = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
return dataRoot.resolve(encrypted);
}
public String getEncryptedFilename(String cleartextFilename) throws IOException {
return getEncryptedFilename(cleartextFilename, FILE_EXT, LONG_FILE_EXT);
}
public String getEncryptedDirFileName(String cleartextDirName) throws IOException {
return getEncryptedFilename(cleartextDirName, DIR_EXT, LONG_DIR_EXT);
}
/**
* Encryption will blow up the filename length due to aes block sizes, IVs and base32 encoding. The result may be too long for some old file systems.<br/>
* This means that we need a workaround for filenames longer than the limit defined in {@link FileConstants#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
* <br/>
* For filenames longer than this limit we use a metadata file containing the full encrypted paths. For the actual filename a unique alternative is created by concatenating the metadata filename
* and a unique id.
*/
private String getEncryptedFilename(String cleartextFilename, String basicExt, String longExt) throws IOException {
final String ivAndCiphertext = cryptor.encryptFilename(cleartextFilename);
if (ivAndCiphertext.length() + basicExt.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
final String metadataGroup = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
final String longFilename = metadataGroup + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + longExt;
this.writeMetadata(metadataGroup, metadata);
return longFilename;
} else {
return ivAndCiphertext + basicExt;
}
}
public String getCleartextFilename(String encryptedFilename) throws DecryptFailedException, IOException {
final String ciphertext;
if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_FILE_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_FILE_EXT);
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, FILE_EXT)) {
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, FILE_EXT);
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_DIR_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_DIR_EXT);
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, DIR_EXT)) {
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, DIR_EXT);
} else {
throw new IllegalArgumentException("Unsupported path component: " + encryptedFilename);
}
return cryptor.decryptFilename(ciphertext);
}
/* Locked I/O */
private void writeAllBytesAtomically(Path path, byte[] bytes) throws IOException {
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
c.write(ByteBuffer.wrap(bytes));
}
}
private byte[] readAllBytesAtomically(Path path) throws IOException {
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.DSYNC); final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
c.read(buffer);
return buffer.array();
}
}
/* Long name metadata files */
private void writeMetadata(String metadataGroup, LongFilenameMetadata metadata) throws IOException {
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
Files.createDirectories(metadataDir);
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
// evict previously cached entries:
try {
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
metadataCache.remove(key);
} catch (FileNotFoundException | NoSuchFileException e) {
// didn't exist yet? then we don't need to do anything anyway.
}
// write:
final byte[] metadataContent = objectMapper.writeValueAsBytes(metadata);
writeAllBytesAtomically(metadataFile, metadataContent);
// add to cache:
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
metadataCache.put(key, metadata);
}
private LongFilenameMetadata readMetadata(String metadataGroup) throws IOException {
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
try {
// use cached metadata, if possible:
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
LongFilenameMetadata metadata = metadataCache.get(key);
// else read from filesystem:
if (metadata == null) {
final byte[] metadataContent = readAllBytesAtomically(metadataFile);
metadata = objectMapper.readValue(metadataContent, LongFilenameMetadata.class);
metadataCache.put(key, metadata);
}
return metadata;
} catch (FileNotFoundException | NoSuchFileException e) {
// not yet existing:
return new LongFilenameMetadata();
}
}
private static class LongFilenameMetadata implements Serializable {
private static final long serialVersionUID = 6214509403824421320L;
@JsonDeserialize(as = DualHashBidiMap.class)
private BidiMap<UUID, String> encryptedFilenames = new DualHashBidiMap<>();
/* Getter/Setter */
public synchronized String getEncryptedFilenameForUUID(final UUID uuid) {
return encryptedFilenames.get(uuid);
}
public synchronized UUID getOrCreateUuidForEncryptedFilename(String encryptedFilename) {
UUID uuid = encryptedFilenames.getKey(encryptedFilename);
if (uuid == null) {
uuid = UUID.randomUUID();
encryptedFilenames.put(uuid, encryptedFilename);
}
return uuid;
}
// used by jackson
@SuppressWarnings("unused")
public BidiMap<UUID, String> getEncryptedFilenames() {
return encryptedFilenames;
}
// used by jackson
@SuppressWarnings("unused")
public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
this.encryptedFilenames = encryptedFilenames;
}
}
}

View File

@@ -1,4 +1,4 @@
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import org.apache.jackrabbit.webdav.property.AbstractDavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;

View File

@@ -6,25 +6,27 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.file.Path;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.jackrabbit.CryptoResourceFactory.NonExistingParentException;
public class NonExistingNode extends AbstractEncryptedNode {
class NonExistingNode extends AbstractEncryptedNode {
public NonExistingNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor, null);
}
@Override
@@ -34,7 +36,12 @@ public class NonExistingNode extends AbstractEncryptedNode {
@Override
public boolean isCollection() {
throw new UnsupportedOperationException("Resource doesn't exist.");
return false;
}
@Override
public long getModificationTime() {
return -1;
}
@Override
@@ -58,8 +65,40 @@ public class NonExistingNode extends AbstractEncryptedNode {
}
@Override
protected void determineProperties() {
// do nothing.
public void move(AbstractEncryptedNode destination) throws DavException {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
@Override
public void copy(AbstractEncryptedNode destination, boolean shallow) throws DavException {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
@Override
public void setProperty(DavProperty<?> property) throws DavException {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
/**
* @return lazily resolved file path, e.g. needed during MOVE operations.
*/
public Path materializeFilePath() {
try {
return factory.getEncryptedFilePath(locator.getResourcePath(), true);
} catch (NonExistingParentException e) {
throw new IllegalStateException(e);
}
}
/**
* @return lazily resolved directory file path, e.g. needed during MOVE operations.
*/
public Path materializeDirFilePath() {
try {
return factory.getEncryptedDirectoryFilePath(locator.getResourcePath(), true);
} catch (NonExistingParentException e) {
throw new IllegalStateException(e);
}
}
}

View File

@@ -0,0 +1,56 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.OverlappingFileLockException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Instances of this class wrap a file lock, that is created upon construction and destroyed by {@link #close()}.
*
* If the construction fails (e.g. if the file system does not support locks) no exception will be thrown and no lock is created.
*/
class SilentlyFailingFileLock implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(SilentlyFailingFileLock.class);
private final FileLock lock;
/**
* Invokes #SilentlyFailingFileLock(FileChannel, long, long, boolean) with a position of 0 and a size of {@link Long#MAX_VALUE}.
*/
SilentlyFailingFileLock(FileChannel channel, boolean shared) {
this(channel, 0L, Long.MAX_VALUE, shared);
}
/**
* @throws NonReadableChannelException If shared is true this channel was not opened for reading
* @throws NonWritableChannelException If shared is false but this channel was not opened for writing
* @see FileChannel#lock(long, long, boolean)
*/
SilentlyFailingFileLock(FileChannel channel, long position, long size, boolean shared) {
FileLock lock = null;
try {
lock = channel.tryLock(position, size, shared);
} catch (IOException | OverlappingFileLockException e) {
if (LOG.isDebugEnabled()) {
LOG.trace("Unable to lock file.");
}
} finally {
this.lock = lock;
}
}
@Override
public void close() throws IOException {
if (lock != null) {
lock.close();
}
}
}

View File

@@ -8,45 +8,50 @@
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavLocatorFactory;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavSessionProvider;
import org.apache.jackrabbit.webdav.WebdavRequest;
import org.apache.jackrabbit.webdav.WebdavResponse;
import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebDavServlet extends AbstractWebdavServlet {
private static final long serialVersionUID = 7965170007048673022L;
private static final Logger LOG = LoggerFactory.getLogger(WebDavServlet.class);
public static final String CFG_FS_ROOT = "cfg.fs.root";
public static final String CFG_HTTP_ROOT = "cfg.http.root";
public static final String CFG_CHECK_FILE_INTEGRITY = "cfg.checkFileIntegrity";
private DavSessionProvider davSessionProvider;
private DavLocatorFactory davLocatorFactory;
private DavResourceFactory davResourceFactory;
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
public WebDavServlet(final Cryptor cryptor) {
public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
super();
this.cryptor = cryptor;
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection, whitelistedResourceCollection);
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
davSessionProvider = new DavSessionProviderImpl();
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
final String httpRoot = config.getInitParameter(CFG_HTTP_ROOT);
final boolean checkFileIntegrity = Boolean.parseBoolean(config.getInitParameter(CFG_CHECK_FILE_INTEGRITY));
this.davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, httpRoot, cryptor);
this.davResourceFactory = new DavResourceFactoryImpl(cryptor, checkFileIntegrity);
davSessionProvider = new DavSessionProviderImpl();
davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath());
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, fsRoot);
}
@Override
@@ -84,4 +89,30 @@ public class WebDavServlet extends AbstractWebdavServlet {
this.davResourceFactory = resourceFactory;
}
@Override
protected void doPut(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
long t0 = System.nanoTime();
super.doPut(request, response, resource);
if (LOG.isDebugEnabled()) {
long t1 = System.nanoTime();
LOG.trace("PUT TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
}
}
@Override
protected void doGet(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
long t0 = System.nanoTime();
try {
super.doGet(request, response, resource);
} catch (MacAuthenticationFailedException e) {
LOG.warn("File integrity violation for " + resource.getLocator().getResourcePath());
cryptoWarningHandler.macAuthFailed(resource.getLocator().getResourcePath());
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
if (LOG.isDebugEnabled()) {
long t1 = System.nanoTime();
LOG.trace("GET TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
}
}
}

View File

@@ -1,178 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.ResourceType;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.exceptions.DavRuntimeException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EncryptedDir extends AbstractEncryptedNode {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
public EncryptedDir(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
}
@Override
public boolean isCollection() {
return true;
}
@Override
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
if (resource.isCollection()) {
this.addMemberDir(resource, inputContext);
} else {
this.addMemberFile(resource, inputContext);
}
}
private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException {
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
try {
Files.createDirectories(childPath);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (IOException e) {
LOG.error("Failed to create subdirectory.", e);
throw new IORuntimeException(e);
}
}
private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException {
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
cryptor.encryptFile(inputContext.getInputStream(), channel);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (IOException e) {
LOG.error("Failed to create file.", e);
throw new IORuntimeException(e);
} finally {
IOUtils.closeQuietly(channel);
IOUtils.closeQuietly(inputContext.getInputStream());
}
}
@Override
public DavResourceIterator getMembers() {
final Path dir = ResourcePathUtils.getPhysicalPath(this);
try {
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
final List<DavResource> result = new ArrayList<>();
for (final Path childPath : directoryStream) {
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), childPath.toString(), false);
final DavResource resource = factory.createResource(childLocator, session);
result.add(resource);
}
return new DavResourceIteratorImpl(result);
} catch (IOException e) {
LOG.error("Exception during getMembers.", e);
throw new IORuntimeException(e);
} catch (DavException e) {
LOG.error("Exception during getMembers.", e);
throw new DavRuntimeException(e);
}
}
@Override
public void removeMember(DavResource member) throws DavException {
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
try {
Files.walkFileTree(memberPath, new DeletingFileVisitor());
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
@Override
public void spool(OutputContext outputContext) throws IOException {
// do nothing
}
@Override
protected void determineProperties() {
final Path path = ResourcePathUtils.getPhysicalPath(this);
properties.add(new ResourceType(ResourceType.COLLECTION));
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
if (Files.exists(path)) {
try {
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
// don't add any further properties
}
}
}
/**
* Deletes all files and folders, it visits.
*/
private static class DeletingFileVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
if (attributes.isRegularFile()) {
Files.delete(file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
LOG.error("Failed to delete file " + file.toString(), exc);
return FileVisitResult.TERMINATE;
}
}
}

View File

@@ -1,119 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EncryptedFile extends AbstractEncryptedNode {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
protected final boolean checkIntegrity;
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, boolean checkIntegrity) {
super(factory, locator, session, lockManager, cryptor);
this.checkIntegrity = checkIntegrity;
}
@Override
public boolean isCollection() {
return false;
}
@Override
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
throw new UnsupportedOperationException("Can not add member to file.");
}
@Override
public DavResourceIterator getMembers() {
throw new UnsupportedOperationException("Can not list members of file.");
}
@Override
public void removeMember(DavResource member) throws DavException {
throw new UnsupportedOperationException("Can not remove member to file.");
}
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
if (checkIntegrity && !cryptor.authenticateContent(channel)) {
throw new DecryptFailedException("File content compromised: " + path.toString());
}
outputContext.setContentLength(cryptor.decryptedContentLength(channel));
if (outputContext.hasStream()) {
cryptor.decryptedFile(channel, outputContext.getOutputStream());
}
} catch (EOFException e) {
LOG.warn("Unexpected end of stream (possibly client hung up).");
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
} finally {
IOUtils.closeQuietly(channel);
}
}
}
@Override
protected void determineProperties() {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
final Long contentLength = cryptor.decryptedContentLength(channel);
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
throw new IORuntimeException(e);
} finally {
IOUtils.closeQuietly(channel);
}
}
}
}

View File

@@ -1,146 +0,0 @@
package org.cryptomator.webdav.jackrabbit.resources;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletRequest;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Delivers only the requested range of bytes from a file.
*
* @see {@link https://tools.ietf.org/html/rfc7233#section-4}
*/
public class EncryptedFilePart extends EncryptedFile {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
private static final String BYTE_UNIT_PREFIX = "bytes=";
private static final char RANGE_SET_SEP = ',';
private static final char RANGE_SEP = '-';
/**
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
*/
private static final Long SUFFIX_BYTE_RANGE_LOWER = -1L;
/**
* e.g. range 500- (gets all bytes from 500) -> (500, MAX_LONG)
*/
private static final Long SUFFIX_BYTE_RANGE_UPPER = Long.MAX_VALUE;
private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, boolean checkIntegrity) {
super(factory, locator, session, lockManager, cryptor, checkIntegrity);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (rangeHeader == null) {
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
}
determineByteRanges(rangeHeader);
}
private void determineByteRanges(String rangeHeader) {
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, BYTE_UNIT_PREFIX);
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
if (byteRanges.length == 0) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
for (final String byteRange : byteRanges) {
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
final Long lower = bytePos[0].isEmpty() ? SUFFIX_BYTE_RANGE_LOWER : Long.valueOf(bytePos[0]);
final Long upper = bytePos[1].isEmpty() ? SUFFIX_BYTE_RANGE_UPPER : Long.valueOf(bytePos[1]);
if (lower > upper) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
requestedContentRanges.add(new ImmutablePair<Long, Long>(lower, upper));
}
}
/**
* @return One range, that spans all requested ranges.
*/
private Pair<Long, Long> getUnionRange(Long fileSize) {
final long lastByte = fileSize - 1;
final MutablePair<Long, Long> result = new MutablePair<Long, Long>();
for (Pair<Long, Long> range : requestedContentRanges) {
final long left;
final long right;
if (SUFFIX_BYTE_RANGE_LOWER.equals(range.getLeft())) {
left = lastByte - range.getRight();
right = lastByte;
} else if (SUFFIX_BYTE_RANGE_UPPER.equals(range.getRight())) {
left = range.getLeft();
right = lastByte;
} else {
left = range.getLeft();
right = range.getRight();
}
if (result.getLeft() == null || left < result.getLeft()) {
result.setLeft(left);
}
if (result.getRight() == null || right > result.getRight()) {
result.setRight(right);
}
}
return result;
}
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
if (checkIntegrity && !cryptor.authenticateContent(channel)) {
throw new DecryptFailedException("File content compromised: " + path.toString());
}
final Long fileSize = cryptor.decryptedContentLength(channel);
final Pair<Long, Long> range = getUnionRange(fileSize);
final Long rangeLength = range.getRight() - range.getLeft() + 1;
outputContext.setContentLength(rangeLength);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
if (outputContext.hasStream()) {
cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
}
} catch (EOFException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
}
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
} finally {
IOUtils.closeQuietly(channel);
}
}
}
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
}
}

View File

@@ -1,31 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceLocator;
public final class ResourcePathUtils {
private ResourcePathUtils() {
throw new IllegalStateException("not instantiable");
}
public static Path getPhysicalPath(DavResource resource) {
return getPhysicalPath(resource.getLocator());
}
public static Path getPhysicalPath(DavResourceLocator locator) {
return FileSystems.getDefault().getPath(locator.getRepositoryPath());
}
}

View File

@@ -0,0 +1,271 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.webdav.WebDavServer;
import org.cryptomator.webdav.WebDavServer.ServletLifeCycleAdapter;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.io.Files;
public class RangeRequestTest {
private static final Logger LOG = LoggerFactory.getLogger(RangeRequestTest.class);
private static final Aes256Cryptor CRYPTOR = new Aes256Cryptor();
private static final WebDavServer SERVER = new WebDavServer();
private static final File TMP_VAULT = Files.createTempDir();
private static ServletLifeCycleAdapter SERVLET;
private static URI VAULT_BASE_URI;
@BeforeClass
public static void startServer() throws URISyntaxException {
SERVER.start();
SERVLET = SERVER.createServlet(TMP_VAULT.toPath(), CRYPTOR, new ArrayList<String>(), new ArrayList<String>(), "JUnitTestVault");
SERVLET.start();
VAULT_BASE_URI = new URI("http", SERVLET.getServletUri().getSchemeSpecificPart() + "/", null);
Assert.assertTrue(SERVLET.isRunning());
Assert.assertNotNull(VAULT_BASE_URI);
}
@AfterClass
public static void stopServer() {
SERVLET.stop();
SERVER.stop();
FileUtils.deleteQuietly(TMP_VAULT);
}
@Test
public void testFullFileDecryption() throws IOException, URISyntaxException {
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "fullFileDecryptionTestFile.txt");
final HttpClient client = new HttpClient();
// prepare 64MiB test data:
final byte[] plaintextData = new byte[16777216 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 16777216; i++) {
bbIn.putInt(i);
}
final InputStream plaintextDataInputStream = new ByteArrayInputStream(plaintextData);
// put request:
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
putMethod.setRequestEntity(new ByteArrayRequestEntity(plaintextData));
final int putResponse = client.executeMethod(putMethod);
putMethod.releaseConnection();
Assert.assertEquals(201, putResponse);
// get request:
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
final int statusCode = client.executeMethod(getMethod);
Assert.assertEquals(200, statusCode);
// final byte[] received = new byte[plaintextData.length];
// IOUtils.read(getMethod.getResponseBodyAsStream(), received);
// Assert.assertArrayEquals(plaintextData, received);
Assert.assertTrue(IOUtils.contentEquals(plaintextDataInputStream, getMethod.getResponseBodyAsStream()));
getMethod.releaseConnection();
}
@Test
public void testAsyncRangeRequests() throws IOException, URISyntaxException, InterruptedException {
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "asyncRangeRequestTestFile.txt");
final MultiThreadedHttpConnectionManager cm = new MultiThreadedHttpConnectionManager();
cm.getParams().setDefaultMaxConnectionsPerHost(50);
final HttpClient client = new HttpClient(cm);
// prepare 8MiB test data:
final byte[] plaintextData = new byte[2097152 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 2097152; i++) {
bbIn.putInt(i);
}
// put request:
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
putMethod.setRequestEntity(new ByteArrayRequestEntity(plaintextData));
final int putResponse = client.executeMethod(putMethod);
putMethod.releaseConnection();
Assert.assertEquals(201, putResponse);
// multiple async range requests:
final List<ForkJoinTask<?>> tasks = new ArrayList<>();
final Random generator = new Random(System.currentTimeMillis());
final AtomicBoolean success = new AtomicBoolean(true);
// 10 full interrupted requests:
for (int i = 0; i < 10; i++) {
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
final int statusCode = client.executeMethod(getMethod);
if (statusCode != 200) {
LOG.error("Invalid status code for interrupted full request");
success.set(false);
}
getMethod.getResponseBodyAsStream().read();
getMethod.getResponseBodyAsStream().close();
getMethod.releaseConnection();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
// 50 crappy interrupted range requests:
for (int i = 0; i < 50; i++) {
final int lower = generator.nextInt(plaintextData.length);
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "bytes=" + lower + "-");
final int statusCode = client.executeMethod(getMethod);
if (statusCode != 206) {
LOG.error("Invalid status code for interrupted range request");
success.set(false);
}
getMethod.getResponseBodyAsStream().read();
getMethod.getResponseBodyAsStream().close();
getMethod.releaseConnection();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
// 50 normal open range requests:
for (int i = 0; i < 50; i++) {
final int lower = generator.nextInt(plaintextData.length - 512);
final int upper = plaintextData.length - 1;
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "bytes=" + lower + "-");
final byte[] expected = Arrays.copyOfRange(plaintextData, lower, upper + 1);
final int statusCode = client.executeMethod(getMethod);
final byte[] responseBody = new byte[upper - lower + 10];
final int bytesRead = IOUtils.read(getMethod.getResponseBodyAsStream(), responseBody);
getMethod.releaseConnection();
if (statusCode != 206) {
LOG.error("Invalid status code for open range request");
success.set(false);
} else if (upper - lower + 1 != bytesRead) {
LOG.error("Invalid response length for open range request");
success.set(false);
} else if (!Arrays.equals(expected, Arrays.copyOfRange(responseBody, 0, bytesRead))) {
LOG.error("Invalid response body for open range request");
success.set(false);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
// 200 normal closed range requests:
for (int i = 0; i < 200; i++) {
final int pos1 = generator.nextInt(plaintextData.length - 512);
final int pos2 = pos1 + 512;
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final int lower = Math.min(pos1, pos2);
final int upper = Math.max(pos1, pos2);
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "bytes=" + lower + "-" + upper);
final byte[] expected = Arrays.copyOfRange(plaintextData, lower, upper + 1);
final int statusCode = client.executeMethod(getMethod);
final byte[] responseBody = new byte[upper - lower + 1];
final int bytesRead = IOUtils.read(getMethod.getResponseBodyAsStream(), responseBody);
getMethod.releaseConnection();
if (statusCode != 206) {
LOG.error("Invalid status code for closed range request");
success.set(false);
} else if (upper - lower + 1 != bytesRead) {
LOG.error("Invalid response length for closed range request");
success.set(false);
} else if (!Arrays.equals(expected, Arrays.copyOfRange(responseBody, 0, bytesRead))) {
LOG.error("Invalid response body for closed range request");
success.set(false);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
Collections.shuffle(tasks, generator);
final ForkJoinPool pool = new ForkJoinPool(4);
for (ForkJoinTask<?> task : tasks) {
pool.execute(task);
}
for (ForkJoinTask<?> task : tasks) {
task.join();
}
pool.shutdown();
cm.shutdown();
Assert.assertTrue(success.get());
}
@Test
public void testUnsatisfiableRangeRequest() throws IOException, URISyntaxException {
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "unsatisfiableRangeRequestTestFile.txt");
final HttpClient client = new HttpClient();
// prepare file content:
final byte[] fileContent = "This is some test file content.".getBytes();
// put request:
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
putMethod.setRequestEntity(new ByteArrayRequestEntity(fileContent));
final int putResponse = client.executeMethod(putMethod);
putMethod.releaseConnection();
Assert.assertEquals(201, putResponse);
// get request:
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "chunks=1-2");
final int getResponse = client.executeMethod(getMethod);
final byte[] response = new byte[fileContent.length];
IOUtils.read(getMethod.getResponseBodyAsStream(), response);
getMethod.releaseConnection();
Assert.assertEquals(416, getResponse);
Assert.assertArrayEquals(fileContent, response);
}
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (c) 2014 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - log4j config for WebDAV unit tests
-->
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
</Appenders>
<Loggers>
<!-- show our own debug messages: -->
<Logger name="org.cryptomator" level="DEBUG" />
<!-- mute dependencies: -->
<Root level="INFO">
<AppenderRef ref="Console" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.4.0</version>
<version>0.10.0</version>
</parent>
<artifactId>crypto-aes</artifactId>
<name>Cryptomator cryptographic module (AES)</name>

View File

@@ -8,6 +8,9 @@
******************************************************************************/
package org.cryptomator.crypto.aes256;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
interface AesCryptographicConfiguration {
/**
@@ -26,7 +29,7 @@ interface AesCryptographicConfiguration {
int SCRYPT_BLOCK_SIZE = 8;
/**
* Number of bytes of the master key. Should be the maximum possible AES key length to provide best security.
* Preferred number of bytes of the master key.
*/
int PREF_MASTER_KEY_LENGTH_IN_BITS = 256;
@@ -60,18 +63,18 @@ interface AesCryptographicConfiguration {
String AES_KEYWRAP_CIPHER = "AESWrap";
/**
* Cipher specs for file name and file content encryption. Using CTR-mode for random access.
* Cipher specs for file content encryption. Using CTR-mode for random access.<br/>
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
*/
String AES_CTR_CIPHER = "AES/CTR/NoPadding";
/**
* Cipher specs for single block encryption (like file size).
* Cipher specs for file header encryption (fixed-length block cipher).<br/>
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl
*/
String AES_ECB_CIPHER = "AES/ECB/PKCS5Padding";
String AES_CBC_CIPHER = "AES/CBC/PKCS5Padding";
/**
* AES block size is 128 bit or 16 bytes.
@@ -79,10 +82,13 @@ interface AesCryptographicConfiguration {
int AES_BLOCK_LENGTH = 16;
/**
* Number of non-zero bytes in the IV used for file name encryption. Less means shorter encrypted filenames, more means higher entropy.
* Maximum length is {@value #AES_BLOCK_LENGTH}. Even the shortest base32 (see {@link FileNamingConventions#ENCRYPTED_FILENAME_CODEC})
* encoded byte array will need 8 chars. The maximum number of bytes that fit in 8 base32 chars is 5. Thus 5 is the ideal length.
* Number of bytes, a content block over which a MAC is calculated consists of.
*/
int FILE_NAME_IV_LENGTH = 5;
int CONTENT_MAC_BLOCK = 32 * 1024;
/**
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
*/
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
}

View File

@@ -0,0 +1,230 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto.aes256;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.util.Arrays;
import javax.crypto.SecretKey;
import org.apache.commons.lang3.ArrayUtils;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.Mac;
import org.bouncycastle.crypto.engines.AESFastEngine;
import org.bouncycastle.crypto.macs.CMac;
import org.bouncycastle.crypto.paddings.ISO7816d4Padding;
import org.bouncycastle.crypto.params.KeyParameter;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
/**
* Implements the RFC 5297 SIV mode.
*/
final class AesSivCipherUtil {
private static final byte[] BYTES_ZERO = new byte[16];
private static final byte DOUBLING_CONST = (byte) 0x87;
static byte[] sivEncrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) {
final byte[] aesKeyBytes = aesKey.getEncoded();
final byte[] macKeyBytes = macKey.getEncoded();
if (aesKeyBytes == null || macKeyBytes == null) {
throw new IllegalArgumentException("Can't get bytes of given key.");
}
try {
return sivEncrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException(ex);
} finally {
Arrays.fill(aesKeyBytes, (byte) 0);
Arrays.fill(macKeyBytes, (byte) 0);
}
}
static byte[] sivEncrypt(byte[] aesKey, byte[] macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException {
if (aesKey.length != 16 && aesKey.length != 24 && aesKey.length != 32) {
throw new InvalidKeyException("Invalid aesKey length " + aesKey.length);
}
final byte[] iv = s2v(macKey, plaintext, additionalData);
final int numBlocks = (plaintext.length + 15) / 16;
// clear out the 31st and 63rd (rightmost) bit:
final byte[] ctr = Arrays.copyOf(iv, 16);
ctr[8] = (byte) (ctr[8] & 0x7F);
ctr[12] = (byte) (ctr[12] & 0x7F);
final ByteBuffer ctrBuf = ByteBuffer.wrap(ctr);
final long initialCtrVal = ctrBuf.getLong(8);
final byte[] x = new byte[numBlocks * 16];
final BlockCipher aes = new AESFastEngine();
aes.init(true, new KeyParameter(aesKey));
for (int i = 0; i < numBlocks; i++) {
final long ctrVal = initialCtrVal + i;
ctrBuf.putLong(8, ctrVal);
aes.processBlock(ctrBuf.array(), 0, x, i * 16);
aes.reset();
}
final byte[] ciphertext = xor(plaintext, x);
return ArrayUtils.addAll(iv, ciphertext);
}
static byte[] sivDecrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws DecryptFailedException {
final byte[] aesKeyBytes = aesKey.getEncoded();
final byte[] macKeyBytes = macKey.getEncoded();
if (aesKeyBytes == null || macKeyBytes == null) {
throw new IllegalArgumentException("Can't get bytes of given key.");
}
try {
return sivDecrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException(ex);
} finally {
Arrays.fill(aesKeyBytes, (byte) 0);
Arrays.fill(macKeyBytes, (byte) 0);
}
}
static byte[] sivDecrypt(byte[] aesKey, byte[] macKey, byte[] ciphertext, byte[]... additionalData) throws DecryptFailedException, InvalidKeyException {
if (aesKey.length != 16 && aesKey.length != 24 && aesKey.length != 32) {
throw new InvalidKeyException("Invalid aesKey length " + aesKey.length);
}
final byte[] iv = Arrays.copyOf(ciphertext, 16);
final byte[] actualCiphertext = Arrays.copyOfRange(ciphertext, 16, ciphertext.length);
final int numBlocks = (actualCiphertext.length + 15) / 16;
// clear out the 31st and 63rd (rightmost) bit:
final byte[] ctr = Arrays.copyOf(iv, 16);
ctr[8] = (byte) (ctr[8] & 0x7F);
ctr[12] = (byte) (ctr[12] & 0x7F);
final ByteBuffer ctrBuf = ByteBuffer.wrap(ctr);
final long initialCtrVal = ctrBuf.getLong(8);
final byte[] x = new byte[numBlocks * 16];
final BlockCipher aes = new AESFastEngine();
aes.init(true, new KeyParameter(aesKey));
for (int i = 0; i < numBlocks; i++) {
final long ctrVal = initialCtrVal + i;
ctrBuf.putLong(8, ctrVal);
aes.processBlock(ctrBuf.array(), 0, x, i * 16);
aes.reset();
}
final byte[] plaintext = xor(actualCiphertext, x);
final byte[] control = s2v(macKey, plaintext, additionalData);
if (MessageDigest.isEqual(control, iv)) {
return plaintext;
} else {
throw new DecryptFailedException("Authentication failed");
}
}
static byte[] s2v(byte[] macKey, byte[] plaintext, byte[]... additionalData) {
final CipherParameters params = new KeyParameter(macKey);
final BlockCipher aes = new AESFastEngine();
final CMac mac = new CMac(aes);
mac.init(params);
byte[] d = mac(mac, BYTES_ZERO);
for (byte[] s : additionalData) {
d = xor(dbl(d), mac(mac, s));
}
final byte[] t;
if (plaintext.length >= 16) {
t = xorend(plaintext, d);
} else {
t = xor(dbl(d), pad(plaintext));
}
return mac(mac, t);
}
private static byte[] mac(Mac mac, byte[] in) {
byte[] result = new byte[mac.getMacSize()];
mac.update(in, 0, in.length);
mac.doFinal(result, 0);
return result;
}
/**
* First bit 1, following bits 0.
*/
private static byte[] pad(byte[] in) {
final byte[] result = Arrays.copyOf(in, 16);
new ISO7816d4Padding().addPadding(result, in.length);
return result;
}
/**
* Code taken from {@link org.bouncycastle.crypto.macs.CMac}
*/
private static int shiftLeft(byte[] block, byte[] output) {
int i = block.length;
int bit = 0;
while (--i >= 0) {
int b = block[i] & 0xff;
output[i] = (byte) ((b << 1) | bit);
bit = (b >>> 7) & 1;
}
return bit;
}
/**
* Code taken from {@link org.bouncycastle.crypto.macs.CMac}
*/
private static byte[] dbl(byte[] in) {
byte[] ret = new byte[in.length];
int carry = shiftLeft(in, ret);
int xor = 0xff & DOUBLING_CONST;
/*
* NOTE: This construction is an attempt at a constant-time implementation.
*/
ret[in.length - 1] ^= (xor >>> ((1 - carry) << 3));
return ret;
}
private static byte[] xor(byte[] in1, byte[] in2) {
if (in1 == null || in2 == null || in1.length > in2.length) {
throw new IllegalArgumentException("Length of first input must be <= length of second input.");
}
final byte[] result = new byte[in1.length];
for (int i = 0; i < result.length; i++) {
result[i] = (byte) (in1[i] ^ in2[i]);
}
return result;
}
private static byte[] xorend(byte[] in1, byte[] in2) {
if (in1 == null || in2 == null || in1.length < in2.length) {
throw new IllegalArgumentException("Length of first input must be >= length of second input.");
}
final byte[] result = Arrays.copyOf(in1, in1.length);
final int diff = in1.length - in2.length;
for (int i = 0; i < in2.length; i++) {
result[i + diff] = (byte) (result[i + diff] ^ in2[i]);
}
return result;
}
}

View File

@@ -0,0 +1,22 @@
package org.cryptomator.crypto.aes256;
import java.nio.ByteBuffer;
class BlocksData {
public static final int MAX_NUM_BLOCKS = 128;
final ByteBuffer buffer;
final long startBlockNum;
final int numBlocks;
BlocksData(ByteBuffer buffer, long startBlockNum, int numBlocks) {
if (numBlocks > MAX_NUM_BLOCKS) {
throw new IllegalArgumentException("Too many blocks to process at once: " + numBlocks);
}
this.buffer = buffer;
this.startBlockNum = startBlockNum;
this.numBlocks = numBlocks;
}
}

View File

@@ -0,0 +1,64 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import org.cryptomator.crypto.exceptions.CryptingException;
abstract class CryptoWorker implements Callable<Void> {
static final BlocksData POISON = new BlocksData(ByteBuffer.allocate(0), -1L, 0);
final Lock lock;
final Condition blockDone;
final AtomicLong currentBlock;
final BlockingQueue<BlocksData> queue;
public CryptoWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue) {
this.lock = lock;
this.blockDone = blockDone;
this.currentBlock = currentBlock;
this.queue = queue;
}
@Override
public final Void call() throws IOException {
try {
while (!Thread.currentThread().isInterrupted()) {
final BlocksData blocksData = queue.take();
if (blocksData == POISON) {
// put poison back in for other threads:
break;
}
final ByteBuffer processedBytes = this.process(blocksData);
lock.lock();
try {
while (currentBlock.get() != blocksData.startBlockNum) {
blockDone.await();
}
assert currentBlock.get() == blocksData.startBlockNum;
// yay, its my turn!
this.write(processedBytes);
// signal worker working on next block:
currentBlock.set(blocksData.startBlockNum + blocksData.numBlocks);
blockDone.signalAll();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
protected abstract ByteBuffer process(BlocksData block) throws CryptingException;
protected abstract void write(ByteBuffer processedBytes) throws IOException;
}

View File

@@ -0,0 +1,112 @@
package org.cryptomator.crypto.aes256;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class CryptoWorkerExecutor {
private static final Logger LOG = LoggerFactory.getLogger(CryptoWorkerExecutor.class);
private final int numWorkers;
private final Lock lock;
private final Condition blockDone;
private final AtomicLong currentBlock;
private final BlockingQueue<BlocksData> inputQueue;
private final ExecutorService executorService;
private final CompletionService<Void> completionService;
private boolean acceptWork;
/**
* Starts as many {@link CryptoWorker} as specified in the constructor, that start working immediately on the items submitted via {@link #offer(BlocksData, long, TimeUnit)}.
*/
public CryptoWorkerExecutor(int numWorkers, WorkerFactory workerFactory) {
this.numWorkers = numWorkers;
this.lock = new ReentrantLock();
this.blockDone = lock.newCondition();
this.currentBlock = new AtomicLong();
this.inputQueue = new LinkedBlockingQueue<>(numWorkers * 2); // one cycle read-ahead
this.executorService = Executors.newFixedThreadPool(numWorkers);
this.completionService = new ExecutorCompletionService<>(executorService);
this.acceptWork = true;
// start workers:
for (int i = 0; i < numWorkers; i++) {
final CryptoWorker worker = workerFactory.createWorker(lock, blockDone, currentBlock, inputQueue);
completionService.submit(worker);
}
}
/**
* Adds work to the work queue. On timeout all workers will be shut down.
*
* @see BlockingQueue#offer(Object, long, TimeUnit)
* @return <code>true</code> if the work has been added in time. <code>false</code> in any other case.
*/
public boolean offer(BlocksData data, long timeout, TimeUnit unit) {
if (!acceptWork) {
return false;
}
try {
final boolean success = inputQueue.offer(data, timeout, unit);
if (!success) {
this.acceptWork = false;
inputQueue.clear();
poisonWorkers();
}
return success;
} catch (InterruptedException e) {
LOG.error("Interrupted thread.", e);
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
return false;
}
/**
* Graceful shutdown of this executor, waiting for all jobs to finish (normally or by throwing exceptions).
*
* @throws ExecutionException If any of the workers failed.
*/
public void waitUntilDone() throws ExecutionException {
this.acceptWork = false;
try {
poisonWorkers();
// now workers will one after another finish their work, potentially throwing an ExecutionException:
for (int i = 0; i < numWorkers; i++) {
completionService.take().get();
}
} catch (InterruptedException e) {
LOG.error("Interrupted thread.", e);
Thread.currentThread().interrupt();
} finally {
// shutdown either after normal decryption or if ANY worker threw an exception:
executorService.shutdownNow();
}
}
private void poisonWorkers() throws InterruptedException {
// add enough poison for each worker:
for (int i = 0; i < numWorkers; i++) {
inputQueue.put(CryptoWorker.POISON);
}
}
@FunctionalInterface
interface WorkerFactory {
CryptoWorker createWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> inputQueue);
}
}

View File

@@ -0,0 +1,75 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import org.cryptomator.crypto.exceptions.CryptingException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
abstract class DecryptWorker extends CryptoWorker implements AesCryptographicConfiguration {
private final boolean shouldAuthenticate;
private final WritableByteChannel out;
public DecryptWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue, boolean shouldAuthenticate, WritableByteChannel out) {
super(lock, blockDone, currentBlock, queue);
this.shouldAuthenticate = shouldAuthenticate;
this.out = out;
}
@Override
protected ByteBuffer process(BlocksData data) throws CryptingException {
final Cipher cipher = initCipher(data.startBlockNum);
final Mac mac = initMac();
final ByteBuffer plaintextBuf = ByteBuffer.allocate(cipher.getOutputSize(CONTENT_MAC_BLOCK) * data.numBlocks);
final ByteBuffer ciphertextBuf = data.buffer.asReadOnlyBuffer();
final ByteBuffer macBuf = data.buffer.asReadOnlyBuffer();
for (long blockNum = data.startBlockNum; blockNum < data.startBlockNum + data.numBlocks; blockNum++) {
assert (blockNum - data.startBlockNum) < BlocksData.MAX_NUM_BLOCKS;
assert (blockNum - data.startBlockNum) * CONTENT_MAC_BLOCK < Integer.MAX_VALUE;
final int pos = (int) (blockNum - data.startBlockNum) * (CONTENT_MAC_BLOCK + mac.getMacLength());
ciphertextBuf.limit(Math.min(data.buffer.limit() - mac.getMacLength(), pos + CONTENT_MAC_BLOCK));
ciphertextBuf.position(pos);
try {
macBuf.limit(ciphertextBuf.limit() + mac.getMacLength());
macBuf.position(ciphertextBuf.limit());
} catch (IllegalArgumentException e) {
throw new DecryptFailedException("Invalid file content, missing MAC.");
}
if (shouldAuthenticate) {
checkMac(mac, blockNum, ciphertextBuf, macBuf);
}
ciphertextBuf.position(pos);
decrypt(cipher, ciphertextBuf, plaintextBuf);
}
plaintextBuf.flip();
return plaintextBuf;
}
@Override
protected void write(ByteBuffer processedBytes) throws IOException {
out.write(processedBytes);
}
protected abstract Cipher initCipher(long startBlockNum);
protected abstract Mac initMac();
protected abstract void checkMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf, ByteBuffer macBuf) throws MacAuthenticationFailedException;
protected abstract void decrypt(Cipher cipher, ByteBuffer ciphertextBuf, ByteBuffer plaintextBuf) throws DecryptFailedException;
}

View File

@@ -0,0 +1,61 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import org.cryptomator.crypto.exceptions.CryptingException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
abstract class EncryptWorker extends CryptoWorker implements AesCryptographicConfiguration {
private final WritableByteChannel out;
public EncryptWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue, WritableByteChannel out) {
super(lock, blockDone, currentBlock, queue);
this.out = out;
}
@Override
protected ByteBuffer process(BlocksData data) throws CryptingException {
final Cipher cipher = initCipher(data.startBlockNum);
final Mac mac = initMac();
final ByteBuffer ciphertextBuf = ByteBuffer.allocate((cipher.getOutputSize(CONTENT_MAC_BLOCK) + mac.getMacLength()) * data.numBlocks);
final ByteBuffer plaintextBuf = data.buffer.asReadOnlyBuffer();
for (long blockNum = data.startBlockNum; blockNum < data.startBlockNum + data.numBlocks; blockNum++) {
final int pos = (int) (blockNum - data.startBlockNum) * CONTENT_MAC_BLOCK;
plaintextBuf.limit(Math.min(data.buffer.limit(), pos + CONTENT_MAC_BLOCK));
encrypt(cipher, plaintextBuf, ciphertextBuf);
final ByteBuffer toMac = ciphertextBuf.asReadOnlyBuffer();
toMac.limit(ciphertextBuf.position());
toMac.position((int) (blockNum - data.startBlockNum) * (CONTENT_MAC_BLOCK + mac.getMacLength()));
ciphertextBuf.put(calcMac(mac, blockNum, toMac));
}
ciphertextBuf.flip();
return ciphertextBuf;
}
@Override
protected void write(ByteBuffer processedBytes) throws IOException {
out.write(processedBytes);
}
protected abstract Cipher initCipher(long startBlockNum);
protected abstract Mac initMac();
protected abstract byte[] calcMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf);
protected abstract void encrypt(Cipher cipher, ByteBuffer plaintextBuf, ByteBuffer ciphertextBuf) throws EncryptFailedException;
}

View File

@@ -1,66 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto.aes256;
import java.nio.file.FileSystems;
import java.nio.file.PathMatcher;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
interface FileNamingConventions {
/**
* Extension of masterkey files inside the root directory of the encrypted storage.
*/
String MASTERKEY_FILE_EXT = ".masterkey.json";
/**
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
*/
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
/**
* Maximum length possible on file systems with a filename limit of 255 chars.<br/>
* Also we would need a few chars for our file extension, so lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
*/
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 250;
/**
* For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String BASIC_FILE_EXT = ".aes";
/**
* Prefix in front of the actual encrypted file name used as IV.
*/
String IV_PREFIX_SEPARATOR = "_";
/**
* For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String LONG_NAME_FILE_EXT = ".lng.aes";
/**
* Prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
*/
String LONG_NAME_PREFIX_SEPARATOR = "_";
/**
* For metadata files for a certain group of files. The cryptor may decide what files to assign to the same group; hopefully using some
* kind of uniform distribution for better load balancing.
*/
String METADATA_FILE_EXT = ".meta";
/**
* Matches both, {@value #BASIC_FILE_EXT} and {@value #LONG_NAME_FILE_EXT} files.
*/
PathMatcher ENCRYPTED_FILE_GLOB_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**/*{" + BASIC_FILE_EXT + "," + LONG_NAME_FILE_EXT + "}");
}

View File

@@ -4,10 +4,13 @@ import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder(value = {"scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
public class KeyFile implements Serializable {
static final Integer CURRENT_VERSION = 2;
private static final long serialVersionUID = 8578363158959619885L;
private Integer version;
private byte[] scryptSalt;
private int scryptCostParam;
private int scryptBlockSize;
@@ -15,6 +18,14 @@ public class KeyFile implements Serializable {
private byte[] primaryMasterKey;
private byte[] hMacMasterKey;
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
public byte[] getScryptSalt() {
return scryptSalt;
}

View File

@@ -0,0 +1,47 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class LengthLimitingOutputStream extends FilterOutputStream {
private final long limit;
private volatile long bytesWritten;
public LengthLimitingOutputStream(OutputStream out, long limit) {
super(out);
this.limit = limit;
this.bytesWritten = 0;
}
@Override
public void write(int b) throws IOException {
if (bytesWritten < limit) {
out.write(b);
increaseNumberOfWrittenBytes(1);
}
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
final long bytesAvailable = limit - bytesWritten;
final int adjustedLen = (int) Math.min(len, bytesAvailable);
if (adjustedLen > 0) {
out.write(b, off, adjustedLen);
increaseNumberOfWrittenBytes(adjustedLen);
}
}
public long getBytesWritten() {
return bytesWritten;
}
private void increaseNumberOfWrittenBytes(int amount) throws IOException {
bytesWritten += amount;
if (bytesWritten >= limit) {
out.flush();
}
}
}

View File

@@ -0,0 +1,128 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.io.IOUtils;
/**
* Not thread-safe!
*/
public class LengthObfuscatingInputStream extends FilterInputStream {
private final byte[] padding;
private int paddingLength = -1;
private long inputBytesRead = 0;
private int paddingBytesRead = 0;
LengthObfuscatingInputStream(InputStream in, byte[] padding) {
super(in);
this.padding = padding;
}
long getRealInputLength() {
return inputBytesRead;
}
private void choosePaddingLengthOnce() {
if (paddingLength == -1) {
long upperBound = Math.min(Math.max(inputBytesRead / 10, 4096), 16 * 1024 * 1024); // 10% of original bytes (at least 4KiB), but not more than 16MiBs
paddingLength = (int) (Math.random() * upperBound);
}
}
@Override
public int read() throws IOException {
final int b = in.read();
if (b != -1) {
// stream available:
inputBytesRead++;
return b;
} else {
choosePaddingLengthOnce();
return readFromPadding();
}
}
private int readFromPadding() {
if (paddingLength == -1) {
throw new IllegalStateException("No padding length chosen yet.");
}
if (paddingBytesRead < paddingLength) {
// padding available:
return padding[paddingBytesRead++ % padding.length];
} else {
// end of stream AND padding
return -1;
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
final int bytesRead = IOUtils.read(in, b, off, len); // 0 on EOF
inputBytesRead += bytesRead;
if (bytesRead == len) {
return bytesRead;
} else if (bytesRead < len) {
choosePaddingLengthOnce();
final int additionalBytesNeeded = len - bytesRead;
final int additionalBytesRead = readFromPadding(b, off + bytesRead, additionalBytesNeeded);
return (bytesRead == 0 && additionalBytesRead == 0) ? -1 : bytesRead + additionalBytesRead;
} else {
// bytesRead > len:
throw new IllegalStateException("Read more bytes than requested.");
}
}
/**
* @return bytes read from padding (0, if fully read)
*/
private int readFromPadding(byte[] b, int off, int len) {
if (len < 0) {
throw new IllegalArgumentException("Length must not be negative");
}
if (paddingLength == -1) {
throw new IllegalStateException("No padding length chosen yet.");
}
final int remainingPadding = paddingLength - paddingBytesRead;
if (remainingPadding > len) {
// padding available:
for (int i = 0; i < len; i++) {
b[off + i] = padding[paddingBytesRead++ % padding.length];
}
return len;
} else {
// partly available:
for (int i = 0; i < remainingPadding; i++) {
b[off + i] = padding[paddingBytesRead++ % padding.length];
}
return remainingPadding;
}
}
@Override
public long skip(long n) throws IOException {
throw new IOException("Skip not supported");
}
@Override
public int available() throws IOException {
if (paddingLength == -1) {
// EOF not yet reached; delegate original stream to answer this rather complicated question:
return in.available();
} else {
// EOF already reached, read from remaining padding:
return paddingLength - paddingBytesRead;
}
}
@Override
public boolean markSupported() {
return false;
}
}

View File

@@ -1,49 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto.aes256;
import java.io.Serializable;
import java.util.UUID;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
class LongFilenameMetadata implements Serializable {
private static final long serialVersionUID = 6214509403824421320L;
@JsonDeserialize(as = DualHashBidiMap.class)
private BidiMap<UUID, String> encryptedFilenames = new DualHashBidiMap<>();
/* Getter/Setter */
public synchronized String getEncryptedFilenameForUUID(final UUID uuid) {
return encryptedFilenames.get(uuid);
}
public synchronized UUID getOrCreateUuidForEncryptedFilename(String encryptedFilename) {
UUID uuid = encryptedFilenames.getKey(encryptedFilename);
if (uuid == null) {
uuid = UUID.randomUUID();
encryptedFilenames.put(uuid, encryptedFilename);
}
return uuid;
}
public BidiMap<UUID, String> getEncryptedFilenames() {
return encryptedFilenames;
}
public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
this.encryptedFilenames = encryptedFilenames;
}
}

View File

@@ -1,39 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.crypto.Mac;
/**
* Updates a {@link Mac} with the bytes read from this stream.
*/
class MacInputStream extends FilterInputStream {
private final Mac mac;
/**
* @param in Stream from which to read contents, which will update the Mac.
* @param mac Mac to be updated during writes.
*/
public MacInputStream(InputStream in, Mac mac) {
super(in);
this.mac = mac;
}
@Override
public int read() throws IOException {
int b = in.read();
mac.update((byte) b);
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = in.read(b, off, len);
mac.update(b, off, len);
return read;
}
}

View File

@@ -1,37 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import javax.crypto.Mac;
/**
* Updates a {@link Mac} with the bytes written to this stream.
*/
class MacOutputStream extends FilterOutputStream {
private final Mac mac;
/**
* @param out Stream to redirect contents to after updating the mac.
* @param mac Mac to be updated during writes.
*/
public MacOutputStream(OutputStream out, Mac mac) {
super(out);
this.mac = mac;
}
@Override
public void write(int b) throws IOException {
mac.update((byte) b);
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
mac.update(b, off, len);
out.write(b, off, len);
}
}

View File

@@ -15,31 +15,29 @@ import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import javax.security.auth.DestroyFailedException;
import org.apache.commons.io.IOUtils;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.junit.Assert;
import org.junit.Test;
public class Aes256CryptorTest {
private static final Random TEST_PRNG = new Random();
@Test
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException {
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
final ByteArrayOutputStream out = new ByteArrayOutputStream();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
cryptor.destroy();
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor decryptor = new Aes256Cryptor();
final InputStream in = new ByteArrayInputStream(out.toByteArray());
decryptor.decryptMasterKey(in, pw);
@@ -48,17 +46,17 @@ public class Aes256CryptorTest {
}
@Test
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
final ByteArrayOutputStream out = new ByteArrayOutputStream();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
cryptor.destroy();
IOUtils.closeQuietly(out);
// all these passwords are expected to fail.
final String[] wrongPws = {"a", "as", "asdf", "sdf", "das", "dsa", "foo", "bar", "baz"};
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor decryptor = new Aes256Cryptor();
for (final String wrongPw : wrongPws) {
final InputStream in = new ByteArrayInputStream(out.toByteArray());
try {
@@ -72,17 +70,17 @@ public class Aes256CryptorTest {
}
}
@Test
public void testIntegrityAuthentication() throws IOException {
@Test(expected = DecryptFailedException.class)
public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -90,11 +88,6 @@ public class Aes256CryptorTest {
encryptedData.position(0);
// authenticate unmodified content:
final SeekableByteChannel encryptedIn1 = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean isContentUnmodified1 = cryptor.authenticateContent(encryptedIn1);
Assert.assertTrue(isContentUnmodified1);
// toggle one bit inf first content byte:
encryptedData.position(64);
final byte fifthByte = encryptedData.get();
@@ -103,23 +96,23 @@ public class Aes256CryptorTest {
encryptedData.position(0);
// authenticate modified content:
final SeekableByteChannel encryptedIn2 = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean isContentUnmodified2 = cryptor.authenticateContent(encryptedIn2);
Assert.assertFalse(isContentUnmodified2);
// decrypt modified content (should fail with DecryptFailedException):
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
cryptor.decryptFile(encryptedIn, plaintextOut, true);
}
@Test
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096 + 32); // header + content + maximum possible size obfuscation padding + 32 bytes mac (per each 32k)
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -134,7 +127,7 @@ public class Aes256CryptorTest {
// decrypt:
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptedFile(encryptedIn, plaintextOut);
final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut, true);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
@@ -145,20 +138,20 @@ public class Aes256CryptorTest {
}
@Test
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
// our test plaintext data:
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
// 8MiB test plaintext data:
final byte[] plaintextData = new byte[2097152 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 65536; i++) {
for (int i = 0; i < 2097152; i++) {
bbIn.putInt(i);
}
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
final ByteBuffer encryptedData = ByteBuffer.allocate((int) (104 + plaintextData.length * 1.2));
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -169,54 +162,43 @@ public class Aes256CryptorTest {
// decrypt:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 25000 * Integer.BYTES, 30000 * Integer.BYTES);
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 260000 * Integer.BYTES, 4000 * Integer.BYTES, true);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertTrue(numDecryptedBytes > 0);
// check decrypted data:
final byte[] result = plaintextOut.toByteArray();
final byte[] expected = Arrays.copyOfRange(plaintextData, 25000 * Integer.BYTES, 55000 * Integer.BYTES);
final byte[] expected = Arrays.copyOfRange(plaintextData, 260000 * Integer.BYTES, 264000 * Integer.BYTES);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testEncryptionOfFilenames() throws IOException {
final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
public void testEncryptionOfFilenames() throws IOException, DecryptFailedException {
final Aes256Cryptor cryptor = new Aes256Cryptor();
// short path components
// directory paths
final String originalPath1 = "foo/bar/baz";
final String encryptedPath1a = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
final String encryptedPath1b = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
final String encryptedPath1a = cryptor.encryptDirectoryPath(originalPath1, "/");
final String encryptedPath1b = cryptor.encryptDirectoryPath(originalPath1, "/");
Assert.assertEquals(encryptedPath1a, encryptedPath1b);
final String decryptedPath1 = cryptor.decryptPath(encryptedPath1a, '/', '/', ioSupportMock);
Assert.assertEquals(originalPath1, decryptedPath1);
// long path components
// long file names
final String str50chars = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
final String originalPath2 = "foo/" + str50chars + str50chars + str50chars + str50chars + str50chars + "/baz";
final String encryptedPath2a = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
final String encryptedPath2b = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
final String originalPath2 = str50chars + str50chars + str50chars + str50chars + str50chars + "_isLongerThan255Chars.txt";
final String encryptedPath2a = cryptor.encryptFilename(originalPath2);
final String encryptedPath2b = cryptor.encryptFilename(originalPath2);
Assert.assertEquals(encryptedPath2a, encryptedPath2b);
final String decryptedPath2 = cryptor.decryptPath(encryptedPath2a, '/', '/', ioSupportMock);
final String decryptedPath2 = cryptor.decryptFilename(encryptedPath2a);
Assert.assertEquals(originalPath2, decryptedPath2);
}
private static class CryptoIOSupportMock implements CryptorIOSupport {
private final Map<String, byte[]> map = new HashMap<>();
@Override
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) {
map.put(encryptedPath, encryptedMetadata);
}
@Override
public byte[] readPathSpecificMetadata(String encryptedPath) {
return map.get(encryptedPath);
}
// block size length file names
final String originalPath3 = "aaaabbbbccccdddd";
final String encryptedPath3a = cryptor.encryptFilename(originalPath3);
final String encryptedPath3b = cryptor.encryptFilename(originalPath3);
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
final String decryptedPath3 = cryptor.decryptFilename(encryptedPath3a);
Assert.assertEquals(originalPath3, decryptedPath3);
}
}

View File

@@ -0,0 +1,224 @@
package org.cryptomator.crypto.aes256;
import java.security.InvalidKeyException;
import org.apache.commons.codec.DecoderException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.junit.Assert;
import org.junit.Test;
/**
* Official RFC 5297 test vector taken from https://tools.ietf.org/html/rfc5297#appendix-A.1
*/
public class AesSivCipherUtilTest {
@Test
public void testS2v() throws DecoderException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93};
final byte[] result = AesSivCipherUtil.s2v(macKey, plaintext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testSivEncrypt() throws InvalidKeyException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
(byte) 0xfe, (byte) 0x5c};
final byte[] result = AesSivCipherUtil.sivEncrypt(aesKey, macKey, plaintext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testSivDecrypt() throws DecryptFailedException, InvalidKeyException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
(byte) 0xfe, (byte) 0x5c};
final byte[] expected = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] result = AesSivCipherUtil.sivDecrypt(aesKey, macKey, ciphertext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test(expected = DecryptFailedException.class)
public void testSivDecryptWithInvalidKey() throws DecryptFailedException, InvalidKeyException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0x00};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
(byte) 0xfe, (byte) 0x5c};
final byte[] expected = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] result = AesSivCipherUtil.sivDecrypt(aesKey, macKey, ciphertext, ad);
Assert.assertArrayEquals(expected, result);
}
/**
* https://tools.ietf.org/html/rfc5297#appendix-A.2
*/
@Test
public void testNonceBasedAuthenticatedEncryption() throws InvalidKeyException {
final byte[] macKey = {(byte) 0x7f, (byte) 0x7e, (byte) 0x7d, (byte) 0x7c, //
(byte) 0x7b, (byte) 0x7a, (byte) 0x79, (byte) 0x78, //
(byte) 0x77, (byte) 0x76, (byte) 0x75, (byte) 0x74, //
(byte) 0x73, (byte) 0x72, (byte) 0x71, (byte) 0x70};
final byte[] aesKey = {(byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, //
(byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, //
(byte) 0x48, (byte) 0x49, (byte) 0x4a, (byte) 0x4b, //
(byte) 0x4c, (byte) 0x4d, (byte) 0x4e, (byte) 0x4f};
final byte[] ad1 = {(byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33, //
(byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, //
(byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb, //
(byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff, //
(byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, //
(byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, //
(byte) 0xff, (byte) 0xee, (byte) 0xdd, (byte) 0xcc, //
(byte) 0xbb, (byte) 0xaa, (byte) 0x99, (byte) 0x88, //
(byte) 0x77, (byte) 0x66, (byte) 0x55, (byte) 0x44, //
(byte) 0x33, (byte) 0x22, (byte) 0x11, (byte) 0x00};
final byte[] ad2 = {(byte) 0x10, (byte) 0x20, (byte) 0x30, (byte) 0x40, //
(byte) 0x50, (byte) 0x60, (byte) 0x70, (byte) 0x80, //
(byte) 0x90, (byte) 0xa0};
final byte[] nonce = {(byte) 0x09, (byte) 0xf9, (byte) 0x11, (byte) 0x02, //
(byte) 0x9d, (byte) 0x74, (byte) 0xe3, (byte) 0x5b, //
(byte) 0xd8, (byte) 0x41, (byte) 0x56, (byte) 0xc5, //
(byte) 0x63, (byte) 0x56, (byte) 0x88, (byte) 0xc0};
final byte[] plaintext = {(byte) 0x74, (byte) 0x68, (byte) 0x69, (byte) 0x73, //
(byte) 0x20, (byte) 0x69, (byte) 0x73, (byte) 0x20, //
(byte) 0x73, (byte) 0x6f, (byte) 0x6d, (byte) 0x65, //
(byte) 0x20, (byte) 0x70, (byte) 0x6c, (byte) 0x61, //
(byte) 0x69, (byte) 0x6e, (byte) 0x74, (byte) 0x65, //
(byte) 0x78, (byte) 0x74, (byte) 0x20, (byte) 0x74, //
(byte) 0x6f, (byte) 0x20, (byte) 0x65, (byte) 0x6e, //
(byte) 0x63, (byte) 0x72, (byte) 0x79, (byte) 0x70, //
(byte) 0x74, (byte) 0x20, (byte) 0x75, (byte) 0x73, //
(byte) 0x69, (byte) 0x6e, (byte) 0x67, (byte) 0x20, //
(byte) 0x53, (byte) 0x49, (byte) 0x56, (byte) 0x2d, //
(byte) 0x41, (byte) 0x45, (byte) 0x53};
final byte[] result = AesSivCipherUtil.sivEncrypt(aesKey, macKey, plaintext, ad1, ad2, nonce);
final byte[] expected = {(byte) 0x7b, (byte) 0xdb, (byte) 0x6e, (byte) 0x3b, //
(byte) 0x43, (byte) 0x26, (byte) 0x67, (byte) 0xeb, //
(byte) 0x06, (byte) 0xf4, (byte) 0xd1, (byte) 0x4b, //
(byte) 0xff, (byte) 0x2f, (byte) 0xbd, (byte) 0x0f, //
(byte) 0xcb, (byte) 0x90, (byte) 0x0f, (byte) 0x2f, //
(byte) 0xdd, (byte) 0xbe, (byte) 0x40, (byte) 0x43, //
(byte) 0x26, (byte) 0x60, (byte) 0x19, (byte) 0x65, //
(byte) 0xc8, (byte) 0x89, (byte) 0xbf, (byte) 0x17, //
(byte) 0xdb, (byte) 0xa7, (byte) 0x7c, (byte) 0xeb, //
(byte) 0x09, (byte) 0x4f, (byte) 0xa6, (byte) 0x63, //
(byte) 0xb7, (byte) 0xa3, (byte) 0xf7, (byte) 0x48, //
(byte) 0xba, (byte) 0x8a, (byte) 0xf8, (byte) 0x29, //
(byte) 0xea, (byte) 0x64, (byte) 0xad, (byte) 0x54, //
(byte) 0x4a, (byte) 0x27, (byte) 0x2e, (byte) 0x9c, //
(byte) 0x48, (byte) 0x5b, (byte) 0x62, (byte) 0xa3, //
(byte) 0xfd, (byte) 0x5c, (byte) 0x0d};
Assert.assertArrayEquals(expected, result);
}
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (c) 2014 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - log4j config for WebDAV unit tests
-->
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
</Appenders>
<Loggers>
<!-- show our own debug messages: -->
<Logger name="org.cryptomator" level="DEBUG" />
<!-- mute dependencies: -->
<Root level="INFO">
<AppenderRef ref="Console" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>

View File

@@ -12,12 +12,13 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.4.0</version>
<version>0.10.0</version>
</parent>
<artifactId>crypto-api</artifactId>
<name>Cryptomator cryptographic module API</name>
<dependencies>
<!-- commons -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
@@ -26,5 +27,9 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,38 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto;
import java.util.HashSet;
import java.util.Set;
public abstract class AbstractCryptor implements Cryptor {
private final Set<SensitiveDataSwipeListener> swipeListeners = new HashSet<>();
@Override
public final void swipeSensitiveData() {
this.swipeSensitiveDataInternal();
for (final SensitiveDataSwipeListener sensitiveDataSwipeListener : swipeListeners) {
sensitiveDataSwipeListener.swipeSensitiveData();
}
}
protected abstract void swipeSensitiveDataInternal();
@Override
public final void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
this.swipeListeners.add(listener);
}
@Override
public final void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
this.swipeListeners.remove(listener);
}
}

View File

@@ -0,0 +1,80 @@
package org.cryptomator.crypto;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import javax.security.auth.DestroyFailedException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
public class AbstractCryptorDecorator implements Cryptor {
protected final Cryptor cryptor;
public AbstractCryptorDecorator(Cryptor cryptor) {
this.cryptor = cryptor;
}
@Override
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
cryptor.encryptMasterKey(out, password);
}
@Override
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException {
cryptor.decryptMasterKey(in, password);
}
@Override
public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
return cryptor.encryptDirectoryPath(cleartextDirectoryId, nativePathSep);
}
@Override
public String encryptFilename(String cleartextName) {
return cryptor.encryptFilename(cleartextName);
}
@Override
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
return cryptor.decryptFilename(ciphertextName);
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
return cryptor.decryptedContentLength(encryptedFile);
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
return cryptor.decryptFile(encryptedFile, plaintextFile, authenticate);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length, authenticate);
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
return cryptor.encryptFile(plaintextFile, encryptedFile);
}
@Override
public void destroy() throws DestroyFailedException {
cryptor.destroy();
}
@Override
public boolean isDestroyed() {
return cryptor.isDestroyed();
}
}

View File

@@ -12,17 +12,20 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import javax.security.auth.Destroyable;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
/**
* Provides access to cryptographic functions. All methods are threadsafe.
*/
public interface Cryptor extends SensitiveDataSwipeListener {
public interface Cryptor extends Destroyable {
/**
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
@@ -33,57 +36,50 @@ public interface Cryptor extends SensitiveDataSwipeListener {
* Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
*
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong
* password. In this case a DecryptFailedException will be thrown.
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
* this case Java JCE needs to be installed.
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong password. In this case a DecryptFailedException will be thrown.
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In this case Java JCE needs to be installed.
* @throws UnsupportedVaultException If the masterkey file is too old or too modern.
*/
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException;
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException;
/**
* Encrypts each plaintext path component for its own.
* Encrypts a given plaintext path representing a directory structure. See {@link #encryptFilename(String, CryptorMetadataSupport)} for contents inside directories.
*
* @param cleartextPath A relative path (UTF-8 encoded)
* @param encryptedPathSep Path separator char like '/' used on local file system. Must not be null, even if cleartextPath is a sole
* file name without any path separators.
* @param cleartextPathSep Path separator char like '/' used in webdav URIs. Must not be null, even if cleartextPath is a sole file name
* without any path separators.
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
* @return Encrypted path components concatenated by the given encryptedPathSep. Must not start with encryptedPathSep, unless the
* encrypted path is explicitly absolute.
* @param cleartextDirectoryId A unique directory id
* @param nativePathSep Path separator like "/" used on local file system. Must not be null, even if cleartextPath is a sole file name without any path separators.
* @return Encrypted path.
*/
String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep);
/**
* Decrypts each encrypted path component for its own.
* Encrypts the name of a file. See {@link #encryptDirectoryPath(String, char)} for parent dir.
*
* @param encryptedPath A relative path (UTF-8 encoded)
* @param encryptedPathSep Path separator char like '/' used on local file system. Must not be null, even if encryptedPath is a sole
* file name without any path separators.
* @param cleartextPathSep Path separator char like '/' used in webdav URIs. Must not be null, even if encryptedPath is a sole file name
* without any path separators.
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
* @return Decrypted path components concatenated by the given cleartextPathSep. Must not start with cleartextPathSep, unless the
* cleartext path is explicitly absolute.
* @param cleartextName A plaintext filename without any preceeding directory paths.
* @return Encrypted filename.
*/
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
String encryptFilename(String cleartextName);
/**
* @return <code>true</code> If the integrity of the file can be assured.
* Decrypts the name of a file.
*
* @param ciphertextName A ciphertext filename without any preceeding directory paths.
* @return Decrypted filename.
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
*/
boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException;
String decryptFilename(String ciphertextName) throws DecryptFailedException;
/**
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
* @return Content length of the decrypted file or <code>null</code> if unknown.
* @throws MacAuthenticationFailedException If the MAC auth failed.
*/
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException;
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException;
/**
* @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
* @throws DecryptFailedException If decryption failed
*/
Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException;
/**
* @param pos First byte (inclusive)
@@ -91,21 +87,11 @@ public interface Cryptor extends SensitiveDataSwipeListener {
* @return Number of decrypted bytes. This might not be equal to the number of bytes requested due to potential overheads.
* @throws DecryptFailedException If decryption failed
*/
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException;
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException;
/**
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
*/
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException;
/**
* @return A filter, that returns <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting
* metadata file of the {@link Cryptor}.
*/
Filter<Path> getPayloadFilesFilter();
void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException;
}

View File

@@ -16,11 +16,11 @@ public interface CryptorIOSampling {
/**
* @return Number of encrypted bytes since the last reset.
*/
Long pollEncryptedBytes(boolean resetCounter);
long pollEncryptedBytes(boolean resetCounter);
/**
* @return Number of decrypted bytes since the last reset.
*/
Long pollDecryptedBytes(boolean resetCounter);
long pollDecryptedBytes(boolean resetCounter);
}

View File

@@ -1,31 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto;
import java.io.IOException;
/**
* Methods that may be called by the Cryptor when accessing a path.
*/
public interface CryptorIOSupport {
/**
* Persists encryptedMetadata to the given encryptedPath.
*
* @param encryptedPath A relative path
* @throws IOException
*/
void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException;
/**
* @return Previously written encryptedMetadata stored at the given encryptedPath or <code>null</code> if no such file exists.
*/
byte[] readPathSpecificMetadata(String encryptedPath) throws IOException;
}

View File

@@ -0,0 +1,65 @@
package org.cryptomator.crypto;
import java.util.Map;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.AbstractDualBidiMap;
import org.apache.commons.collections4.map.LRUMap;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
public class PathCachingCryptorDecorator extends AbstractCryptorDecorator {
private static final int MAX_CACHED_PATHS = 5000;
private static final int MAX_CACHED_NAMES = 5000;
private final Map<String, String> pathCache = new LRUMap<>(MAX_CACHED_PATHS); // <cleartextDirectoryId, ciphertextPath>
private final BidiMap<String, String> nameCache = new BidiLRUMap<>(MAX_CACHED_NAMES); // <cleartextName, ciphertextName>
private PathCachingCryptorDecorator(Cryptor cryptor) {
super(cryptor);
}
public static Cryptor decorate(Cryptor cryptor) {
return new PathCachingCryptorDecorator(cryptor);
}
/* Cryptor */
@Override
public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
return pathCache.computeIfAbsent(cleartextDirectoryId, id -> cryptor.encryptDirectoryPath(id, nativePathSep));
}
@Override
public String encryptFilename(String cleartextName) {
return nameCache.computeIfAbsent(cleartextName, name -> cryptor.encryptFilename(name));
}
@Override
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
String cleartextName = nameCache.getKey(ciphertextName);
if (cleartextName == null) {
cleartextName = cryptor.decryptFilename(ciphertextName);
nameCache.put(cleartextName, ciphertextName);
}
return cleartextName;
}
private static class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
BidiLRUMap(int maxSize) {
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
}
protected BidiLRUMap(final Map<K, V> normalMap, final Map<V, K> reverseMap, final BidiMap<V, K> inverseBidiMap) {
super(normalMap, reverseMap, inverseBidiMap);
}
@Override
protected BidiMap<V, K> createBidiMap(Map<V, K> normalMap, Map<K, V> reverseMap, BidiMap<K, V> inverseMap) {
return new BidiLRUMap<V, K>(normalMap, reverseMap, inverseMap);
}
}
}

View File

@@ -0,0 +1,118 @@
package org.cryptomator.crypto;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import java.util.concurrent.atomic.LongAdder;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
/**
* Decorates the Cryptor by decorating the In- and OutputStreams used during de-/encryption.
*/
public class SamplingCryptorDecorator extends AbstractCryptorDecorator implements CryptorIOSampling {
private final LongAdder encryptedBytes;
private final LongAdder decryptedBytes;
private SamplingCryptorDecorator(Cryptor cryptor) {
super(cryptor);
encryptedBytes = new LongAdder();
decryptedBytes = new LongAdder();
}
public static Cryptor decorate(Cryptor cryptor) {
return new SamplingCryptorDecorator(cryptor);
}
@Override
public long pollEncryptedBytes(boolean resetCounter) {
if (resetCounter) {
return encryptedBytes.sumThenReset();
} else {
return encryptedBytes.sum();
}
}
@Override
public long pollDecryptedBytes(boolean resetCounter) {
if (resetCounter) {
return decryptedBytes.sumThenReset();
} else {
return decryptedBytes.sum();
}
}
/* Cryptor */
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptFile(encryptedFile, countingOutputStream, authenticate);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptRange(encryptedFile, countingOutputStream, pos, length, authenticate);
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
return cryptor.encryptFile(countingInputStream, encryptedFile);
}
private class CountingInputStream extends InputStream {
private final InputStream in;
private final LongAdder counter;
private CountingInputStream(LongAdder counter, InputStream in) {
this.in = in;
this.counter = counter;
}
@Override
public int read() throws IOException {
int count = in.read();
counter.add(count);
return count;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int count = in.read(b, off, len);
counter.add(count);
return count;
}
}
private class CountingOutputStream extends OutputStream {
private final OutputStream out;
private final LongAdder counter;
private CountingOutputStream(LongAdder counter, OutputStream out) {
this.out = out;
this.counter = counter;
}
@Override
public void write(int b) throws IOException {
counter.increment();
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
counter.add(len);
out.write(b, off, len);
}
}
}

View File

@@ -1,172 +0,0 @@
package org.cryptomator.crypto;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
public class SamplingDecorator implements Cryptor, CryptorIOSampling {
private final Cryptor cryptor;
private final AtomicLong encryptedBytes;
private final AtomicLong decryptedBytes;
private SamplingDecorator(Cryptor cryptor) {
this.cryptor = cryptor;
encryptedBytes = new AtomicLong();
decryptedBytes = new AtomicLong();
}
public static Cryptor decorate(Cryptor cryptor) {
return new SamplingDecorator(cryptor);
}
@Override
public void swipeSensitiveData() {
cryptor.swipeSensitiveData();
}
@Override
public Long pollEncryptedBytes(boolean resetCounter) {
if (resetCounter) {
return encryptedBytes.getAndSet(0);
} else {
return encryptedBytes.get();
}
}
@Override
public Long pollDecryptedBytes(boolean resetCounter) {
if (resetCounter) {
return decryptedBytes.getAndSet(0);
} else {
return decryptedBytes.get();
}
}
/* Cryptor */
@Override
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
cryptor.encryptMasterKey(out, password);
}
@Override
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
cryptor.decryptMasterKey(in, password);
}
@Override
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
encryptedBytes.addAndGet(StringUtils.length(cleartextPath));
return cryptor.encryptPath(cleartextPath, encryptedPathSep, cleartextPathSep, ioSupport);
}
@Override
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
decryptedBytes.addAndGet(StringUtils.length(encryptedPath));
return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport);
}
@Override
public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.authenticateContent(encryptedFile);
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.decryptedContentLength(encryptedFile);
}
@Override
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptedFile(encryptedFile, countingInputStream);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
return cryptor.encryptFile(countingInputStream, encryptedFile);
}
@Override
public Filter<Path> getPayloadFilesFilter() {
return cryptor.getPayloadFilesFilter();
}
@Override
public void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
cryptor.addSensitiveDataSwipeListener(listener);
}
@Override
public void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
cryptor.removeSensitiveDataSwipeListener(listener);
}
private class CountingInputStream extends InputStream {
private final InputStream in;
private final AtomicLong counter;
private CountingInputStream(AtomicLong counter, InputStream in) {
this.in = in;
this.counter = counter;
}
@Override
public int read() throws IOException {
int count = in.read();
counter.addAndGet(count);
return count;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int count = in.read(b, off, len);
counter.addAndGet(count);
return count;
}
}
private class CountingOutputStream extends OutputStream {
private final OutputStream out;
private final AtomicLong counter;
private CountingOutputStream(AtomicLong counter, OutputStream out) {
this.out = out;
this.counter = counter;
}
@Override
public void write(int b) throws IOException {
counter.incrementAndGet();
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
counter.addAndGet(len);
out.write(b, off, len);
}
}
}

View File

@@ -1,19 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto;
public interface SensitiveDataSwipeListener {
/**
* Removes sensitive data from memory. Depending on the data (e.g. for passwords) it might be necessary to overwrite the memory before
* freeing the object.
*/
void swipeSensitiveData();
}

View File

@@ -0,0 +1,15 @@
package org.cryptomator.crypto.exceptions;
import java.io.IOException;
public class CryptingException extends IOException {
private static final long serialVersionUID = -6622699014483319376L;
public CryptingException(String string) {
super(string);
}
public CryptingException(String string, Throwable t) {
super(string, t);
}
}

View File

@@ -1,6 +1,6 @@
package org.cryptomator.crypto.exceptions;
public class DecryptFailedException extends StorageCryptingException {
public class DecryptFailedException extends CryptingException {
private static final long serialVersionUID = -3855673600374897828L;
public DecryptFailedException(Throwable t) {

View File

@@ -0,0 +1,13 @@
package org.cryptomator.crypto.exceptions;
public class EncryptFailedException extends CryptingException {
private static final long serialVersionUID = -3855673600374897828L;
public EncryptFailedException(Throwable t) {
super("Encryption failed.", t);
}
public EncryptFailedException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,11 @@
package org.cryptomator.crypto.exceptions;
public class MacAuthenticationFailedException extends DecryptFailedException {
private static final long serialVersionUID = -5577052361643658772L;
public MacAuthenticationFailedException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,11 @@
package org.cryptomator.crypto.exceptions;
public class MasterkeyDecryptionException extends Exception {
private static final long serialVersionUID = -6241452734672333206L;
public MasterkeyDecryptionException(String string) {
super(string);
}
}

View File

@@ -1,13 +0,0 @@
package org.cryptomator.crypto.exceptions;
public class StorageCryptingException extends Exception {
private static final long serialVersionUID = -6622699014483319376L;
public StorageCryptingException(String string) {
super(string);
}
public StorageCryptingException(String string, Throwable t) {
super(string, t);
}
}

View File

@@ -1,6 +1,6 @@
package org.cryptomator.crypto.exceptions;
public class UnsupportedKeyLengthException extends StorageCryptingException {
public class UnsupportedKeyLengthException extends MasterkeyDecryptionException {
private static final long serialVersionUID = 8114147446419390179L;
private final int requestedLength;

View File

@@ -0,0 +1,32 @@
package org.cryptomator.crypto.exceptions;
public class UnsupportedVaultException extends Exception {
private static final long serialVersionUID = -5147549533387945622L;
private final Integer detectedVersion;
private final Integer supportedVersion;
public UnsupportedVaultException(Integer detectedVersion, Integer supportedVersion) {
super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion);
this.detectedVersion = detectedVersion;
this.supportedVersion = supportedVersion;
}
public Integer getDetectedVersion() {
return detectedVersion;
}
public Integer getSupportedVersion() {
return supportedVersion;
}
public boolean isVaultOlderThanSoftware() {
return detectedVersion == null || detectedVersion < supportedVersion;
}
public boolean isSoftwareOlderThanVault() {
return detectedVersion > supportedVersion;
}
}

View File

@@ -1,6 +1,6 @@
package org.cryptomator.crypto.exceptions;
public class WrongPasswordException extends StorageCryptingException {
public class WrongPasswordException extends MasterkeyDecryptionException {
private static final long serialVersionUID = -602047799678568780L;
public WrongPasswordException() {

View File

@@ -1,90 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto.io;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
public class SeekableByteChannelInputStream extends InputStream {
private final SeekableByteChannel channel;
private volatile long markedPos = 0;
public SeekableByteChannelInputStream(SeekableByteChannel channel) {
this.channel = channel;
}
@Override
public int read() throws IOException {
final ByteBuffer buffer = ByteBuffer.allocate(1);
final int read = channel.read(buffer);
if (read == 1) {
return buffer.get(0);
} else {
return -1;
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
return channel.read(buffer);
}
@Override
public int available() throws IOException {
long available = channel.size() - channel.position();
if (available > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
} else {
return (int) available;
}
}
@Override
public long skip(long n) throws IOException {
final long pos = channel.position();
final long max = channel.size();
final long maxSkip = max - pos;
final long actualSkip = Math.min(n, maxSkip);
channel.position(channel.position() + actualSkip);
return actualSkip;
}
@Override
public void close() throws IOException {
channel.close();
super.close();
}
@Override
public synchronized void mark(int readlimit) {
try {
markedPos = channel.position();
} catch (IOException e) {
markedPos = 0;
}
}
@Override
public synchronized void reset() throws IOException {
channel.position(markedPos);
}
public synchronized void resetTo(long position) throws IOException {
channel.position(position);
}
@Override
public boolean markSupported() {
return true;
}
}

View File

@@ -1,64 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto.io;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
public class SeekableByteChannelOutputStream extends OutputStream {
private final SeekableByteChannel channel;
public SeekableByteChannelOutputStream(SeekableByteChannel channel) {
this.channel = channel;
}
@Override
public void write(int b) throws IOException {
final byte actualByte = (byte) (b & 0x000000FF);
final ByteBuffer buffer = ByteBuffer.allocate(1);
buffer.put(actualByte);
channel.write(buffer);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
channel.write(buffer);
}
@Override
public void close() throws IOException {
channel.close();
}
/**
* @see SeekableByteChannel#truncate(long)
*/
public void truncate(long size) throws IOException {
channel.truncate(size);
}
/**
* @see SeekableByteChannel#position()
*/
public long position() throws IOException {
return channel.position();
}
/**
* @see SeekableByteChannel#position(long)
*/
public void position(long newPosition) throws IOException {
channel.position(newPosition);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -0,0 +1,16 @@
Package: APPLICATION_PACKAGE
Version: APPLICATION_VERSION
Section: contrib/utils
Maintainer: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Homepage: https://cryptomator.org
Vcs-Git: https://github.com/totalvoidness/cryptomator.git
Vcs-Browser: https://github.com/totalvoidness/cryptomator
Priority: optional
Architecture: APPLICATION_ARCH
Provides: APPLICATION_PACKAGE
Installed-Size: APPLICATION_INSTALLED_SIZE
Depends: gvfs-bin, gvfs-backends, gvfs-fuse, xdg-utils
Description: Multi-platform client-side encryption of your cloud files.
Cryptomator provides free client-side AES encryption for your cloud files.
Create encrypted vaults, which get mounted as virtual volumes. Whatever
you save on one of these volumes will end up encrypted inside your vault.

View File

@@ -0,0 +1,23 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: cryptomator
Source: <https://github.com/totalvoidness/cryptomator>
Copyright: 2015 Sebastian Stenzel <sebastian.stenzel@gmail.com> and contributors.
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,88 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.10.0</version>
</parent>
<artifactId>installer-debian</artifactId>
<packaging>pom</packaging>
<name>Cryptomator Debian installer</name>
<properties>
<javafx.application.name>Cryptomator</javafx.application.name>
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
</properties>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<phase>prepare-package</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>create-deployment-bundle</id>
<phase>install</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
<!-- Define application to build -->
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<!-- Create main application jar -->
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
<fx:application refid="fxApp" />
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
</fx:resources>
<fx:manifest>
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
<fx:attribute name="Implementation-Version" value="${project.version}" />
</fx:manifest>
</fx:jar>
<!-- Create native package -->
<fx:deploy nativeBundles="deb" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application refid="fxApp"/>
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform javafx="2.2+" j2se="8.0">
<fx:property name="logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx2048m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />
</fx:deploy>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

View File

@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSMinimumSystemVersion</key>
<string>10.7.4</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleExecutable</key>
<string>DEPLOY_LAUNCHER_NAME</string>
<key>CFBundleIconFile</key>
<string>DEPLOY_ICON_FILE</string>
<key>CFBundleIdentifier</key>
<string>DEPLOY_BUNDLE_IDENTIFIER</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>DEPLOY_BUNDLE_NAME</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>DEPLOY_BUNDLE_SHORT_VERSION</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See http://developer.apple.com/library/mac/#releasenotes/General/SubmittingToMacAppStore/_index.html for list of AppStore categories -->
<key>LSApplicationCategoryType</key>
<string>DEPLOY_BUNDLE_CATEGORY</string>
<key>CFBundleVersion</key>
<string>100</string>
<key>NSHumanReadableCopyright</key>
<string>DEPLOY_BUNDLE_COPYRIGHT</string>
<key>JVMRuntime</key>
<string>DEPLOY_JAVA_RUNTIME_NAME</string>
<key>JVMMainClassName</key>
<string>DEPLOY_LAUNCHER_CLASS</string>
<key>JVMAppClasspath</key>
<string>DEPLOY_APP_CLASSPATH</string>
<key>JVMMainJarName</key>
<string>DEPLOY_MAIN_JAR_NAME</string>
<key>JVMPreferencesID</key>
<string>DEPLOY_PREFERENCES_ID</string>
<key>JVMOptions</key>
<array>
DEPLOY_JVM_OPTIONS
</array>
<key>JVMUserOptions</key>
<dict>
DEPLOY_JVM_USER_OPTIONS
</dict>
<key>NSHighResolutionCapable</key>
<string>true</string>
<!-- hide from dock -->
<key>LSUIElement</key>
<string>1</string>
<!-- register .cryptomator bundle extension -->
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>cryptomator</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>Cryptomator.icns</string>
<key>CFBundleTypeName</key>
<string>Cryptomator Vault</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSItemContentTypes</key>
<array>
<string>org.cryptomator.folder</string>
</array>
<key>LSTypeIsPackage</key>
<true/>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>com.apple.package</string>
</array>
<key>UTTypeDescription</key>
<string>Cryptomator Vault</string>
<key>UTTypeIconFile</key>
<string>Cryptomator.icns</string>
<key>UTTypeIdentifier</key>
<string>org.cryptomator.folder</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>cryptomator</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,88 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.10.0</version>
</parent>
<artifactId>installer-osx</artifactId>
<packaging>pom</packaging>
<name>Cryptomator Mac OS X installer</name>
<properties>
<javafx.application.name>Cryptomator</javafx.application.name>
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
</properties>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<phase>prepare-package</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>create-deployment-bundle</id>
<phase>install</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
<!-- Define application to build -->
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<!-- Create main application jar -->
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
<fx:application refid="fxApp" />
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
</fx:resources>
<fx:manifest>
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
<fx:attribute name="Implementation-Version" value="${project.version}" />
</fx:manifest>
</fx:jar>
<!-- Create native package -->
<fx:deploy nativeBundles="dmg" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application refid="fxApp"/>
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform javafx="2.2+" j2se="8.0">
<fx:property name="logPath" value="~/Library/Logs/Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx2048m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />
</fx:deploy>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -0,0 +1,74 @@
;This file will be executed next to the application bundle image
;I.e. current directory will contain folder APPLICATION_NAME with application files
[Setup]
AppId={{PRODUCT_APP_IDENTIFIER}}
AppName=APPLICATION_NAME
AppVersion=APPLICATION_VERSION
AppVerName=APPLICATION_NAME APPLICATION_VERSION
AppPublisher=APPLICATION_VENDOR
AppComments=APPLICATION_COMMENTS
AppCopyright=APPLICATION_COPYRIGHT
AppPublisherURL=https://cryptomator.org/
;AppSupportURL=http://java.com/
;AppUpdatesURL=http://java.com/
DefaultDirName=APPLICATION_INSTALL_ROOT\APPLICATION_NAME
DisableStartupPrompt=Yes
DisableDirPage=No
DisableProgramGroupPage=Yes
DisableReadyPage=Yes
DisableFinishedPage=No
DisableWelcomePage=Yes
DefaultGroupName=APPLICATION_GROUP
;Optional License
LicenseFile=APPLICATION_LICENSE_FILE
;WinXP or above
MinVersion=0,5.1
OutputBaseFilename=INSTALLER_FILE_NAME
Compression=lzma
SolidCompression=yes
PrivilegesRequired=admin
SetupIconFile=APPLICATION_NAME\APPLICATION_NAME.ico
UninstallDisplayIcon={app}\APPLICATION_NAME.ico
UninstallDisplayName=APPLICATION_NAME
WizardImageStretch=No
WizardSmallImageFile=Cryptomator-setup-icon.bmp
WizardImageBackColor=$ffffff
ArchitecturesInstallIn64BitMode=ARCHITECTURE_BIT_MODE
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "APPLICATION_NAME\APPLICATION_NAME.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "APPLICATION_NAME\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_MENU_SHORTCUT()
Name: "{commondesktop}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_DESKTOP_SHORTCUT()
[Run]
Filename: "{app}\RUN_FILENAME.exe"; Description: "{cm:LaunchProgram,APPLICATION_NAME}"; Flags: nowait postinstall skipifsilent; Check: APPLICATION_NOT_SERVICE()
Filename: "{app}\RUN_FILENAME.exe"; Parameters: "-install -svcName ""APPLICATION_NAME"" -svcDesc ""APPLICATION_DESCRIPTION"" -mainExe ""APPLICATION_LAUNCHER_FILENAME"" START_ON_INSTALL RUN_AT_STARTUP"; Check: APPLICATION_SERVICE()
[UninstallRun]
Filename: "{app}\RUN_FILENAME.exe "; Parameters: "-uninstall -svcName APPLICATION_NAME STOP_ON_UNINSTALL"; Check: APPLICATION_SERVICE()
[Code]
function returnTrue(): Boolean;
begin
Result := True;
end;
function returnFalse(): Boolean;
begin
Result := False;
end;
function InitializeSetup(): Boolean;
begin
// Possible future improvements:
// if version less or same => just launch app
// if upgrade => check if same app is running and wait for it to exit
// Add pack200/unpack200 support?
Result := True;
end;

View File

@@ -0,0 +1,88 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.10.0</version>
</parent>
<artifactId>installer-win-portable</artifactId>
<packaging>pom</packaging>
<name>Cryptomator (Portable) Windows installer</name>
<properties>
<javafx.application.name>Cryptomator</javafx.application.name>
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
</properties>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<phase>prepare-package</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>create-deployment-bundle</id>
<phase>install</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
<!-- Define application to build -->
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<!-- Create main application jar -->
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
<fx:application refid="fxApp" />
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
</fx:resources>
<fx:manifest>
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
<fx:attribute name="Implementation-Version" value="${project.version}" />
</fx:manifest>
</fx:jar>
<!-- Create native package -->
<fx:deploy nativeBundles="exe" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application refid="fxApp"/>
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform javafx="2.2+" j2se="8.0">
<fx:property name="settingsPath" value="./settings.json" />
<fx:property name="logPath" value="cryptomator.log" />
</fx:platform>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="false" menu="false" shortcut="false" />
</fx:deploy>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -0,0 +1,80 @@
;This file will be executed next to the application bundle image
;I.e. current directory will contain folder APPLICATION_NAME with application files
[Setup]
AppId={{PRODUCT_APP_IDENTIFIER}}
AppName=APPLICATION_NAME
AppVersion=APPLICATION_VERSION
AppVerName=APPLICATION_NAME APPLICATION_VERSION
AppPublisher=APPLICATION_VENDOR
AppComments=APPLICATION_COMMENTS
AppCopyright=APPLICATION_COPYRIGHT
AppPublisherURL=https://cryptomator.org/
;AppSupportURL=http://java.com/
;AppUpdatesURL=http://java.com/
DefaultDirName=APPLICATION_INSTALL_ROOT\APPLICATION_NAME
DisableStartupPrompt=Yes
DisableDirPage=No
DisableProgramGroupPage=Yes
DisableReadyPage=Yes
DisableFinishedPage=No
DisableWelcomePage=Yes
DefaultGroupName=APPLICATION_GROUP
;Optional License
LicenseFile=APPLICATION_LICENSE_FILE
;WinXP or above
MinVersion=0,5.1
OutputBaseFilename=INSTALLER_FILE_NAME
Compression=lzma
SolidCompression=yes
PrivilegesRequired=admin
SetupIconFile=APPLICATION_NAME\APPLICATION_NAME.ico
UninstallDisplayIcon={app}\APPLICATION_NAME.ico
UninstallDisplayName=APPLICATION_NAME
WizardImageStretch=No
WizardSmallImageFile=Cryptomator-setup-icon.bmp
WizardImageBackColor=$ffffff
ArchitecturesInstallIn64BitMode=ARCHITECTURE_BIT_MODE
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Registry]
;Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Internet Settings"; ValueType: dword; ValueName: "AutoDetect"; ValueData: "0"
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Services\WebClient\Parameters"; ValueType: dword; ValueName: "FileSizeLimitInBytes"; ValueData: "$ffffffff"
[Files]
Source: "APPLICATION_NAME\APPLICATION_NAME.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "APPLICATION_NAME\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_MENU_SHORTCUT()
Name: "{commondesktop}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_DESKTOP_SHORTCUT()
[Run]
Filename: "{app}\RUN_FILENAME.exe"; Description: "{cm:LaunchProgram,APPLICATION_NAME}"; Flags: nowait postinstall skipifsilent; Check: APPLICATION_NOT_SERVICE()
Filename: "{app}\RUN_FILENAME.exe"; Parameters: "-install -svcName ""APPLICATION_NAME"" -svcDesc ""APPLICATION_DESCRIPTION"" -mainExe ""APPLICATION_LAUNCHER_FILENAME"" START_ON_INSTALL RUN_AT_STARTUP"; Check: APPLICATION_SERVICE()
Filename: "net"; Parameters: "stop webclient"; Description: "Stopping WebClient..."; Flags: waituntilterminated runhidden
Filename: "net"; Parameters: "start webclient"; Description: "Restarting WebClient..."; Flags: waituntilterminated runhidden
[UninstallRun]
Filename: "{app}\RUN_FILENAME.exe "; Parameters: "-uninstall -svcName APPLICATION_NAME STOP_ON_UNINSTALL"; Check: APPLICATION_SERVICE()
[Code]
function returnTrue(): Boolean;
begin
Result := True;
end;
function returnFalse(): Boolean;
begin
Result := False;
end;
function InitializeSetup(): Boolean;
begin
// Possible future improvements:
// if version less or same => just launch app
// if upgrade => check if same app is running and wait for it to exit
// Add pack200/unpack200 support?
Result := True;
end;

View File

@@ -0,0 +1,87 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.10.0</version>
</parent>
<artifactId>installer-win</artifactId>
<packaging>pom</packaging>
<name>Cryptomator Windows installer</name>
<properties>
<javafx.application.name>Cryptomator</javafx.application.name>
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
</properties>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<phase>prepare-package</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>create-deployment-bundle</id>
<phase>install</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
<!-- Define application to build -->
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<!-- Create main application jar -->
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
<fx:application refid="fxApp" />
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
</fx:resources>
<fx:manifest>
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
<fx:attribute name="Implementation-Version" value="${project.version}" />
</fx:manifest>
</fx:jar>
<!-- Create native package -->
<fx:deploy nativeBundles="exe" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application refid="fxApp"/>
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform javafx="2.2+" j2se="8.0" >
<fx:property name="logPath" value="%appdata%/Cryptomator/cryptomator.log" />
</fx:platform>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />
</fx:deploy>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,10 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. Contributors: Sebastian Stenzel - initial API and implementation -->
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.4.0</version>
<version>0.10.0</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -32,8 +39,10 @@
<commons-collections.version>4.0</commons-collections.version>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-codec.version>1.10</commons-codec.version>
<jackson-databind.version>2.4.4</jackson-databind.version>
</properties>
<commons-httpclient.version>3.1</commons-httpclient.version>
<jackson-databind.version>2.4.4</jackson-databind.version>
<mockito.version>1.10.19</mockito.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -102,6 +111,32 @@
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<!-- org.apache.httpcomponents:httpclient is newer, but jackrabbit uses this version. We don't have a reason to upgrade -->
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>${commons-httpclient.version}</version>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>2.0.1</version>
<scope>provided</scope>
</dependency>
<!-- JSON -->
<dependency>
@@ -110,13 +145,19 @@
<version>${jackson-databind.version}</version>
</dependency>
<!-- JUnit -->
<!-- JUnit / Mockito -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -137,6 +178,10 @@
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
</dependencies>
<modules>
@@ -146,7 +191,60 @@
<module>ui</module>
</modules>
<profiles>
<profile>
<id>debian</id>
<modules>
<module>installer-debian</module>
</modules>
</profile>
<profile>
<id>osx</id>
<modules>
<module>installer-osx</module>
</modules>
</profile>
<profile>
<id>win</id>
<modules>
<module>installer-win</module>
</modules>
</profile>
<profile>
<id>win-portable</id>
<modules>
<module>installer-win-portable</module>
</modules>
</profile>
<profile>
<id>uber-jar</id>
<modules>
<module>uber-jar</module>
</modules>
</profile>
</profiles>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/libs</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

57
main/uber-jar/pom.xml Normal file
View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.10.0</version>
</parent>
<artifactId>uber-jar</artifactId>
<packaging>pom</packaging>
<name>Single über jar with all dependencies</name>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<finalName>Cryptomator-${project.parent.version}</finalName>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifestEntries>
<Main-Class>org.cryptomator.ui.Cryptomator</Main-Class>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -12,18 +12,11 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.4.0</version>
<version>0.10.0</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
<properties>
<javafx.application.name>Cryptomator</javafx.application.name>
<exec.mainClass>org.cryptomator.ui.MainApplication</exec.mainClass>
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
<controlsfx.version>8.20.8</controlsfx.version>
</properties>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
@@ -39,6 +32,12 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- apache commons -->
<dependency>
@@ -49,72 +48,20 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- UI -->
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>${controlsfx.version}</version>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>${javafx.application.name}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifestEntries>
<Main-Class>${exec.mainClass}</Main-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>create-deployment-bundle</id>
<phase>install</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
<fx:deploy nativeBundles="all" outdir="${project.build.directory}/dist" outfile="${project.build.finalName}" verbose="false">
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform javafx="2.2+" j2se="8.0" />
<fx:resources>
<fx:fileset dir="${project.build.directory}" includes="${javafx.application.name}.jar" />
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />
</fx:deploy>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,149 @@
/*******************************************************************************
* Copyright (c) 2014 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
* Sebastian Stenzel - refactoring
******************************************************************************/
package org.cryptomator.ui;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import javafx.application.Application;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.SingleInstanceManager;
import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Cryptomator {
public static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
public static final CompletableFuture<Consumer<File>> OPEN_FILE_HANDLER = new CompletableFuture<>();
private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
public static void main(String[] args) {
if (SystemUtils.IS_OS_MAC_OSX) {
/*
* On OSX we're in an awkward position. We need to register a handler in the main thread of this application. However, we can't
* even pass objects to the application, so we're forced to use a static CompletableFuture for the handler, which actually opens
* the file in the application.
*
* Code taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
try {
final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
final Method getApplication = applicationClass.getMethod("getApplication");
final Object application = getApplication.invoke(null);
final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
final OpenFilesHandlerClassHandler openFilesHandlerHandler = new OpenFilesHandlerClassHandler();
final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandlerHandler);
setOpenFileHandler.invoke(application, openFilesHandlerObject);
} catch (ReflectiveOperationException | RuntimeException e) {
// Since we're trying to call OS-specific code, we'll just have
// to hope for the best.
LOG.error("exception adding OSX file open handler", e);
}
}
/*
* Perform certain things on VM termination.
*/
Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER);
/*
* Before starting the application, we check if there is already an instance running on this computer. If so, we send our command
* line arguments to that instance and quit.
*/
final Optional<RemoteInstance> remoteInstance = SingleInstanceManager.getRemoteInstance(MainApplication.APPLICATION_KEY);
if (remoteInstance.isPresent()) {
try (RemoteInstance instance = remoteInstance.get()) {
LOG.info("An instance of Cryptomator is already running at {}.", instance.getRemotePort());
for (int i = 0; i < args.length; i++) {
remoteInstance.get().sendMessage(args[i], 100);
}
} catch (Exception e) {
LOG.error("Error forwarding arguments to remote instance", e);
}
} else {
Application.launch(MainApplication.class, args);
}
}
public static void addShutdownTask(Runnable r) {
SHUTDOWN_TASKS.add(r);
}
public static void removeShutdownTask(Runnable r) {
SHUTDOWN_TASKS.remove(r);
}
private static class CleanShutdownPerformer extends Thread {
@Override
public void run() {
LOG.debug("Shutting down");
SHUTDOWN_TASKS.forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
LOG.error("exception while shutting down", e);
}
});
SHUTDOWN_TASKS.clear();
}
}
private static void handleOpenFileRequest(File file) {
try {
OPEN_FILE_HANDLER.get().accept(file);
} catch (Exception e) {
LOG.error("exception handling file open event for file " + file.getAbsolutePath(), e);
throw new RuntimeException(e);
}
}
/**
* Handler class taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
private static class OpenFilesHandlerClassHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("openFiles")) {
final Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
final Method getFiles = openFilesEventClass.getMethod("getFiles");
Object e = args[0];
try {
@SuppressWarnings("unchecked")
final List<File> ff = (List<File>) getFiles.invoke(e);
for (File f : ff) {
handleOpenFileRequest(f);
}
} catch (RuntimeException ee) {
throw ee;
} catch (Exception ee) {
throw new RuntimeException(ee);
}
}
return null;
}
}
}

View File

@@ -0,0 +1,20 @@
package org.cryptomator.ui;
import java.util.concurrent.ExecutorService;
import javax.inject.Singleton;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.util.DeferredCloser;
import dagger.Component;
@Singleton
@Component(modules = CryptomatorModule.class)
interface CryptomatorComponent {
ExecutorService executorService();
DeferredCloser deferredCloser();
MainController mainController();
}

View File

@@ -0,0 +1,101 @@
package org.cryptomator.ui;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingCryptorDecorator;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.model.VaultObjectMapperProvider;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.SettingsProvider;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.DeferredCloser.Closer;
import org.cryptomator.ui.util.SemVerComparator;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.ui.util.mount.WebDavMounterProvider;
import org.cryptomator.webdav.WebDavServer;
import com.fasterxml.jackson.databind.ObjectMapper;
import dagger.Module;
import dagger.Provides;
import javafx.application.Application;
@Module
class CryptomatorModule {
private final Application application;
private final DeferredCloser deferredCloser;
public CryptomatorModule(Application application) {
this.application = application;
this.deferredCloser = new DeferredCloser();
}
@Provides
@Singleton
Application provideApplication() {
return application;
}
@Provides
@Singleton
DeferredCloser provideDeferredCloser() {
return deferredCloser;
}
@Provides
@Singleton
@Named("SemVer")
Comparator<String> provideSemVerComparator() {
return new SemVerComparator();
}
@Provides
@Singleton
@Named("VaultJsonMapper")
ObjectMapper provideVaultObjectMapper(VaultObjectMapperProvider vaultObjectMapperProvider) {
return vaultObjectMapperProvider.get();
}
@Provides
@Singleton
Settings provideSettings(SettingsProvider settingsProvider) {
return settingsProvider.get();
}
@Provides
@Singleton
ExecutorService provideExecutorService() {
return closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown);
}
@Provides
@Singleton
WebDavMounter provideWebDavMounter(WebDavMounterProvider webDavMounterProvider) {
return webDavMounterProvider.get();
}
@Provides
@Singleton
WebDavServer provideWebDavServer() {
final WebDavServer webDavServer = new WebDavServer();
webDavServer.start();
return closeLater(webDavServer, WebDavServer::stop);
}
@Provides
Cryptor provideCryptor() {
return SamplingCryptorDecorator.decorate(new Aes256Cryptor());
}
private <T> T closeLater(T object, Closer<T> closer) {
return deferredCloser.closeLater(object, closer).get().get();
}
}

View File

@@ -1,238 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.Future;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.files.EncryptingFileVisitor;
import org.cryptomator.ui.controls.ClearOnDisableListener;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.util.FXThreads;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InitializeController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private static final int MAX_USERNAME_LENGTH = 250;
private ResourceBundle localization;
private Directory directory;
private InitializationListener listener;
@FXML
private TextField usernameField;
@FXML
private SecPasswordField passwordField;
@FXML
private SecPasswordField retypePasswordField;
@FXML
private Button okButton;
@FXML
private ProgressIndicator progressIndicator;
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.localization = rb;
usernameField.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
usernameField.textProperty().addListener(this::usernameFieldDidChange);
passwordField.textProperty().addListener(this::passwordFieldDidChange);
retypePasswordField.textProperty().addListener(this::retypePasswordFieldDidChange);
retypePasswordField.disableProperty().addListener(new ClearOnDisableListener(retypePasswordField));
}
// ****************************************
// Username field
// ****************************************
public void filterAlphanumericKeyEvents(KeyEvent t) {
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
return;
}
char c = t.getCharacter().charAt(0);
if (!CharUtils.isAsciiAlphanumeric(c)) {
t.consume();
}
}
public void usernameFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (StringUtils.length(newValue) > MAX_USERNAME_LENGTH) {
usernameField.setText(newValue.substring(0, MAX_USERNAME_LENGTH));
}
passwordField.setDisable(StringUtils.isEmpty(newValue));
}
// ****************************************
// Password field
// ****************************************
private void passwordFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
retypePasswordField.setDisable(StringUtils.isEmpty(newValue));
}
// ****************************************
// Retype password field
// ****************************************
private void retypePasswordFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText());
okButton.setDisable(!passwordsAreEqual);
}
// ****************************************
// OK button
// ****************************************
@FXML
protected void initializeVault(ActionEvent event) {
setControlsDisabled(true);
if (!isDirectoryEmpty() && !shouldEncryptExistingFiles()) {
return;
}
final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
final CharSequence password = passwordField.getCharacters();
OutputStream masterKeyOutputStream = null;
try {
progressIndicator.setVisible(true);
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
final Future<?> futureDone = FXThreads.runOnBackgroundThread(this::encryptExistingContents);
FXThreads.runOnMainThreadWhenFinished(futureDone, (result) -> {
progressIndicator.setVisible(false);
progressIndicator.setVisible(false);
directory.getCryptor().swipeSensitiveData();
if (listener != null) {
listener.didInitialize(this);
}
});
} catch (FileAlreadyExistsException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (InvalidPathException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
} catch (IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
LOG.error("I/O Exception", ex);
} finally {
usernameField.setText(null);
passwordField.swipe();
retypePasswordField.swipe();
IOUtils.closeQuietly(masterKeyOutputStream);
}
}
private void setControlsDisabled(boolean disable) {
usernameField.setDisable(disable);
passwordField.setDisable(disable);
retypePasswordField.setDisable(disable);
okButton.setDisable(disable);
}
private boolean isDirectoryEmpty() {
try {
final DirectoryStream<Path> dirContents = Files.newDirectoryStream(directory.getPath());
return !dirContents.iterator().hasNext();
} catch (IOException e) {
LOG.error("Failed to analyze directory.", e);
throw new IllegalStateException(e);
}
}
private boolean shouldEncryptExistingFiles() {
final Alert alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle(localization.getString("initialize.alert.directoryIsNotEmpty.title"));
alert.setHeaderText(null);
alert.setContentText(localization.getString("initialize.alert.directoryIsNotEmpty.content"));
final Optional<ButtonType> result = alert.showAndWait();
return ButtonType.OK.equals(result.get());
}
private void encryptExistingContents() {
try {
final FileVisitor<Path> visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile);
Files.walkFileTree(directory.getPath(), visitor);
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
}
}
private boolean shouldEncryptExistingFile(Path path) {
final String name = path.getFileName().toString();
return !directory.getPath().equals(path) && !name.endsWith(Aes256Cryptor.BASIC_FILE_EXT) && !name.endsWith(Aes256Cryptor.METADATA_FILE_EXT) && !name.endsWith(Aes256Cryptor.MASTERKEY_FILE_EXT);
}
/* Getter/Setter */
public Directory getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
this.directory = directory;
}
public InitializationListener getListener() {
return listener;
}
public void setListener(InitializationListener listener) {
this.listener = listener;
}
/* callback */
interface InitializationListener {
void didInitialize(InitializeController ctrl);
}
}

Some files were not shown because too many files have changed in this diff Show More