Compare commits

...

106 Commits
0.3.0 ... 0.6.0

Author SHA1 Message Date
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
Sebastian Stenzel
b49eb82f38 - Beta Version 0.4.0 2015-01-13 11:01:42 +01:00
Sebastian Stenzel
523f38c69e - Updated L&F for Mac OS X: Greyed out controls, if window is inactive. 2015-01-10 19:40:20 +01:00
Sebastian Stenzel
3cd3012a05 - fixes #13 2015-01-10 17:01:34 +01:00
Sebastian Stenzel
3ff8d6bc19 - fixed error during exception handling, if trying to decrypt vault with unsupported key length 2015-01-10 15:51:46 +01:00
Sebastian Stenzel
7ce6ed6abb - shows application icon in notification center 2015-01-10 15:23:49 +01:00
Sebastian Stenzel
be0b4859e3 - Adjusted win L&F of checkbox 2015-01-09 15:45:45 +01:00
Sebastian Stenzel
760b2c028f - Some minor improvements, renamed some classes 2015-01-09 15:25:44 +01:00
Sebastian Stenzel
deb10c1256 - Allows the user to configure optional MAC verification before decrypting content (Fixes #17) 2015-01-07 20:00:09 +01:00
Sebastian Stenzel
b6b3360325 - Bugfix broken settings file 2015-01-07 19:59:00 +01:00
Sebastian Stenzel
2e67910a60 - added file integrity check (#17) - not yet visible to the user 2015-01-06 11:39:31 +01:00
Sebastian Stenzel
e19cf1c942 - Changed file layout, added MAC (see #17)
- Obfuscates file size (fixes #18)
2015-01-06 01:23:16 +01:00
Sebastian Stenzel
55e758315d - bugfix: using hmac key for hmac operations 2015-01-05 22:34:02 +01:00
Sebastian Stenzel
75fe462eb3 Update README.md 2015-01-05 22:02:00 +01:00
Sebastian Stenzel
0e288f0c84 - fixes #8: Using Scrypt key derivation function now 2015-01-04 18:19:13 +01:00
Sebastian Stenzel
3f2ef3a83a - Using RFC AES 3394 Key Wrap algorithm for storing master keys
- Storing HMac key and encryption key separately
- Thanks to key wrap, simplified keyfile (no more IV needed)
2015-01-04 16:32:50 +01:00
Sebastian Stenzel
e90e001718 - Clarified license name (#10) 2015-01-01 22:30:13 +01:00
Sebastian Stenzel
1f8d4c5846 Merge pull request #12 from based2/patch-1
Various dependencies updates
2015-01-01 18:04:51 +01:00
based2
d9253be888 update to indent with tabs 2015-01-01 16:56:50 +01:00
based2
2d9fc0a8d8 Various dependencies updates 2014-12-31 13:50:03 +01:00
Sebastian Stenzel
1a076d9c1b - Using hmac_sha256(key, plaintext) instead of sha256(key || plaintext) for IV generation during filename encryption. Still references #7 2014-12-31 11:06:56 +01:00
Sebastian Stenzel
9fe135ef0f - fixes #6, simplifies password verification
- improves filename IV -> SIV using substring from sha256(secondaryKey + plaintextFilename). References #7
2014-12-31 01:21:08 +01:00
Sebastian Stenzel
4cb9da7252 - file name encryption is deterministic again (broken by fix for #7)
- improved unit test to avoid this mistake in the future
2014-12-30 20:06:05 +01:00
Sebastian Stenzel
ebea3dae65 - Increased file name IV length 2014-12-30 18:13:43 +01:00
Sebastian Stenzel
d8c9279f6f - fixes #7
- removes any use of CBC mode (might affect issue #9)
2014-12-30 17:38:57 +01:00
Sebastian Stenzel
4f91adb822 - allow reordering of directories via drag'n'drop 2014-12-28 16:46:14 +01:00
Sebastian Stenzel
cc35430dee - fixes #4 2014-12-28 14:25:53 +01:00
Sebastian Stenzel
f057fb0e8e - Updated License, included all 3rd party libraries 2014-12-28 14:19:23 +01:00
Sebastian Stenzel
f4c7dc1bbd - fixed requestFocus of password field when entering wrong password 2014-12-24 15:12:54 +01:00
Sebastian Stenzel
5bbaf62c67 - Updated version to 0.4.0-SNAPSHOT 2014-12-24 14:39:33 +01:00
125 changed files with 6405 additions and 2007 deletions

View File

@@ -2,3 +2,10 @@ language: java
jdk:
- oraclejdk8
script: mvn -fmain/pom.xml clean package
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/7d429ab35361726e26f2
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: false # default: false

20
LICENSE
View File

@@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 Sebastian Stenzel
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,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

12
LICENSES/BSD-License.txt Normal file
View File

@@ -0,0 +1,12 @@
Copyright (c) <YEAR>, <OWNER>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) <year> <copyright holders>
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.

96
NOTICE.md Normal file
View File

@@ -0,0 +1,96 @@
# CRYPTOMATOR
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.
### 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.
### 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/).
### Apache Commons Collections
Copyright 2001-2013 The Apache Software Foundation
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
### Apache Commons Codec
Copyright 2002-2013 The Apache Software Foundation
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java contains test data
from http://aspell.net/test/orig/batch0.tab. Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org)
### Apache Commons IO
Copyright 2002-2012 The Apache Software Foundation
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
### Apache Commons Lang
Copyright 2001-2011 The Apache Software Foundation
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
This product includes software from the Spring Framework,
under the Apache License 2.0 (see: StringUtils.containsWhitespace())
### ControlsFX
Copyright (c) 2013, ControlsFX
Licensed under the accompanying BSD license file.
### Apache Log4j
Copyright 1999-2012 Apache Software Foundation
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
ResolverUtil.java Copyright 2005-2006 Tim Fennell
### JUnit
Copyright (c) 2000-2006, www.hamcrest.org
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/totalvoidness/cryptomator.svg?branch=master)](https://travis-ci.org/totalvoidness/cryptomator)
[![Join the chat at https://gitter.im/totalvoidness/cryptomator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/totalvoidness/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
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.2.0/Cryptomator.dmg), [Cryptomator.exe](https://github.com/totalvoidness/cryptomator/releases/download/v0.2.0/Cryptomator.exe) or [Cryptomator.jar](https://github.com/totalvoidness/cryptomator/releases/download/v0.2.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
- No commerical 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)
- PBKDF2 key generation
### 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 masterkey 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
### Consistency
- HMAC over file contents to recognize changed ciphertext before decryption
- 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).
- 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/totalvoidness/cryptomator.git
cd cryptomator/main
git checkout v0.5.1
mvn clean install
```
## License
Distributed under the MIT license. See the LICENSE file for more info.
Distributed under the MIT X Consortium 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)

View File

@@ -12,14 +12,14 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.6.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>
<jetty.version>9.2.10.v20150310</jetty.version>
<jackrabbit.version>2.9.1</jackrabbit.version>
<commons.transaction.version>1.2</commons.transaction.version>
<jta.version>1.1</jta.version>
</properties>
@@ -48,7 +48,13 @@
<artifactId>jackrabbit-webdav</artifactId>
<version>${jackrabbit.version}</version>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- I/O -->
<dependency>
<groupId>commons-io</groupId>

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,37 @@ 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();
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 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, 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,25 +73,102 @@ 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 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 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);
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 ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection));
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
result.setInitParameter(WebDavServlet.CFG_HTTP_ROOT, contextPath);
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

@@ -6,13 +6,16 @@
* 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.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.FileTime;
import java.util.List;
import org.apache.commons.io.FilenameUtils;
@@ -132,6 +135,27 @@ abstract class AbstractEncryptedNode implements DavResource {
@Override
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());
}
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
@Override

View File

@@ -1,36 +0,0 @@
package org.cryptomator.webdav.jackrabbit;
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.DavServletRequest;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
abstract class AbstractSessionAwareWebDavResourceFactory implements DavResourceFactory {
@Override
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
final DavSession session = request.getDavSession();
if (session != null && session instanceof WebDavSession) {
return createDavResource(locator, (WebDavSession) session, request, response);
} else {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Unsupported session type.");
}
}
protected abstract DavResource createDavResource(DavResourceLocator locator, WebDavSession session, DavServletRequest request, DavServletResponse response) throws DavException;
@Override
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
if (session != null && session instanceof WebDavSession) {
return createDavResource(locator, (WebDavSession) session);
} else {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Unsupported session type.");
}
}
protected abstract DavResource createDavResource(DavResourceLocator locator, WebDavSession session);
}

View File

@@ -0,0 +1,19 @@
package org.cryptomator.webdav.jackrabbit;
import java.util.Collection;
class CryptoWarningHandler {
private final Collection<String> resourcesWithInvalidMac;
public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac) {
this.resourcesWithInvalidMac = resourcesWithInvalidMac;
}
public void macAuthFailed(String resourceName) {
if (!resourcesWithInvalidMac.contains(resourceName)) {
resourcesWithInvalidMac.add(resourceName);
}
}
}

View File

@@ -0,0 +1,242 @@
/*******************************************************************************
* 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.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.jackrabbit.webdav.DavLocatorFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.util.EncodeUtil;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.SensitiveDataSwipeListener;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
class DavLocatorFactoryImpl implements DavLocatorFactory, 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, Cryptor cryptor) {
this.fsRoot = FileSystems.getDefault().getPath(fsRoot);
this.cryptor = cryptor;
cryptor.addSensitiveDataSwipeListener(this);
}
/* DavLocatorFactory */
@Override
public DavResourceLocator createResourceLocator(String prefix, String href) {
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
final String relativeHref = StringUtils.removeStart(href, fullPrefix);
final String resourcePath = EncodeUtil.unescape(StringUtils.removeStart(relativeHref, "/"));
return new DavResourceLocatorImpl(fullPrefix, resourcePath);
}
/**
* @throws DecryptFailedRuntimeException, which should a checked exception, but Jackrabbit doesn't allow that.
*/
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
try {
final String resourcePath = (isResourcePath) ? path : getResourcePath(path);
return new DavResourceLocatorImpl(fullPrefix, resourcePath);
} catch (DecryptFailedException e) {
throw new DecryptFailedRuntimeException(e);
}
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
try {
return createResourceLocator(prefix, workspacePath, resourcePath, true);
} catch (DecryptFailedRuntimeException e) {
throw new IllegalStateException("Tried to decrypt resourcePath. Only repositoryPaths can be encrypted.", e);
}
}
/* Encryption/Decryption */
/**
* @return Encrypted absolute paths on the file system.
*/
private String getRepositoryPath(String resourcePath) {
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.
*/
private String getResourcePath(String repositoryPath) throws DecryptFailedException {
String decryptedPath = pathCache.getKey(repositoryPath);
if (decryptedPath == null) {
decryptedPath = decryptResourcePath(repositoryPath);
pathCache.put(decryptedPath, repositoryPath);
}
return decryptedPath;
}
private String decryptResourcePath(String repositoryPath) throws DecryptFailedException {
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;
}
}
/* CryptorIOSupport */
@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);
}
}
/* SensitiveDataSwipeListener */
@Override
public void swipeSensitiveData() {
pathCache.clear();
}
/* Locator */
private class DavResourceLocatorImpl implements DavResourceLocator {
private final String prefix;
private final String resourcePath;
private DavResourceLocatorImpl(String prefix, String resourcePath) {
this.prefix = prefix;
this.resourcePath = FilenameUtils.normalizeNoEndSeparator(resourcePath, true);
}
@Override
public String getPrefix() {
return prefix;
}
@Override
public String getResourcePath() {
return resourcePath;
}
@Override
public String getWorkspacePath() {
return isRootLocation() ? null : "";
}
@Override
public String getWorkspaceName() {
return getPrefix();
}
@Override
public boolean isSameWorkspace(DavResourceLocator locator) {
return (locator == null) ? false : isSameWorkspace(locator.getWorkspaceName());
}
@Override
public boolean isSameWorkspace(String workspaceName) {
return getWorkspaceName().equals(workspaceName);
}
@Override
public String getHref(boolean isCollection) {
final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
final String href = getPrefix().concat(encodedResourcePath);
if (isCollection && !href.endsWith("/")) {
return href.concat("/");
} else if (!isCollection && href.endsWith("/")) {
return href.substring(0, href.length() - 1);
} else {
return href;
}
}
@Override
public boolean isRootLocation() {
return getResourcePath() == null;
}
@Override
public DavLocatorFactory getFactory() {
return DavLocatorFactoryImpl.this;
}
@Override
public String getRepositoryPath() {
return DavLocatorFactoryImpl.this.getRepositoryPath(getResourcePath());
}
@Override
public int hashCode() {
final HashCodeBuilder builder = new HashCodeBuilder();
builder.append(prefix);
builder.append(resourcePath);
return builder.toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof DavResourceLocatorImpl) {
final DavResourceLocatorImpl other = (DavResourceLocatorImpl) obj;
final EqualsBuilder builder = new EqualsBuilder();
builder.append(this.prefix, other.prefix);
builder.append(this.resourcePath, other.resourcePath);
return builder.isEquals();
} else {
return false;
}
}
}
}

View File

@@ -10,6 +10,7 @@ package org.cryptomator.webdav.jackrabbit;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.jackrabbit.webdav.DavException;
@@ -23,20 +24,19 @@ 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 WebDavResourceFactory implements DavResourceFactory {
class DavResourceFactoryImpl implements DavResourceFactory {
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
private final ExecutorService backgroundTaskExecutor;
WebDavResourceFactory(Cryptor cryptor) {
DavResourceFactoryImpl(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor) {
this.cryptor = cryptor;
this.cryptoWarningHandler = cryptoWarningHandler;
this.backgroundTaskExecutor = backgroundTaskExecutor;
}
@Override
@@ -60,9 +60,9 @@ class WebDavResourceFactory implements DavResourceFactory {
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
final Path path = ResourcePathUtils.getPhysicalPath(locator);
if (Files.isRegularFile(path)) {
if (path != null && Files.isRegularFile(path)) {
return createFile(locator, session);
} else if (Files.isDirectory(path)) {
} else if (path != null && Files.isDirectory(path)) {
return createDirectory(locator, session);
} else {
return createNonExisting(locator, session);
@@ -70,11 +70,11 @@ class WebDavResourceFactory implements DavResourceFactory {
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor);
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
return new EncryptedFile(this, locator, session, lockManager, cryptor);
return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler);
}
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session) {

View File

@@ -8,49 +8,38 @@
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.util.HashSet;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.WebdavRequest;
class WebDavSession implements DavSession {
private final WebdavRequest request;
WebDavSession(WebdavRequest request) {
this.request = request;
}
class DavSessionImpl implements DavSession {
private final HashSet<String> lockTokens = new HashSet<String>();
private final HashSet<Object> references = new HashSet<Object>();
@Override
public void addReference(Object reference) {
// TODO Auto-generated method stub
references.add(reference);
}
@Override
public void removeReference(Object reference) {
// TODO Auto-generated method stub
references.remove(reference);
}
@Override
public void addLockToken(String token) {
// TODO Auto-generated method stub
lockTokens.add(token);
}
@Override
public String[] getLockTokens() {
// TODO Auto-generated method stub
return null;
return lockTokens.toArray(new String[lockTokens.size()]);
}
@Override
public void removeLockToken(String token) {
// TODO Auto-generated method stub
}
public WebdavRequest getRequest() {
return request;
lockTokens.remove(token);
}
}

View File

@@ -9,21 +9,28 @@
package org.cryptomator.webdav.jackrabbit;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.DavSessionProvider;
import org.apache.jackrabbit.webdav.WebdavRequest;
class WebDavSessionProvider implements DavSessionProvider {
class DavSessionProviderImpl implements DavSessionProvider {
@Override
public boolean attachSession(WebdavRequest request) throws DavException {
// every request gets a session
request.setDavSession(new WebDavSession(request));
final DavSession session = new DavSessionImpl();
session.addReference(request);
request.setDavSession(session);
return true;
}
@Override
public void releaseSession(WebdavRequest request) {
// do nothing
final DavSession session = request.getDavSession();
if (session != null) {
session.removeReference(request);
request.setDavSession(null);
}
}
}

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.io.IOException;
import java.nio.channels.SeekableByteChannel;
@@ -36,12 +36,15 @@ 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.CounterOverflowException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.webdav.exceptions.DavRuntimeException;
import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EncryptedDir extends AbstractEncryptedNode {
class EncryptedDir extends AbstractEncryptedNode {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
@@ -77,17 +80,20 @@ public class EncryptedDir extends AbstractEncryptedNode {
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);
try (final SeekableByteChannel channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
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);
} catch (CounterOverflowException e) {
// lets indicate this to the client as a "file too big" error
throw new DavException(DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE, e);
} catch (EncryptFailedException e) {
LOG.error("Encryption failed for unknown reasons.", e);
throw new IllegalStateException("Encryption failed for unknown reasons.", e);
} finally {
IOUtils.closeQuietly(channel);
IOUtils.closeQuietly(inputContext.getInputStream());
}
}
@@ -100,9 +106,14 @@ public class EncryptedDir extends AbstractEncryptedNode {
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);
try {
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), childPath.toString(), false);
final DavResource resource = factory.createResource(childLocator, session);
result.add(resource);
} catch (DecryptFailedRuntimeException e) {
LOG.warn("Decryption of resource failed: " + childPath);
continue;
}
}
return new DavResourceIteratorImpl(result);
} catch (IOException e) {
@@ -118,7 +129,9 @@ public class EncryptedDir extends AbstractEncryptedNode {
public void removeMember(DavResource member) throws DavException {
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
try {
Files.walkFileTree(memberPath, new DeletingFileVisitor());
if (Files.exists(memberPath)) {
Files.walkFileTree(memberPath, new DeletingFileVisitor());
}
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (IOException e) {

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.io.EOFException;
import java.io.IOException;
@@ -16,7 +16,6 @@ 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;
@@ -29,18 +28,23 @@ 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.crypto.exceptions.MacAuthenticationFailedException;
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 {
class EncryptedFile extends AbstractEncryptedNode {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
protected final CryptoWarningHandler cryptoWarningHandler;
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler) {
super(factory, locator, session, lockManager, cryptor);
this.cryptoWarningHandler = cryptoWarningHandler;
}
@Override
@@ -66,23 +70,23 @@ public class EncryptedFile extends AbstractEncryptedNode {
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
if (Files.isRegularFile(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);
outputContext.setContentLength(cryptor.decryptedContentLength(channel));
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final Long contentLength = cryptor.decryptedContentLength(channel);
if (contentLength != null) {
outputContext.setContentLength(contentLength);
}
if (outputContext.hasStream()) {
cryptor.decryptedFile(channel, outputContext.getOutputStream());
cryptor.decryptFile(channel, outputContext.getOutputStream());
}
} catch (EOFException e) {
LOG.warn("Unexpected end of stream (possibly client hung up).");
} catch (IOException e) {
LOG.error("Error reading file " + path.toString(), e);
throw new IORuntimeException(e);
} finally {
IOUtils.closeQuietly(channel);
} catch (MacAuthenticationFailedException e) {
cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
}
}
}
@@ -91,12 +95,15 @@ public class EncryptedFile extends AbstractEncryptedNode {
protected void determineProperties() {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final Long contentLength = cryptor.decryptedContentLength(channel);
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
} catch (IOException e) {
LOG.error("Error reading filesize " + path.toString(), e);
throw new IORuntimeException(e);
}
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())));
@@ -104,8 +111,6 @@ public class EncryptedFile extends AbstractEncryptedNode {
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
throw new IORuntimeException(e);
} finally {
IOUtils.closeQuietly(channel);
}
}
}

View File

@@ -1,15 +1,17 @@
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.ClosedByInterruptException;
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 java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
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;
@@ -21,22 +23,26 @@ 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.webdav.exceptions.IORuntimeException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
/**
* 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 {
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 = '-';
private static final Cache<DavResourceLocator, MacAuthenticationJob> cachedMacAuthenticationJobs = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
/**
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
@@ -50,13 +56,23 @@ public class EncryptedFilePart extends EncryptedFile {
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) {
super(factory, locator, session, lockManager, cryptor);
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler,
ExecutorService backgroundTaskExecutor) {
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (rangeHeader == null) {
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
}
determineByteRanges(rangeHeader);
synchronized (cachedMacAuthenticationJobs) {
if (cachedMacAuthenticationJobs.getIfPresent(locator) == null) {
final MacAuthenticationJob macAuthJob = new MacAuthenticationJob(locator);
cachedMacAuthenticationJobs.put(locator, macAuthJob);
backgroundTaskExecutor.submit(macAuthJob);
}
}
}
private void determineByteRanges(String rangeHeader) {
@@ -111,11 +127,9 @@ public class EncryptedFilePart extends EncryptedFile {
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
if (Files.isRegularFile(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final Long fileSize = cryptor.decryptedContentLength(channel);
final Pair<Long, Long> range = getUnionRange(fileSize);
final Long rangeLength = range.getRight() - range.getLeft() + 1;
@@ -128,11 +142,8 @@ public class EncryptedFilePart extends EncryptedFile {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
}
} catch (IOException e) {
LOG.error("Error reading file " + path.toString(), e);
throw new IORuntimeException(e);
} finally {
IOUtils.closeQuietly(channel);
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
}
}
}
@@ -141,4 +152,48 @@ public class EncryptedFilePart extends EncryptedFile {
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
}
private class MacAuthenticationJob implements Runnable {
private final DavResourceLocator locator;
public MacAuthenticationJob(final DavResourceLocator locator) {
if (locator == null) {
throw new IllegalArgumentException("locator must not be null.");
}
this.locator = locator;
}
@Override
public void run() {
final Path path = ResourcePathUtils.getPhysicalPath(locator);
if (Files.isRegularFile(path) && Files.isReadable(path)) {
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final boolean authentic = cryptor.isAuthentic(channel);
if (!authentic) {
cryptoWarningHandler.macAuthFailed(locator.getResourcePath());
}
} catch (ClosedByInterruptException ex) {
LOG.debug("Couldn't finish MAC verification due to interruption of worker thread.");
} catch (IOException e) {
LOG.error("IOException during MAC verification of " + path.toString(), e);
}
}
}
@Override
public int hashCode() {
return locator.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof MacAuthenticationJob) {
final MacAuthenticationJob other = (MacAuthenticationJob) obj;
return this.locator.equals(other.locator);
} else {
return false;
}
}
}
}

View File

@@ -6,9 +6,10 @@
* 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;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
@@ -25,4 +26,9 @@ final class FileTimeUtils {
return DateTimeFormatter.RFC_1123_DATE_TIME.format(date);
}
static FileTime fromRfc1123String(String string) {
final Instant instant = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(string));
return FileTime.from(instant);
}
}

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,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
@@ -21,7 +21,7 @@ import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.cryptomator.crypto.Cryptor;
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);
@@ -34,7 +34,7 @@ public class NonExistingNode extends AbstractEncryptedNode {
@Override
public boolean isCollection() {
throw new UnsupportedOperationException("Resource doesn't exist.");
return false;
}
@Override

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.FileSystems;
import java.nio.file.Path;
@@ -14,7 +14,7 @@ import java.nio.file.Path;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceLocator;
public final class ResourcePathUtils {
final class ResourcePathUtils {
private ResourcePathUtils() {
throw new IllegalStateException("not instantiable");

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 WebDavLocatorFactory 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>
WebDavLocatorFactory(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

@@ -8,6 +8,11 @@
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
@@ -22,29 +27,44 @@ import org.cryptomator.crypto.Cryptor;
public class WebDavServlet extends AbstractWebdavServlet {
private static final long serialVersionUID = 7965170007048673022L;
public static final String CFG_FS_ROOT = "oce.fs.root";
public static final String CFG_HTTP_ROOT = "oce.http.root";
public static final String CFG_FS_ROOT = "cfg.fs.root";
private DavSessionProvider davSessionProvider;
private DavLocatorFactory davLocatorFactory;
private DavResourceFactory davResourceFactory;
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
private ExecutorService backgroundTaskExecutor;
public WebDavServlet(final Cryptor cryptor) {
public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection) {
super();
this.cryptor = cryptor;
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection);
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
davSessionProvider = new WebDavSessionProvider();
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
final String httpRoot = config.getInitParameter(CFG_HTTP_ROOT);
this.davLocatorFactory = new WebDavLocatorFactory(fsRoot, httpRoot, cryptor);
backgroundTaskExecutor = Executors.newCachedThreadPool();
davSessionProvider = new DavSessionProviderImpl();
davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, cryptor);
davResourceFactory = new DavResourceFactoryImpl(cryptor, cryptoWarningHandler, backgroundTaskExecutor);
}
this.davResourceFactory = new WebDavResourceFactory(cryptor);
@Override
public void destroy() {
backgroundTaskExecutor.shutdown();
try {
final boolean tasksFinished = backgroundTaskExecutor.awaitTermination(2, TimeUnit.SECONDS);
if (!tasksFinished) {
backgroundTaskExecutor.shutdownNow();
}
} catch (InterruptedException e) {
backgroundTaskExecutor.shutdownNow();
Thread.currentThread().interrupt();
} finally {
super.destroy();
}
}
@Override

View File

@@ -12,17 +12,28 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.6.0</version>
</parent>
<artifactId>crypto-aes</artifactId>
<name>Cryptomator cryptographic module (AES)</name>
<description>Provides stream ciphers and filename pseudonymization functions.</description>
<properties>
<bouncycastle.version>1.51</bouncycastle.version>
</properties>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>crypto-api</artifactId>
</dependency>
<!-- Bouncycastle -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- Commons -->
<dependency>

View File

@@ -8,46 +8,49 @@
******************************************************************************/
package org.cryptomator.crypto.aes256;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.zip.CRC32;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.crypto.generators.SCrypt;
import org.cryptomator.crypto.AbstractCryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.aes256.CounterAwareInputStream.CounterAwareInputLimitReachedException;
import org.cryptomator.crypto.exceptions.CounterOverflowException;
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.WrongPasswordException;
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
@@ -58,26 +61,18 @@ import com.fasterxml.jackson.databind.ObjectMapper;
public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicConfiguration, FileNamingConventions {
/**
* Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction
* Policy Files isn't installed. Those files can be downloaded here: http://www.oracle.com/technetwork/java/javase/downloads/.
*/
private static final int AES_KEY_LENGTH_IN_BITS;
/**
* PRNG for cryptographically secure random numbers. Defaults to SHA1-based number generator.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecureRandom
*/
private static final SecureRandom SECURE_PRNG;
/**
* Factory for deriveing keys. Defaults to PBKDF2/HMAC-SHA1.
*
* @see PKCS #5, defined in RFC 2898
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecretKeyFactory
*/
private static final SecretKeyFactory PBKDF2_FACTORY;
/**
* Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE isn't installed. JCE can be
* installed from here: http://www.oracle.com/technetwork/java/javase/downloads/.
*/
private static final int AES_KEY_LENGTH;
private final SecureRandom securePrng;
/**
* Jackson JSON-Mapper.
@@ -85,19 +80,20 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* The decrypted master key. Its lifecycle starts with {@link #randomData(int)} or {@link #encryptMasterKey(Path, CharSequence)}. Its
* lifecycle ends with {@link #swipeSensitiveData()}.
* The decrypted master key. Its lifecycle starts with the construction of an Aes256Cryptor instance or
* {@link #decryptMasterKey(InputStream, CharSequence)}. Its lifecycle ends with {@link #swipeSensitiveData()}.
*/
private final byte[] masterKey = new byte[MASTER_KEY_LENGTH];
private SecretKey primaryMasterKey;
private static final int SIZE_OF_LONG = Long.BYTES;
/**
* Decrypted secondary key used for hmac operations.
*/
private SecretKey hMacMasterKey;
static {
try {
PBKDF2_FACTORY = SecretKeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
SECURE_PRNG = SecureRandom.getInstance(PRNG_ALGORITHM);
final int maxKeyLen = Cipher.getMaxAllowedKeyLength(CRYPTO_ALGORITHM);
AES_KEY_LENGTH = (maxKeyLen >= 256) ? 256 : maxKeyLen;
final int maxKeyLength = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
AES_KEY_LENGTH_IN_BITS = (maxKeyLength >= PREF_MASTER_KEY_LENGTH_IN_BITS) ? PREF_MASTER_KEY_LENGTH_IN_BITS : maxKeyLength;
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Algorithm should exist.", e);
}
@@ -107,18 +103,19 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* Creates a new Cryptor with a newly initialized PRNG.
*/
public Aes256Cryptor() {
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
SECURE_PRNG.nextBytes(this.masterKey);
}
/**
* Creates a new Cryptor with the given PRNG.<br/>
* <strong>DO NOT USE IN PRODUCTION</strong>. This constructor must only be used in in unit tests. Do not change method visibility.
*
* @param prng Fast, possibly insecure PRNG.
*/
Aes256Cryptor(Random prng) {
prng.nextBytes(this.masterKey);
byte[] bytes = new byte[AES_KEY_LENGTH_IN_BITS / Byte.SIZE];
try {
securePrng = SecureRandom.getInstance(PRNG_ALGORITHM);
securePrng.setSeed(securePrng.generateSeed(PRNG_SEED_LENGTH));
securePrng.nextBytes(bytes);
this.primaryMasterKey = new SecretKeySpec(bytes, AES_KEY_ALGORITHM);
securePrng.nextBytes(bytes);
this.hMacMasterKey = new SecretKeySpec(bytes, HMAC_KEY_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("PRNG algorithm should exist.", e);
} finally {
Arrays.fill(bytes, (byte) 0);
}
}
/**
@@ -128,26 +125,25 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
try {
// derive key:
final byte[] userSalt = randomData(SALT_LENGTH);
final SecretKey userKey = pbkdf2(password, userSalt, PBKDF2_PW_ITERATIONS, AES_KEY_LENGTH);
final byte[] kekSalt = randomData(SCRYPT_SALT_LENGTH);
final SecretKey kek = scrypt(password, kekSalt, SCRYPT_COST_PARAM, SCRYPT_BLOCK_SIZE, AES_KEY_LENGTH_IN_BITS);
// encrypt:
final byte[] iv = randomData(AES_BLOCK_LENGTH);
final Cipher encCipher = this.cipher(MASTERKEY_CIPHER, userKey, iv, Cipher.ENCRYPT_MODE);
byte[] encryptedUserKey = encCipher.doFinal(userKey.getEncoded());
byte[] encryptedMasterKey = encCipher.doFinal(this.masterKey);
final Cipher encCipher = aesKeyWrapCipher(kek, Cipher.WRAP_MODE);
byte[] wrappedPrimaryKey = encCipher.wrap(primaryMasterKey);
byte[] wrappedSecondaryKey = encCipher.wrap(hMacMasterKey);
// save encrypted masterkey:
final Key key = new Key();
key.setIterations(PBKDF2_PW_ITERATIONS);
key.setIv(iv);
key.setKeyLength(AES_KEY_LENGTH);
key.setMasterkey(encryptedMasterKey);
key.setSalt(userSalt);
key.setPwVerification(encryptedUserKey);
objectMapper.writeValue(out, key);
} catch (IllegalBlockSizeException | BadPaddingException ex) {
throw new IllegalStateException("Block size hard coded. Padding irrelevant in ENCRYPT_MODE. IV must exist in CBC mode.", ex);
final KeyFile keyfile = new KeyFile();
keyfile.setScryptSalt(kekSalt);
keyfile.setScryptCostParam(SCRYPT_COST_PARAM);
keyfile.setScryptBlockSize(SCRYPT_BLOCK_SIZE);
keyfile.setKeyLength(AES_KEY_LENGTH_IN_BITS);
keyfile.setPrimaryMasterKey(wrappedPrimaryKey);
keyfile.setHMacMasterKey(wrappedSecondaryKey);
objectMapper.writeValue(out, keyfile);
} catch (InvalidKeyException | IllegalBlockSizeException ex) {
throw new IllegalStateException("Invalid hard coded configuration.", ex);
}
}
@@ -162,55 +158,63 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
*/
@Override
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
byte[] decrypted = new byte[0];
try {
// load encrypted masterkey:
final Key key = objectMapper.readValue(in, Key.class);
final KeyFile keyfile = objectMapper.readValue(in, KeyFile.class);
// check, whether the key length is supported:
final int maxKeyLen = Cipher.getMaxAllowedKeyLength(CRYPTO_ALGORITHM);
if (key.getKeyLength() > maxKeyLen) {
throw new UnsupportedKeyLengthException(key.getKeyLength(), maxKeyLen);
final int maxKeyLen = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
if (keyfile.getKeyLength() > maxKeyLen) {
throw new UnsupportedKeyLengthException(keyfile.getKeyLength(), maxKeyLen);
}
// derive key:
final SecretKey userKey = pbkdf2(password, key.getSalt(), key.getIterations(), key.getKeyLength());
final SecretKey kek = scrypt(password, keyfile.getScryptSalt(), keyfile.getScryptCostParam(), keyfile.getScryptBlockSize(), AES_KEY_LENGTH_IN_BITS);
// check password:
final Cipher encCipher = this.cipher(MASTERKEY_CIPHER, userKey, key.getIv(), Cipher.ENCRYPT_MODE);
byte[] encryptedUserKey = encCipher.doFinal(userKey.getEncoded());
if (!Arrays.equals(key.getPwVerification(), encryptedUserKey)) {
throw new WrongPasswordException();
}
// decrypt and check password by catching AEAD exception
final Cipher decCipher = aesKeyWrapCipher(kek, Cipher.UNWRAP_MODE);
SecretKey primary = (SecretKey) decCipher.unwrap(keyfile.getPrimaryMasterKey(), AES_KEY_ALGORITHM, Cipher.SECRET_KEY);
SecretKey secondary = (SecretKey) decCipher.unwrap(keyfile.getHMacMasterKey(), HMAC_KEY_ALGORITHM, Cipher.SECRET_KEY);
// decrypt:
final Cipher decCipher = this.cipher(MASTERKEY_CIPHER, userKey, key.getIv(), Cipher.DECRYPT_MODE);
decrypted = decCipher.doFinal(key.getMasterkey());
// everything ok, move decrypted data to masterkey:
final ByteBuffer masterKeyBuffer = ByteBuffer.wrap(this.masterKey);
masterKeyBuffer.put(decrypted);
} catch (IllegalBlockSizeException | BadPaddingException | BufferOverflowException ex) {
throw new DecryptFailedException(ex);
// everything ok, assign decrypted keys:
this.primaryMasterKey = primary;
this.hMacMasterKey = secondary;
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("Algorithm should exist.", ex);
} finally {
Arrays.fill(decrypted, (byte) 0);
} catch (InvalidKeyException e) {
throw new WrongPasswordException();
}
}
/**
* Overwrites the {@link #masterKey} with zeros. As masterKey is a final field, this operation is ensured to work on its actual data.
* Otherwise developers could accidentally just assign a new object to the variable.
*/
@Override
public void swipeSensitiveDataInternal() {
Arrays.fill(this.masterKey, (byte) 0);
destroyQuietly(primaryMasterKey);
destroyQuietly(hMacMasterKey);
}
private Cipher cipher(String cipherTransformation, SecretKey key, byte[] iv, int cipherMode) {
private void destroyQuietly(Destroyable d) {
try {
final Cipher cipher = Cipher.getInstance(cipherTransformation);
d.destroy();
} catch (DestroyFailedException e) {
// ignore
}
}
private Cipher aesKeyWrapCipher(SecretKey key, int cipherMode) {
try {
final Cipher cipher = Cipher.getInstance(AES_KEYWRAP_CIPHER);
cipher.init(cipherMode, key);
return cipher;
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException("Invalid key.", ex);
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
throw new IllegalStateException("Algorithm/Padding should exist and accept GCM specs.", ex);
}
}
private Cipher aesCtrCipher(SecretKey key, byte[] iv, int cipherMode) {
try {
final Cipher cipher = Cipher.getInstance(AES_CTR_CIPHER);
cipher.init(cipherMode, key, new IvParameterSpec(iv));
return cipher;
} catch (InvalidKeyException ex) {
@@ -220,66 +224,64 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
private Cipher aesEcbCipher(SecretKey key, int cipherMode) {
try {
final Cipher cipher = Cipher.getInstance(AES_ECB_CIPHER);
cipher.init(cipherMode, key);
return cipher;
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException("Invalid key.", ex);
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
throw new AssertionError("Every implementation of the Java platform is required to support AES/ECB/PKCS5Padding.", ex);
}
}
private Mac hmacSha256(SecretKey key) {
try {
final Mac mac = Mac.getInstance(HMAC_KEY_ALGORITHM);
mac.init(key);
return mac;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256.", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("Invalid key", e);
}
}
private byte[] randomData(int length) {
final byte[] result = new byte[length];
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
SECURE_PRNG.nextBytes(result);
securePrng.nextBytes(result);
return result;
}
private SecretKey pbkdf2(byte[] password, byte[] salt, int iterations, int keyLength) {
final char[] pw = new char[password.length];
private SecretKey scrypt(CharSequence password, byte[] salt, int costParam, int blockSize, int keyLengthInBits) {
// use sb, as password.toString's implementation is unknown
final StringBuilder sb = new StringBuilder(password);
final byte[] pw = sb.toString().getBytes();
try {
byteToChar(password, pw);
return pbkdf2(CharBuffer.wrap(pw), salt, iterations, keyLength);
final byte[] key = SCrypt.generate(pw, salt, costParam, blockSize, 1, keyLengthInBits / Byte.SIZE);
return new SecretKeySpec(key, AES_KEY_ALGORITHM);
} finally {
Arrays.fill(pw, (char) 0);
// destroy copied bytes of the plaintext password:
Arrays.fill(pw, (byte) 0);
for (int i = 0; i < password.length(); i++) {
sb.setCharAt(i, (char) 0);
}
}
}
private SecretKey pbkdf2(CharSequence password, byte[] salt, int iterations, int keyLength) {
final int pwLen = password.length();
final char[] pw = new char[pwLen];
CharBuffer.wrap(password).get(pw, 0, pwLen);
try {
final KeySpec specs = new PBEKeySpec(pw, salt, iterations, keyLength);
final SecretKey pbkdf2Key = PBKDF2_FACTORY.generateSecret(specs);
final SecretKey aesKey = new SecretKeySpec(pbkdf2Key.getEncoded(), CRYPTO_ALGORITHM);
return aesKey;
} catch (InvalidKeySpecException ex) {
throw new IllegalStateException("Specs are hard-coded.", ex);
} finally {
Arrays.fill(pw, (char) 0);
}
}
private void byteToChar(byte[] source, char[] destination) {
if (source.length != destination.length) {
throw new IllegalArgumentException("char[] needs to be the same length as byte[]");
}
for (int i = 0; i < source.length; i++) {
destination[i] = (char) (source[i] & 0xFF);
}
}
private long crc32Sum(byte[] source) {
final CRC32 crc32 = new CRC32();
crc32.update(source);
return crc32.getValue();
}
@Override
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
try {
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
final String[] cleartextPathComps = StringUtils.split(cleartextPath, cleartextPathSep);
final List<String> encryptedPathComps = new ArrayList<>(cleartextPathComps.length);
for (final String cleartext : cleartextPathComps) {
final String encrypted = encryptPathComponent(cleartext, key, ioSupport);
final String encrypted = encryptPathComponent(cleartext, primaryMasterKey, hMacMasterKey, ioSupport);
encryptedPathComps.add(encrypted);
}
return StringUtils.join(encryptedPathComps, encryptedPathSep);
} catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
} catch (InvalidKeyException | IOException e) {
throw new IllegalStateException("Unable to encrypt path: " + cleartextPath, e);
}
}
@@ -299,36 +301,36 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* These alternative names consist of the checksum, a unique id and a special file extension defined in
* {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
*/
private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.ENCRYPT_MODE);
final byte[] cleartextBytes = cleartext.getBytes(Charsets.UTF_8);
final byte[] encryptedBytes = cipher.doFinal(cleartextBytes);
final String encrypted = ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes) + BASIC_FILE_EXT;
private String encryptPathComponent(final String cleartext, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException {
final byte[] cleartextBytes = cleartext.getBytes(StandardCharsets.UTF_8);
if (encrypted.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
final String crc32 = Long.toHexString(crc32Sum(encrypted.getBytes()));
final String metadataFilename = crc32 + METADATA_FILE_EXT;
// encrypt:
final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(aesKey, macKey, cleartextBytes);
final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
final String groupPrefix = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
final String metadataFilename = groupPrefix + METADATA_FILE_EXT;
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
final String alternativeFileName = crc32 + LONG_NAME_PREFIX_SEPARATOR + metadata.getOrCreateUuidForEncryptedFilename(encrypted).toString() + LONG_NAME_FILE_EXT;
final String alternativeFileName = groupPrefix + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + LONG_NAME_FILE_EXT;
this.storeMetadata(ioSupport, metadataFilename, metadata);
return alternativeFileName;
} else {
return encrypted;
return ivAndCiphertext + BASIC_FILE_EXT;
}
}
@Override
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException {
try {
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep);
final List<String> cleartextPathComps = new ArrayList<>(encryptedPathComps.length);
for (final String encrypted : encryptedPathComps) {
final String cleartext = decryptPathComponent(encrypted, key, ioSupport);
final String cleartext = decryptPathComponent(encrypted, primaryMasterKey, hMacMasterKey, ioSupport);
cleartextPathComps.add(new String(cleartext));
}
return StringUtils.join(cleartextPathComps, cleartextPathSep);
} catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
} catch (InvalidKeyException | IOException e) {
throw new IllegalStateException("Unable to decrypt path: " + encryptedPath, e);
}
}
@@ -336,13 +338,13 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
/**
* @see #encryptPathComponent(String, SecretKey, CryptorIOSupport)
*/
private String decryptPathComponent(final String encrypted, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
private String decryptPathComponent(final String encrypted, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException, DecryptFailedException {
final String ciphertext;
if (encrypted.endsWith(LONG_NAME_FILE_EXT)) {
final String basename = StringUtils.removeEnd(encrypted, LONG_NAME_FILE_EXT);
final String crc32 = StringUtils.substringBefore(basename, LONG_NAME_PREFIX_SEPARATOR);
final String uuid = StringUtils.substringAfter(basename, LONG_NAME_PREFIX_SEPARATOR);
final String metadataFilename = crc32 + METADATA_FILE_EXT;
final String groupPrefix = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
final String metadataFilename = groupPrefix + METADATA_FILE_EXT;
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (encrypted.endsWith(BASIC_FILE_EXT)) {
@@ -351,10 +353,11 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
throw new IllegalArgumentException("Unsupported path component: " + encrypted);
}
final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.DECRYPT_MODE);
// decrypt:
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
final byte[] cleartextBytes = cipher.doFinal(encryptedBytes);
return new String(cleartextBytes, Charsets.UTF_8);
final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(aesKey, macKey, encryptedBytes);
return new String(cleartextBytes, StandardCharsets.UTF_8);
}
private LongFilenameMetadata getMetadata(CryptorIOSupport ioSupport, String metadataFile) throws IOException {
@@ -372,61 +375,151 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
final ByteBuffer sizeBuffer = ByteBuffer.allocate(SIZE_OF_LONG);
final int read = encryptedFile.read(sizeBuffer);
if (read == SIZE_OF_LONG) {
return sizeBuffer.getLong(0);
} else {
// skip 128bit IV + 256 bit MAC:
encryptedFile.position(48);
// read encrypted value:
final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
final int numFileSizeBytesRead = encryptedFile.read(encryptedFileSizeBuffer);
// return "unknown" value, if EOF
if (numFileSizeBytesRead != encryptedFileSizeBuffer.capacity()) {
return null;
}
// decrypt size:
try {
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.DECRYPT_MODE);
final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedFileSizeBuffer.array());
final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize);
return fileSizeBuffer.getLong();
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException(e);
}
}
@Override
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
// skip content size:
encryptedFile.position(SIZE_OF_LONG);
private void encryptedContentLength(SeekableByteChannel encryptedFile, Long contentLength) throws IOException {
final ByteBuffer encryptedFileSizeBuffer;
// read iv:
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
final int read = encryptedFile.read(countingIv);
if (read != AES_BLOCK_LENGTH) {
throw new IOException("Failed to read encrypted file header.");
// encrypt content length in ECB mode (content length is less than one block):
try {
final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
fileSizeBuffer.putLong(contentLength);
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
encryptedFileSizeBuffer = ByteBuffer.wrap(encryptedFileSize);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
}
// derive secret key and generate cipher:
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.DECRYPT_MODE);
// skip 128bit IV + 256 bit MAC:
encryptedFile.position(48);
// write result:
encryptedFile.write(encryptedFileSizeBuffer);
}
@Override
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
// init mac:
final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
// read stored mac:
encryptedFile.position(16);
final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
final int numMacBytesRead = encryptedFile.read(storedMac);
// check validity of header:
if (numMacBytesRead != calculatedMac.getMacLength()) {
throw new IOException("Failed to read file header.");
}
// go to begin of content:
encryptedFile.position(64);
// calculated MAC
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream macIn = new MacInputStream(in, calculatedMac);
IOUtils.copyLarge(macIn, new NullOutputStream());
// compare (in constant time):
return MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
// read iv:
encryptedFile.position(0);
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
final int numIvBytesRead = encryptedFile.read(countingIv);
// init mac:
final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
// read stored mac:
final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
final int numMacBytesRead = encryptedFile.read(storedMac);
// read file size:
final Long fileSize = decryptedContentLength(encryptedFile);
// check validity of header:
if (numIvBytesRead != AES_BLOCK_LENGTH || numMacBytesRead != calculatedMac.getMacLength() || fileSize == null) {
throw new IOException("Failed to read file header.");
}
// go to begin of content:
encryptedFile.position(64);
// generate cipher:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream cipheredIn = new CipherInputStream(in, cipher);
return IOUtils.copyLarge(cipheredIn, plaintextFile);
final InputStream macIn = new MacInputStream(in, calculatedMac);
final InputStream cipheredIn = new CipherInputStream(macIn, cipher);
final long bytesDecrypted = IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
// drain remaining bytes to /dev/null to complete MAC calculation:
IOUtils.copyLarge(macIn, new NullOutputStream());
// compare (in constant time):
final boolean macMatches = MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
if (!macMatches) {
// This exception will be thrown AFTER we sent the decrypted content to the user.
// This has two advantages:
// - we don't need to read files twice
// - we can still restore files suffering from non-malicious bit rotting
// Anyway me MUST make sure to warn the user. This will be done by the UI when catching this exception.
throw new MacAuthenticationFailedException("MAC authentication failed.");
}
return bytesDecrypted;
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
// skip content size:
encryptedFile.position(SIZE_OF_LONG);
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
// read iv:
encryptedFile.position(0);
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
final int read = encryptedFile.read(countingIv);
if (read != AES_BLOCK_LENGTH) {
throw new IOException("Failed to read encrypted file header.");
final int numIvBytesRead = encryptedFile.read(countingIv);
// check validity of header:
if (numIvBytesRead != AES_BLOCK_LENGTH) {
throw new IOException("Failed to read file header.");
}
// seek relevant position and update iv:
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, firstRelevantBlock);
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, (int) firstRelevantBlock); // int-cast is possible, as max file size is 64GiB
// fast forward stream:
encryptedFile.position(SIZE_OF_LONG + AES_BLOCK_LENGTH + beginOfFirstRelevantBlock);
encryptedFile.position(64l + beginOfFirstRelevantBlock);
// derive secret key and generate cipher:
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.DECRYPT_MODE);
// generate cipher:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
@@ -435,41 +528,68 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
// truncate file
encryptedFile.truncate(0);
// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, 0l);
countingIv.position(0);
// derive secret key and generate cipher:
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.ENCRYPT_MODE);
// 8 bytes (file size: temporarily -1):
final ByteBuffer fileSize = ByteBuffer.allocate(SIZE_OF_LONG);
fileSize.putLong(-1L);
fileSize.position(0);
encryptedFile.write(fileSize);
// 16 bytes (iv):
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0);
encryptedFile.write(countingIv);
// init crypto stuff:
final Mac mac = this.hmacSha256(hMacMasterKey);
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.ENCRYPT_MODE);
// init mac buffer and skip 32 bytes
final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
encryptedFile.write(macBuffer);
// encrypt and write "zero length" as a placeholder, which will be read by concurrent requests, as long as encryption didn't finish:
encryptedContentLength(encryptedFile, 0l);
// write content:
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
final OutputStream cipheredOut = new CipherOutputStream(out, cipher);
final Long actualSize = IOUtils.copyLarge(plaintextFile, cipheredOut);
final OutputStream macOut = new MacOutputStream(out, mac);
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
final InputStream lengthLimitingIn = new CounterAwareInputStream(plaintextFile);
final Long plaintextSize;
try {
plaintextSize = IOUtils.copyLarge(lengthLimitingIn, blockSizeBufferedOut);
} catch (CounterAwareInputLimitReachedException ex) {
encryptedFile.truncate(64l + CounterAwareInputStream.SIXTY_FOUR_GIGABYE);
encryptedContentLength(encryptedFile, CounterAwareInputStream.SIXTY_FOUR_GIGABYE);
// no additional padding needed here, as 64GiB is a multiple of 128bit
throw new CounterOverflowException("File size exceeds limit (64Gib). Aborting to prevent counter overflow.");
}
// write filesize
fileSize.position(0);
fileSize.putLong(actualSize);
fileSize.position(0);
encryptedFile.position(0);
encryptedFile.write(fileSize);
// ensure total byte count is a multiple of the block size, in CTR mode:
final int remainderToFillLastBlock = AES_BLOCK_LENGTH - (int) (plaintextSize % AES_BLOCK_LENGTH);
blockSizeBufferedOut.write(new byte[remainderToFillLastBlock]);
return actualSize;
// for filesizes of up to 16GiB: append a few blocks of fake data:
if (plaintextSize < (long) (Integer.MAX_VALUE / 4) * AES_BLOCK_LENGTH) {
final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks);
final byte[] emptyBytes = this.randomData(AES_BLOCK_LENGTH);
for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
blockSizeBufferedOut.write(emptyBytes);
}
}
blockSizeBufferedOut.flush();
// write MAC of total ciphertext:
macBuffer.clear();
macBuffer.put(mac.doFinal());
macBuffer.flip();
encryptedFile.position(16); // right behind the IV
encryptedFile.write(macBuffer); // 256 bit MAC
// encrypt and write plaintextSize:
encryptedContentLength(encryptedFile, plaintextSize);
return plaintextSize;
}
@Override

View File

@@ -11,29 +11,29 @@ package org.cryptomator.crypto.aes256;
interface AesCryptographicConfiguration {
/**
* Number of bytes used as seed for the PRNG.
* Number of bytes used as salt, where needed.
*/
int PRNG_SEED_LENGTH = 16;
int SCRYPT_SALT_LENGTH = 8;
/**
* Scrypt CPU/Memory cost parameter.
*/
int SCRYPT_COST_PARAM = 1 << 14;
/**
* Scrypt block size (affects memory consumption)
*/
int SCRYPT_BLOCK_SIZE = 8;
/**
* Number of bytes of the master key. Should be the maximum possible AES key length to provide best security.
*/
int MASTER_KEY_LENGTH = 256;
int PREF_MASTER_KEY_LENGTH_IN_BITS = 256;
/**
* Number of bytes used as salt, where needed.
* Number of bytes used as seed for the PRNG.
*/
int SALT_LENGTH = 8;
/**
* 0-filled salt.
*/
byte[] EMPTY_SALT = new byte[SALT_LENGTH];
/**
* Algorithm used for key derivation.
*/
String KEY_FACTORY_ALGORITHM = "PBKDF2WithHmacSHA1";
int PRNG_SEED_LENGTH = 32;
/**
* Algorithm used for random number generation.
@@ -45,28 +45,34 @@ interface AesCryptographicConfiguration {
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#AlgorithmParameters
*/
String CRYPTO_ALGORITHM = "AES";
String AES_KEY_ALGORITHM = "AES";
/**
* Cipher specs for masterkey encryption.
* Key algorithm for keyed MAC.
*/
String HMAC_KEY_ALGORITHM = "HmacSHA256";
/**
* Cipher specs for RFC 3394 masterkey encryption.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
*/
String MASTERKEY_CIPHER = "AES/CBC/PKCS5Padding";
String AES_KEYWRAP_CIPHER = "AESWrap";
/**
* Cipher specs for file name encryption.
* Cipher specs for file name and file content encryption. Using CTR-mode for random access.<br/>
* <strong>Important</strong>: As JCE doesn't support a padding, input must be a multiple of the block size.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
*/
String FILE_NAME_CIPHER = "AES/CBC/PKCS5Padding";
String AES_CTR_CIPHER = "AES/CTR/NoPadding";
/**
* Cipher specs for content encryption. Using CTR-mode for random access.
* Cipher specs for single block encryption (like file size).
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl
*/
String FILE_CONTENT_CIPHER = "AES/CTR/NoPadding";
String AES_ECB_CIPHER = "AES/ECB/PKCS5Padding";
/**
* AES block size is 128 bit or 16 bytes.
@@ -74,19 +80,10 @@ interface AesCryptographicConfiguration {
int AES_BLOCK_LENGTH = 16;
/**
* 0-filled initialization vector.
* 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.
*/
byte[] EMPTY_IV = new byte[AES_BLOCK_LENGTH];
/**
* Number of iterations for key derived from user pw. High iteration count for better resistance to bruteforcing.
*/
int PBKDF2_PW_ITERATIONS = 1000;
/**
* Number of iterations for key derived from masterkey. Low iteration count for better performance. No additional security is added by
* high values.
*/
int PBKDF2_MASTERKEY_ITERATIONS = 1;
int FILE_NAME_IV_LENGTH = 5;
}

View File

@@ -0,0 +1,226 @@
/*******************************************************************************
* 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) throws InvalidKeyException {
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);
} 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 InvalidKeyException, 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);
} 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,59 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicLong;
import javax.crypto.Mac;
/**
* Updates a {@link Mac} with the bytes read from this stream.
*/
class CounterAwareInputStream extends FilterInputStream {
static final long SIXTY_FOUR_GIGABYE = 1024l * 1024l * 1024l * 64l;
private final AtomicLong counter;
/**
* @param in Stream from which to read contents, which will update the Mac.
* @param mac Mac to be updated during writes.
*/
public CounterAwareInputStream(InputStream in) {
super(in);
this.counter = new AtomicLong(0l);
}
@Override
public int read() throws IOException {
int b = in.read();
if (b != -1) {
final long currentValue = counter.incrementAndGet();
failWhen64GibReached(currentValue);
}
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = in.read(b, off, len);
if (read > 0) {
final long currentValue = counter.addAndGet(read);
failWhen64GibReached(currentValue);
}
return read;
}
private void failWhen64GibReached(long currentValue) throws CounterAwareInputLimitReachedException {
if (currentValue > SIXTY_FOUR_GIGABYE) {
throw new CounterAwareInputLimitReachedException();
}
}
static class CounterAwareInputLimitReachedException extends IOException {
private static final long serialVersionUID = -1905012809288019359L;
}
}

View File

@@ -17,12 +17,7 @@ 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.
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
*/
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
@@ -37,15 +32,20 @@ interface FileNamingConventions {
*/
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.
* Length of prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
*/
String LONG_NAME_PREFIX_SEPARATOR = "_";
int LONG_NAME_PREFIX_LENGTH = 8;
/**
* For metadata files for a certain group of files. The cryptor may decide what files to assign to the same group; hopefully using some

View File

@@ -1,67 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder(value = { "salt", "iv", "iterations", "keyLength", "masterkey" })
public class Key implements Serializable {
private static final long serialVersionUID = 8578363158959619885L;
private byte[] salt;
private byte[] iv;
private int iterations;
private int keyLength;
private byte[] pwVerification;
private byte[] masterkey;
public byte[] getSalt() {
return salt;
}
public void setSalt(byte[] salt) {
this.salt = salt;
}
public byte[] getIv() {
return iv;
}
public void setIv(byte[] iv) {
this.iv = iv;
}
public int getIterations() {
return iterations;
}
public void setIterations(int iterations) {
this.iterations = iterations;
}
public int getKeyLength() {
return keyLength;
}
public void setKeyLength(int keyLength) {
this.keyLength = keyLength;
}
public byte[] getPwVerification() {
return pwVerification;
}
public void setPwVerification(byte[] pwVerification) {
this.pwVerification = pwVerification;
}
public byte[] getMasterkey() {
return masterkey;
}
public void setMasterkey(byte[] masterkey) {
this.masterkey = masterkey;
}
}

View File

@@ -0,0 +1,66 @@
package org.cryptomator.crypto.aes256;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder(value = {"scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
public class KeyFile implements Serializable {
private static final long serialVersionUID = 8578363158959619885L;
private byte[] scryptSalt;
private int scryptCostParam;
private int scryptBlockSize;
private int keyLength;
private byte[] primaryMasterKey;
private byte[] hMacMasterKey;
public byte[] getScryptSalt() {
return scryptSalt;
}
public void setScryptSalt(byte[] scryptSalt) {
this.scryptSalt = scryptSalt;
}
public int getScryptCostParam() {
return scryptCostParam;
}
public void setScryptCostParam(int scryptCostParam) {
this.scryptCostParam = scryptCostParam;
}
public int getScryptBlockSize() {
return scryptBlockSize;
}
public void setScryptBlockSize(int scryptBlockSize) {
this.scryptBlockSize = scryptBlockSize;
}
public int getKeyLength() {
return keyLength;
}
public void setKeyLength(int keyLength) {
this.keyLength = keyLength;
}
public byte[] getPrimaryMasterKey() {
return primaryMasterKey;
}
public void setPrimaryMasterKey(byte[] primaryMasterKey) {
this.primaryMasterKey = primaryMasterKey;
}
public byte[] getHMacMasterKey() {
return hMacMasterKey;
}
public void setHMacMasterKey(byte[] hMacMasterKey) {
this.hMacMasterKey = hMacMasterKey;
}
}

View File

@@ -0,0 +1,43 @@
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();
if (b != -1) {
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);
if (read > 0) {
mac.update(b, off, read);
}
return read;
}
}

View File

@@ -0,0 +1,37 @@
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

@@ -17,11 +17,11 @@ import java.nio.channels.SeekableByteChannel;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
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.WrongPasswordException;
import org.junit.Assert;
@@ -29,17 +29,15 @@ import org.junit.Test;
public class Aes256CryptorTest {
private static final Random TEST_PRNG = new Random();
@Test
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException {
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();
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor decryptor = new Aes256Cryptor();
final InputStream in = new ByteArrayInputStream(out.toByteArray());
decryptor.decryptMasterKey(in, pw);
@@ -47,46 +45,124 @@ public class Aes256CryptorTest {
IOUtils.closeQuietly(in);
}
@Test(expected = WrongPasswordException.class)
@Test
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
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();
final String wrongPw = "foo";
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
final InputStream in = new ByteArrayInputStream(out.toByteArray());
decryptor.decryptMasterKey(in, wrongPw);
IOUtils.closeQuietly(out);
IOUtils.closeQuietly(in);
// all these passwords are expected to fail.
final String[] wrongPws = {"a", "as", "asdf", "sdf", "das", "dsa", "foo", "bar", "baz"};
final Aes256Cryptor decryptor = new Aes256Cryptor();
for (final String wrongPw : wrongPws) {
final InputStream in = new ByteArrayInputStream(out.toByteArray());
try {
decryptor.decryptMasterKey(in, wrongPw);
Assert.fail("should not succeed.");
} catch (WrongPasswordException e) {
continue;
} finally {
IOUtils.closeQuietly(in);
}
}
}
@Test
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
public void testIntegrityAuthentication() 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(plaintextData.length + 200);
final ByteBuffer encryptedData = ByteBuffer.allocate(96);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
// decrypt:
encryptedData.position(0);
// toggle one bit inf first content byte:
encryptedData.position(64);
final byte fifthByte = encryptedData.get();
encryptedData.position(64);
encryptedData.put((byte) (fifthByte ^ 0x01));
encryptedData.position(0);
// check mac (should return false)
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean authentic = cryptor.isAuthentic(encryptedIn);
Assert.assertFalse(authentic);
}
@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();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(96);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
encryptedData.position(0);
// toggle one bit inf first content byte:
encryptedData.position(64);
final byte fifthByte = encryptedData.get();
encryptedData.position(64);
encryptedData.put((byte) (fifthByte ^ 0x01));
encryptedData.position(0);
// decrypt modified content (should fail with DecryptFailedException):
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptedFile(encryptedIn, plaintextOut);
cryptor.decryptFile(encryptedIn, plaintextOut);
}
@Test
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();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(96);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
encryptedData.position(0);
// decrypt file size:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final Long filesize = cryptor.decryptedContentLength(encryptedIn);
Assert.assertEquals(plaintextData.length, filesize.longValue());
// decrypt:
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertTrue(numDecryptedBytes > 0);
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
// check decrypted data:
final byte[] result = plaintextOut.toByteArray();
@@ -94,7 +170,7 @@ public class Aes256CryptorTest {
}
@Test
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
// our test plaintext data:
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
@@ -104,15 +180,17 @@ public class Aes256CryptorTest {
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(plaintextData.length + 200);
final ByteBuffer encryptedData = ByteBuffer.allocate((int) (64 + plaintextData.length * 1.2));
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
encryptedData.position(0);
// decrypt:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
@@ -128,22 +206,34 @@ public class Aes256CryptorTest {
}
@Test
public void testEncryptionOfFilenames() throws IOException {
public void testEncryptionOfFilenames() throws IOException, DecryptFailedException {
final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
// short path components
final String originalPath1 = "foo/bar/baz";
final String encryptedPath1 = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
final String decryptedPath1 = cryptor.decryptPath(encryptedPath1, '/', '/', ioSupportMock);
final String encryptedPath1a = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
final String encryptedPath1b = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
Assert.assertEquals(encryptedPath1a, encryptedPath1b);
final String decryptedPath1 = cryptor.decryptPath(encryptedPath1a, '/', '/', ioSupportMock);
Assert.assertEquals(originalPath1, decryptedPath1);
// long path components
final String str50chars = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
final String originalPath2 = "foo/" + str50chars + str50chars + str50chars + str50chars + str50chars + "/baz";
final String encryptedPath2 = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
final String decryptedPath2 = cryptor.decryptPath(encryptedPath2, '/', '/', ioSupportMock);
final String encryptedPath2a = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
final String encryptedPath2b = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
Assert.assertEquals(encryptedPath2a, encryptedPath2b);
final String decryptedPath2 = cryptor.decryptPath(encryptedPath2a, '/', '/', ioSupportMock);
Assert.assertEquals(originalPath2, decryptedPath2);
// block size length path components
final String originalPath3 = "aaaabbbbccccdddd";
final String encryptedPath3a = cryptor.encryptPath(originalPath3, '/', '/', ioSupportMock);
final String encryptedPath3b = cryptor.encryptPath(originalPath3, '/', '/', ioSupportMock);
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
final String decryptedPath3 = cryptor.decryptPath(encryptedPath3a, '/', '/', ioSupportMock);
Assert.assertEquals(originalPath3, decryptedPath3);
}
private static class CryptoIOSupportMock implements CryptorIOSupport {

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

@@ -12,12 +12,13 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.6.0</version>
</parent>
<artifactId>crypto-api</artifactId>
<name>Cryptomator cryptographic module API</name>
<dependencies>
<!-- commons -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>

View File

@@ -16,6 +16,7 @@ import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
@@ -65,8 +66,9 @@ public interface Cryptor extends SensitiveDataSwipeListener {
* @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.
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
*/
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException;
/**
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
@@ -75,21 +77,28 @@ public interface Cryptor extends SensitiveDataSwipeListener {
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException;
/**
* @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
* @return true, if the stored MAC matches the calculated one.
*/
Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException;
boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException;
/**
* @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 decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
/**
* @param pos First byte (inclusive)
* @param length Number of requested bytes beginning at pos.
* @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;
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) 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;
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException;
/**
* @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

View File

@@ -10,6 +10,7 @@ import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
@@ -71,7 +72,7 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
}
@Override
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException {
decryptedBytes.addAndGet(StringUtils.length(encryptedPath));
return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport);
}
@@ -82,19 +83,24 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
}
@Override
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptedFile(encryptedFile, countingInputStream);
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.isAuthentic(encryptedFile);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptFile(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 {
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
return cryptor.encryptFile(countingInputStream, encryptedFile);
}

View File

@@ -0,0 +1,10 @@
package org.cryptomator.crypto.exceptions;
public class CounterOverflowException extends EncryptFailedException {
private static final long serialVersionUID = 380066751064534731L;
public CounterOverflowException(String msg) {
super(msg);
}
}

View File

@@ -6,4 +6,8 @@ public class DecryptFailedException extends StorageCryptingException {
public DecryptFailedException(Throwable t) {
super("Decryption failed.", t);
}
public DecryptFailedException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,9 @@
package org.cryptomator.crypto.exceptions;
public class EncryptFailedException extends StorageCryptingException {
private static final long serialVersionUID = -3855673600374897828L;
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

@@ -7,7 +7,7 @@ public class UnsupportedKeyLengthException extends StorageCryptingException {
private final int supportedLength;
public UnsupportedKeyLengthException(int length, int maxLength) {
super(String.format("Key length (%i) exceeds policy maximum (%i).", length, maxLength));
super(String.format("Key length (%d) exceeds policy maximum (%d).", length, maxLength));
this.requestedLength = length;
this.supportedLength = maxLength;
}

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.3.0-SNAPSHOT</version>
<version>0.6.0</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -27,11 +34,14 @@
<!-- dependency versions -->
<log4j.version>2.1</log4j.version>
<slf4j.version>1.7.7</slf4j.version>
<junit.version>4.11</junit.version>
<junit.version>4.12</junit.version>
<commons-io.version>2.4</commons-io.version>
<commons-collections.version>4.0</commons-collections.version>
<commons-lang3.version>3.1</commons-lang3.version>
<commons-codec.version>1.9</commons-codec.version>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-codec.version>1.10</commons-codec.version>
<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>
@@ -101,19 +111,46 @@
<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.inject</groupId>
<artifactId>guice</artifactId>
<version>3.0</version>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.4.2</version>
<version>${jackson-databind.version}</version>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<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>
@@ -136,6 +173,10 @@
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
</dependencies>
<modules>
@@ -150,7 +191,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

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.

Binary file not shown.

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" ?>
<!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>
<!-- register .cryptomator bundle extension -->
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<true/>
<key>CFBundleTypeIconFile</key>
<string>Cryptomator.icns</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>cryptomator</string>
</array>
<key>CFBundleTypeName</key>
<string>org.cryptomator.folder</string>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
</array>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

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

@@ -12,16 +12,15 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.6.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>
<exec.mainClass>org.cryptomator.ui.Cryptomator</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>
@@ -49,12 +48,15 @@
<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.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
</dependencies>
@@ -80,6 +82,7 @@
<archive>
<manifestEntries>
<Main-Class>${exec.mainClass}</Main-Class>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
@@ -102,7 +105,7 @@
<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 basedir="" javafx="2.2+" j2se="8.0" />
<fx:platform javafx="2.2+" j2se="8.0" />
<fx:resources>
<fx:fileset dir="${project.build.directory}" includes="${javafx.application.name}.jar" />
</fx:resources>

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

@@ -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);
}
}

View File

@@ -9,8 +9,11 @@
package org.cryptomator.ui;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import javafx.application.Application;
import javafx.application.Platform;
@@ -20,25 +23,71 @@ import javafx.scene.Scene;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.MainModule.ControllerFactory;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.SingleInstanceManager;
import org.cryptomator.ui.util.SingleInstanceManager.LocalInstance;
import org.cryptomator.ui.util.TrayIconUtil;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Guice;
import com.google.inject.Injector;
public class MainApplication extends Application {
private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
public static final String APPLICATION_KEY = "CryptomatorGUI";
public static void main(String[] args) {
Application.launch(args);
Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER);
private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
private final CleanShutdownPerformer cleanShutdownPerformer = new CleanShutdownPerformer();
private final ExecutorService executorService;
private final ControllerFactory controllerFactory;
private final DeferredCloser closer;
public MainApplication() {
this(getInjector());
}
private static Injector getInjector() {
return Guice.createInjector(new MainModule());
}
public MainApplication(Injector injector) {
this(injector.getInstance(ExecutorService.class), injector.getInstance(ControllerFactory.class), injector.getInstance(DeferredCloser.class), injector.getInstance(MainApplicationReference.class));
}
public MainApplication(ExecutorService executorService, ControllerFactory controllerFactory, DeferredCloser closer, MainApplicationReference appRef) {
super();
this.executorService = executorService;
this.controllerFactory = controllerFactory;
this.closer = closer;
appRef.set(this);
}
@Override
public void start(final Stage primaryStage) throws IOException {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
FXMLLoader.setDefaultClassLoader(contextClassLoader);
Platform.runLater(() -> {
/*
* This fixes a bug on OSX where the magic file open handler leads to no context class loader being set in the AppKit (event)
* thread if the application is not started opening a file.
*/
if (Thread.currentThread().getContextClassLoader() == null) {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
});
Runtime.getRuntime().addShutdownHook(cleanShutdownPerformer);
chooseNativeStylesheet();
final ResourceBundle rb = ResourceBundle.getBundle("localization");
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"), rb);
loader.setControllerFactory(controllerFactory);
final Parent root = loader.load();
final MainController ctrl = loader.getController();
ctrl.setStage(primaryStage);
@@ -48,9 +97,48 @@ public class MainApplication extends Application {
primaryStage.sizeToScene();
primaryStage.setResizable(false);
primaryStage.show();
ActiveWindowStyleSupport.startObservingFocus(primaryStage);
TrayIconUtil.init(primaryStage, rb, () -> {
quit();
});
for (String arg : getParameters().getUnnamed()) {
handleCommandLineArg(ctrl, arg);
}
if (SystemUtils.IS_OS_MAC_OSX) {
Cryptomator.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(ctrl, file.getAbsolutePath()));
}
LocalInstance cryptomatorGuiInstance = closer.closeLater(SingleInstanceManager.startLocalInstance(APPLICATION_KEY, executorService), LocalInstance::close).get().get();
cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(ctrl, arg));
}
void handleCommandLineArg(final MainController ctrl, String arg) {
// only open files with our file extension:
if (!arg.endsWith(Vault.VAULT_FILE_EXTENSION)) {
LOG.warn("Invalid vault path %s", arg);
return;
}
// find correct location:
final Path path = FileSystems.getDefault().getPath(arg);
final Path vaultPath;
if (Files.isDirectory(path)) {
vaultPath = path;
} else if (Files.isRegularFile(path) && path.getParent().getFileName().toString().endsWith(Vault.VAULT_FILE_EXTENSION)) {
vaultPath = path.getParent();
} else {
LOG.warn("Invalid vault path %s", arg);
return;
}
// add vault to ctrl:
Platform.runLater(() -> {
ctrl.addVault(vaultPath, true);
ctrl.toFront();
});
}
private void chooseNativeStylesheet() {
@@ -65,8 +153,7 @@ public class MainApplication extends Application {
private void quit() {
Platform.runLater(() -> {
CLEAN_SHUTDOWN_PERFORMER.run();
Settings.save();
stop();
Platform.exit();
System.exit(0);
});
@@ -74,26 +161,40 @@ public class MainApplication extends Application {
@Override
public void stop() {
CLEAN_SHUTDOWN_PERFORMER.run();
Settings.save();
}
closer.close();
try {
Runtime.getRuntime().removeShutdownHook(cleanShutdownPerformer);
} catch (Exception e) {
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() {
SHUTDOWN_TASKS.forEach(r -> {
r.run();
});
SHUTDOWN_TASKS.clear();
}
}
private class CleanShutdownPerformer extends Thread {
@Override
public void run() {
closer.close();
}
}
/**
* Needed to inject MainApplication. Problem: Application needs to be set asap after injector creation.
*/
static class MainApplicationReference {
private Application application;
private void set(Application application) {
this.application = application;
}
public Application get() {
if (application == null) {
throw new IllegalStateException("not yet ready.");
} else {
return application;
}
}
}
}

View File

@@ -1,200 +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.File;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import org.cryptomator.ui.InitializeController.InitializationListener;
import org.cryptomator.ui.UnlockController.UnlockListener;
import org.cryptomator.ui.UnlockedController.LockListener;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
private Stage stage;
@FXML
private ContextMenu directoryContextMenu;
@FXML
private HBox rootPane;
@FXML
private ListView<Directory> directoryList;
@FXML
private Pane contentPane;
private ResourceBundle rb;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
final ObservableList<Directory> items = FXCollections.observableList(Settings.load().getDirectories());
directoryList.setItems(items);
directoryList.setCellFactory(this::createDirecoryListCell);
directoryList.getSelectionModel().getSelectedItems().addListener(this::selectedDirectoryDidChange);
}
@FXML
private void didClickAddDirectory(ActionEvent event) {
final DirectoryChooser dirChooser = new DirectoryChooser();
final File file = dirChooser.showDialog(stage);
if (file != null && file.canWrite()) {
final Directory dir = new Directory(file.toPath());
directoryList.getItems().add(dir);
directoryList.getSelectionModel().selectLast();
}
}
private ListCell<Directory> createDirecoryListCell(ListView<Directory> param) {
final DirectoryListCell cell = new DirectoryListCell();
cell.setContextMenu(directoryContextMenu);
return cell;
}
private void selectedDirectoryDidChange(ListChangeListener.Change<? extends Directory> change) {
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
if (selectedDir == null) {
stage.setTitle(rb.getString("app.name"));
showWelcomeView();
} else {
stage.setTitle(selectedDir.getName());
showDirectory(selectedDir);
}
}
@FXML
private void didClickRemoveSelectedEntry(ActionEvent e) {
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
directoryList.getItems().remove(selectedDir);
directoryList.getSelectionModel().clearSelection();
}
// ****************************************
// Subcontroller for right panel
// ****************************************
private void showDirectory(Directory directory) {
try {
if (directory.isUnlocked()) {
this.showUnlockedView(directory);
} else if (directory.containsMasterKey()) {
this.showUnlockView(directory);
} else {
this.showInitializeView(directory);
}
} catch (IOException e) {
LOG.error("Failed to analyze directory.", e);
}
}
private <T> T showView(String fxml) {
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml), rb);
final Parent root = loader.load();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
return loader.getController();
} catch (IOException e) {
throw new IllegalStateException("Failed to load fxml file.", e);
}
}
private void showWelcomeView() {
this.showView("/fxml/welcome.fxml");
}
private void showInitializeView(Directory directory) {
final InitializeController ctrl = showView("/fxml/initialize.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@Override
public void didInitialize(InitializeController ctrl) {
showUnlockView(ctrl.getDirectory());
}
private void showUnlockView(Directory directory) {
final UnlockController ctrl = showView("/fxml/unlock.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@Override
public void didUnlock(UnlockController ctrl) {
showUnlockedView(ctrl.getDirectory());
Platform.setImplicitExit(false);
}
private void showUnlockedView(Directory directory) {
final UnlockedController ctrl = showView("/fxml/unlocked.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@Override
public void didLock(UnlockedController ctrl) {
showUnlockView(ctrl.getDirectory());
if (getUnlockedDirectories().isEmpty()) {
Platform.setImplicitExit(true);
}
}
/* Convenience */
public Collection<Directory> getDirectories() {
return directoryList.getItems();
}
public Collection<Directory> getUnlockedDirectories() {
return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
}
/* public Getter/Setter */
public Stage getStage() {
return stage;
}
public void setStage(Stage stage) {
this.stage = stage;
}
}

View File

@@ -0,0 +1,111 @@
/*******************************************************************************
* 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
******************************************************************************/
package org.cryptomator.ui;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.util.Callback;
import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingDecorator;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.MainApplication.MainApplicationReference;
import org.cryptomator.ui.model.VaultFactory;
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 com.google.inject.AbstractModule;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.Provides;
import com.google.inject.name.Names;
public class MainModule extends AbstractModule {
private final DeferredCloser deferredCloser = new DeferredCloser();
public static interface ControllerFactory extends Callback<Class<?>, Object> {
}
@Override
protected void configure() {
bind(DeferredCloser.class).toInstance(deferredCloser);
bind(ObjectMapper.class).annotatedWith(Names.named("VaultJsonMapper")).toProvider(VaultObjectMapperProvider.class);
bind(Settings.class).toProvider(SettingsProvider.class);
bind(WebDavMounter.class).toProvider(WebDavMounterProvider.class).asEagerSingleton();
}
@Provides
@Singleton
ControllerFactory getControllerFactory(Injector injector) {
return cls -> injector.getInstance(cls);
}
@Provides
@Singleton
MainApplicationReference getApplicationBinding() {
return new MainApplicationReference();
}
@Provides
Application getApplication(MainApplicationReference ref) {
return ref.get();
}
@Provides
@Named("SemVer")
@Singleton
Comparator<String> getSemVerComparator() {
return new SemVerComparator();
}
@Provides
@Singleton
ExecutorService getExec() {
return closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown);
}
@Provides
Cryptor getCryptor() {
return SamplingDecorator.decorate(new Aes256Cryptor());
}
@Provides
@Singleton
VaultFactory getVaultFactory(WebDavServer server, Provider<Cryptor> cryptorProvider, WebDavMounter mounter, DeferredCloser closer) {
return new VaultFactory(server, cryptorProvider, mounter, closer);
}
@Provides
@Singleton
WebDavServer getServer() {
final WebDavServer webDavServer = new WebDavServer();
webDavServer.start();
return closeLater(webDavServer, WebDavServer::stop);
}
<T> T closeLater(T object, Closer<T> closer) {
return deferredCloser.closeLater(object, closer).get().get();
}
}

View File

@@ -0,0 +1,175 @@
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
public class ChangePasswordController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
private ResourceBundle rb;
private ChangePasswordListener listener;
private Vault vault;
@FXML
private SecPasswordField oldPasswordField;
@FXML
private SecPasswordField newPasswordField;
@FXML
private SecPasswordField retypePasswordField;
@FXML
private Button changePasswordButton;
@FXML
private Label messageLabel;
@Inject
public ChangePasswordController() {
super();
}
@Override
public void initialize(URL location, ResourceBundle rb) {
this.rb = rb;
oldPasswordField.textProperty().addListener(this::passwordFieldsDidChange);
newPasswordField.textProperty().addListener(this::passwordFieldsDidChange);
retypePasswordField.textProperty().addListener(this::passwordFieldsDidChange);
}
// ****************************************
// Password fields
// ****************************************
private void passwordFieldsDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
boolean oldPasswordIsEmpty = oldPasswordField.getText().isEmpty();
boolean newPasswordIsEmpty = newPasswordField.getText().isEmpty();
boolean passwordsAreEqual = newPasswordField.getText().equals(retypePasswordField.getText());
changePasswordButton.setDisable(oldPasswordIsEmpty || newPasswordIsEmpty || !passwordsAreEqual);
}
// ****************************************
// Change password button
// ****************************************
@FXML
private void didClickChangePasswordButton(ActionEvent event) {
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
// decrypt with old password:
final CharSequence oldPassword = oldPasswordField.getCharacters();
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword);
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
} catch (DecryptFailedException | IOException ex) {
messageLabel.setText(rb.getString("changePassword.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
newPasswordField.swipe();
retypePasswordField.swipe();
return;
} catch (WrongPasswordException e) {
messageLabel.setText(rb.getString("changePassword.errorMessage.wrongPassword"));
newPasswordField.swipe();
retypePasswordField.swipe();
Platform.runLater(oldPasswordField::requestFocus);
return;
} catch (UnsupportedKeyLengthException ex) {
messageLabel.setText(rb.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
newPasswordField.swipe();
retypePasswordField.swipe();
return;
} finally {
oldPasswordField.swipe();
}
// when we reach this line, decryption was successful.
// encrypt with new password:
final CharSequence newPassword = newPasswordField.getCharacters();
try (final OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC)) {
vault.getCryptor().encryptMasterKey(masterKeyOutputStream, newPassword);
messageLabel.setText(rb.getString("changePassword.infoMessage.success"));
Platform.runLater(this::didChangePassword);
// At this point the backup is still using the old password.
// It will be changed as soon as the user unlocks the vault the next time.
// This way he can still restore the old password, if he doesn't remember the new one.
} catch (IOException ex) {
LOG.error("Re-encryption failed for technical reasons. Restoring Backup.", ex);
this.restoreBackupQuietly();
} finally {
newPasswordField.swipe();
retypePasswordField.swipe();
}
}
private void restoreBackupQuietly() {
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
try {
Files.copy(masterKeyBackupPath, masterKeyPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ex) {
LOG.error("Restoring Backup failed.", ex);
}
}
private void didChangePassword() {
if (listener != null) {
listener.didChangePassword(this);
}
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
public void setVault(Vault vault) {
this.vault = vault;
}
public ChangePasswordListener getListener() {
return listener;
}
public void setListener(ChangePasswordListener listener) {
this.listener = listener;
}
/* callback */
interface ChangePasswordListener {
void didChangePassword(ChangePasswordController ctrl);
}
}

View File

@@ -0,0 +1,127 @@
/*******************************************************************************
* 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.controllers;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InitializeController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private ResourceBundle localization;
private Vault vault;
private InitializationListener listener;
@FXML
private SecPasswordField passwordField;
@FXML
private SecPasswordField retypePasswordField;
@FXML
private Button okButton;
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.localization = rb;
passwordField.textProperty().addListener(this::passwordFieldsDidChange);
retypePasswordField.textProperty().addListener(this::passwordFieldsDidChange);
}
// ****************************************
// Password fields
// ****************************************
private void passwordFieldsDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
boolean passwordIsEmpty = passwordField.getText().isEmpty();
boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText());
okButton.setDisable(passwordIsEmpty || !passwordsAreEqual);
}
// ****************************************
// OK button
// ****************************************
@FXML
protected void initializeVault(ActionEvent event) {
setControlsDisabled(true);
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final CharSequence password = passwordField.getCharacters();
try (OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
vault.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
if (listener != null) {
listener.didInitialize(this);
}
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (InvalidPathException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
} finally {
setControlsDisabled(false);
passwordField.swipe();
retypePasswordField.swipe();
}
}
private void setControlsDisabled(boolean disable) {
passwordField.setDisable(disable);
retypePasswordField.setDisable(disable);
okButton.setDisable(disable);
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
public void setVault(Vault vault) {
this.vault = vault;
}
public InitializationListener getListener() {
return listener;
}
public void setListener(InitializationListener listener) {
this.listener = listener;
}
/* callback */
interface InitializationListener {
void didInitialize(InitializeController ctrl);
}
}

View File

@@ -0,0 +1,58 @@
package org.cryptomator.ui.controllers;
import javafx.application.Application;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
import javax.inject.Inject;
public class MacWarningsController {
@FXML
private ListView<String> warningsList;
private Stage stage;
private final Application application;
@Inject
public MacWarningsController(Application application) {
this.application = application;
}
@FXML
private void didClickDismissButton(ActionEvent event) {
stage.hide();
}
@FXML
private void didClickMoreInformationButton(ActionEvent event) {
application.getHostServices().showDocument("https://cryptomator.org/help.html#macWarning");
}
public void setMacWarnings(ObservableList<String> macWarnings) {
this.warningsList.setItems(macWarnings);
this.warningsList.getItems().addListener(new WeakListChangeListener<String>(this::warningsDidChange));
}
// closes this window automatically, if all warnings disappeared (e.g. due to an unmount event)
private void warningsDidChange(Change<? extends String> change) {
if (change.getList().isEmpty()) {
stage.hide();
}
}
public Stage getStage() {
return stage;
}
public void setStage(Stage stage) {
this.stage = stage;
}
}

View File

@@ -0,0 +1,387 @@
/*******************************************************************************
* 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.controllers;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.SetChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import org.cryptomator.ui.MainModule.ControllerFactory;
import org.cryptomator.ui.controllers.ChangePasswordController.ChangePasswordListener;
import org.cryptomator.ui.controllers.InitializeController.InitializationListener;
import org.cryptomator.ui.controllers.UnlockController.UnlockListener;
import org.cryptomator.ui.controllers.UnlockedController.LockListener;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.ObservableSetAggregator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener, ChangePasswordListener {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
private Stage stage;
@FXML
private ContextMenu vaultListCellContextMenu;
@FXML
private ContextMenu addVaultContextMenu;
@FXML
private HBox rootPane;
@FXML
private ListView<Vault> vaultList;
@FXML
private ToggleButton addVaultButton;
@FXML
private Pane contentPane;
private final ControllerFactory controllerFactory;
private final Settings settings;
private final VaultFactory vaultFactoy;
private final ObservableList<String> aggregatedMacWarnings;
private final SetChangeListener<String> macWarningsAggregator;
private final AtomicBoolean macWarningsWindowVisible;
private ResourceBundle rb;
@Inject
public MainController(ControllerFactory controllerFactory, Settings settings, VaultFactory vaultFactoy) {
super();
this.controllerFactory = controllerFactory;
this.settings = settings;
this.vaultFactoy = vaultFactoy;
this.aggregatedMacWarnings = FXCollections.observableList(new ArrayList<>());
this.macWarningsAggregator = new ObservableSetAggregator<>(this.aggregatedMacWarnings);
this.macWarningsWindowVisible = new AtomicBoolean();
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
final ObservableList<Vault> items = FXCollections.observableList(settings.getDirectories());
vaultList.setItems(items);
vaultList.setCellFactory(this::createDirecoryListCell);
vaultList.getSelectionModel().getSelectedItems().addListener(this::selectedVaultDidChange);
aggregatedMacWarnings.addListener(this::macWarningsDidChange);
}
@FXML
private void didClickAddVault(ActionEvent event) {
if (addVaultContextMenu.isShowing()) {
addVaultContextMenu.hide();
} else {
addVaultContextMenu.show(addVaultButton, Side.RIGHT, 0.0, 0.0);
}
}
@FXML
private void willShowAddVaultContextMenu(WindowEvent event) {
addVaultButton.setSelected(true);
}
@FXML
private void didHideAddVaultContextMenu(WindowEvent event) {
addVaultButton.setSelected(false);
}
@FXML
private void didClickCreateNewVault(ActionEvent event) {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*" + Vault.VAULT_FILE_EXTENSION));
final File file = fileChooser.showSaveDialog(stage);
if (file == null) {
return;
}
try {
final Path vaultDir;
// enforce .cryptomator file extension:
if (!file.getName().endsWith(Vault.VAULT_FILE_EXTENSION)) {
vaultDir = file.toPath().resolveSibling(file.getName() + Vault.VAULT_FILE_EXTENSION);
} else {
vaultDir = file.toPath();
}
if (!Files.exists(vaultDir)) {
Files.createDirectory(vaultDir);
}
addVault(vaultDir, true);
} catch (IOException e) {
LOG.error("Unable to create vault", e);
}
}
@FXML
private void didClickAddExistingVaults(ActionEvent event) {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*" + Vault.VAULT_FILE_EXTENSION));
final List<File> files = fileChooser.showOpenMultipleDialog(stage);
if (files != null) {
for (final File file : files) {
addVault(file.toPath(), false);
}
}
}
/**
* adds the given directory or selects it if it is already in the list of directories.
*
* @param path non-null, writable, existing directory
*/
public void addVault(final Path path, boolean select) {
if (path == null || !Files.isWritable(path)) {
return;
}
final Path vaultPath;
if (path != null && Files.isDirectory(path)) {
vaultPath = path;
} else if (path != null && Files.isRegularFile(path) && path.getParent().getFileName().toString().endsWith(Vault.VAULT_FILE_EXTENSION)) {
vaultPath = path.getParent();
} else {
return;
}
final Vault vault = vaultFactoy.createVault(vaultPath);
if (!vaultList.getItems().contains(vault)) {
vaultList.getItems().add(vault);
}
vaultList.getSelectionModel().select(vault);
}
private ListCell<Vault> createDirecoryListCell(ListView<Vault> param) {
final DirectoryListCell cell = new DirectoryListCell();
cell.setVaultContextMenu(vaultListCellContextMenu);
return cell;
}
private void selectedVaultDidChange(ListChangeListener.Change<? extends Vault> change) {
final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem();
if (selectedVault == null) {
stage.setTitle(rb.getString("app.name"));
showWelcomeView();
} else if (!Files.isDirectory(selectedVault.getPath())) {
Platform.runLater(() -> {
vaultList.getItems().remove(selectedVault);
vaultList.getSelectionModel().clearSelection();
});
stage.setTitle(rb.getString("app.name"));
showWelcomeView();
} else {
stage.setTitle(selectedVault.getName());
showVault(selectedVault);
}
}
@FXML
private void didClickRemoveSelectedEntry(ActionEvent e) {
final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem();
vaultList.getItems().remove(selectedVault);
vaultList.getSelectionModel().clearSelection();
}
@FXML
private void didClickChangePassword(ActionEvent e) {
final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem();
showChangePasswordView(selectedVault);
}
private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
if (aggregatedMacWarnings.size() > 0) {
Platform.runLater(this::showMacWarningsWindow);
}
}
// ****************************************
// Subcontroller for right panel
// ****************************************
private void showVault(Vault vault) {
try {
if (vault.isUnlocked()) {
this.showUnlockedView(vault);
} else if (vault.containsMasterKey()) {
this.showUnlockView(vault);
} else {
this.showInitializeView(vault);
}
} catch (IOException e) {
LOG.error("Failed to analyze directory.", e);
}
}
private <T> T showView(String fxml) {
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml), rb);
loader.setControllerFactory(controllerFactory);
final Parent root = loader.load();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
return loader.getController();
} catch (IOException e) {
throw new IllegalStateException("Failed to load fxml file.", e);
}
}
private void showWelcomeView() {
this.showView("/fxml/welcome.fxml");
}
private void showInitializeView(Vault vault) {
final InitializeController ctrl = showView("/fxml/initialize.fxml");
ctrl.setVault(vault);
ctrl.setListener(this);
}
@Override
public void didInitialize(InitializeController ctrl) {
showUnlockView(ctrl.getVault());
}
private void showUnlockView(Vault vault) {
final UnlockController ctrl = showView("/fxml/unlock.fxml");
ctrl.setVault(vault);
ctrl.setListener(this);
}
@Override
public void didUnlock(UnlockController ctrl) {
ctrl.getVault().getNamesOfResourcesWithInvalidMac().addListener(this.macWarningsAggregator);
showUnlockedView(ctrl.getVault());
Platform.setImplicitExit(false);
}
private void showUnlockedView(Vault vault) {
final UnlockedController ctrl = showView("/fxml/unlocked.fxml");
ctrl.setVault(vault);
ctrl.setListener(this);
}
@Override
public void didLock(UnlockedController ctrl) {
ctrl.getVault().getNamesOfResourcesWithInvalidMac().removeListener(this.macWarningsAggregator);
showUnlockView(ctrl.getVault());
if (getUnlockedDirectories().isEmpty()) {
Platform.setImplicitExit(true);
}
}
private void showChangePasswordView(Vault vault) {
final ChangePasswordController ctrl = showView("/fxml/change_password.fxml");
ctrl.setVault(vault);
ctrl.setListener(this);
}
@Override
public void didChangePassword(ChangePasswordController ctrl) {
showUnlockView(ctrl.getVault());
}
private void showMacWarningsWindow() {
if (macWarningsWindowVisible.getAndSet(true) == false) {
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/mac_warnings.fxml"), rb);
loader.setControllerFactory(controllerFactory);
final Parent root = loader.load();
final Stage stage = new Stage();
stage.setTitle(rb.getString("macWarnings.windowTitle"));
stage.setScene(new Scene(root));
stage.sizeToScene();
stage.setResizable(false);
stage.setOnHidden(this::onHideMacWarningsWindow);
ActiveWindowStyleSupport.startObservingFocus(stage);
final MacWarningsController ctrl = loader.getController();
ctrl.setMacWarnings(this.aggregatedMacWarnings);
ctrl.setStage(stage);
stage.show();
} catch (IOException e) {
throw new IllegalStateException("Failed to load fxml file.", e);
}
}
}
private void onHideMacWarningsWindow(WindowEvent event) {
macWarningsWindowVisible.set(false);
aggregatedMacWarnings.clear();
}
/* Convenience */
public Collection<Vault> getDirectories() {
return vaultList.getItems();
}
public Collection<Vault> getUnlockedDirectories() {
return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
}
/* public Getter/Setter */
public Stage getStage() {
return stage;
}
public void setStage(Stage stage) {
this.stage = stage;
}
/**
* Attempts to make the application window visible.
*/
public void toFront() {
stage.setIconified(false);
stage.show();
stage.toFront();
}
}

View File

@@ -6,16 +6,17 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui;
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import javafx.application.Platform;
@@ -24,37 +25,37 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
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.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.apache.commons.lang3.CharUtils;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.FXThreads;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
public class UnlockController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
private ResourceBundle rb;
private UnlockListener listener;
private Directory directory;
@FXML
private ComboBox<String> usernameBox;
private Vault vault;
@FXML
private SecPasswordField passwordField;
@FXML
private TextField mountName;
@FXML
private Button unlockButton;
@@ -64,22 +65,30 @@ public class UnlockController implements Initializable {
@FXML
private Label messageLabel;
private final ExecutorService exec;
@Inject
public UnlockController(ExecutorService exec) {
super();
this.exec = exec;
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
usernameBox.valueProperty().addListener(this::didChooseUsername);
passwordField.textProperty().addListener(this::passwordFieldsDidChange);
mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
mountName.textProperty().addListener(this::mountNameDidChange);
}
// ****************************************
// Username box
// Password field
// ****************************************
public void didChooseUsername(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (newValue != null) {
Platform.runLater(passwordField::requestFocus);
}
passwordField.setDisable(StringUtils.isEmpty(newValue));
private void passwordFieldsDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
boolean passwordIsEmpty = passwordField.getText().isEmpty();
unlockButton.setDisable(passwordIsEmpty);
}
// ****************************************
@@ -89,25 +98,22 @@ public class UnlockController implements Initializable {
@FXML
private void didClickUnlockButton(ActionEvent event) {
setControlsDisabled(true);
final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
progressIndicator.setVisible(true);
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
final CharSequence password = passwordField.getCharacters();
InputStream masterKeyInputStream = null;
try {
progressIndicator.setVisible(true);
masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
directory.getCryptor().decryptMasterKey(masterKeyInputStream, password);
if (!directory.startServer()) {
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
vault.getCryptor().decryptMasterKey(masterKeyInputStream, password);
if (!vault.startServer()) {
messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed"));
directory.getCryptor().swipeSensitiveData();
vault.getCryptor().swipeSensitiveData();
return;
}
directory.setUnlocked(true);
final Future<Boolean> futureMount = FXThreads.runOnBackgroundThread(directory::mount);
FXThreads.runOnMainThreadWhenFinished(futureMount, this::didUnlockAndMount);
FXThreads.runOnMainThreadWhenFinished(futureMount, (result) -> {
setControlsDisabled(false);
});
// at this point we know for sure, that the masterkey can be decrypted, so lets make a backup:
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
vault.setUnlocked(true);
final Future<Boolean> futureMount = exec.submit(() -> vault.mount());
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::unlockAndMountFinished);
} catch (DecryptFailedException | IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
@@ -117,7 +123,7 @@ public class UnlockController implements Initializable {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(rb.getString("unlock.errorMessage.wrongPassword"));
passwordField.requestFocus();
Platform.runLater(passwordField::requestFocus);
} catch (UnsupportedKeyLengthException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
@@ -125,51 +131,54 @@ public class UnlockController implements Initializable {
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
} finally {
passwordField.swipe();
IOUtils.closeQuietly(masterKeyInputStream);
}
}
private void setControlsDisabled(boolean disable) {
usernameBox.setDisable(disable);
passwordField.setDisable(disable);
mountName.setDisable(disable);
unlockButton.setDisable(disable);
}
private void findExistingUsernames() {
try {
DirectoryStream<Path> ds = MasterKeyFilter.filteredDirectory(directory.getPath());
final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase();
usernameBox.getItems().clear();
for (final Path path : ds) {
final String fileName = path.getFileName().toString();
final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt);
final String baseName = fileName.substring(0, beginOfExt);
usernameBox.getItems().add(baseName);
}
if (usernameBox.getItems().size() == 1) {
usernameBox.getSelectionModel().selectFirst();
}
} catch (IOException e) {
LOG.trace("Invalid path: " + directory.getPath(), e);
private void unlockAndMountFinished(boolean mountSuccess) {
progressIndicator.setVisible(false);
setControlsDisabled(false);
if (vault.isUnlocked() && !mountSuccess) {
vault.stopServer();
}
if (mountSuccess && listener != null) {
listener.didUnlock(this);
}
}
private void didUnlockAndMount(boolean mountSuccess) {
progressIndicator.setVisible(false);
if (listener != null) {
listener.didUnlock(this);
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();
}
}
private void mountNameDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
// newValue is guaranteed to be a-z0-9, see #filterAlphanumericKeyEvents
if (newValue.isEmpty()) {
mountName.setText(vault.getMountName());
} else {
vault.setMountName(newValue);
}
}
/* Getter/Setter */
public Directory getDirectory() {
return directory;
public Vault getVault() {
return vault;
}
public void setDirectory(Directory directory) {
this.directory = directory;
this.findExistingUsernames();
public void setVault(Vault vault) {
this.vault = vault;
this.mountName.setText(vault.getMountName());
}
public UnlockListener getListener() {

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui;
package org.cryptomator.ui.controllers;
import java.net.URL;
import java.util.ResourceBundle;
@@ -26,15 +26,15 @@ import javafx.scene.control.Label;
import javafx.util.Duration;
import org.cryptomator.crypto.CryptorIOSampling;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.mount.CommandFailedException;
public class UnlockedController implements Initializable {
private static final int IO_SAMPLING_STEPS = 100;
private static final double IO_SAMPLING_INTERVAL = 0.25;
private ResourceBundle rb;
private LockListener listener;
private Directory directory;
private Vault vault;
private Timeline ioAnimation;
@FXML
@@ -46,6 +46,8 @@ public class UnlockedController implements Initializable {
@FXML
private NumberAxis xAxis;
private ResourceBundle rb;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
@@ -53,9 +55,14 @@ public class UnlockedController implements Initializable {
@FXML
private void didClickCloseVault(ActionEvent event) {
directory.unmount();
directory.stopServer();
directory.setUnlocked(false);
try {
vault.unmount();
} catch (CommandFailedException e) {
messageLabel.setText(rb.getString("unlocked.label.unmountFailed"));
return;
}
vault.stopServer();
vault.setUnlocked(false);
if (listener != null) {
listener.didLock(this);
}
@@ -117,14 +124,12 @@ public class UnlockedController implements Initializable {
/* Getter/Setter */
public Directory getDirectory() {
return directory;
public Vault getVault() {
return vault;
}
public void setDirectory(Directory directory) {
this.directory = directory;
final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), directory.getServer().getPort());
messageLabel.setText(msg);
public void setVault(Vault directory) {
this.vault = directory;
if (directory.getCryptor() instanceof CryptorIOSampling) {
startIoSampling((CryptorIOSampling) directory.getCryptor());

View File

@@ -0,0 +1,114 @@
/*******************************************************************************
* 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.controllers;
import java.io.IOException;
import java.net.URL;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Hyperlink;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.lang3.SystemUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class WelcomeController implements Initializable {
@FXML
private ImageView botImageView;
@FXML
private Hyperlink updateLink;
private final Application app;
private final Comparator<String> semVerComparator;
private final ExecutorService executor;
private ResourceBundle rb;
@Inject
public WelcomeController(Application app, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
this.app = app;
this.semVerComparator = semVerComparator;
this.executor = executor;
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
this.botImageView.setImage(new Image(WelcomeController.class.getResource("/bot_welcome.png").toString()));
executor.execute(this::checkForUpdates);
}
private void checkForUpdates() {
final HttpClient client = new HttpClient();
final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json");
client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
client.getParams().setConnectionManagerTimeout(5000);
try {
client.executeMethod(method);
if (method.getStatusCode() == HttpStatus.SC_OK) {
final byte[] responseData = method.getResponseBody();
final ObjectMapper mapper = new ObjectMapper();
final Map<String, String> map = mapper.readValue(responseData, new TypeReference<HashMap<String, String>>() {
});
this.compareVersions(map);
}
} catch (IOException e) {
// no error handling required. Maybe next time the version check is successful.
}
}
private void compareVersions(final Map<String, String> latestVersions) {
final String latestVersion;
if (SystemUtils.IS_OS_MAC_OSX) {
latestVersion = latestVersions.get("mac");
} else if (SystemUtils.IS_OS_WINDOWS) {
latestVersion = latestVersions.get("win");
} else if (SystemUtils.IS_OS_LINUX) {
latestVersion = latestVersions.get("linux");
} else {
// no version check possible on unsupported OS
return;
}
final String currentVersion = WelcomeController.class.getPackage().getImplementationVersion();
if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) {
final String msg = String.format(rb.getString("welcome.newVersionMessage"), latestVersion, currentVersion);
Platform.runLater(() -> {
this.updateLink.setText(msg);
this.updateLink.setVisible(true);
});
}
}
@FXML
public void didClickUpdateLink(ActionEvent event) {
app.getHostServices().showDocument("https://cryptomator.org/#download");
}
}

View File

@@ -1,30 +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.controls;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextInputControl;
public class ClearOnDisableListener implements ChangeListener<Boolean> {
final TextInputControl control;
public ClearOnDisableListener(TextInputControl control) {
this.control = control;
}
@Override
public void changed(ObservableValue<? extends Boolean> property, Boolean wasDisabled, Boolean isDisabled) {
if (isDisabled) {
control.clear();
}
}
}

View File

@@ -3,15 +3,15 @@ package org.cryptomator.ui.controls;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ListCell;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Tooltip;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.model.Vault;
public class DirectoryListCell extends ListCell<Directory> implements ChangeListener<Boolean> {
public class DirectoryListCell extends DraggableListCell<Vault> implements ChangeListener<Boolean> {
// fill: #FD4943, stroke: #E1443F
private static final Color RED_FILL = Color.rgb(253, 73, 67);
@@ -22,6 +22,7 @@ public class DirectoryListCell extends ListCell<Directory> implements ChangeList
private static final Color GREEN_STROKE = Color.rgb(48, 183, 64);
private final Circle statusIndicator = new Circle(4.5);
private ContextMenu vaultContextMenu;
public DirectoryListCell() {
setGraphic(statusIndicator);
@@ -30,8 +31,8 @@ public class DirectoryListCell extends ListCell<Directory> implements ChangeList
}
@Override
protected void updateItem(Directory item, boolean empty) {
final Directory oldItem = super.getItem();
protected void updateItem(Vault item, boolean empty) {
final Vault oldItem = super.getItem();
if (oldItem != null) {
oldItem.unlockedProperty().removeListener(this);
}
@@ -39,6 +40,7 @@ public class DirectoryListCell extends ListCell<Directory> implements ChangeList
if (item == null) {
setText(null);
setTooltip(null);
setContextMenu(null);
statusIndicator.setVisible(false);
} else {
setText(item.getName());
@@ -46,12 +48,14 @@ public class DirectoryListCell extends ListCell<Directory> implements ChangeList
statusIndicator.setVisible(true);
item.unlockedProperty().addListener(this);
updateStatusIndicator();
updateContextMenu();
}
}
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
updateStatusIndicator();
updateContextMenu();
}
private void updateStatusIndicator() {
@@ -61,4 +65,16 @@ public class DirectoryListCell extends ListCell<Directory> implements ChangeList
statusIndicator.setStroke(strokeColor);
}
private void updateContextMenu() {
if (getItem().isUnlocked()) {
this.setContextMenu(null);
} else {
this.setContextMenu(vaultContextMenu);
}
}
public void setVaultContextMenu(ContextMenu contextMenu) {
this.vaultContextMenu = contextMenu;
}
}

View File

@@ -0,0 +1,132 @@
package org.cryptomator.ui.controls;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.geometry.Insets;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderImage;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
class DraggableListCell<T> extends ListCell<T> {
private static final double DROP_LINE_WIDTH = 4.0;
private static final Paint DROP_LINE_COLOR = Color.gray(0.0, 0.6);
private final List<BorderStroke> defaultBorderStrokes;
private final List<BorderImage> defaultBorderImages;
public DraggableListCell() {
setOnDragDetected(this::onDragDetected);
setOnDragOver(this::onDragOver);
setOnDragEntered(this::onDragEntered);
setOnDragExited(this::onDragExited);
setOnDragDropped(this::onDragDropped);
setOnDragDone(DragEvent::consume);
this.defaultBorderStrokes = this.getBorder() == null ? Collections.emptyList() : this.getBorder().getStrokes();
this.defaultBorderImages = this.getBorder() == null ? Collections.emptyList() : this.getBorder().getImages();
}
private Border createDropPositionBorder(double verticalCursorPosition) {
boolean isUpperHalf = verticalCursorPosition < this.getHeight() / 2.0;
final double topBorder = isUpperHalf ? DROP_LINE_WIDTH : 0.0;
final double bottomBorder = !isUpperHalf ? DROP_LINE_WIDTH : 0.0;
final BorderWidths borderWidths = new BorderWidths(topBorder, 0.0, bottomBorder, 0.0);
final BorderStroke dragStroke = new BorderStroke(DROP_LINE_COLOR, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, borderWidths, Insets.EMPTY);
final List<BorderStroke> strokes = new ArrayList<BorderStroke>(defaultBorderStrokes);
strokes.add(0, dragStroke);
return new Border(strokes, defaultBorderImages);
}
private void onDragDetected(MouseEvent event) {
if (getItem() == null) {
return;
}
final ClipboardContent content = new ClipboardContent();
content.putString(Integer.toString(getIndex()));
final Image snapshot = this.snapshot(new SnapshotParameters(), null);
final Dragboard dragboard = startDragAndDrop(TransferMode.MOVE);
dragboard.setDragView(snapshot);
dragboard.setContent(content);
event.consume();
}
private void onDragOver(DragEvent event) {
if (getItem() == null) {
return;
}
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
event.acceptTransferModes(TransferMode.MOVE);
setBorder(createDropPositionBorder(event.getY()));
}
event.consume();
}
private void onDragEntered(DragEvent event) {
if (getItem() == null) {
return;
}
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
setBorder(createDropPositionBorder(event.getY()));
}
}
private void onDragExited(DragEvent event) {
if (getItem() == null) {
return;
}
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
setBorder(new Border(defaultBorderStrokes, defaultBorderImages));
}
}
private void onDragDropped(DragEvent event) {
if (getItem() == null) {
return;
}
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getDragboard().hasString()) {
final List<T> list = getListView().getItems();
try {
// where to insert what?
final int draggedIdx = Integer.parseInt(event.getDragboard().getString());
final T currentItem = this.getItem();
final T draggedItem = list.remove(draggedIdx);
final int currentItemIdx = list.indexOf(currentItem);
// insert before or after currentItem?
boolean insertBefore = event.getY() < this.getHeight() / 2.0;
final int insertPosition = insertBefore ? currentItemIdx : currentItemIdx + 1;
// insert!
getListView().getItems().add(insertPosition, draggedItem);
getListView().getSelectionModel().select(insertPosition);
event.setDropCompleted(true);
} catch (NumberFormatException e) {
event.setDropCompleted(false);
}
}
event.consume();
}
}

View File

@@ -1,154 +0,0 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingDecorator;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.MainApplication;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WebDavMount;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.webdav.WebDavServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize(using = DirectorySerializer.class)
@JsonDeserialize(using = DirectoryDeserializer.class)
public class Directory implements Serializable {
private static final long serialVersionUID = 3754487289683599469L;
private static final Logger LOG = LoggerFactory.getLogger(Directory.class);
private final WebDavServer server = new WebDavServer();
private final Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor());
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
private final Path path;
// private boolean unlocked;
private WebDavMount webDavMount;
private final Runnable shutdownTask = new ShutdownTask();
public Directory(final Path path) {
if (!Files.isDirectory(path)) {
throw new IllegalArgumentException("Not a directory: " + path);
}
this.path = path;
}
public boolean containsMasterKey() throws IOException {
return MasterKeyFilter.filteredDirectory(path).iterator().hasNext();
}
public synchronized boolean startServer() {
if (server.start(path.toString(), cryptor)) {
MainApplication.addShutdownTask(shutdownTask);
return true;
} else {
return false;
}
}
public synchronized void stopServer() {
if (server.isRunning()) {
MainApplication.removeShutdownTask(shutdownTask);
this.unmount();
server.stop();
cryptor.swipeSensitiveData();
}
}
public boolean mount() {
try {
webDavMount = WebDavMounter.mount(server.getPort());
return true;
} catch (CommandFailedException e) {
LOG.warn("mount failed", e);
return false;
}
}
public boolean unmount() {
try {
if (webDavMount != null) {
webDavMount.unmount();
webDavMount = null;
}
return true;
} catch (CommandFailedException e) {
LOG.warn("unmount failed", e);
return false;
}
}
/* Getter/Setter */
public Path getPath() {
return path;
}
/**
* @return Directory name without preceeding path components
*/
public String getName() {
return path.getFileName().toString();
}
public Cryptor getCryptor() {
return cryptor;
}
public ObjectProperty<Boolean> unlockedProperty() {
return unlocked;
}
public boolean isUnlocked() {
return unlocked.get();
}
public void setUnlocked(boolean unlocked) {
this.unlocked.set(unlocked);
}
public WebDavServer getServer() {
return server;
}
/* hashcode/equals */
@Override
public int hashCode() {
return path.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Directory) {
final Directory other = (Directory) obj;
return this.path.equals(other.path);
} else {
return false;
}
}
/* graceful shutdown */
private class ShutdownTask implements Runnable {
@Override
public void run() {
stopServer();
}
}
}

View File

@@ -1,23 +0,0 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
public class DirectoryDeserializer extends JsonDeserializer<Directory> {
@Override
public Directory deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final JsonNode node = jp.readValueAsTree();
final String pathStr = node.get("path").asText();
final Path path = FileSystems.getDefault().getPath(pathStr);
return new Directory(path);
}
}

View File

@@ -1,19 +0,0 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class DirectorySerializer extends JsonSerializer<Directory> {
@Override
public void serialize(Directory value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("path", value.getPath().toString());
jgen.writeEndObject();
}
}

View File

@@ -0,0 +1,218 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Optional;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.ui.util.DeferredClosable;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.FXThreads;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WebDavMount;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.webdav.WebDavServer;
import org.cryptomator.webdav.WebDavServer.ServletLifeCycleAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Vault implements Serializable {
private static final long serialVersionUID = 3754487289683599469L;
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
public static final String VAULT_FILE_EXTENSION = ".cryptomator";
public static final String VAULT_MASTERKEY_FILE = "masterkey.cryptomator";
public static final String VAULT_MASTERKEY_BACKUP_FILE = "masterkey.cryptomator.bkup";
private final Path path;
private final WebDavServer server;
private final Cryptor cryptor;
private final WebDavMounter mounter;
private final DeferredCloser closer;
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
private final ObservableSet<String> namesOfResourcesWithInvalidMac = FXThreads.observableSetOnMainThread(FXCollections.observableSet());
private String mountName;
private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
private DeferredClosable<WebDavMount> webDavMount = DeferredClosable.empty();
/**
* Package private constructor, use {@link VaultFactory}.
*/
Vault(final Path vaultDirectoryPath, final WebDavServer server, final Cryptor cryptor, final WebDavMounter mounter, final DeferredCloser closer) {
this.path = vaultDirectoryPath;
this.server = server;
this.cryptor = cryptor;
this.mounter = mounter;
this.closer = closer;
try {
setMountName(getName());
} catch (IllegalArgumentException e) {
// mount name needs to be set by the user explicitly later
}
}
public boolean isValidVaultDirectory() {
return Files.isDirectory(path) && path.getFileName().toString().endsWith(VAULT_FILE_EXTENSION);
}
public boolean containsMasterKey() throws IOException {
final Path masterKeyPath = path.resolve(VAULT_MASTERKEY_FILE);
return Files.isRegularFile(masterKeyPath);
}
public synchronized boolean startServer() {
namesOfResourcesWithInvalidMac.clear();
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
if (o.isPresent() && o.get().isRunning()) {
return false;
}
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, mountName);
if (servlet.start()) {
webDavServlet = closer.closeLater(servlet);
return true;
}
return false;
}
public void stopServer() {
try {
unmount();
} catch (CommandFailedException e) {
LOG.warn("Unmounting failed. Locking anyway...", e);
}
webDavServlet.close();
cryptor.swipeSensitiveData();
setUnlocked(false);
namesOfResourcesWithInvalidMac.clear();
}
public boolean mount() {
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
if (!o.isPresent() || !o.get().isRunning()) {
return false;
}
try {
webDavMount = closer.closeLater(mounter.mount(o.get().getServletUri(), mountName));
return true;
} catch (CommandFailedException e) {
LOG.warn("mount failed", e);
return false;
}
}
public void unmount() throws CommandFailedException {
final WebDavMount mnt = webDavMount.get().orElse(null);
if (mnt != null) {
mnt.unmount();
}
webDavMount = DeferredClosable.empty();
}
/* Getter/Setter */
public Path getPath() {
return path;
}
/**
* @return Directory name without preceeding path components and file extension
*/
public String getName() {
return StringUtils.removeEnd(path.getFileName().toString(), VAULT_FILE_EXTENSION);
}
public Cryptor getCryptor() {
return cryptor;
}
public ObjectProperty<Boolean> unlockedProperty() {
return unlocked;
}
public boolean isUnlocked() {
return unlocked.get();
}
public void setUnlocked(boolean unlocked) {
this.unlocked.set(unlocked);
}
public String getMountName() {
return mountName;
}
public ObservableSet<String> getNamesOfResourcesWithInvalidMac() {
return namesOfResourcesWithInvalidMac;
}
/**
* Tries to form a similar string using the regular latin alphabet.
*
* @param string
* @return a string composed of a-z, A-Z, 0-9, and _.
*/
public static String normalize(String string) {
String normalized = Normalizer.normalize(string, Form.NFD);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < normalized.length(); i++) {
char c = normalized.charAt(i);
if (Character.isWhitespace(c)) {
if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
builder.append('_');
}
} else if (c < 127 && Character.isLetterOrDigit(c)) {
builder.append(c);
} else if (c < 127) {
if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
builder.append('_');
}
}
}
return builder.toString();
}
/**
* sets the mount name while normalizing it
*
* @param mountName
* @throws IllegalArgumentException if the name is empty after normalization
*/
public void setMountName(String mountName) throws IllegalArgumentException {
mountName = normalize(mountName);
if (StringUtils.isEmpty(mountName)) {
throw new IllegalArgumentException("mount name is empty");
}
this.mountName = mountName;
}
/* hashcode/equals */
@Override
public int hashCode() {
return path.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Vault) {
final Vault other = (Vault) obj;
return this.path.equals(other.path);
} else {
return false;
}
}
}

View File

@@ -0,0 +1,32 @@
package org.cryptomator.ui.model;
import java.nio.file.Path;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.webdav.WebDavServer;
import com.google.inject.Inject;
import com.google.inject.Provider;
public class VaultFactory {
private final WebDavServer server;
private final Provider<Cryptor> cryptorProvider;
private final WebDavMounter mounter;
private final DeferredCloser closer;
@Inject
public VaultFactory(WebDavServer server, Provider<Cryptor> cryptorProvider, WebDavMounter mounter, DeferredCloser closer) {
this.server = server;
this.cryptorProvider = cryptorProvider;
this.mounter = mounter;
this.closer = closer;
}
public Vault createVault(Path path) {
return new Vault(path, server, cryptorProvider.get(), mounter, closer);
}
}

View File

@@ -0,0 +1,68 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import javax.inject.Inject;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.inject.Provider;
public class VaultObjectMapperProvider implements Provider<ObjectMapper> {
private final VaultFactory vaultFactoy;
@Inject
public VaultObjectMapperProvider(final VaultFactory vaultFactoy) {
this.vaultFactoy = vaultFactoy;
}
@Override
public ObjectMapper get() {
final ObjectMapper om = new ObjectMapper();
final SimpleModule module = new SimpleModule("VaultJsonMapper");
module.addSerializer(Vault.class, new VaultSerializer());
module.addDeserializer(Vault.class, new VaultDeserializer());
om.registerModule(module);
return om;
}
private class VaultSerializer extends JsonSerializer<Vault> {
@Override
public void serialize(Vault value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("path", value.getPath().toString());
jgen.writeStringField("mountName", value.getMountName().toString());
jgen.writeEndObject();
}
}
private class VaultDeserializer extends JsonDeserializer<Vault> {
@Override
public Vault deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final JsonNode node = jp.readValueAsTree();
final String pathStr = node.get("path").asText();
final Path path = FileSystems.getDefault().getPath(pathStr);
final Vault vault = vaultFactoy.createVault(path);
if (node.has("mountName")) {
vault.setMountName(node.get("mountName").asText());
}
return vault;
}
}
}

View File

@@ -8,111 +8,39 @@
******************************************************************************/
package org.cryptomator.ui.settings;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.model.Directory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.cryptomator.ui.model.Vault;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.ObjectMapper;
@JsonPropertyOrder(value = {"webdavWorkDir"})
@JsonPropertyOrder(value = {"directories"})
public class Settings implements Serializable {
private static final long serialVersionUID = 7609959894417878744L;
private static final Logger LOG = LoggerFactory.getLogger(Settings.class);
private static final Path SETTINGS_DIR;
private static final String SETTINGS_FILE = "settings.json";
private static final ObjectMapper JSON_OM = new ObjectMapper();
private static Settings INSTANCE = null;
static {
final String appdata = System.getenv("APPDATA");
final FileSystem fs = FileSystems.getDefault();
private List<Vault> directories;
if (SystemUtils.IS_OS_WINDOWS && appdata != null) {
SETTINGS_DIR = fs.getPath(appdata, "Cryptomator");
} else if (SystemUtils.IS_OS_WINDOWS && appdata == null) {
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
} else if (SystemUtils.IS_OS_MAC_OSX) {
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator");
} else {
// (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix"))
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
}
}
/**
* Package-private constructor; use {@link SettingsProvider}.
*/
Settings() {
private List<Directory> directories;
private String username;
private Settings() {
// private constructor
}
public static synchronized Settings load() {
if (INSTANCE == null) {
try {
Files.createDirectories(SETTINGS_DIR);
final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
final InputStream in = Files.newInputStream(settingsFile, StandardOpenOption.READ);
INSTANCE = JSON_OM.readValue(in, Settings.class);
return INSTANCE;
} catch (IOException e) {
LOG.warn("Failed to load settings, creating new one.");
INSTANCE = Settings.defaultSettings();
}
}
return INSTANCE;
}
public static synchronized void save() {
if (INSTANCE != null) {
try {
Files.createDirectories(SETTINGS_DIR);
final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
final OutputStream out = Files.newOutputStream(settingsFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
JSON_OM.writeValue(out, INSTANCE);
} catch (IOException e) {
LOG.error("Failed to save settings.", e);
}
}
}
private static Settings defaultSettings() {
return new Settings();
}
/* Getter/Setter */
public List<Directory> getDirectories() {
public List<Vault> getDirectories() {
if (directories == null) {
directories = new ArrayList<>();
}
return directories;
}
public void setDirectories(List<Directory> directories) {
public void setDirectories(List<Vault> directories) {
this.directories = directories;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

View File

@@ -0,0 +1,85 @@
package org.cryptomator.ui.settings;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.DeferredCloser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Provider;
public class SettingsProvider implements Provider<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(Settings.class);
private static final Path SETTINGS_DIR;
private static final String SETTINGS_FILE = "settings.json";
static {
final String appdata = System.getenv("APPDATA");
final FileSystem fs = FileSystems.getDefault();
if (SystemUtils.IS_OS_WINDOWS && appdata != null) {
SETTINGS_DIR = fs.getPath(appdata, "Cryptomator");
} else if (SystemUtils.IS_OS_WINDOWS && appdata == null) {
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
} else if (SystemUtils.IS_OS_MAC_OSX) {
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator");
} else {
// (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix"))
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
}
}
private final DeferredCloser deferredCloser;
private final ObjectMapper objectMapper;
@Inject
public SettingsProvider(DeferredCloser deferredCloser, @Named("VaultJsonMapper") ObjectMapper objectMapper) {
this.deferredCloser = deferredCloser;
this.objectMapper = objectMapper;
}
@Override
public Settings get() {
Settings settings = null;
try {
Files.createDirectories(SETTINGS_DIR);
final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
final InputStream in = Files.newInputStream(settingsFile, StandardOpenOption.READ);
settings = objectMapper.readValue(in, Settings.class);
settings.getDirectories().removeIf(v -> !v.isValidVaultDirectory());
} catch (IOException e) {
LOG.warn("Failed to load settings, creating new one.");
settings = new Settings();
}
deferredCloser.closeLater(settings, this::save);
return settings;
}
private void save(Settings settings) {
if (settings == null) {
return;
}
try {
Files.createDirectories(SETTINGS_DIR);
final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
final OutputStream out = Files.newOutputStream(settingsFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
objectMapper.writeValue(out, settings);
} catch (IOException e) {
LOG.error("Failed to save settings.", e);
}
}
}

View File

@@ -0,0 +1,55 @@
package org.cryptomator.ui.util;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.stage.Window;
public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
public static final String ACTIVE_WINDOW_STYLE_CLASS = "active-window";
public static final String INACTIVE_WINDOW_STYLE_CLASS = "inactive-window";
private final Window window;
private ActiveWindowStyleSupport(Window window) {
this.window = window;
this.addActiveWindowClassIfFocused(window.isFocused());
}
/**
* Creates and registers a listener on the given window, that will add the class {@value #ACTIVE_WINDOW_STYLE_CLASS} to the scenes root
* element, if the window is active. Otherwise {@value #INACTIVE_WINDOW_STYLE_CLASS} will be added. Allows CSS rules to be defined
* depending on the window's focus.<br/>
* <br/>
* Example:<br/>
* <code>
* .root.inactive-window .button {-fx-background-color: grey;}<br/>
* .root.active-window .button {-fx-background-color: blue;}
* </code>
*
* @param window The window to observe
* @return The observer
*/
public static ChangeListener<Boolean> startObservingFocus(final Window window) {
final ChangeListener<Boolean> observer = new WeakChangeListener<Boolean>(new ActiveWindowStyleSupport(window));
window.focusedProperty().addListener(observer);
return observer;
}
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
this.addActiveWindowClassIfFocused(newValue);
}
private void addActiveWindowClassIfFocused(Boolean focused) {
if (Boolean.TRUE.equals(focused)) {
window.getScene().getRoot().getStyleClass().add(ACTIVE_WINDOW_STYLE_CLASS);
window.getScene().getRoot().getStyleClass().remove(INACTIVE_WINDOW_STYLE_CLASS);
} else {
window.getScene().getRoot().getStyleClass().remove(ACTIVE_WINDOW_STYLE_CLASS);
window.getScene().getRoot().getStyleClass().add(INACTIVE_WINDOW_STYLE_CLASS);
}
}
}

View File

@@ -0,0 +1,43 @@
/*******************************************************************************
* 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
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.Optional;
/**
* Wrapper around an object, which should be closed later - explicitly or by a
* {@link DeferredCloser}. The wrapped object can be accessed as long as the
* resource has not been closed.
*
* @author Tillmann Gaida
*
* @param <T>
* any type
*/
public interface DeferredClosable<T> extends AutoCloseable {
/**
* Returns the wrapped Object.
*
* @return empty if the object has been closed.
*/
public Optional<T> get();
/**
* Quietly closes the Object. If the object was closed before, nothing
* happens.
*/
public void close();
/**
* @return an empty object.
*/
public static <T> DeferredClosable<T> empty() {
return DeferredCloser.empty();
}
}

View File

@@ -0,0 +1,123 @@
/*******************************************************************************
* 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
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.cryptomator.ui.controllers.MainController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* Tries to bring open-close symmetry in contexts where the resource outlives
* the current scope by introducing a manager, which closes the resources if
* they haven't been closed before.
* </p>
*
* <p>
* If you have a {@link DeferredCloser} instance present, call
* {@link #closeLater(Object, Closer)} immediately after you have opened the
* resource and return a resource handle. If {@link #close()} is called, the
* resource will be closed. Calling {@link DeferredClosable#close()} on the resource
* handle will also close the resource and prevent a second closing by
* {@link #close()}.
* </p>
*
* @author Tillmann Gaida
*/
public class DeferredCloser implements AutoCloseable {
public static interface Closer<T> {
void close(T object) throws Exception;
}
static class EmptyResource<T> implements DeferredClosable<T> {
@Override
public Optional<T> get() {
return Optional.empty();
}
@Override
public void close() {
}
}
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
final Map<Long, ManagedResource<?>> cleanups = new ConcurrentSkipListMap<>();
final AtomicLong counter = new AtomicLong();
public class ManagedResource<T> implements DeferredClosable<T> {
private final long number = counter.incrementAndGet();
private final AtomicReference<T> object = new AtomicReference<>();
private final Closer<T> closer;
ManagedResource(T object, Closer<T> closer) {
super();
this.object.set(object);
this.closer = closer;
}
public void close() {
final T oldObject = object.getAndSet(null);
if (oldObject != null) {
cleanups.remove(number);
try {
closer.close(oldObject);
} catch (Exception e) {
LOG.error("exception closing resource", e);
}
}
}
public Optional<T> get() throws IllegalStateException {
return Optional.ofNullable(object.get());
}
}
/**
* Closes all added objects which have not been closed before.
*/
public void close() {
for (ManagedResource<?> closableProvider : cleanups.values()) {
closableProvider.close();
}
}
public <T> DeferredClosable<T> closeLater(T object, Closer<T> closer) {
Objects.requireNonNull(object);
Objects.requireNonNull(closer);
final ManagedResource<T> resource = new ManagedResource<T>(object, closer);
cleanups.put(resource.number, resource);
return resource;
}
public <T extends AutoCloseable> DeferredClosable<T> closeLater(T object) {
Objects.requireNonNull(object);
final ManagedResource<T> resource = new ManagedResource<T>(object, AutoCloseable::close);
cleanups.put(resource.number, resource);
return resource;
}
private static final EmptyResource<?> EMPTY_RESOURCE = new EmptyResource<>();
@SuppressWarnings("unchecked")
public static <T> DeferredClosable<T> empty() {
return (DeferredClosable<T>) EMPTY_RESOURCE;
}
}

View File

@@ -10,12 +10,12 @@
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.concurrent.Callable;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javafx.application.Platform;
import javafx.collections.ObservableSet;
/**
* Use this utility class to spawn background tasks and wait for them to finish. <br/>
@@ -48,61 +48,14 @@ import javafx.application.Platform;
*/
public final class FXThreads {
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
private static final CallbackWhenTaskFailed DUMMY_EXCEPTION_CALLBACK = (e) -> {
// ignore.
};
private FXThreads() {
throw new AssertionError("Not instantiable.");
}
/**
* Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*
* <pre>
* // examples:
*
* Future&lt;String&gt; futureBookName1 = runOnBackgroundThread(restResource::getBookName);
*
* Future&lt;String&gt; futureBookName2 = runOnBackgroundThread(() -&gt; {
* return restResource.getBookName();
* });
* </pre>
*
* @param task The task to be executed on a background thread.
* @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*/
public static <T> Future<T> runOnBackgroundThread(Callable<T> task) {
return EXECUTOR.submit(task);
}
/**
* Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*
* <pre>
* // examples:
*
* Future&lt;?&gt; futureDone1 = runOnBackgroundThread(this::doSomeComplexCalculation);
*
* Future&lt;?&gt; futureDone2 = runOnBackgroundThread(() -&gt; {
* doSomeComplexCalculation();
* });
* </pre>
*
* @param task The task to be executed on a background thread.
* @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*/
public static Future<?> runOnBackgroundThread(Runnable task) {
return EXECUTOR.submit(task);
}
/**
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
* called. If you are interested in the exception, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
* {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
*
* <pre>
* // example:
@@ -112,20 +65,18 @@ public final class FXThreads {
* });
* </pre>
*
* @param executor
* @param task The task to wait for.
* @param successCallback The action to perform, when the task finished.
*/
public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback) {
runOnBackgroundThread(() -> {
return "asd";
});
FXThreads.runOnMainThreadWhenFinished(task, successCallback, DUMMY_EXCEPTION_CALLBACK);
public static <T> void runOnMainThreadWhenFinished(ExecutorService executor, Future<T> task, CallbackWhenTaskFinished<T> successCallback) {
runOnMainThreadWhenFinished(executor, task, successCallback, DUMMY_EXCEPTION_CALLBACK);
}
/**
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
* called. If you are interested in the exception, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
* {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
*
* <pre>
* // example:
@@ -137,14 +88,16 @@ public final class FXThreads {
* });
* </pre>
*
* @param executor The service to execute the background task on
* @param task The task to wait for.
* @param successCallback The action to perform, when the task finished.
* @param exceptionCallback
*/
public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback, CallbackWhenTaskFailed exceptionCallback) {
assertParamNotNull(task, "task must not be null.");
assertParamNotNull(successCallback, "successCallback must not be null.");
assertParamNotNull(exceptionCallback, "exceptionCallback must not be null.");
EXECUTOR.execute(() -> {
public static <T> void runOnMainThreadWhenFinished(ExecutorService executor, Future<T> task, CallbackWhenTaskFinished<T> successCallback, CallbackWhenTaskFailed exceptionCallback) {
Objects.requireNonNull(task, "task must not be null.");
Objects.requireNonNull(successCallback, "successCallback must not be null.");
Objects.requireNonNull(exceptionCallback, "exceptionCallback must not be null.");
executor.execute(() -> {
try {
final T result = task.get();
Platform.runLater(() -> {
@@ -158,12 +111,6 @@ public final class FXThreads {
});
}
private static void assertParamNotNull(Object param, String msg) {
if (param == null) {
throw new IllegalArgumentException(msg);
}
}
public interface CallbackWhenTaskFinished<T> {
void taskFinished(T result);
}
@@ -172,4 +119,8 @@ public final class FXThreads {
void taskFailed(Throwable t);
}
public static <E> ObservableSet<E> observableSetOnMainThread(ObservableSet<E> set) {
return new ObservableSetOnMainThread<E>(set);
}
}

View File

@@ -0,0 +1,80 @@
/*******************************************************************************
* 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
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
/**
* Manages and broadcasts events to a set of listeners. The types of the
* listener and event are entirely unbound. Instead, a method must be supplied
* to broadcast an event to a single listener.
*
* @author Tillmann Gaida
*
* @param <LISTENER>
* The type of listener.
* @param <EVENT>
* The type of event.
*/
public class ListenerRegistry<LISTENER, EVENT> {
final BiConsumer<LISTENER, EVENT> listenerCaller;
/**
* Constructs a new registry.
*
* @param listenerCaller
* The method which broadcasts an event to a single listener.
*/
public ListenerRegistry(BiConsumer<LISTENER, EVENT> listenerCaller) {
super();
this.listenerCaller = listenerCaller;
}
/**
* The handle of a registered listener.
*/
public interface ListenerRegistration {
void unregister();
}
final AtomicLong serial = new AtomicLong();
/*
* Since this is a {@link ConcurrentSkipListMap}, we can at the same time
* add to, remove from, and iterate over it. More importantly, a Listener
* can remove itself while being called from the {@link #broadcast(Object)}
* method.
*/
final Map<Long, LISTENER> listeners = new ConcurrentSkipListMap<>();
public ListenerRegistration registerListener(LISTENER listener) {
final long s = serial.incrementAndGet();
listeners.put(s, listener);
return () -> {
listeners.remove(s);
};
}
/**
* Broadcasts the given event to all registered listeners. If a listener
* causes an unchecked exception, that exception is thrown immediately
* without calling the other listeners.
*
* @param event
*/
public void broadcast(EVENT event) {
for (LISTENER listener : listeners.values()) {
listenerCaller.accept(listener, event);
}
}
}

View File

@@ -1,34 +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.util;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Files;
import java.nio.file.Path;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
public class MasterKeyFilter implements Filter<Path> {
public static MasterKeyFilter FILTER = new MasterKeyFilter();
private final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase();
@Override
public boolean accept(Path child) throws IOException {
return child.getFileName().toString().toLowerCase().endsWith(masterKeyExt);
}
public static final DirectoryStream<Path> filteredDirectory(Path dir) throws IOException {
return Files.newDirectoryStream(dir, FILTER);
}
}

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
* 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:
* Sebastian Stenzel - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.Collection;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
/**
* From the moment on, this aggregator is added as an observer to one or many {@link ObservableSet}s, change-events will be passed through
* to the given aggregation.
*/
public class ObservableSetAggregator<E> implements SetChangeListener<E> {
private final Collection<E> aggregation;
/**
* @param aggregation Set to which elements from observed subsets shall be added.
*/
public ObservableSetAggregator(final Collection<E> aggregation) {
this.aggregation = aggregation;
}
@Override
public void onChanged(Change<? extends E> change) {
if (change.getSet() == aggregation) {
// break cycle if aggregator observes aggregation
return;
}
if (change.wasAdded()) {
aggregation.add(change.getElementAdded());
} else if (change.wasRemoved()) {
aggregation.remove(change.getElementRemoved());
}
}
}

View File

@@ -0,0 +1,163 @@
package org.cryptomator.ui.util;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import javafx.collections.SetChangeListener.Change;
class ObservableSetOnMainThread<E> implements ObservableSet<E> {
private final ObservableSet<E> set;
private final Collection<InvalidationListener> invalidationListeners;
private final Collection<SetChangeListener<? super E>> setChangeListeners;
public ObservableSetOnMainThread(ObservableSet<E> set) {
this.set = set;
this.invalidationListeners = new HashSet<>();
this.setChangeListeners = new HashSet<>();
this.set.addListener(this::invalidated);
this.set.addListener(this::onChanged);
}
@Override
public int size() {
return set.size();
}
@Override
public boolean isEmpty() {
return set.isEmpty();
}
@Override
public boolean contains(Object o) {
return set.contains(o);
}
@Override
public Iterator<E> iterator() {
return set.iterator();
}
@Override
public Object[] toArray() {
return set.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return set.toArray(a);
}
@Override
public boolean add(E e) {
return set.add(e);
}
@Override
public boolean remove(Object o) {
return set.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return set.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return set.addAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return set.retainAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return set.removeAll(c);
}
@Override
public void clear() {
set.clear();
}
private void invalidated(Observable observable) {
Platform.runLater(() -> {
for (InvalidationListener listener : invalidationListeners) {
listener.invalidated(this);
}
});
}
@Override
public void addListener(InvalidationListener listener) {
invalidationListeners.add(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
invalidationListeners.remove(listener);
}
private void onChanged(Change<? extends E> change) {
final Change<? extends E> c = new SetChange(this, change.getElementAdded(), change.getElementRemoved());
Platform.runLater(() -> {
for (SetChangeListener<? super E> listener : setChangeListeners) {
listener.onChanged(c);
}
});
}
@Override
public void addListener(SetChangeListener<? super E> listener) {
setChangeListeners.add(listener);
}
@Override
public void removeListener(SetChangeListener<? super E> listener) {
setChangeListeners.add(listener);
}
private class SetChange extends SetChangeListener.Change<E> {
private final E added;
private final E removed;
public SetChange(ObservableSet<E> set, E added, E removed) {
super(set);
this.added = added;
this.removed = removed;
}
@Override
public boolean wasAdded() {
return added != null;
}
@Override
public boolean wasRemoved() {
return removed != null;
}
@Override
public E getElementAdded() {
return added;
}
@Override
public E getElementRemoved() {
return removed;
}
}
}

View File

@@ -0,0 +1,34 @@
package org.cryptomator.ui.util;
import java.util.Comparator;
import org.apache.commons.lang3.StringUtils;
public class SemVerComparator implements Comparator<String> {
@Override
public int compare(String version1, String version2) {
final String[] vComps1 = StringUtils.split(version1, '.');
final String[] vComps2 = StringUtils.split(version2, '.');
final int commonCompCount = Math.min(vComps1.length, vComps2.length);
for (int i = 0; i < commonCompCount; i++) {
int subversionComparisionResult = 0;
try {
final int v1 = Integer.parseInt(vComps1[i]);
final int v2 = Integer.parseInt(vComps2[i]);
subversionComparisionResult = v1 - v2;
} catch (NumberFormatException ex) {
// ok, lets compare this fragment lexicographically
subversionComparisionResult = vComps1[i].compareTo(vComps2[i]);
}
if (subversionComparisionResult != 0) {
return subversionComparisionResult;
}
}
// all in common so far? longest version string wins:
return vComps1.length - vComps2.length;
}
}

View File

@@ -0,0 +1,365 @@
/*******************************************************************************
* 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
******************************************************************************/
package org.cryptomator.ui.util;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.prefs.Preferences;
import org.apache.commons.io.IOUtils;
import org.cryptomator.ui.Cryptomator;
import org.cryptomator.ui.util.ListenerRegistry.ListenerRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Classes and methods to manage running this application in a mode, which only
* shows one instance.
*
* @author Tillmann Gaida
*/
public class SingleInstanceManager {
private static final Logger LOG = LoggerFactory.getLogger(SingleInstanceManager.class);
/**
* Connection to a running instance
*/
public static class RemoteInstance implements Closeable {
final SocketChannel channel;
RemoteInstance(SocketChannel channel) {
super();
this.channel = channel;
}
/**
* Sends a message to the running instance.
*
* @param string
* May not be longer than 2^16 - 1 bytes.
* @param timeout
* timeout in milliseconds. this should be larger than the
* precision of {@link System#currentTimeMillis()}.
* @return true if the message was sent within the given timeout.
* @throws IOException
*/
public boolean sendMessage(String string, long timeout) throws IOException {
Objects.requireNonNull(string);
byte[] message = string.getBytes();
if (message.length >= 256 * 256) {
throw new IOException("Message too long.");
}
ByteBuffer buf = ByteBuffer.allocate(message.length + 2);
buf.put((byte) (message.length / 256));
buf.put((byte) (message.length % 256));
buf.put(message);
buf.flip();
TimeoutTask.attempt(t -> {
if (channel.write(buf) < 0) {
return true;
}
return !buf.hasRemaining();
}, timeout, 10);
return !buf.hasRemaining();
}
@Override
public void close() throws IOException {
channel.close();
}
public int getRemotePort() throws IOException {
return ((InetSocketAddress) channel.getRemoteAddress()).getPort();
}
}
public static interface MessageListener {
void handleMessage(String message);
}
/**
* Represents a socket making this the main instance of the application.
*/
public static class LocalInstance implements Closeable {
private class ChannelState {
ByteBuffer write = ByteBuffer.wrap(applicationKey.getBytes());
ByteBuffer readLength = ByteBuffer.allocate(2);
ByteBuffer readMessage = null;
}
final ListenerRegistry<MessageListener, String> registry = new ListenerRegistry<>(MessageListener::handleMessage);
final String applicationKey;
final ServerSocketChannel channel;
final Selector selector;
int port = 0;
public LocalInstance(String applicationKey, ServerSocketChannel channel, Selector selector) {
Objects.requireNonNull(applicationKey);
this.applicationKey = applicationKey;
this.channel = channel;
this.selector = selector;
}
/**
* Register a listener for
*
* @param listener
* @return
*/
public ListenerRegistration registerListener(MessageListener listener) {
Objects.requireNonNull(listener);
return registry.registerListener(listener);
}
void handleSelection(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
final SocketChannel accepted = channel.accept();
if (accepted != null) {
LOG.info("accepted incoming connection");
accepted.configureBlocking(false);
accepted.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
}
if (key.attachment() == null) {
key.attach(new ChannelState());
}
ChannelState state = (ChannelState) key.attachment();
if (key.isWritable() && state.write != null) {
((WritableByteChannel) key.channel()).write(state.write);
if (!state.write.hasRemaining()) {
state.write = null;
}
LOG.debug("wrote welcome. switching to read only.");
key.interestOps(SelectionKey.OP_READ);
}
if (key.isReadable()) {
ByteBuffer buffer = state.readLength != null ? state.readLength : state.readMessage;
if (((ReadableByteChannel) key.channel()).read(buffer) < 0) {
key.cancel();
}
if (!buffer.hasRemaining()) {
buffer.flip();
if (state.readLength != null) {
int length = (buffer.get() + 256) % 256;
length = length * 256 + ((buffer.get() + 256) % 256);
state.readLength = null;
state.readMessage = ByteBuffer.allocate(length);
} else {
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
state.readMessage = null;
state.readLength = ByteBuffer.allocate(2);
registry.broadcast(new String(bytes, "UTF-8"));
}
}
}
}
public void close() {
IOUtils.closeQuietly(selector);
IOUtils.closeQuietly(channel);
if (getSavedPort(applicationKey).orElse(-1).equals(port)) {
Preferences.userNodeForPackage(Cryptomator.class).remove(applicationKey);
}
}
void selectionLoop() {
try {
final Set<SelectionKey> keysToRemove = new HashSet<>();
while (selector.select() > 0) {
final Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (Thread.interrupted()) {
return;
}
try {
handleSelection(key);
} catch (IOException | IllegalStateException e) {
LOG.error("exception in selector", e);
} finally {
keysToRemove.add(key);
}
}
keys.removeAll(keysToRemove);
}
} catch (ClosedSelectorException e) {
return;
} catch (Exception e) {
LOG.error("error while selecting", e);
}
}
}
/**
* Checks if there is a valid port at
* {@link Preferences#userNodeForPackage(Class)} for {@link Cryptomator} under the
* given applicationKey, tries to connect to the port at the loopback
* address and checks if the port identifies with the applicationKey.
*
* @param applicationKey
* key used to load the port and check the identity of the
* connection.
* @return
*/
public static Optional<RemoteInstance> getRemoteInstance(String applicationKey) {
Optional<Integer> port = getSavedPort(applicationKey);
if (!port.isPresent()) {
return Optional.empty();
}
SocketChannel channel = null;
boolean close = true;
try {
channel = SocketChannel.open();
channel.configureBlocking(false);
LOG.info("connecting to instance {}", port.get());
channel.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), port.get()));
SocketChannel fChannel = channel;
if (!TimeoutTask.attempt(t -> fChannel.finishConnect(), 1000, 10)) {
return Optional.empty();
}
LOG.info("connected to instance {}", port.get());
final byte[] bytes = applicationKey.getBytes();
ByteBuffer buf = ByteBuffer.allocate(bytes.length);
tryFill(channel, buf, 1000);
if (buf.hasRemaining()) {
return Optional.empty();
}
buf.flip();
for (int i = 0; i < bytes.length; i++) {
if (buf.get() != bytes[i]) {
return Optional.empty();
}
}
close = false;
return Optional.of(new RemoteInstance(channel));
} catch (Exception e) {
return Optional.empty();
} finally {
if (close) {
IOUtils.closeQuietly(channel);
}
}
}
static Optional<Integer> getSavedPort(String applicationKey) {
int port = Preferences.userNodeForPackage(Cryptomator.class).getInt(applicationKey, -1);
if (port == -1) {
LOG.info("no running instance found");
return Optional.empty();
}
return Optional.of(port);
}
/**
* Creates a server socket on a free port and saves the port in
* {@link Preferences#userNodeForPackage(Class)} for {@link Cryptomator} under the
* given applicationKey.
*
* @param applicationKey
* key used to save the port and identify upon connection.
* @param exec
* the task which is submitted is interruptable.
* @return
* @throws IOException
*/
public static LocalInstance startLocalInstance(String applicationKey, ExecutorService exec) throws IOException {
final ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
final int port = ((InetSocketAddress) channel.getLocalAddress()).getPort();
Preferences.userNodeForPackage(Cryptomator.class).putInt(applicationKey, port);
LOG.info("InstanceManager bound to port {}", port);
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_ACCEPT);
LocalInstance instance = new LocalInstance(applicationKey, channel, selector);
exec.submit(() -> {
try {
instance.port = ((InetSocketAddress) channel.getLocalAddress()).getPort();
} catch (IOException e) {
}
instance.selectionLoop();
});
return instance;
}
/**
* tries to fill the given buffer for the given time
*
* @param channel
* @param buf
* @param timeout
* @throws ClosedChannelException
* @throws IOException
*/
public static <T extends SelectableChannel & ReadableByteChannel> void tryFill(T channel, final ByteBuffer buf, int timeout) throws IOException {
if (channel.isBlocking()) {
throw new IllegalStateException("Channel is in blocking mode.");
}
try (Selector selector = Selector.open()) {
channel.register(selector, SelectionKey.OP_READ);
TimeoutTask.attempt(remainingTime -> {
if (!buf.hasRemaining()) {
return true;
}
if (selector.select(remainingTime) > 0) {
if (channel.read(buf) < 0) {
return true;
}
}
return !buf.hasRemaining();
}, timeout, 1);
}
}
}

View File

@@ -0,0 +1,81 @@
/*******************************************************************************
* 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
******************************************************************************/
package org.cryptomator.ui.util;
/**
* A task which is supposed to be repeated until it succeeds.
*
* @author Tillmann Gaida
*
* @param <E>
* The type of checked exception that this task may throw.
*/
public interface TimeoutTask<E extends Exception> {
/**
* Attempts to execute the task.
*
* @param timeout
* The time remaining to finish the task.
* @return true if the task finished, false if it needs to be attempted
* again.
* @throws E
* @throws InterruptedException
*/
boolean attempt(long timeout) throws E, InterruptedException;
/**
* Attempts a task until a timeout occurs. Checks for this timeout are based
* on {@link System#currentTimeMillis()}, so they are very crude. The task
* is guaranteed to be attempted once.
*
* @param task
* the task to perform.
* @param timeout
* time in millis before this method stops attempting to finish
* the task. greater than zero.
* @param sleepTimes
* time in millis to sleep between attempts. greater than zero.
* @return true if the task was finished, false if the task never always
* returned false or as soon as the task throws an
* {@link InterruptedException}.
* @throws E
* From the task.
*/
public static <E extends Exception> boolean attempt(TimeoutTask<E> task, long timeout, long sleepTimes) throws E {
if (timeout <= 0 || sleepTimes <= 0) {
throw new IllegalArgumentException();
}
long currentTime = System.currentTimeMillis();
long tryUntil = currentTime + timeout;
for (;; currentTime = System.currentTimeMillis()) {
if (currentTime >= tryUntil) {
return false;
}
try {
if (task.attempt(tryUntil - currentTime)) {
return true;
}
currentTime = System.currentTimeMillis();
if (currentTime + sleepTimes < tryUntil) {
Thread.sleep(sleepTimes);
} else {
return false;
}
} catch (InterruptedException e) {
return false;
}
}
}
}

View File

@@ -15,6 +15,9 @@ import java.util.ResourceBundle;
import javafx.application.Platform;
import javafx.stage.Stage;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.swing.SwingUtilities;
import org.apache.commons.lang3.SystemUtils;
@@ -88,8 +91,14 @@ public final class TrayIconUtil {
final String notificationCenterAppleScript = String.format("display notification \"%s\" with title \"%s\"", msg, title);
notificationCmd = () -> {
try {
Runtime.getRuntime().exec(new String[] {"/usr/bin/osascript", "-e", notificationCenterAppleScript});
} catch (IOException e) {
final ScriptEngineManager mgr = new ScriptEngineManager();
final ScriptEngine engine = mgr.getEngineByName("AppleScriptEngine");
if (engine != null) {
engine.eval(notificationCenterAppleScript);
} else {
Runtime.getRuntime().exec(new String[] {"/usr/bin/osascript", "-e", notificationCenterAppleScript});
}
} catch (ScriptException | IOException e) {
// ignore, user will notice the tray icon anyway.
}
};

View File

@@ -0,0 +1,10 @@
package org.cryptomator.ui.util.mount;
abstract class AbstractWebDavMount implements WebDavMount {
@Override
public void close() throws Exception {
this.unmount();
}
}

View File

@@ -8,6 +8,8 @@
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.net.URI;
/**
* A WebDavMounter acting as fallback if no other mounter works.
*
@@ -21,9 +23,14 @@ final class FallbackWebDavMounter implements WebDavMounterStrategy {
}
@Override
public WebDavMount mount(int localPort) {
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, String name) {
displayMountInstructions();
return new WebDavMount() {
return new AbstractWebDavMount() {
@Override
public void unmount() {
displayUnmountInstructions();

View File

@@ -9,6 +9,8 @@
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.net.URI;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
@@ -28,20 +30,25 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
return false;
}
}
@Override
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(int localPort) throws CommandFailedException {
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
final Script mountScript = Script.fromLines(
"set -x",
"gvfs-mount \"dav://[::1]:$PORT\"",
"xdg-open \"$URI\"")
.addEnv("PORT", String.valueOf(localPort));
"gvfs-mount \"dav:$DAV_SSP\"",
"xdg-open \"dav:$DAV_SSP\"")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
final Script unmountScript = Script.fromLines(
"set -x",
"gvfs-mount -u \"dav://[::1]:$PORT\"")
.addEnv("URI", String.valueOf(localPort));
"gvfs-mount -u \"dav:$DAV_SSP\"")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
mountScript.execute();
return new WebDavMount() {
return new AbstractWebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();

View File

@@ -9,6 +9,9 @@
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.net.URI;
import java.util.UUID;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
@@ -20,19 +23,27 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy {
}
@Override
public WebDavMount mount(int localPort) throws CommandFailedException {
final String path = "/Volumes/Cryptomator" + localPort;
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
// we don't use the uri to derive a path, as it *could* be longer than 255 chars.
final String path = "/Volumes/Cryptomator_" + UUID.randomUUID().toString();
final Script mountScript = Script.fromLines(
"mkdir \"$MOUNT_PATH\"",
"mount_webdav -S -v Cryptomator \"[::1]:$PORT\" \"$MOUNT_PATH\"",
"mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"",
"open \"$MOUNT_PATH\"")
.addEnv("PORT", String.valueOf(localPort))
.addEnv("MOUNT_PATH", path);
.addEnv("DAV_AUTHORITY", uri.getRawAuthority())
.addEnv("DAV_PATH", uri.getRawPath())
.addEnv("MOUNT_PATH", path)
.addEnv("MOUNT_NAME", name);
final Script unmountScript = Script.fromLines(
"umount $MOUNT_PATH")
"diskutil umount $MOUNT_PATH")
.addEnv("MOUNT_PATH", path);
mountScript.execute();
return new WebDavMount() {
return new AbstractWebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();

View File

@@ -8,13 +8,12 @@
******************************************************************************/
package org.cryptomator.ui.util.mount;
/**
* A mounted webdav share.
*
* @author Markus Kreusch
*/
public interface WebDavMount {
public interface WebDavMount extends AutoCloseable {
/**
* Unmounts this {@code WebDavMount}.
@@ -22,5 +21,5 @@ public interface WebDavMount {
* @throws CommandFailedException if the unmount operation fails
*/
void unmount() throws CommandFailedException;
}

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