Compare commits

...

59 Commits
0.2.0 ... 0.4.0

Author SHA1 Message Date
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
Sebastian Stenzel
3f32e4ee4b - Fixed initial encryption of vaults, that already contain files
- Disabled some UI controls during background tasks
- Simplified background vs UI thread switches using https://github.com/totalvoidness/FXThreads
2014-12-24 14:10:30 +01:00
Sebastian Stenzel
be5cf287c8 - win7/8 theme 2014-12-23 22:28:51 +01:00
Sebastian Stenzel
71892108b3 - L&F improvements on OS X 2014-12-22 22:39:22 +01:00
Sebastian Stenzel
1770bab699 - updated metadata file names 2014-12-21 20:08:09 +01:00
Sebastian Stenzel
1d05e878ab - Support for HTTP Range header fields, thus vastly improved performance for video streaming
- Simplified cryptor implementation for partial decryption
2014-12-21 16:54:47 +01:00
Sebastian Stenzel
f76091ddc0 - Made unit tests I/O-independent 2014-12-20 16:46:50 +01:00
Sebastian Stenzel
6dff296872 - using java.util.Random in unit tests again, as performance doesn't change by using non-random PRNG - of course still using a cryptographically secure PRNG in production ;-) 2014-12-20 11:18:12 +01:00
Sebastian Stenzel
6d98442f7e - preparation for http range requests: cryptor supports partial decryption now 2014-12-20 10:47:26 +01:00
Sebastian Stenzel
3cdda99c67 - closing _all_ process streams
- allowing multiple accesses to stdout / stderr in O(1)
2014-12-16 20:46:48 +01:00
Sebastian Stenzel
6b45d62aa1 - reduced visibility 2014-12-16 17:18:20 +01:00
Sebastian Stenzel
b7f3f00ce2 - Further simplification by using Futures :) 2014-12-16 16:56:42 +01:00
Sebastian Stenzel
dbadf54893 - General Simplification
- Refactoring: Using Concurrency API now.
- TODO: Use Futures instead of blocking methods
2014-12-16 12:14:54 +01:00
Sebastian Stenzel
38a0cfb2eb - faster unit test using insecure PRNG - test only ;) 2014-12-16 12:13:01 +01:00
Sebastian Stenzel
7d6d061d95 - removed admin privileges in native installer 2014-12-16 01:44:57 +01:00
Sebastian Stenzel
c743fa8bdc - fixed clean unmounting
- fixed correct subprocess status codes (not using status code of parent shell)
2014-12-16 01:35:00 +01:00
Sebastian Stenzel
8c2fe14e41 - bugfix: slow webdav on windows (http://support.microsoft.com/kb/2445570)
- bugfix: windows mount on non-german installations
- bugfix: system-dependent implementation of mount commands now done in specific strategy. no linux-specific URI outside of mount package, thus working on OS X again and simplified windows code
- change: now using ipv6
2014-12-15 23:46:06 +01:00
Sebastian Stenzel
ac4f10ce93 Merge pull request #3 from markuskreusch/master
Refactoring of WebDav mounting
2014-12-15 23:05:52 +01:00
Markus Kreusch
4f15645bf9 Merge branch 'webdav-mounting' 2014-12-15 22:54:03 +01:00
Markus Kreusch
c1f4ab6ada Refactored script execution 2014-12-15 22:50:53 +01:00
Sebastian Stenzel
fd54393f36 Merge branch 'master' of https://github.com/totalvoidness/cryptomator 2014-12-15 09:39:11 +01:00
Markus Kreusch
a2c3b38a75 refactored WebDavMounter, now using strategy pattern 2014-12-14 21:54:10 +01:00
Sebastian Stenzel
2fb35c59d4 - remove vaults using context menu
- locked/unlocked indicator
2014-12-13 21:24:48 +01:00
Sebastian Stenzel
afc62656bf - learning mathematics 2014-12-13 15:08:26 +01:00
Sebastian Stenzel
9c8e4fbf3b Merge branch 'master' of https://github.com/totalvoidness/cryptomator 2014-12-11 20:07:56 +01:00
Sebastian Stenzel
470a609938 Makes test work on windows 2014-12-11 20:07:46 +01:00
Sebastian Stenzel
863b2ec423 - Added throuput statistics 2014-12-11 19:46:57 +01:00
Sebastian Stenzel
d0a420d6c0 - FileTimes used to create RFC 1123 strings are now interpreted as UTC dates 2014-12-11 17:11:29 +01:00
Sebastian Stenzel
51e2e94ca9 - All modules use Java 8 now
- Fixed incorrect "last modified" date
- Simpler warning dialog when using non-empty directory as new vault location
2014-12-11 17:03:19 +01:00
Sebastian Stenzel
d7efd7fc2f - Updated version number 2014-12-11 01:03:29 +01:00
Sebastian Stenzel
db36cfa22e Updated download Links 2014-12-11 01:01:22 +01:00
84 changed files with 7948 additions and 849 deletions

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

@@ -3,7 +3,7 @@ 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
If you run OS X and want to take a look at the current alpha version, go ahead and [download Cryptomator.dmg](https://github.com/totalvoidness/cryptomator/releases/download/v0.1.0/Cryptomator.dmg).
If you want to take a look at the current beta version, go ahead and download [Cryptomator.dmg](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.dmg), [Cryptomator.exe](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.exe) or [Cryptomator.jar](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.jar).
## Features
- Totally transparent: Just work on the encrypted volume, as if it was an USB drive
@@ -18,8 +18,7 @@ If you run OS X and want to take a look at the current alpha version, go ahead a
## Security
- Default key length is 256 bit (falls back to 128 bit, if JCE isn't installed)
- PBKDF2 key generation
- 4096 bit internal masterkey
- Scrypt key generation
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
- Sensitive data is swiped from the heap asap
- Lightweight: Complexity kills security
@@ -30,23 +29,19 @@ If you run OS X and want to take a look at the current alpha version, go ahead a
- *NEW:* No Metadata at all. Encrypted files can be decrypted even on completely shuffled file systems (if their contents are undamaged).
## Dependencies
- Java 8 (for UI only - runs headless on Java 7)
- Maven
- Awesome 3rd party open source libraries (Apache Commons, Apache Jackrabbit, Jetty, Jackson, ...)
- Java 8
- see pom.xml ;-)
## TODO
### Core
- Support for HTTP range requests
### UI
- Automount of WebDAV volumes for Win/Tux
- Native L&F
- Drive icons in WebDAV volumes
- Change password functionality
- Better explanations on UI
## License
Distributed under the MIT license. See the LICENSE file for more info.
Distributed under the MIT X Consortium license license. See the LICENSE file for more info.
[![Build Status](https://travis-ci.org/totalvoidness/cryptomator.svg?branch=master)](https://travis-ci.org/totalvoidness/cryptomator)

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.2.0</version>
<version>0.4.0</version>
</parent>
<artifactId>core</artifactId>
<name>Cryptomator core I/O module</name>
@@ -63,18 +63,4 @@
<artifactId>commons-collections4</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,6 +1,8 @@
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;
@@ -24,7 +26,7 @@ public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements Cr
this.cryptor = cryptor;
this.encryptionDecider = encryptionDecider;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (rootDir.equals(dir) || encryptionDecider.shouldEncrypt(dir)) {
@@ -36,12 +38,15 @@ public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements Cr
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (encryptionDecider.shouldEncrypt(file)) {
final String plaintext = file.getFileName().toString();
final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this);
final Path newPath = file.resolveSibling(encrypted);
Files.move(file, newPath, StandardCopyOption.ATOMIC_MOVE);
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;
}
@@ -68,9 +73,9 @@ public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements Cr
final Path path = currentDir.resolve(metadataFile);
return Files.readAllBytes(path);
}
/* callback */
public interface EncryptionDecider {
boolean shouldEncrypt(Path path);
}

View File

@@ -23,10 +23,10 @@ import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class WebDAVServer {
public final class WebDavServer {
private static final Logger LOG = LoggerFactory.getLogger(WebDAVServer.class);
private static final String LOCALHOST = "127.0.0.1";
private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class);
private static final String LOCALHOST = "::1";
private static final int MAX_PENDING_REQUESTS = 200;
private static final int MAX_THREADS = 200;
private static final int MIN_THREADS = 4;
@@ -34,7 +34,7 @@ public final class WebDAVServer {
private final Server server;
private int port;
public WebDAVServer() {
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);
@@ -45,14 +45,15 @@ public final class WebDAVServer {
* @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) {
public synchronized boolean start(final String workDir, final boolean checkFileIntegrity, final Cryptor cryptor) {
final ServerConnector connector = new ServerConnector(server);
connector.setHost(LOCALHOST);
final String contextPath = "/";
final String servletPathSpec = "/*";
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.addServlet(getMiltonServletHolder(workDir, contextPath, cryptor), "/*");
context.addServlet(getWebDavServletHolder(workDir, contextPath, checkFileIntegrity, cryptor), servletPathSpec);
context.setContextPath(contextPath);
server.setHandler(context);
@@ -81,10 +82,11 @@ public final class WebDAVServer {
return server.isStopped();
}
private ServletHolder getMiltonServletHolder(final String workDir, final String contextPath, final Cryptor cryptor) {
private ServletHolder getWebDavServletHolder(final String workDir, final String contextPath, final boolean checkFileIntegrity, final Cryptor cryptor) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor));
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
result.setInitParameter(WebDavServlet.CFG_HTTP_ROOT, contextPath);
result.setInitParameter(WebDavServlet.CFG_CHECK_FILE_INTEGRITY, Boolean.toString(checkFileIntegrity));
return result;
}

View File

@@ -8,7 +8,7 @@ import org.apache.commons.collections4.map.LRUMap;
final class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
public BidiLRUMap(int maxSize) {
BidiLRUMap(int maxSize) {
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
}

View File

@@ -21,14 +21,14 @@ import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.SensitiveDataSwipeListener;
public class WebDavLocatorFactory extends AbstractLocatorFactory implements SensitiveDataSwipeListener, CryptorIOSupport {
class DavLocatorFactoryImpl extends AbstractLocatorFactory implements SensitiveDataSwipeListener, CryptorIOSupport {
private static final int MAX_CACHED_PATHS = 10000;
private final Path fsRoot;
private final Cryptor cryptor;
private final BidiMap<String, String> pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // <decryptedPath, encryptedPath>
public WebDavLocatorFactory(String fsRoot, String httpRoot, Cryptor cryptor) {
DavLocatorFactoryImpl(String fsRoot, String httpRoot, Cryptor cryptor) {
super(httpRoot);
this.fsRoot = FileSystems.getDefault().getPath(fsRoot);
this.cryptor = cryptor;

View File

@@ -11,6 +11,7 @@ package org.cryptomator.webdav.jackrabbit;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavMethods;
import org.apache.jackrabbit.webdav.DavResource;
@@ -24,28 +25,34 @@ 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.PathUtils;
import org.cryptomator.webdav.jackrabbit.resources.ResourcePathUtils;
import org.eclipse.jetty.http.HttpHeader;
public class WebDavResourceFactory implements DavResourceFactory {
class DavResourceFactoryImpl implements DavResourceFactory {
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final boolean checkFileIntegrity;
public WebDavResourceFactory(Cryptor cryptor) {
DavResourceFactoryImpl(Cryptor cryptor, boolean checkFileIntegrity) {
this.cryptor = cryptor;
this.checkFileIntegrity = checkFileIntegrity;
}
@Override
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
final Path path = PathUtils.getPhysicalPath(locator);
final Path path = ResourcePathUtils.getPhysicalPath(locator);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (Files.exists(path)) {
return createResource(locator, request.getDavSession());
} else if (DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
return createDirectory(locator, request.getDavSession());
} else if (DavMethods.METHOD_PUT.equals(request.getMethod())) {
if (Files.isRegularFile(path) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
return createFilePart(locator, request.getDavSession(), request);
} else if (Files.isRegularFile(path) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
return createFile(locator, request.getDavSession());
} else if (Files.isDirectory(path) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
return createDirectory(locator, request.getDavSession());
} else {
return createNonExisting(locator, request.getDavSession());
}
@@ -53,19 +60,23 @@ public class WebDavResourceFactory implements DavResourceFactory {
@Override
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
final Path path = PathUtils.getPhysicalPath(locator);
final Path path = ResourcePathUtils.getPhysicalPath(locator);
if (Files.isDirectory(path)) {
return createDirectory(locator, session);
} else if (Files.isRegularFile(path)) {
if (Files.isRegularFile(path)) {
return createFile(locator, session);
} else if (Files.isDirectory(path)) {
return createDirectory(locator, session);
} else {
return createNonExisting(locator, session);
}
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, checkFileIntegrity);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
return new EncryptedFile(this, locator, session, lockManager, cryptor);
return new EncryptedFile(this, locator, session, lockManager, cryptor, checkFileIntegrity);
}
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session) {

View File

@@ -8,38 +8,38 @@
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.util.HashSet;
import org.apache.jackrabbit.webdav.DavSession;
public class WebDavSession implements DavSession {
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
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;
public class WebDavSessionProvider implements DavSessionProvider {
class DavSessionProviderImpl implements DavSessionProvider {
@Override
public boolean attachSession(WebdavRequest request) throws DavException {
// every user gets a session
request.setDavSession(new WebDavSession());
// every request gets a session
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

@@ -22,8 +22,9 @@ 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";
public static final String CFG_HTTP_ROOT = "cfg.http.root";
public static final String CFG_CHECK_FILE_INTEGRITY = "cfg.checkFileIntegrity";
private DavSessionProvider davSessionProvider;
private DavLocatorFactory davLocatorFactory;
private DavResourceFactory davResourceFactory;
@@ -38,13 +39,14 @@ public class WebDavServlet extends AbstractWebdavServlet {
public void init(ServletConfig config) throws ServletException {
super.init(config);
davSessionProvider = new WebDavSessionProvider();
davSessionProvider = new DavSessionProviderImpl();
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
final String httpRoot = config.getInitParameter(CFG_HTTP_ROOT);
this.davLocatorFactory = new WebDavLocatorFactory(fsRoot, httpRoot, cryptor);
final boolean checkFileIntegrity = Boolean.parseBoolean(config.getInitParameter(CFG_CHECK_FILE_INTEGRITY));
this.davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, httpRoot, cryptor);
this.davResourceFactory = new WebDavResourceFactory(cryptor);
this.davResourceFactory = new DavResourceFactoryImpl(cryptor, checkFileIntegrity);
}
@Override

View File

@@ -11,8 +11,11 @@ package org.cryptomator.webdav.jackrabbit.resources;
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;
@@ -38,7 +41,7 @@ import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AbstractEncryptedNode implements DavResource {
abstract class AbstractEncryptedNode implements DavResource {
private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class);
private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
@@ -72,7 +75,7 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public boolean exists() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
return Files.exists(path);
}
@@ -104,7 +107,7 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public long getModificationTime() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
try {
return Files.getLastModifiedTime(path).toMillis();
} catch (IOException e) {
@@ -132,6 +135,27 @@ public 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
@@ -173,8 +197,8 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public void move(DavResource dest) throws DavException {
final Path src = PathUtils.getPhysicalPath(this);
final Path dst = PathUtils.getPhysicalPath(dest);
final Path src = ResourcePathUtils.getPhysicalPath(this);
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
try {
// check for conflicts:
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
@@ -195,8 +219,8 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public void copy(DavResource dest, boolean shallow) throws DavException {
final Path src = PathUtils.getPhysicalPath(this);
final Path dst = PathUtils.getPhysicalPath(dest);
final Path src = ResourcePathUtils.getPhysicalPath(this);
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
try {
// check for conflicts:
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {

View File

@@ -64,7 +64,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
}
private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException {
final Path childPath = PathUtils.getPhysicalPath(resource);
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
try {
Files.createDirectories(childPath);
} catch (SecurityException e) {
@@ -76,7 +76,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
}
private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException {
final Path childPath = PathUtils.getPhysicalPath(resource);
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
@@ -94,7 +94,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
@Override
public DavResourceIterator getMembers() {
final Path dir = PathUtils.getPhysicalPath(this);
final Path dir = ResourcePathUtils.getPhysicalPath(this);
try {
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
final List<DavResource> result = new ArrayList<>();
@@ -116,7 +116,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
@Override
public void removeMember(DavResource member) throws DavException {
final Path memberPath = PathUtils.getPhysicalPath(member);
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
try {
Files.walkFileTree(memberPath, new DeletingFileVisitor());
} catch (SecurityException e) {
@@ -133,14 +133,14 @@ public class EncryptedDir extends AbstractEncryptedNode {
@Override
protected void determineProperties() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
properties.add(new ResourceType(ResourceType.COLLECTION));
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
if (Files.exists(path)) {
try {
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<Long>(DavPropertyName.CREATIONDATE, attrs.creationTime().toMillis()));
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETLASTMODIFIED, attrs.lastModifiedTime().toMillis()));
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
// don't add any further properties

View File

@@ -29,7 +29,10 @@ import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,8 +40,11 @@ public 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 boolean checkIntegrity;
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, boolean checkIntegrity) {
super(factory, locator, session, lockManager, cryptor);
this.checkIntegrity = checkIntegrity;
}
@Override
@@ -63,31 +69,33 @@ public class EncryptedFile extends AbstractEncryptedNode {
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
if (checkIntegrity && !cryptor.authenticateContent(channel)) {
throw new DecryptFailedException("File content compromised: " + path.toString());
}
outputContext.setContentLength(cryptor.decryptedContentLength(channel));
if (outputContext.hasStream()) {
cryptor.decryptedFile(channel, outputContext.getOutputStream());
}
} catch (EOFException e) {
LOG.warn("Unexpected end of stream (possibly client hung up).");
} catch (IOException e) {
LOG.error("Error reading file " + path.toString(), e);
throw new IORuntimeException(e);
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
} finally {
IOUtils.closeQuietly(channel);
}
}
}
@Override
protected void determineProperties() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
SeekableByteChannel channel = null;
try {
@@ -96,8 +104,9 @@ public class EncryptedFile extends AbstractEncryptedNode {
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<Long>(DavPropertyName.CREATIONDATE, attrs.creationTime().toMillis()));
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETLASTMODIFIED, attrs.lastModifiedTime().toMillis()));
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
throw new IORuntimeException(e);

View File

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

View File

@@ -0,0 +1,34 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
final class FileTimeUtils {
private FileTimeUtils() {
throw new IllegalStateException("not instantiable");
}
static String toRfc1123String(FileTime time) {
final Temporal date = OffsetDateTime.ofInstant(time.toInstant(), ZoneOffset.UTC);
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

@@ -0,0 +1,20 @@
package org.cryptomator.webdav.jackrabbit.resources;
import org.apache.jackrabbit.webdav.property.AbstractDavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
class HttpHeaderProperty extends AbstractDavProperty<String> {
private final String value;
public HttpHeaderProperty(String key, String value) {
super(DavPropertyName.create(key), true);
this.value = value;
}
@Override
public String getValue() {
return value;
}
}

View File

@@ -14,9 +14,9 @@ import java.nio.file.Path;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceLocator;
public final class PathUtils {
public final class ResourcePathUtils {
private PathUtils() {
private ResourcePathUtils() {
throw new IllegalStateException("not instantiable");
}

View File

@@ -12,17 +12,28 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.2.0</version>
<version>0.4.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>
@@ -48,18 +59,4 @@
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -11,9 +11,7 @@ package org.cryptomator.crypto.aes256;
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.file.DirectoryStream.Filter;
import java.nio.file.Path;
@@ -21,29 +19,32 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
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.ArrayUtils;
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.exceptions.DecryptFailedException;
@@ -65,23 +66,10 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
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
* 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 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 static final byte[] EMPTY_MASTER_KEY = new byte[MASTER_KEY_LENGTH];
private static final int AES_KEY_LENGTH_IN_BITS;
/**
* Jackson JSON-Mapper.
@@ -89,62 +77,88 @@ 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 = Arrays.copyOf(EMPTY_MASTER_KEY, MASTER_KEY_LENGTH);
private SecretKey primaryMasterKey;
private static final int SIZE_OF_LONG = Long.SIZE / Byte.SIZE;
private static final int SIZE_OF_INT = Integer.SIZE / Byte.SIZE;
/**
* 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);
}
}
/**
* Fills the masterkey with new random bytes.
* Creates a new Cryptor with a newly initialized PRNG.
*/
public void randomizeMasterKey() {
public Aes256Cryptor() {
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
SECURE_PRNG.nextBytes(this.masterKey);
byte[] bytes = new byte[AES_KEY_LENGTH_IN_BITS / Byte.SIZE];
try {
SECURE_PRNG.nextBytes(bytes);
this.primaryMasterKey = new SecretKeySpec(bytes, AES_KEY_ALGORITHM);
SECURE_PRNG.nextBytes(bytes);
this.hMacMasterKey = new SecretKeySpec(bytes, HMAC_KEY_ALGORITHM);
} finally {
Arrays.fill(bytes, (byte) 0);
}
}
/**
* 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) {
byte[] bytes = new byte[AES_KEY_LENGTH_IN_BITS / Byte.SIZE];
try {
prng.nextBytes(bytes);
this.primaryMasterKey = new SecretKeySpec(bytes, AES_KEY_ALGORITHM);
prng.nextBytes(bytes);
this.hMacMasterKey = new SecretKeySpec(bytes, HMAC_KEY_ALGORITHM);
} finally {
Arrays.fill(bytes, (byte) 0);
}
}
/**
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
*/
@Override
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
if (ArrayUtils.isEquals(this.masterKey, EMPTY_MASTER_KEY)) {
throw new IllegalStateException("Masterkey not yet initialized.");
}
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);
}
}
@@ -157,56 +171,65 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
* this case Java JCE needs to be installed.
*/
@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) {
@@ -216,6 +239,31 @@ 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));
@@ -223,38 +271,19 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
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);
}
}
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);
// 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);
}
}
}
@@ -267,11 +296,10 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
@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, ioSupport);
encryptedPathComps.add(encrypted);
}
return StringUtils.join(encryptedPathComps, encryptedPathSep);
@@ -296,31 +324,34 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* {@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[] mac = hmacSha256(hMacMasterKey).doFinal(cleartext.getBytes());
final byte[] partialIv = ArrayUtils.subarray(mac, 0, 10);
final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
iv.put(partialIv);
final Cipher cipher = this.aesCtrCipher(key, iv.array(), 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;
final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(partialIv) + IV_PREFIX_SEPARATOR + ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
if (encrypted.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
final String crc32 = String.valueOf(crc32Sum(encrypted.getBytes()));
if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
final String crc32 = Long.toHexString(crc32Sum(ivAndCiphertext.getBytes()));
final String metadataFilename = crc32 + 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 = crc32 + LONG_NAME_PREFIX_SEPARATOR + 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) {
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, ioSupport);
cleartextPathComps.add(new String(cleartext));
}
return StringUtils.join(cleartextPathComps, cleartextPathSep);
@@ -333,21 +364,26 @@ 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 {
final String ciphertext;
final String ivAndCiphertext;
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 LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
ivAndCiphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (encrypted.endsWith(BASIC_FILE_EXT)) {
ciphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
ivAndCiphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
} else {
throw new IllegalArgumentException("Unsupported path component: " + encrypted);
}
final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.DECRYPT_MODE);
final String partialIvStr = StringUtils.substringBefore(ivAndCiphertext, IV_PREFIX_SEPARATOR);
final String ciphertext = StringUtils.substringAfter(ivAndCiphertext, IV_PREFIX_SEPARATOR);
final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
iv.put(ENCRYPTED_FILENAME_CODEC.decode(partialIvStr));
final Cipher cipher = this.aesCtrCipher(key, iv.array(), Cipher.DECRYPT_MODE);
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
final byte[] cleartextBytes = cipher.doFinal(encryptedBytes);
return new String(cleartextBytes, Charsets.UTF_8);
@@ -366,37 +402,114 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
}
@Override
public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
// read file size:
final Long fileSize = decryptedContentLength(encryptedFile);
// init mac:
final Mac mac = this.hmacSha256(hMacMasterKey);
// read stored mac:
encryptedFile.position(16);
final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
final int numMacBytesRead = encryptedFile.read(macBuffer);
// check validity of header:
if (numMacBytesRead != mac.getMacLength() || fileSize == null) {
throw new IOException("Failed to read file header.");
}
// read all encrypted data and calculate mac:
encryptedFile.position(64);
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream macIn = new MacInputStream(in, mac);
IOUtils.copyLarge(macIn, new NullOutputStream(), 0, fileSize);
// compare:
return Arrays.equals(macBuffer.array(), mac.doFinal());
}
@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);
// 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);
// read file size:
final Long fileSize = decryptedContentLength(encryptedFile);
// check validity of header:
if (numIvBytesRead != AES_BLOCK_LENGTH || fileSize == null) {
throw new IOException("Failed to read file header.");
}
// 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);
// 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 OutputStream cipheredOut = new CipherOutputStream(plaintextFile, cipher);
return IOUtils.copyLarge(in, cipheredOut);
final InputStream cipheredIn = new CipherInputStream(in, cipher);
return IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
// read iv:
encryptedFile.position(0);
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
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 - Long.BYTES, firstRelevantBlock);
// fast forward stream:
encryptedFile.position(64 + beginOfFirstRelevantBlock);
// 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, offsetInsideFirstRelevantBlock, length);
}
@Override
@@ -404,31 +517,60 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// truncate file
encryptedFile.truncate(0);
// use an IV, whose last 4 bytes store an integer used in counter mode and write initial value to file.
// 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.putInt(AES_BLOCK_LENGTH - SIZE_OF_INT, 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);
// skip 8 bytes (reserved for file size):
encryptedFile.position(SIZE_OF_LONG);
// write iv:
countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0l);
countingIv.position(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);
// init filesize buffer and skip 16 bytes
final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
encryptedFile.write(encryptedFileSizeBuffer);
// write content:
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
final OutputStream cipheredOut = new CipherOutputStream(out, cipher);
final OutputStream macOut = new MacOutputStream(out, mac);
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
final Long actualSize = IOUtils.copyLarge(plaintextFile, cipheredOut);
// write filesize
final ByteBuffer actualSizeBuffer = ByteBuffer.allocate(SIZE_OF_LONG);
actualSizeBuffer.putLong(actualSize);
actualSizeBuffer.position(0);
encryptedFile.position(0);
encryptedFile.write(actualSizeBuffer);
// copy MAC:
macBuffer.position(0);
macBuffer.put(mac.doFinal());
// append fake content:
final int randomContentLength = (int) Math.ceil((Math.random() + 1.0) * actualSize / 20.0);
final byte[] emptyBytes = new byte[AES_BLOCK_LENGTH];
for (int i = 0; i < randomContentLength; i += AES_BLOCK_LENGTH) {
cipheredOut.write(emptyBytes);
}
cipheredOut.flush();
// encrypt actualSize
try {
final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
fileSizeBuffer.putLong(actualSize);
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
encryptedFileSizeBuffer.position(0);
encryptedFileSizeBuffer.put(encryptedFileSize);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException(e);
}
// write file header
encryptedFile.position(16); // skip already written 128 bit IV
macBuffer.position(0);
encryptedFile.write(macBuffer); // 256 bit MAC
encryptedFileSizeBuffer.position(0);
encryptedFile.write(encryptedFileSizeBuffer); // 128 bit encrypted file size
return actualSize;
}

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 = 16;
/**
* Algorithm used for random number generation.
@@ -45,28 +45,33 @@ 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.
*
* @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 +79,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

@@ -22,7 +22,7 @@ interface FileNamingConventions {
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,6 +37,11 @@ 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.
*/

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

View File

@@ -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

@@ -8,131 +8,198 @@
******************************************************************************/
package org.cryptomator.crypto.aes256;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.ByteBuffer;
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.FileUtils;
import org.apache.commons.io.IOUtils;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class Aes256CryptorTest {
private Path tmpDir;
private Path masterKey;
@Before
public void prepareTmpDir() throws IOException {
final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir");
final Path path = FileSystems.getDefault().getPath(tmpDirName);
tmpDir = Files.createTempDirectory(path, "oce-crypto-test");
masterKey = tmpDir.resolve("test" + Aes256Cryptor.MASTERKEY_FILE_EXT);
}
@After
public void dropTmpDir() throws IOException {
FileUtils.deleteDirectory(tmpDir.toFile());
}
/* ------------------------------------------------------------------------------- */
@Test(expected = IllegalStateException.class)
public void testUninitializedMasterKey() throws IOException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.encryptMasterKey(out, pw);
}
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();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.randomizeMasterKey();
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
final Aes256Cryptor decryptor = new Aes256Cryptor();
final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ);
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
final InputStream in = new ByteArrayInputStream(out.toByteArray());
decryptor.decryptMasterKey(in, pw);
IOUtils.closeQuietly(out);
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();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.randomizeMasterKey();
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
IOUtils.closeQuietly(out);
final String wrongPw = "foo";
final Aes256Cryptor decryptor = new Aes256Cryptor();
final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ);
decryptor.decryptMasterKey(in, wrongPw);
// all these passwords are expected to fail.
final String[] wrongPws = {"a", "as", "asdf", "sdf", "das", "dsa", "foo", "bar", "baz"};
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
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(expected = NoSuchFileException.class)
public void testWrongLocation() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.randomizeMasterKey();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@Test
public void testIntegrityAuthentication() throws IOException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
final Path wrongMasterKey = tmpDir.resolve("notExistingMasterKey.json");
final Aes256Cryptor decryptor = new Aes256Cryptor();
final InputStream in = Files.newInputStream(wrongMasterKey, StandardOpenOption.READ);
decryptor.decryptMasterKey(in, pw);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
encryptedData.position(0);
// authenticate unmodified content:
final SeekableByteChannel encryptedIn1 = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean isContentUnmodified1 = cryptor.authenticateContent(encryptedIn1);
Assert.assertTrue(isContentUnmodified1);
// toggle one bit inf first content byte:
encryptedData.position(64);
final byte fifthByte = encryptedData.get();
encryptedData.position(64);
encryptedData.put((byte) (fifthByte ^ 0x01));
encryptedData.position(0);
// authenticate modified content:
final SeekableByteChannel encryptedIn2 = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean isContentUnmodified2 = cryptor.authenticateContent(encryptedIn2);
Assert.assertFalse(isContentUnmodified2);
}
@Test(expected = FileAlreadyExistsException.class)
public void testReInitialization() throws IOException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.randomizeMasterKey();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@Test
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
final OutputStream outAgain = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
cryptor.encryptMasterKey(outAgain, pw);
cryptor.swipeSensitiveData();
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
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.decryptedFile(encryptedIn, plaintextOut);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
// check decrypted data:
final byte[] result = plaintextOut.toByteArray();
Assert.assertArrayEquals(plaintextData, result);
}
@Test
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
// our test plaintext data:
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 65536; i++) {
bbIn.putInt(i);
}
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
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();
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 25000 * Integer.BYTES, 30000 * Integer.BYTES);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertTrue(numDecryptedBytes > 0);
// check decrypted data:
final byte[] result = plaintextOut.toByteArray();
final byte[] expected = Arrays.copyOfRange(plaintextData, 25000 * Integer.BYTES, 55000 * Integer.BYTES);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testEncryptionOfFilenames() throws IOException {
final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
final Aes256Cryptor cryptor = new Aes256Cryptor();
cryptor.randomizeMasterKey();
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// 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);
}

View File

@@ -0,0 +1,79 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
class ByteBufferBackedSeekableChannel implements SeekableByteChannel {
private final ByteBuffer buffer;
private boolean open = true;
ByteBufferBackedSeekableChannel(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public boolean isOpen() {
return open;
}
@Override
public void close() throws IOException {
open = false;
}
@Override
public int read(ByteBuffer dst) throws IOException {
if (buffer.remaining() == 0) {
return -1;
}
int num = Math.min(dst.remaining(), buffer.remaining());
byte[] bytes = new byte[num];
buffer.get(bytes);
dst.put(bytes);
return num;
}
@Override
public int write(ByteBuffer src) throws IOException {
int num = src.remaining();
if (buffer.remaining() < src.remaining()) {
buffer.limit(buffer.limit() + src.remaining());
}
buffer.put(src);
return num;
}
@Override
public long position() throws IOException {
return buffer.position();
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
if (newPosition > Integer.MAX_VALUE) {
throw new UnsupportedOperationException();
}
if (newPosition > buffer.limit()) {
buffer.limit((int) newPosition);
}
buffer.position((int) newPosition);
return this;
}
@Override
public long size() throws IOException {
return buffer.limit();
}
@Override
public SeekableByteChannel truncate(long size) throws IOException {
if (size > Integer.MAX_VALUE) {
throw new UnsupportedOperationException();
}
buffer.limit((int) size);
return this;
}
}

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.2.0</version>
<version>0.4.0</version>
</parent>
<artifactId>crypto-api</artifactId>
<name>Cryptomator cryptographic module API</name>
@@ -22,19 +22,9 @@
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,3 +1,11 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto;
import java.util.HashSet;

View File

@@ -15,11 +15,31 @@ import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
/**
* Provides access to cryptographic functions. All methods are threadsafe.
*/
public interface Cryptor extends SensitiveDataSwipeListener {
/**
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
*/
void encryptMasterKey(OutputStream out, CharSequence password) throws IOException;
/**
* Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
*
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong
* password. In this case a DecryptFailedException will be thrown.
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
* this case Java JCE needs to be installed.
*/
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException;
/**
* Encrypts each plaintext path component for its own.
*
@@ -48,6 +68,11 @@ public interface Cryptor extends SensitiveDataSwipeListener {
*/
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
/**
* @return <code>true</code> If the integrity of the file can be assured.
*/
boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException;
/**
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
* @return Content length of the decrypted file or <code>null</code> if unknown.
@@ -56,8 +81,17 @@ public interface Cryptor extends SensitiveDataSwipeListener {
/**
* @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
* @throws DecryptFailedException If decryption failed
*/
Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException;
Long decryptedFile(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, DecryptFailedException;
/**
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.

View File

@@ -0,0 +1,26 @@
/*******************************************************************************
* 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;
/**
* Optional monitoring interface. If a cryptor implements this interface, it counts bytes de- and encrypted in a thread-safe manner.
*/
public interface CryptorIOSampling {
/**
* @return Number of encrypted bytes since the last reset.
*/
Long pollEncryptedBytes(boolean resetCounter);
/**
* @return Number of decrypted bytes since the last reset.
*/
Long pollDecryptedBytes(boolean resetCounter);
}

View File

@@ -1,3 +1,11 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto;
import java.io.IOException;

View File

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

View File

@@ -1,3 +1,11 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto;
public interface SensitiveDataSwipeListener {

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

@@ -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,13 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the
terms of the MIT license. See the LICENSE.txt file for more info. Contributors:
Sebastian Stenzel - initial API and implementation -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 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.2.0</version>
<version>0.4.0</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -29,12 +26,14 @@
<!-- dependency versions -->
<log4j.version>2.1</log4j.version>
<junit.version>4.11</junit.version>
<slf4j.version>1.7.7</slf4j.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>
</properties>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-codec.version>1.10</commons-codec.version>
<jackson-databind.version>2.4.4</jackson-databind.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -61,6 +60,11 @@
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
@@ -103,14 +107,14 @@
<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>
</dependencies>
@@ -142,4 +146,18 @@
<module>ui</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.2.0</version>
<version>0.4.0</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
@@ -60,16 +60,6 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
@@ -112,11 +102,11 @@
<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>
<fx:permissions elevated="true" />
<fx:permissions elevated="false" />
<fx:preferences install="true" />
</fx:deploy>
</target>

View File

@@ -20,6 +20,7 @@ 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;
@@ -30,6 +31,7 @@ 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;
@@ -41,6 +43,7 @@ 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;
@@ -65,6 +68,9 @@ public class InitializeController implements Initializable {
@FXML
private Button okButton;
@FXML
private ProgressIndicator progressIndicator;
@FXML
private Label messageLabel;
@@ -123,6 +129,7 @@ public class InitializeController implements Initializable {
@FXML
protected void initializeVault(ActionEvent event) {
setControlsDisabled(true);
if (!isDirectoryEmpty() && !shouldEncryptExistingFiles()) {
return;
}
@@ -131,19 +138,29 @@ public class InitializeController implements Initializable {
final CharSequence password = passwordField.getCharacters();
OutputStream masterKeyOutputStream = null;
try {
progressIndicator.setVisible(true);
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
directory.getCryptor().randomizeMasterKey();
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
encryptExistingContents();
directory.getCryptor().swipeSensitiveData();
if (listener != null) {
listener.didInitialize(this);
}
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);
@@ -152,7 +169,14 @@ public class InitializeController implements Initializable {
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());
@@ -162,22 +186,26 @@ public class InitializeController implements Initializable {
throw new IllegalStateException(e);
}
}
private boolean shouldEncryptExistingFiles() {
final Alert alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle(localization.getString("initialize.alert.directoryIsNotEmpty.title"));
alert.setHeaderText(localization.getString("initialize.alert.directoryIsNotEmpty.header"));
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() throws IOException {
final FileVisitor<Path> visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile);
Files.walkFileTree(directory.getPath(), visitor);
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);

View File

@@ -19,7 +19,9 @@ import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.TrayIconUtil;
import org.eclipse.jetty.util.ConcurrentHashSet;
@@ -35,8 +37,9 @@ public class MainApplication extends Application {
@Override
public void start(final Stage primaryStage) throws IOException {
chooseNativeStylesheet();
final ResourceBundle rb = ResourceBundle.getBundle("localization");
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/main.fxml"), rb);
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"), rb);
final Parent root = loader.load();
final MainController ctrl = loader.getController();
ctrl.setStage(primaryStage);
@@ -46,11 +49,22 @@ public class MainApplication extends Application {
primaryStage.sizeToScene();
primaryStage.setResizable(false);
primaryStage.show();
ActiveWindowStyleSupport.startObservingFocus(primaryStage);
TrayIconUtil.init(primaryStage, rb, () -> {
quit();
});
}
private void chooseNativeStylesheet() {
if (SystemUtils.IS_OS_MAC_OSX) {
setUserAgentStylesheet(getClass().getResource("/css/mac_theme.css").toString());
} else if (SystemUtils.IS_OS_LINUX) {
setUserAgentStylesheet(getClass().getResource("/css/linux_theme.css").toString());
} else if (SystemUtils.IS_OS_WINDOWS) {
setUserAgentStylesheet(getClass().getResource("/css/win_theme.css").toString());
}
}
private void quit() {
Platform.runLater(() -> {
CLEAN_SHUTDOWN_PERFORMER.run();

View File

@@ -16,12 +16,15 @@ 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;
@@ -44,6 +47,9 @@ public class MainController implements Initializable, InitializationListener, Un
private Stage stage;
@FXML
private ContextMenu directoryContextMenu;
@FXML
private HBox rootPane;
@@ -58,9 +64,11 @@ public class MainController implements Initializable, InitializationListener, Un
@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);
directoryList.getItems().addAll(Settings.load().getDirectories());
}
@FXML
@@ -69,23 +77,41 @@ public class MainController implements Initializable, InitializationListener, Un
final File file = dirChooser.showDialog(stage);
if (file != null && file.canWrite()) {
final Directory dir = new Directory(file.toPath());
directoryList.getItems().add(dir);
Settings.load().getDirectories().clear();
Settings.load().getDirectories().addAll(directoryList.getItems());
directoryList.getSelectionModel().selectLast();
if (!directoryList.getItems().contains(dir)) {
directoryList.getItems().add(dir);
}
directoryList.getSelectionModel().select(dir);
}
}
private ListCell<Directory> createDirecoryListCell(ListView<Directory> param) {
return new DirectoryListCell();
final DirectoryListCell cell = new DirectoryListCell();
cell.setContextMenu(directoryContextMenu);
return cell;
}
private void selectedDirectoryDidChange(ListChangeListener.Change<? extends Directory> change) {
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
stage.setTitle(selectedDir.getName());
showDirectory(selectedDir);
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()) {
@@ -100,10 +126,6 @@ public class MainController implements Initializable, InitializationListener, Un
}
}
// ****************************************
// Subcontroller for right panel
// ****************************************
private <T> T showView(String fxml) {
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml), rb);
@@ -116,8 +138,12 @@ public class MainController implements Initializable, InitializationListener, Un
}
}
private void showWelcomeView() {
this.showView("/fxml/welcome.fxml");
}
private void showInitializeView(Directory directory) {
final InitializeController ctrl = showView("/initialize.fxml");
final InitializeController ctrl = showView("/fxml/initialize.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@@ -128,7 +154,7 @@ public class MainController implements Initializable, InitializationListener, Un
}
private void showUnlockView(Directory directory) {
final UnlockController ctrl = showView("/unlock.fxml");
final UnlockController ctrl = showView("/fxml/unlock.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@@ -140,7 +166,7 @@ public class MainController implements Initializable, InitializationListener, Un
}
private void showUnlockedView(Directory directory) {
final UnlockedController ctrl = showView("/unlocked.fxml");
final UnlockedController ctrl = showView("/fxml/unlocked.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}

View File

@@ -16,14 +16,18 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import java.util.concurrent.Future;
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.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
@@ -33,6 +37,7 @@ 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.util.FXThreads;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,6 +56,15 @@ public class UnlockController implements Initializable {
@FXML
private SecPasswordField passwordField;
@FXML
private CheckBox checkIntegrity;
@FXML
private Button unlockButton;
@FXML
private ProgressIndicator progressIndicator;
@FXML
private Label messageLabel;
@@ -77,13 +91,16 @@ public class UnlockController implements Initializable {
// ****************************************
@FXML
protected void didClickUnlockButton(ActionEvent event) {
private void didClickUnlockButton(ActionEvent event) {
setControlsDisabled(true);
final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
final CharSequence password = passwordField.getCharacters();
InputStream masterKeyInputStream = null;
try {
progressIndicator.setVisible(true);
masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
directory.setVerifyFileIntegrity(checkIntegrity.isSelected());
directory.getCryptor().decryptMasterKey(masterKeyInputStream, password);
if (!directory.startServer()) {
messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed"));
@@ -91,16 +108,24 @@ public class UnlockController implements Initializable {
return;
}
directory.setUnlocked(true);
directory.mount();
if (listener != null) {
listener.didUnlock(this);
}
final Future<Boolean> futureMount = FXThreads.runOnBackgroundThread(directory::mount);
FXThreads.runOnMainThreadWhenFinished(futureMount, this::didUnlockAndMount);
FXThreads.runOnMainThreadWhenFinished(futureMount, (result) -> {
setControlsDisabled(false);
});
} catch (DecryptFailedException | IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (WrongPasswordException e) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(rb.getString("unlock.errorMessage.wrongPassword"));
Platform.runLater(passwordField::requestFocus);
} catch (UnsupportedKeyLengthException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
} finally {
@@ -109,6 +134,13 @@ public class UnlockController implements Initializable {
}
}
private void setControlsDisabled(boolean disable) {
usernameBox.setDisable(disable);
passwordField.setDisable(disable);
checkIntegrity.setDisable(disable);
unlockButton.setDisable(disable);
}
private void findExistingUsernames() {
try {
DirectoryStream<Path> ds = MasterKeyFilter.filteredDirectory(directory.getPath());
@@ -128,6 +160,13 @@ public class UnlockController implements Initializable {
}
}
private void didUnlockAndMount(boolean mountSuccess) {
progressIndicator.setVisible(false);
if (listener != null) {
listener.didUnlock(this);
}
}
/* Getter/Setter */
public Directory getDirectory() {
@@ -137,6 +176,7 @@ public class UnlockController implements Initializable {
public void setDirectory(Directory directory) {
this.directory = directory;
this.findExistingUsernames();
this.checkIntegrity.setSelected(directory.shouldVerifyFileIntegrity());
}
public UnlockListener getListener() {

View File

@@ -11,30 +11,48 @@ package org.cryptomator.ui;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Label;
import javafx.util.Duration;
import org.cryptomator.crypto.CryptorIOSampling;
import org.cryptomator.ui.model.Directory;
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 Timeline ioAnimation;
@FXML
private Label messageLabel;
@FXML
private LineChart<Number, Number> ioGraph;
@FXML
private NumberAxis xAxis;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
}
@FXML
protected void closeVault(ActionEvent event) {
private void didClickCloseVault(ActionEvent event) {
directory.unmount();
directory.stopServer();
directory.setUnlocked(false);
@@ -43,6 +61,60 @@ public class UnlockedController implements Initializable {
}
}
// ****************************************
// IO Graph
// ****************************************
private void startIoSampling(final CryptorIOSampling sampler) {
final Series<Number, Number> decryptedBytes = new Series<>();
decryptedBytes.setName("decrypted");
final Series<Number, Number> encryptedBytes = new Series<>();
encryptedBytes.setName("encrypted");
ioGraph.getData().add(decryptedBytes);
ioGraph.getData().add(encryptedBytes);
ioAnimation = new Timeline();
ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(sampler, decryptedBytes, encryptedBytes)));
ioAnimation.setCycleCount(Animation.INDEFINITE);
ioAnimation.play();
}
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
private static final double BYTES_TO_MEGABYTES_FACTOR = 1.0 / IO_SAMPLING_INTERVAL / 1024.0 / 1024.0;
private final CryptorIOSampling sampler;
private final Series<Number, Number> decryptedBytes;
private final Series<Number, Number> encryptedBytes;
private int step = 0;
public IoSamplingAnimationHandler(CryptorIOSampling sampler, Series<Number, Number> decryptedBytes, Series<Number, Number> encryptedBytes) {
this.sampler = sampler;
this.decryptedBytes = decryptedBytes;
this.encryptedBytes = encryptedBytes;
}
@Override
public void handle(ActionEvent event) {
step++;
final double decryptedMb = sampler.pollDecryptedBytes(true) * BYTES_TO_MEGABYTES_FACTOR;
decryptedBytes.getData().add(new Data<Number, Number>(step, decryptedMb));
if (decryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
decryptedBytes.getData().remove(0);
}
final double encrypteddMb = sampler.pollEncryptedBytes(true) * BYTES_TO_MEGABYTES_FACTOR;
encryptedBytes.getData().add(new Data<Number, Number>(step, encrypteddMb));
if (encryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
encryptedBytes.getData().remove(0);
}
xAxis.setLowerBound(step - IO_SAMPLING_STEPS);
xAxis.setUpperBound(step);
}
}
/* Getter/Setter */
public Directory getDirectory() {
@@ -53,6 +125,12 @@ public class UnlockedController implements Initializable {
this.directory = directory;
final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), directory.getServer().getPort());
messageLabel.setText(msg);
if (directory.getCryptor() instanceof CryptorIOSampling) {
startIoSampling((CryptorIOSampling) directory.getCryptor());
} else {
ioGraph.setVisible(false);
}
}
public LockListener getListener() {

View File

@@ -1,19 +1,63 @@
package org.cryptomator.ui.controls;
import javafx.scene.control.ListCell;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ContentDisplay;
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;
public class DirectoryListCell extends ListCell<Directory> {
public class DirectoryListCell extends DraggableListCell<Directory> implements ChangeListener<Boolean> {
// fill: #FD4943, stroke: #E1443F
private static final Color RED_FILL = Color.rgb(253, 73, 67);
private static final Color RED_STROKE = Color.rgb(225, 68, 63);
// fill: #28CA40, stroke: #30B740
private static final Color GREEN_FILL = Color.rgb(40, 202, 64);
private static final Color GREEN_STROKE = Color.rgb(48, 183, 64);
private final Circle statusIndicator = new Circle(4.5);
public DirectoryListCell() {
setGraphic(statusIndicator);
setGraphicTextGap(12.0);
setContentDisplay(ContentDisplay.LEFT);
}
@Override
protected void updateItem(Directory item, boolean empty) {
final Directory oldItem = super.getItem();
if (oldItem != null) {
oldItem.unlockedProperty().removeListener(this);
}
super.updateItem(item, empty);
if (item == null) {
setText(null);
setTooltip(null);
statusIndicator.setVisible(false);
} else {
setText(item.getName());
setTooltip(new Tooltip(item.getPath().toString()));
statusIndicator.setVisible(true);
item.unlockedProperty().addListener(this);
updateStatusIndicator();
}
}
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
updateStatusIndicator();
}
private void updateStatusIndicator() {
final Paint fillColor = getItem().isUnlocked() ? GREEN_FILL : RED_FILL;
final Paint strokeColor = getItem().isUnlocked() ? GREEN_STROKE : RED_STROKE;
statusIndicator.setFill(fillColor);
statusIndicator.setStroke(strokeColor);
}
}

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

@@ -5,13 +5,18 @@ import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.lang3.StringUtils;
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.WebDavMounter;
import org.cryptomator.ui.util.WebDavMounter.CommandFailedException;
import org.cryptomator.webdav.WebDAVServer;
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;
@@ -25,11 +30,12 @@ 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 Aes256Cryptor cryptor = new Aes256Cryptor();
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 String unmountCommand;
private boolean verifyFileIntegrity;
private WebDavMount webDavMount;
private final Runnable shutdownTask = new ShutdownTask();
public Directory(final Path path) {
@@ -44,7 +50,7 @@ public class Directory implements Serializable {
}
public synchronized boolean startServer() {
if (server.start(path.toString(), cryptor)) {
if (server.start(path.toString(), verifyFileIntegrity, cryptor)) {
MainApplication.addShutdownTask(shutdownTask);
return true;
} else {
@@ -63,7 +69,7 @@ public class Directory implements Serializable {
public boolean mount() {
try {
unmountCommand = WebDavMounter.mount(server.getPort());
webDavMount = WebDavMounter.mount(server.getPort());
return true;
} catch (CommandFailedException e) {
LOG.warn("mount failed", e);
@@ -73,9 +79,9 @@ public class Directory implements Serializable {
public boolean unmount() {
try {
if (StringUtils.isNotEmpty(unmountCommand)) {
WebDavMounter.unmount(unmountCommand);
unmountCommand = null;
if (webDavMount != null) {
webDavMount.unmount();
webDavMount = null;
}
return true;
} catch (CommandFailedException e) {
@@ -90,6 +96,14 @@ public class Directory implements Serializable {
return path;
}
public boolean shouldVerifyFileIntegrity() {
return verifyFileIntegrity;
}
public void setVerifyFileIntegrity(boolean verifyFileIntegrity) {
this.verifyFileIntegrity = verifyFileIntegrity;
}
/**
* @return Directory name without preceeding path components
*/
@@ -97,19 +111,23 @@ public class Directory implements Serializable {
return path.getFileName().toString();
}
public Aes256Cryptor getCryptor() {
public Cryptor getCryptor() {
return cryptor;
}
public boolean isUnlocked() {
public ObjectProperty<Boolean> unlockedProperty() {
return unlocked;
}
public void setUnlocked(boolean unlocked) {
this.unlocked = unlocked;
public boolean isUnlocked() {
return unlocked.get();
}
public WebDAVServer getServer() {
public void setUnlocked(boolean unlocked) {
this.unlocked.set(unlocked);
}
public WebDavServer getServer() {
return server;
}

View File

@@ -17,7 +17,10 @@ public class DirectoryDeserializer extends JsonDeserializer<Directory> {
final JsonNode node = jp.readValueAsTree();
final String pathStr = node.get("path").asText();
final Path path = FileSystems.getDefault().getPath(pathStr);
return new Directory(path);
final Directory dir = new Directory(path);
final boolean verifyFileIntegrity = node.has("checkIntegrity") ? node.get("checkIntegrity").asBoolean() : false;
dir.setVerifyFileIntegrity(verifyFileIntegrity);
return dir;
}
}

View File

@@ -13,6 +13,7 @@ public class DirectorySerializer extends JsonSerializer<Directory> {
public void serialize(Directory value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("path", value.getPath().toString());
jgen.writeBooleanField("checkIntegrity", value.shouldVerifyFileIntegrity());
jgen.writeEndObject();
}

View File

@@ -18,7 +18,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.model.Directory;
@@ -28,7 +28,7 @@ import org.slf4j.LoggerFactory;
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;
@@ -54,8 +54,7 @@ public class Settings implements Serializable {
}
}
private Collection<Directory> directories;
private String username;
private List<Directory> directories;
private Settings() {
// private constructor
@@ -82,7 +81,7 @@ public class Settings implements Serializable {
try {
Files.createDirectories(SETTINGS_DIR);
final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
final OutputStream out = Files.newOutputStream(settingsFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
final OutputStream out = Files.newOutputStream(settingsFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
JSON_OM.writeValue(out, INSTANCE);
} catch (IOException e) {
LOG.error("Failed to save settings.", e);
@@ -96,23 +95,15 @@ public class Settings implements Serializable {
/* Getter/Setter */
public Collection<Directory> getDirectories() {
public List<Directory> getDirectories() {
if (directories == null) {
directories = new ArrayList<>();
}
return directories;
}
public void setDirectories(Collection<Directory> directories) {
public void setDirectories(List<Directory> directories) {
this.directories = directories;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

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,175 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* https://github.com/totalvoidness/FXThreads
*
* Contributors:
* Sebastian Stenzel
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javafx.application.Platform;
/**
* Use this utility class to spawn background tasks and wait for them to finish. <br/>
* <br/>
* <strong>Example use (ignoring exceptions):</strong>
*
* <pre>
* // get some string from a remote server:
* Future&lt;String&gt; futureBookName = runOnBackgroundThread(restResource::getBookName);
*
* // when done, update text label:
* runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
* myLabel.setText(bookName);
* });
* </pre>
*
* <strong>Example use (exception-aware):</strong>
*
* <pre>
* // get some string from a remote server:
* Future&lt;String&gt; futureBookName = runOnBackgroundThread(restResource::getBookName);
*
* // when done, update text label:
* runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
* myLabel.setText(bookName);
* }, (exception) -&gt; {
* myLabel.setText(&quot;An exception occured: &quot; + exception.getMessage());
* });
* </pre>
*/
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.
*
* <pre>
* // example:
*
* runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
* myLabel.setText(bookName);
* });
* </pre>
*
* @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);
}
/**
* 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.
*
* <pre>
* // example:
*
* runOnMainThreadWhenFinished(futureBookNamePossiblyFailing, (bookName) -&gt; {
* myLabel.setText(bookName);
* }, (exception) -&gt; {
* myLabel.setText(&quot;An exception occured: &quot; + exception.getMessage());
* });
* </pre>
*
* @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, 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(() -> {
try {
final T result = task.get();
Platform.runLater(() -> {
successCallback.taskFinished(result);
});
} catch (Exception e) {
Platform.runLater(() -> {
exceptionCallback.taskFailed(e);
});
}
});
}
private static void assertParamNotNull(Object param, String msg) {
if (param == null) {
throw new IllegalArgumentException(msg);
}
}
public interface CallbackWhenTaskFinished<T> {
void taskFinished(T result);
}
public interface CallbackWhenTaskFailed {
void taskFailed(Throwable t);
}
}

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

@@ -1,99 +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.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class WebDavMounter {
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounter.class);
private static final int CMD_DEFAULT_TIMEOUT = 3;
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*[A-Z]:\\s*");
private WebDavMounter() {
throw new IllegalStateException("not instantiable.");
}
/**
* @return Unmount Command
*/
public static synchronized String mount(int localPort) throws CommandFailedException {
if (SystemUtils.IS_OS_MAC_OSX) {
exec("mkdir /Volumes/Cryptomator" + localPort, CMD_DEFAULT_TIMEOUT);
exec("mount_webdav -S -v Cryptomator localhost:" + localPort + " /Volumes/Cryptomator" + localPort, CMD_DEFAULT_TIMEOUT);
exec("open /Volumes/Cryptomator" + localPort, CMD_DEFAULT_TIMEOUT);
return "umount /Volumes/Cryptomator" + localPort;
} else if (SystemUtils.IS_OS_WINDOWS) {
final String result = exec("net use * http://127.0.0.1:" + localPort + " /persistent:no", CMD_DEFAULT_TIMEOUT);
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);
if (matcher.find()) {
final String driveLetter = matcher.group();
return "net use " + driveLetter + " /delete";
}
} else if (SystemUtils.IS_OS_LINUX) {
// TODO check result of "which gvfs-mount" first and choose a good strategy. also refactor this class ;-)
exec("gvfs-mount dav://localhost:" + localPort, CMD_DEFAULT_TIMEOUT);
exec("xdg-open dav://localhost:" + localPort, CMD_DEFAULT_TIMEOUT);
return "gvfs-mount -u dav://localhost:" + localPort;
}
return null;
}
public static void unmount(String command) throws CommandFailedException {
if (command != null) {
exec(command, CMD_DEFAULT_TIMEOUT);
}
}
private static String exec(String cmd, int timoutSeconds) throws CommandFailedException {
try {
final Process proc;
if (SystemUtils.IS_OS_WINDOWS) {
proc = Runtime.getRuntime().exec(new String[] {"cmd", "/C", cmd});
} else {
proc = Runtime.getRuntime().exec(new String[] {"/bin/sh", "-c", cmd});
}
if (!proc.waitFor(timoutSeconds, TimeUnit.SECONDS)) {
proc.destroy();
throw new CommandFailedException("Timeout executing command " + cmd);
}
if (proc.exitValue() != 0) {
throw new CommandFailedException(IOUtils.toString(proc.getErrorStream()));
}
return IOUtils.toString(proc.getInputStream());
} catch (IOException | InterruptedException | IllegalThreadStateException e) {
LOG.error("Command execution failed.", e);
throw new CommandFailedException(e);
}
}
public static class CommandFailedException extends Exception {
private static final long serialVersionUID = 5784853630182321479L;
private CommandFailedException(String message) {
super(message);
}
private CommandFailedException(Throwable cause) {
super(cause);
}
}
}

View File

@@ -0,0 +1,98 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
* Sebastian Stenzel - using Futures, lazy loading for out/err.
******************************************************************************/
package org.cryptomator.ui.util.command;
import static java.lang.String.format;
import java.io.IOException;
import org.apache.commons.io.IOUtils;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class CommandResult {
private static final Logger LOG = LoggerFactory.getLogger(CommandResult.class);
private final Process process;
private final String stdout;
private final String stderr;
private final CommandFailedException exception;
/**
* Constructs a CommandResult from a terminated process and closes all its streams.
* @param process An <strong>already finished</strong> process.
*/
CommandResult(Process process) {
String out = null;
String err = null;
CommandFailedException ex = null;
try {
out = IOUtils.toString(process.getInputStream());
err = IOUtils.toString(process.getErrorStream());
} catch (IOException e) {
ex = new CommandFailedException(e);
} finally {
this.process = process;
this.stdout = out;
this.stderr = err;
this.exception = ex;
IOUtils.closeQuietly(process.getInputStream());
IOUtils.closeQuietly(process.getOutputStream());
IOUtils.closeQuietly(process.getErrorStream());
logDebugInfo();
}
}
/**
* @return Data written to STDOUT
*/
public String getStdOut() throws CommandFailedException {
assertNoException();
return stdout;
}
/**
* @return Data written to STDERR
*/
public String getStdErr() throws CommandFailedException {
assertNoException();
return stderr;
}
/**
* @return Exit value of the process
*/
public int getExitValue() {
return process.exitValue();
}
private void logDebugInfo() {
if (LOG.isDebugEnabled()) {
LOG.debug("Command execution finished. Exit code: {}\n" + "Output:\n" + "{}\n" + "Error:\n" + "{}\n", process.exitValue(), stdout, stderr);
}
}
void assertOk() throws CommandFailedException {
assertNoException();
int exitValue = getExitValue();
if (exitValue != 0) {
throw new CommandFailedException(format("Command execution failed. Exit code: %d\n" + "# Output:\n" + "%s\n" + "# Error:\n" + "%s", exitValue, stdout, stderr));
}
}
private void assertNoException() throws CommandFailedException {
if (exception != null) {
throw exception;
}
}
}

View File

@@ -0,0 +1,100 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
* Sebastian Stenzel - Refactoring
******************************************************************************/
package org.cryptomator.ui.util.command;
import static java.lang.String.format;
import static org.apache.commons.lang3.SystemUtils.IS_OS_UNIX;
import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.mount.CommandFailedException;
/**
* <p>
* Runs commands using a system compatible CLI.
* <p>
* To detect the system type {@link SystemUtils} is used. The following CLIs are used by default:
* <ul>
* <li><i>{@link #WINDOWS_DEFAULT_CLI}</i> if {@link SystemUtils#IS_OS_WINDOWS}
* <li><i>{@link #UNIX_DEFAULT_CLI}</i> if {@link SystemUtils#IS_OS_UNIX}
* </ul>
* <p>
* If the path to the executables differs from the default or the system can not be detected the Java system property
* {@value #CLI_EXECUTABLE_PROPERTY} can be set to define it.
* <p>
* If a CLI executable can not be determined using these methods operation of {@link CommandRunner} will fail with
* {@link IllegalStateException}s.
*
* @author Markus Kreusch
*/
final class CommandRunner {
public static final String CLI_EXECUTABLE_PROPERTY = "cryptomator.cli";
public static final String WINDOWS_DEFAULT_CLI[] = {"cmd", "/C"};
public static final String UNIX_DEFAULT_CLI[] = {"/bin/sh", "-c"};
private static final Executor CMD_EXECUTOR = Executors.newCachedThreadPool();
/**
* Executes all lines in the given script in the specified order. Stops as soon as the first command fails.
*
* @param script Script containing command lines and environment variables.
* @return Result of the last command, if it exited successfully.
* @throws CommandFailedException If one of the command lines in the given script fails.
*/
static CommandResult execute(Script script, long timeout, TimeUnit unit) throws CommandFailedException {
try {
final List<String> env = script.environment().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.toList());
CommandResult result = null;
for (final String line : script.getLines()) {
final String[] cmds = ArrayUtils.add(determineCli(), line);
final Process proc = Runtime.getRuntime().exec(cmds, env.toArray(new String[0]));
result = run(proc, timeout, unit);
result.assertOk();
}
return result;
} catch (IOException e) {
throw new CommandFailedException(e);
}
}
private static CommandResult run(Process process, long timeout, TimeUnit unit) throws CommandFailedException {
try {
final FutureCommandResult futureCommandResult = new FutureCommandResult(process);
CMD_EXECUTOR.execute(futureCommandResult);
return futureCommandResult.get(timeout, unit);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new CommandFailedException("Waiting time elapsed before command execution finished");
}
}
private static String[] determineCli() {
final String cliFromProperty = System.getProperty(CLI_EXECUTABLE_PROPERTY);
if (cliFromProperty != null) {
return cliFromProperty.split("");
} else if (IS_OS_WINDOWS) {
return WINDOWS_DEFAULT_CLI;
} else if (IS_OS_UNIX) {
return UNIX_DEFAULT_CLI;
} else {
throw new IllegalStateException(format("Failed to determine cli to use. Set Java system property %s to the executable.", CLI_EXECUTABLE_PROPERTY));
}
}
}

View File

@@ -0,0 +1,111 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel
******************************************************************************/
package org.cryptomator.ui.util.command;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.cryptomator.ui.util.mount.CommandFailedException;
final class FutureCommandResult implements Future<CommandResult>, Runnable {
private final Process process;
private final AtomicBoolean canceled = new AtomicBoolean();
private final AtomicBoolean done = new AtomicBoolean();
private final Lock lock = new ReentrantLock();
private final Condition doneCondition = lock.newCondition();
private CommandFailedException exception;
FutureCommandResult(Process process) {
this.process = process;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (done.get()) {
return false;
} else if (canceled.compareAndSet(false, true)) {
if (mayInterruptIfRunning) {
process.destroyForcibly();
}
}
return true;
}
@Override
public boolean isCancelled() {
return canceled.get();
}
private void setDone() {
lock.lock();
try {
done.set(true);
doneCondition.signalAll();
} finally {
lock.unlock();
}
}
@Override
public boolean isDone() {
return done.get();
}
@Override
public CommandResult get() throws InterruptedException, ExecutionException {
lock.lock();
try {
while(!done.get()) {
doneCondition.await();
}
} finally {
lock.unlock();
}
if (exception != null) {
throw new ExecutionException(exception);
}
return new CommandResult(process);
}
@Override
public CommandResult get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
lock.lock();
try {
while(!done.get()) {
doneCondition.await(timeout, unit);
}
} finally {
lock.unlock();
}
if (exception != null) {
throw new ExecutionException(exception);
}
return new CommandResult(process);
}
@Override
public void run() {
try {
process.waitFor();
} catch (InterruptedException e) {
exception = new CommandFailedException(e);
} finally {
setDone();
}
}
}

View File

@@ -0,0 +1,65 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
******************************************************************************/
package org.cryptomator.ui.util.command;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.cryptomator.ui.util.mount.CommandFailedException;
public final class Script {
private static final int DEFAULT_TIMEOUT_MILLISECONDS = 3000;
public static Script fromLines(String... commands) {
return new Script(commands);
}
private final String[] lines;
private final Map<String, String> environment = new HashMap<>();
private Script(String[] lines) {
this.lines = lines;
setEnv(System.getenv());
}
public String[] getLines() {
return lines;
}
public CommandResult execute() throws CommandFailedException {
return CommandRunner.execute(this, DEFAULT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
}
public CommandResult execute(long timeout, TimeUnit unit) throws CommandFailedException {
return CommandRunner.execute(this, timeout, unit);
}
Map<String, String> environment() {
return environment;
}
public Script setEnv(Map<String, String> environment) {
this.environment.clear();
addEnv(environment);
return this;
}
public Script addEnv(Map<String, String> environment) {
this.environment.putAll(environment);
return this;
}
public Script addEnv(String name, String value) {
environment.put(name, value);
return this;
}
}

View File

@@ -0,0 +1,24 @@
/*******************************************************************************
* 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
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
public class CommandFailedException extends Exception {
private static final long serialVersionUID = 5784853630182321479L;
public CommandFailedException(String message) {
super(message);
}
public CommandFailedException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,43 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
/**
* A WebDavMounter acting as fallback if no other mounter works.
*
* @author Markus Kreusch
*/
final class FallbackWebDavMounter implements WebDavMounterStrategy {
@Override
public boolean shouldWork() {
return true;
}
@Override
public WebDavMount mount(int localPort) {
displayMountInstructions();
return new WebDavMount() {
@Override
public void unmount() {
displayUnmountInstructions();
}
};
}
private void displayMountInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#mount which describes what to do
// Machine-readable mount instructions: http://tools.ietf.org/html/rfc4709#page-5 :-)
}
private void displayUnmountInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#unmount which describes what to do
}
}

View File

@@ -0,0 +1,52 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel, Markus Kreusch
* 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
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
@Override
public boolean shouldWork() {
if (SystemUtils.IS_OS_LINUX) {
final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open");
try {
checkScripts.execute();
return true;
} catch (CommandFailedException e) {
return false;
}
} else {
return false;
}
}
@Override
public WebDavMount mount(int localPort) throws CommandFailedException {
final Script mountScript = Script.fromLines(
"set -x",
"gvfs-mount \"dav://[::1]:$PORT\"",
"xdg-open \"$URI\"")
.addEnv("PORT", String.valueOf(localPort));
final Script unmountScript = Script.fromLines(
"set -x",
"gvfs-mount -u \"dav://[::1]:$PORT\"")
.addEnv("URI", String.valueOf(localPort));
mountScript.execute();
return new WebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();
}
};
}
}

View File

@@ -0,0 +1,43 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel, Markus Kreusch
* 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, strategy fine tuning
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
final class MacOsXWebDavMounter implements WebDavMounterStrategy {
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_MAC_OSX;
}
@Override
public WebDavMount mount(int localPort) throws CommandFailedException {
final String path = "/Volumes/Cryptomator" + localPort;
final Script mountScript = Script.fromLines(
"mkdir \"$MOUNT_PATH\"",
"mount_webdav -S -v Cryptomator \"[::1]:$PORT\" \"$MOUNT_PATH\"",
"open \"$MOUNT_PATH\"")
.addEnv("PORT", String.valueOf(localPort))
.addEnv("MOUNT_PATH", path);
final Script unmountScript = Script.fromLines(
"umount $MOUNT_PATH")
.addEnv("MOUNT_PATH", path);
mountScript.execute();
return new WebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();
}
};
}
}

View File

@@ -0,0 +1,26 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
/**
* A mounted webdav share.
*
* @author Markus Kreusch
*/
public interface WebDavMount {
/**
* Unmounts this {@code WebDavMount}.
*
* @throws CommandFailedException if the unmount operation fails
*/
void unmount() throws CommandFailedException;
}

View File

@@ -0,0 +1,55 @@
/*******************************************************************************
* 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
* Markus Kreusch - Refactored to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class WebDavMounter {
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounter.class);
private static final WebDavMounterStrategy[] STRATEGIES = {new WindowsWebDavMounter(), new MacOsXWebDavMounter(), new LinuxGvfsWebDavMounter()};
private static volatile WebDavMounterStrategy choosenStrategy;
/**
* Tries to mount a given webdav share.
*
* @param localPort local TCP port of the webdav share
* @return a {@link WebDavMount} representing the mounted share
* @throws CommandFailedException if the mount operation fails
*/
public static WebDavMount mount(int localPort) throws CommandFailedException {
return chooseStrategy().mount(localPort);
}
private static WebDavMounterStrategy chooseStrategy() {
if (choosenStrategy == null) {
choosenStrategy = getStrategyWhichShouldWork();
}
return choosenStrategy;
}
private static WebDavMounterStrategy getStrategyWhichShouldWork() {
for (WebDavMounterStrategy strategy : STRATEGIES) {
if (strategy.shouldWork()) {
LOG.info("Using {}", strategy.getClass().getSimpleName());
return strategy;
}
}
return new FallbackWebDavMounter();
}
private WebDavMounter() {
throw new IllegalStateException("Class is not instantiable.");
}
}

View File

@@ -0,0 +1,34 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
* Sebastian Stenzel - minor strategy fine tuning
******************************************************************************/
package org.cryptomator.ui.util.mount;
/**
* A strategy able to mount a webdav share and display it to the user.
*
* @author Markus Kreusch
*/
interface WebDavMounterStrategy {
/**
* @return {@code false} if this {@code WebDavMounterStrategy} can not work on the local machine, {@code true} if it could work
*/
boolean shouldWork();
/**
* Tries to mount a given webdav share.
*
* @param localPort local TCP port of the webdav share
* @return a {@link WebDavMount} representing the mounted share
* @throws CommandFailedException if the mount operation fails
*/
WebDavMount mount(int localPort) throws CommandFailedException;
}

View File

@@ -0,0 +1,61 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel, Markus Kreusch
* 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, strategy fine tuning
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import static org.cryptomator.ui.util.command.Script.fromLines;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.CommandResult;
import org.cryptomator.ui.util.command.Script;
/**
* A {@link WebDavMounterStrategy} utilizing the "net use" command.
* <p>
* Tested on Windows 7 but should also work on Windows 8.
*
* @author Markus Kreusch
*/
final class WindowsWebDavMounter implements WebDavMounterStrategy {
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]:)\\s*");
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_WINDOWS;
}
@Override
public WebDavMount mount(int localPort) throws CommandFailedException {
final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT% /persistent:no").addEnv("PORT", String.valueOf(localPort));
final CommandResult mountResult = mountScript.execute(30, TimeUnit.SECONDS);
final String driveLetter = getDriveLetter(mountResult.getStdOut());
final Script unmountScript = fromLines("net use " + driveLetter + " /delete").addEnv("DRIVE_LETTER", driveLetter);
return new WebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();
}
};
}
private String getDriveLetter(String result) throws CommandFailedException {
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);
if (matcher.find()) {
return matcher.group(1);
} else {
throw new CommandFailedException("Failed to get a drive letter from net use output.");
}
}
}

View File

@@ -0,0 +1 @@
apple.applescript.AppleScriptEngineFactory

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,906 @@
/*
* 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
*
*/
.root {
-fx-font-family: 'lucida-grande';
-fx-font-smoothing-type: lcd;
-fx-font-size: 13.0;
/***************************************************************************
* *
* The main color palette from which the rest of the colors are derived. *
* *
**************************************************************************/
-fx-base: #FFFFFF;
-fx-background: #ECECEC;
/* Used for the inside of text boxes, password boxes, lists, trees, and
* tables. See also -fx-text-inner-color, which should be used as the
* -fx-text-fill value for text painted on top of backgrounds colored
* with -fx-control-inner-background.
*/
-fx-control-inner-background: #FFFFFF;
/* One of these colors will be chosen based upon a ladder calculation
* that uses the brightness of a background color. Instead of using these
* colors directly as -fx-text-fill values, the sections in this file should
* use a derived color to match the background in use. See also:
*
* -fx-text-base-color for text on top of -fx-base, -fx-color, and -fx-body-color
* -fx-text-background-color for text on top of -fx-background
* -fx-text-inner-color for text on top of -fx-control-inner-color
* -fx-selection-bar-text for text on top of -fx-selection-bar
*/
-fx-dark-text-color: black;
-fx-mid-text-color: #B5B5B5;
-fx-light-text-color: white;
/* A bright blue for highlighting/accenting objects. For example: selected
* text; selected items in menus, lists, trees, and tables; progress bars */
-fx-accent: #B2D7FF;
/* A bright blue for the focus indicator of objects. Typically used as the
* first color in -fx-background-color for the "focused" pseudo-class. Also
* typically used with insets of -1.4 to provide a glowing effect.
*/
-fx-focus-color: #78A6D7;
-fx-faint-focus-color: #8FBDEE;
/* The color that is used in styling controls. The default value is based
* on -fx-base, but is changed by pseudoclasses to change the base color.
* For example, the "hover" pseudoclass will typically set -fx-color to
* -fx-hover-base (see below) and the "armed" pseudoclass will typically
* set -fx-color to -fx-pressed-base.
*/
-fx-color: -fx-base;
/* The opacity level to use for the "disabled" pseudoclass.
*/
-fx-disabled-opacity: 0.6;
/* Chart Color Palette */
CHART_COLOR_1: #28CA40;
CHART_COLOR_2: #FD4943;
CHART_COLOR_3: #2283FB;
CHART_COLOR_4: #FAEA77;
CHART_COLOR_5: #FA9E78;
CHART_COLOR_6: #F47BF8;
CHART_COLOR_7: #c84164;
CHART_COLOR_8: #888888;
/***************************************************************************
* *
* Colors that are derived from the main color palette. *
* *
**************************************************************************/
/* The color to use for -fx-text-fill when text is to be painted on top of
* a background filled with the -fx-background color.
*/
-fx-text-background-color: -fx-dark-text-color;
/* A little darker than -fx-color and used to draw boxes around objects such
* as progress bars, scroll bars, scroll panes, trees, tables, and lists.
*/
-fx-box-border: #C8C8C8;
/* Darker than -fx-background and used to draw boxes around text boxes and
* password boxes.
*/
-fx-text-box-border: #B5B5B5;
/* A gradient that goes from a little darker than -fx-color on the top to
* even more darker than -fx-color on the bottom. Typically is the second
* color in the -fx-background-color list as the small thin border around
* a control. It is typically the same size as the control (i.e., insets
* are 0).
*/
-fx-outer-border: derive(-fx-color,-23%);
/* A gradient that goes from a bit lighter than -fx-color on the top to
* a little darker at the bottom. Typically is the third color in the
* -fx-background-color list as a thin highlight inside the outer border.
* Insets are typically 1.
*/
-fx-inner-border: linear-gradient(to bottom, derive(-fx-color,75%), derive(-fx-color,2%));
/* A gradient that goes from a little lighter than -fx-color at the top to
* a little darker than -fx-color at the bottom and is used to fill the
* body of many controls such as buttons. Typically is the fourth color
* in the -fx-background-color list and represents main body of the control.
* Insets are typically 2.
*/
-fx-body-color: linear-gradient(to bottom, derive(-fx-color,10%) ,derive(-fx-color,-6%));
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-base, -fx-color, and -fx-body-color.
*/
-fx-text-base-color: -fx-dark-text-color;
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-control-inner-background.
*/
-fx-text-inner-color: -fx-dark-text-color;
/* Background for items in list like things such as menus, lists, trees,
* and tables.
*
* TODO: it seems like this should be based upon -fx-accent and we should
* remove the setting -fx-background in all the sections that use
* -fx-selection-bar.
*/
-fx-selection-bar: #0069D9;
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-selection-bar.
*
* TODO: it seems like this should be derived from -fx-selection-bar.
*/
-fx-selection-bar-text: -fx-light-text-color;
/* These are needed for Popup */
-fx-background-color: inherit;
-fx-background-radius: inherit;
-fx-background-insets: inherit;
-fx-padding: inherit;
/***************************************************************************
* *
* Set the default background color for the scene *
* *
**************************************************************************/
-fx-background-color: -fx-background;
}
/*******************************************************************************
* *
* Common Styles *
* *
* These are styles that give a standard look to a whole range of controls *
* *
******************************************************************************/
/* ==== BUTTON LIKE THINGS ============================================== */
.button,
.toggle-button,
.menu-button,
.choice-box,
.color-picker.split-button > .color-picker-label,
.combo-box-base,
.combo-box-base > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #C1C1C1 0%, #A6A6A6 100%), -fx-base;
-fx-background-insets: 0, 1;
-fx-background-radius: 4;
-fx-padding: 0.2em 0.8em 0.2em 0.8em;
-fx-text-fill: -fx-dark-text-color;
-fx-alignment: CENTER;
-fx-focus-traversable: false;
-fx-effect: dropshadow(one-pass-box, #DCDCDC, 0.0, 0.0, 0.0, 1.0);
}
.button:hover,
.toggle-button:hover,
.radio-button:hover > .radio,
.menu-button:hover,
.split-menu-button > .label:hover,
.split-menu-button > .arrow-button:hover,
.slider .thumb:hover,
.scroll-bar > .thumb:hover,
.scroll-bar > .increment-button:hover,
.scroll-bar > .decrement-button:hover,
.choice-box:hover,
.color-picker.split-button > .arrow-button:hover,
.color-picker.split-button > .color-picker-label:hover,
.combo-box-base:hover,
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:hover {
-fx-color: -fx-base;
}
.button:armed,
.button:default:armed,
.toggle-button:armed,
.menu-button:armed,
.split-menu-button:armed > .label,
.split-menu-button > .arrow-button:pressed,
.split-menu-button:showing > .arrow-button,
.slider .thumb:pressed,
.scroll-bar > .thumb:pressed,
.scroll-bar > .increment-button:pressed,
.scroll-bar > .decrement-button:pressed,
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:pressed {
-fx-background-color: linear-gradient(to bottom, #237FFE 0%, #023FDD 100%), linear-gradient(to bottom, #4A97FD 0%, #0867E4 100%);
-fx-text-fill: -fx-light-text-color;
}
.button:focused,
.toggle-button:focused,
.menu-button:focused,
.choice-box:focused,
.color-picker.split-button:focused > .color-picker-label {
-fx-background-color: -fx-faint-focus-color, -fx-focus-color, -fx-inner-border, -fx-body-color;
-fx-background-insets: -2, -0.3, 1, 2;
-fx-background-radius: 7, 6, 4, 3;
}
/* ==== DISABLED THINGS ================================================= */
.button:disabled,
.toggle-button:disabled,
.hyperlink:disabled,
.menu-button:disabled,
.split-menu-button:disabled,
.slider:disabled,
.scroll-pane:disabled,
.progress-bar:disabled,
.progress-indicator:disabled,
.text-input:disabled,
.choice-box:disabled,
.combo-box-base:disabled,
.list-view:disabled,
.tree-view:disabled,
.table-view:disabled,
.tree-table-view:disabled,
.tab-pane:disabled,
.tab-pane > .tab-header-area > .headers-region > .tab:disabled {
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
-fx-background-insets: 0, 1;
-fx-text-fill: -fx-mid-text-color;
-fx-effect: dropshadow(one-pass-box, #E0E0E0, 0.0, 0.0, 0.0, 0.5);
}
/* ==== MNEMONIC THINGS ================================================= */
.button:show-mnemonics .mnemonic-underline,
.toggle-button:show-mnemonics .mnemonic-underline,
.hyperlink:show-mnemonics > .mnemonic-underline,
.split-menu-button:show-mnemonics > .mnemonic-underline,
.menu-button:show-mnemonics > .mnemonic-underline {
-fx-stroke: -fx-text-base-color;
}
/* ==== CHOICE BOX LIKE THINGS ========================================== */
.combo-box-base {
-fx-padding: 0;
}
/* ==== BOX LIKE THINGS ================================================= */
.scroll-pane,
.split-pane,
.list-view,
.tree-view,
.table-view,
.tree-table-view {
-fx-background-color: -fx-box-border, -fx-control-inner-background;
-fx-background-insets: 0, 1;
-fx-padding: 1;
}
/*******************************************************************************
* *
* Label *
* *
******************************************************************************/
.label {
-fx-text-fill: -fx-text-background-color;
}
/*******************************************************************************
* *
* Button & ToggleButton *
* *
******************************************************************************/
/* ==== DEFAULT ========================================================= */
.root.active-window .button:default {
-fx-background-color: linear-gradient(to bottom, #4AA0F9 0%, #045FFF 100%), linear-gradient(to bottom, #69B2FA 0%, #0D81FF 100%);
-fx-text-fill: -fx-light-text-color;
}
.button:default:disabled {
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
-fx-background-insets: 0, 1;
-fx-text-fill: -fx-mid-text-color;
}
/*******************************************************************************
* *
* CheckBox *
* *
******************************************************************************/
.check-box {
-fx-label-padding: 0 0 0 3px;
-fx-text-fill: -fx-text-background-color;
}
.check-box > .box {
-fx-padding: 3px;
-fx-background-color: linear-gradient(to bottom, #A5A5A5 0%, #B8B8B8 100%), #F3F3F3, #FFFFFF;
-fx-background-radius: 2.5, 2.5, 2.5;
-fx-background-insets: 0, 1, 2 1 1 1;
}
.check-box > .box > .mark {
-fx-background-color: transparent;
-fx-padding: 4px;
-fx-shape: "M-1,4, L-1,5.5 L3.5,8.5 L9,0 L9,-1 L7,-1 L3,6 L1,4 Z";
}
.root.active-window .check-box:selected > .box {
-fx-background-color: #2C90FC, #3B99FC;
-fx-background-insets: 0, 1;
}
.root.active-window .check-box:selected > .box > .mark {
-fx-background-color: white;
}
.root.inactive-window .check-box:selected > .box > .mark {
-fx-background-color: #444444;
}
/*******************************************************************************
* *
* ToolBar *
* *
******************************************************************************/
.tool-bar:horizontal {
-fx-background-color: -fx-box-border, -fx-background;
-fx-background-insets: 0;
-fx-padding: 0.4em;
-fx-spacing: 0.2em;
-fx-alignment: CENTER_LEFT;
}
.tool-bar:horizontal > .container > .separator {
-fx-orientation: vertical;
}
/*******************************************************************************
* *
* ScrollBar *
* *
******************************************************************************/
.scroll-bar:horizontal,
.scroll-bar:vertical {
-fx-background-color: #E8E8E8, #FAFAFA;
}
.scroll-bar:disabled {
-fx-opacity: -fx-disabled-opacity;
}
.scroll-bar > .thumb {
-fx-background-color: #C1C1C1;
-fx-background-insets: 2px;
-fx-background-radius: 4px;
}
.scroll-bar > .thumb:hover {
-fx-background-color: #7D7D7D;
}
.scroll-bar > .increment-button,
.scroll-bar > .decrement-button {
-fx-background-color: transparent;
-fx-color: transparent;
}
.scroll-bar:horizontal > .increment-button,
.scroll-bar:horizontal > .decrement-button {
-fx-padding: 6px 0px;
}
.scroll-bar:vertical > .increment-button,
.scroll-bar:vertical > .decrement-button {
-fx-padding: 0px 6px;
}
/*******************************************************************************
* *
* Separator *
* *
******************************************************************************/
.separator:horizontal .line {
-fx-border-color: -fx-text-box-border transparent transparent transparent;
-fx-border-insets: 0, 1 0 0 0;
}
.separator:vertical .line {
-fx-border-color: transparent transparent transparent -fx-text-box-border;
-fx-border-width: 3, 1;
-fx-border-insets: 0, 0 0 0 1;
}
/*******************************************************************************
* *
* ProgressIndicator *
* *
******************************************************************************/
.progress-indicator {
-fx-indeterminate-segment-count: 12.0;
}
.progress-indicator > .determinate-indicator > .indicator {
-fx-background-color:
rgb(208.0, 208.0, 208.0),
linear-gradient(rgb(176.0, 176.0, 176.0), rgb(207.0, 207.0, 207.0)),
linear-gradient(rgb(190.0, 190.0, 190.0) 0.0%, rgb(213.0, 213.0, 213.0) 15.0%, rgb(230.0, 230.0, 230.0) 50.0%, rgb(235.0, 235.0, 235.0) 100.0%),
linear-gradient(to left, rgb(196.0, 196.0, 196.0, 0.5) 0.0%, rgb(220.0, 220.0, 220.0, 0.2) 2.0% , transparent);
-fx-background-insets: 0.5 0.0 -0.5 0.0, 0.0, 0.5, 1.0;
-fx-padding: 0.083333em;
}
.progress-indicator > .determinate-indicator > .progress {
-fx-background-color:
rgb(208.0, 208.0, 208.0),
radial-gradient(center 50.0% 50.0%, radius 50.0%, rgb(84.0, 170.0, 240.0), rgb(90.0, 192.0, 246.0));
-fx-background-insets: 0.0, 0.5;
-fx-padding: 0.166667em;
}
.progress-indicator > .determinate-indicator > .tick {
-fx-background-color: rgb(208.0, 208.0, 208.0), white;
-fx-background-insets: 1.0 0.0 -1.0 0.0, 0.0;
-fx-padding: 0.416667em;
-fx-shape: "m 18.174523,1027.137 c -0.18077,-0.4575 -0.184364,-0.8913 0.115901,-1.1721 0.300265,-0.2809 0.836622,-0.3601 1.288422,-0.041 0.4518,0.3191 2.020453,2.9316 2.020453,2.9316 l 5.41194,-8.0232 c -4e-6,0 0.516257,-0.6671 1.248682,-0.3099 0.648831,0.3165 0.559153,1.0373 0.559153,1.0373 0,0 -5.940433,9.3556 -6.15501,9.6287 -0.214577,0.273 -1.595078,0.2481 -1.817995,-0.027 -0.222917,-0.2751 -2.490777,-3.567 -2.671546,-4.0244 z";
-fx-scale-shape: false;
}
.progress-indicator > .percentage {
-fx-font-size: 0.916667em;
}
.progress-indicator:disabled {
-fx-opacity: -fx-disabled-opacity;
}
.progress-indicator:indeterminate > .spinner {
-fx-padding: 0.083333em;
}
.progress-indicator:indeterminate .segment {
-fx-background-color: rgb(95.0, 95.0, 98.0), rgb(122.0, 122.0, 125.0);
-fx-background-insets:0.0, 0.5;
}
.progress-indicator:indeterminate .segment0 {
-fx-shape:"m 14.321262,6.5816808 c -0.824944,0.3797564 -0.10368,1.8484772 0.718513,1.3544717 L 18.786514,5.9486042 C 19.644992,5.4932031 18.92648,4.1387308 18.068001,4.5941315 z";
}
.progress-indicator:indeterminate .segment1 {
-fx-shape:"m 15.372451,9.2445322 c -0.906719,-0.051108 -0.957826,1.5843588 0,1.5332498 l 4.241273,0 c 0.97179,0 0.97179,-1.5332498 0,-1.5332498 z";
}
.progress-indicator:indeterminate .segment2 {
-fx-shape:"m 14.423504,13.443113 c -0.824945,-0.379757 -0.10368,-1.848478 0.718512,-1.354472 l 3.746739,1.987548 c 0.858478,0.455401 0.139967,1.809873 -0.718512,1.354473 z";
}
.progress-indicator:indeterminate .segment3 {
-fx-shape:"m 12.10997,15.070611 c -0.49762,-0.759687 0.893182,-1.621681 1.327834,-0.766626 l 2.120636,3.673051 c 0.485895,0.841595 -0.841938,1.60822 -1.327833,0.766625 z";
}
.progress-indicator:indeterminate .segment4 {
-fx-shape:"m 9.2224559,19.539943 c -0.051108,0.906718 1.5843581,0.957826 1.5332501,0 l 0,-4.241273 c 0,-0.97179 -1.5332501,-0.97179 -1.5332501,0 z";
}
.progress-indicator:indeterminate .segment5 {
-fx-shape:"M 8.0465401,15.070611 C 8.5441601,14.310924 7.1533584,13.44893 6.7187068,14.303985 l -2.1206366,3.673051 c -0.485895,0.841595 0.8419383,1.60822 1.3278333,0.766625 z";
}
.progress-indicator:indeterminate .segment6 {
-fx-shape:"M 5.7330066,13.443113 C 6.5579512,13.063356 5.8366865,11.594635 5.0144939,12.088641 L 1.2677551,14.076189 C 0.409277,14.53159 1.1277888,15.886062 1.9862674,15.430662 z";
}
.progress-indicator:indeterminate .segment7 {
-fx-shape:"m 0.42171041,9.2083842 c -0.90671825,-0.051108 -0.95782608,1.5843588 0,1.5332498 l 4.24127319,0 c 0.9717899,0 0.9717899,-1.5332498 0,-1.5332498 z";
}
.progress-indicator:indeterminate .segment8 {
-fx-shape:"M 5.7330066,6.5305598 C 6.5579512,6.9103162 5.8366865,8.3790371 5.0144939,7.8850315 L 1.2677551,5.8974832 C 0.409277,5.4420823 1.1277888,4.0876101 1.9862674,4.5430105 z";
}
.progress-indicator:indeterminate .segment9 {
-fx-shape:"M 8.0465401,4.9030617 C 8.5441601,5.6627485 7.1533584,6.5247425 6.7187068,5.6696872 L 4.5980702,1.9966363 C 4.1121752,1.1550418 5.4400085,0.38841683 5.9259035,1.2300114 z";
}
.progress-indicator:indeterminate .segment10 {
-fx-shape:"m 9.2224559,4.62535 c -0.051108,0.9067177 1.5843581,0.957826 1.5332501,0 l 0,-4.24127319 c 0,-0.9717899 -1.5332501,-0.9717899 -1.5332501,0 z";
}
.progress-indicator:indeterminate .segment11 {
-fx-shape:"m 12.007729,4.9541827 c -0.49762,0.7596865 0.893181,1.6216808 1.327833,0.7666252 L 15.456199,2.0477574 C 15.942094,1.2061627 14.61426,0.43953765 14.128365,1.2811324 z";
}
/*******************************************************************************
* *
* Text COMMON *
* *
******************************************************************************/
.text-input {
-fx-text-fill: -fx-dark-text-color;
-fx-highlight-fill: derive(-fx-control-inner-background,-20%);
-fx-highlight-text-fill: -fx-text-inner-color;
-fx-prompt-text-fill: derive(-fx-control-inner-background,-30%);
-fx-border-color: -fx-text-box-border;
-fx-border-width: 1px;
-fx-background-color: #FFFFFF;
-fx-background-insets: 0;
-fx-background-radius: 0;
-fx-cursor: text;
-fx-padding: 2px;
}
.text-input:focused {
-fx-highlight-fill: -fx-accent;
-fx-border-color: -fx-focus-color;
-fx-border-width: 1px;
-fx-background-color: -fx-faint-focus-color, #FFFFFF;
-fx-background-insets: -3, 0;
-fx-background-radius: 3, 0;
-fx-prompt-text-fill: transparent;
}
/*******************************************************************************
* *
* PopupMenu *
* *
******************************************************************************/
.context-menu {
-fx-background-color: rgba(255.0, 255.0, 255.0, 0.9);
-fx-background-insets: 0;
-fx-background-radius: 4.0;
-fx-padding: 4px 0 4px 0;
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.6), 8.0, 0.0, 0.0, 0.0 );
}
.context-menu > .separator {
-fx-padding: 0.0em 0.333333em 0.0em 0.333333em; /* 0 4 0 4 */
}
/*******************************************************************************
* *
* MenuItem *
* *
******************************************************************************/
.menu-item {
-fx-background-color: transparent;
-fx-background-insets:0.0;
-fx-padding:0.2em 1em 0.2em 1em;
-fx-border-width: 0.0 0.0 0.0 0.0;
-fx-border-color:transparent;
}
.menu-item > .left-container {
-fx-padding: 0.458em 0.791em 0.458em 0.458em;
}
.menu-item > .graphic-container {
-fx-padding: 0em 0.333em 0em 0em;
}
.menu-item >.label {
-fx-padding: 0em 0.5em 0em 0em;
-fx-text-fill: -fx-text-base-color;
}
.menu-item:disabled > .label {
-fx-opacity: -fx-disabled-opacity;
}
.menu-item:focused {
-fx-background: -fx-accent;
-fx-background-color: #2283FB;
-fx-text-fill: -fx-selection-bar-text;
}
.menu-item:focused > .label {
-fx-text-fill: white;
}
.menu-item > .right-container {
-fx-padding: 0em 0em 0em 0.5em;
}
.menu-item:show-mnemonics > .mnemonic-underline {
-fx-stroke: -fx-text-fill;
}
.menu > .right-container > .arrow {
-fx-padding: 0.458em 0.167em 0.458em 0.167em; /* 4.5 2 4.5 2 */
-fx-background-color: -fx-color;
-fx-shape: "M0,-4L4,0L0,4Z";
-fx-scale-shape: false;
}
.menu:selected > .right-container > .arrow {
-fx-background-color: white;
}
.menu-item:disabled {
-fx-opacity: -fx-disabled-opacity;
}
/*******************************************************************************
* *
* ListView and ListCell *
* *
******************************************************************************/
.list-view > .virtual-flow > .scroll-bar:vertical {
-fx-background-insets: 0, 0 0 0 1;
-fx-padding: -1 -1 -1 0;
}
.list-view > .virtual-flow > .scroll-bar:horizontal {
-fx-background-insets: 0, 1 0 0 0;
-fx-padding: 0 -1 -1 -1;
}
.list-view > .virtual-flow > .corner {
-fx-background-color: -fx-box-border, -fx-base;
-fx-background-insets: 0, 1 0 0 1;
}
.list-cell {
-fx-background-color: -fx-control-inner-background;
-fx-padding: 0.8em 0.5em 0.8em 0.5em;
-fx-text-fill: -fx-text-inner-color;
-fx-opacity: 1;
}
.root.active-window .list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:focused,
.root.active-window .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected,
.root.active-window .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
-fx-background-color: -fx-selection-bar;
-fx-text-fill: -fx-selection-bar-text;
}
.root.inactive-window .list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:focused,
.root.inactive-window .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected,
.root.inactive-window .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
-fx-background-color: #DCDCDC;
-fx-text-fill: -fx-selection-bar-text;
}
/*******************************************************************************
* *
* ComboBox *
* *
******************************************************************************/
/* Customie the ListCell that appears in the ComboBox button itself */
.combo-box > .list-cell {
-fx-background: transparent;
-fx-background-color: transparent;
-fx-text-fill: -fx-text-base-color;
-fx-padding: 0.2em 0.5em 0.2em 0.5em;
}
.combo-box-popup > .list-view {
-fx-background-color: rgba(255.0, 255.0, 255.0, 0.9);
-fx-background-insets: 0;
-fx-background-radius: 4.0;
-fx-padding: 4px 0 4px 0;
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.6), 8.0, 0.0, 0.0, 0.0);
}
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell {
-fx-background-color: transparent;
-fx-padding: 0.2em 1em 0.2em 1em;
-fx-border-color: transparent;
}
.root.active-window .combo-box-popup > .list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:focused:filled:selected,
.root.active-window .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected,
.root.active-window .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
-fx-background: -fx-accent;
-fx-background-color: #2283FB;
-fx-text-fill: -fx-selection-bar-text;
}
/* Arrow-Button */
.combo-box-base > .arrow-button {
-fx-background-radius: 0 5 5 0, 0 4 4 0;
-fx-padding: 0.2em 0.5em 0.2em 0.5em;
-fx-background-insets: 0 0 0 1, 1;
}
.combo-box-base > .arrow-button > .arrow {
-fx-background-insets: 0 0 -1 0, 0;
-fx-padding: 9px 6px 0 0;
-fx-shape: "M 0 3 l 3 -3 l 3 3 m 0 3 l -3 3 l -3 -3";
}
.root.active-window .combo-box-base > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #4AA0F9 0%, #045FFF 100%), linear-gradient(to bottom, #69B2FA 0%, #0D81FF 100%);
}
.root.active-window .combo-box-base > .arrow-button > .arrow {
-fx-background-color: -fx-light-text-color;
}
.root.inactive-window .combo-box-base > .arrow-button {
-fx-background-color: transparent;
}
.root.inactive-window .combo-box-base > .arrow-button > .arrow {
-fx-background-color: #444444;
}
/*******************************************************************************
* *
* SplitPane *
* *
******************************************************************************/
.split-pane > .split-pane-divider {
-fx-padding: 0 0.25em 0 0.25em; /* 0 3 0 3 */
}
/*******************************************************************************
* *
* Tooltip *
* *
******************************************************************************/
.tooltip {
-fx-background-color: -fx-background;
-fx-padding: 0.2em 0.4em 0.2em 0.4em;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 2, 0, 0, 0);
-fx-font-size: 0.8em;
}
/*******************************************************************************
* *
* Charts *
* *
******************************************************************************/
.chart {
-fx-padding: 5px;
}
.chart-content {
-fx-padding: 10px;
}
.chart-title {
-fx-font-size: 1.4em;
}
.chart-legend {
-fx-background-color: linear-gradient(to bottom, derive(-fx-background, -10%), derive(-fx-background, -5%)),
linear-gradient(from 0px 0px to 0px 5px, derive(-fx-background, -5%), derive(-fx-background, 20%));
-fx-background-insets: 0,1;
-fx-background-radius: 6,5;
-fx-padding: 6px;
}
/*******************************************************************************
* *
* Axis *
* *
******************************************************************************/
.axis {
AXIS_COLOR: derive(-fx-background,-20%);
-fx-tick-label-font-size: 0.833333em; /* 10px */
-fx-tick-label-fill: derive(-fx-text-background-color, 30%);
}
.axis:top {
-fx-border-color: transparent transparent AXIS_COLOR transparent;
}
.axis:right {
-fx-border-color: transparent transparent transparent AXIS_COLOR;
}
.axis:bottom {
-fx-border-color: AXIS_COLOR transparent transparent transparent;
}
.axis:left {
-fx-border-color: transparent AXIS_COLOR transparent transparent;
}
.axis-tick-mark,
.axis-minor-tick-mark {
-fx-fill: null;
-fx-stroke: AXIS_COLOR;
}
/*******************************************************************************
* *
* ChartPlot *
* *
******************************************************************************/
.chart-vertical-grid-lines {
-fx-stroke: derive(-fx-background,-10%);
-fx-stroke-dash-array: 0.25em, 0.25em;
}
.chart-horizontal-grid-lines {
-fx-stroke: derive(-fx-background,-10%);
}
.chart-alternative-column-fill {
-fx-fill: null;
-fx-stroke: null;
}
.chart-alternative-row-fill {
-fx-fill: null;
-fx-stroke: null;
}
.chart-vertical-zero-line,
.chart-horizontal-zero-line {
-fx-stroke: derive(-fx-text-background-color, 40%);
}
/*******************************************************************************
* *
* LineChart *
* *
******************************************************************************/
.chart-line-symbol {
-fx-background-color: #f9d900, white;
-fx-background-insets: 0, 2;
-fx-background-radius: 5px;
-fx-padding: 5px;
}
.chart-series-line {
-fx-stroke: #f9d900;
-fx-stroke-width: 3px;
/*-fx-effect: dropshadow( two-pass-box , rgba(0,0,0,0.3) , 8, 0.0 , 0 , 3 );*/
}
.default-color0.chart-line-symbol { -fx-background-color: CHART_COLOR_1, white; }
.default-color1.chart-line-symbol { -fx-background-color: CHART_COLOR_2, white; }
.default-color2.chart-line-symbol { -fx-background-color: CHART_COLOR_3, white; }
.default-color3.chart-line-symbol { -fx-background-color: CHART_COLOR_4, white; }
.default-color4.chart-line-symbol { -fx-background-color: CHART_COLOR_5, white; }
.default-color5.chart-line-symbol { -fx-background-color: CHART_COLOR_6, white; }
.default-color6.chart-line-symbol { -fx-background-color: CHART_COLOR_7, white; }
.default-color7.chart-line-symbol { -fx-background-color: CHART_COLOR_8, white; }
.default-color0.chart-series-line { -fx-stroke: CHART_COLOR_1; }
.default-color1.chart-series-line { -fx-stroke: CHART_COLOR_2; }
.default-color2.chart-series-line { -fx-stroke: CHART_COLOR_3; }
.default-color3.chart-series-line { -fx-stroke: CHART_COLOR_4; }
.default-color4.chart-series-line { -fx-stroke: CHART_COLOR_5; }
.default-color5.chart-series-line { -fx-stroke: CHART_COLOR_6; }
.default-color6.chart-series-line { -fx-stroke: CHART_COLOR_7; }
.default-color7.chart-series-line { -fx-stroke: CHART_COLOR_8; }
/*******************************************************************************
* *
* Combinations *
* *
* This section is for special handling of when one control is nested inside *
* another control. There are many cases where we would end up with ugly *
* double borders that are fixed here. *
* *
******************************************************************************/
.split-pane > * > .table-view { -fx-padding: 0px; }
.split-pane > * > .list-view { -fx-padding: 0px; }
.split-pane > * > .tree-view { -fx-padding: 0px; }
.split-pane > * > .scroll-pane { -fx-padding: 0px; }
.split-pane > * > .split-pane {
-fx-background-insets: 0, 0;
-fx-padding: 0;
}
/* ############################################################################
# Workaround for RT-27627 #
############################################################################ */
.choice-box > .open-button > .arrow { doh: true; }
.split-menu-button:openvertically > .arrow-button > .arrow { doh: true; }
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button > .arrow { doh: true; }
.tree-table-view { doh: true; }
.tree-table-view:focused { doh: true; }
.tree-table-view > .virtual-flow > .scroll-bar:vertical { doh: true; }
.tree-table-view > .virtual-flow > .scroll-bar:horizontal { doh: true; }
.tree-table-view > .virtual-flow > .corner { doh: true; }
.tree-table-row-cell:focused { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected > .tree-table-cell { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected { doh: true; }
.tree-table-view:row-selection:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected:hover{ doh: true; }
.tree-table-row-cell:filled:selected:focused { doh: true; }
.tree-table-row-cell:filled:selected { doh: true; }
.tree-table-row-cell:selected:disabled { doh: true; }
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:hover { doh: true; }
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:hover { doh: true; }
.tree-table-view:constrained-resize > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:last-visible { doh: true; }
.tree-table-view:constrained-resize > .column-header:last-visible { doh: true; }
.tree-table-view:constrained-resize .filler { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:focused { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled .tree-table-cell:focused:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:selected:hover{ doh: true; }
.tree-table-row-cell:filled > .tree-table-cell:selected:focused { doh: true; }
.tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
.tree-table-cell:selected:disabled { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:hover { doh: true; }
.tree-table-view .column-resize-line { doh: true; }
.tree-table-view > .column-header-background { doh: true; }
.tree-table-view .column-header { doh: true; }
.tree-table-view .filler { doh: true; }
.tree-table-view .column-header .sort-order{ doh: true; }
.tree-table-view > .column-header-background > .show-hide-columns-button{ doh: true; }
.tree-table-view .show-hide-column-image { doh: true; }
.tree-table-view .column-drag-header { doh: true; }
.tree-table-view .column-overlay { doh: true; }
.tree-table-view /*> column-header-background > nested-column-header >*/ .arrow { doh: true; }
.tree-table-view .empty-table { doh: true; }
.axis-minor-tick-mark { doh: true; }
.chart-horizontal-zero-line { doh: true; }
.stacked-bar-chart:horizontal .chart-bar { doh: true; }

View File

@@ -0,0 +1,891 @@
/*
* 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
*
*/
.root {
-fx-font-family: 'Segoe UI Semibold';
-fx-font-smoothing-type: lcd;
-fx-font-size: 12.0;
/***************************************************************************
* *
* The main color palette from which the rest of the colors are derived. *
* *
**************************************************************************/
-fx-base: #EAEAEA;
-fx-background: #F0F0F0;
/* Used for the inside of text boxes, password boxes, lists, trees, and
* tables. See also -fx-text-inner-color, which should be used as the
* -fx-text-fill value for text painted on top of backgrounds colored
* with -fx-control-inner-background.
*/
-fx-control-inner-background: #FFFFFF;
/* One of these colors will be chosen based upon a ladder calculation
* that uses the brightness of a background color. Instead of using these
* colors directly as -fx-text-fill values, the sections in this file should
* use a derived color to match the background in use. See also:
*
* -fx-text-base-color for text on top of -fx-base, -fx-color, and -fx-body-color
* -fx-text-background-color for text on top of -fx-background
* -fx-text-inner-color for text on top of -fx-control-inner-color
* -fx-selection-bar-text for text on top of -fx-selection-bar
*/
-fx-dark-text-color: black;
-fx-mid-text-color: #8B8B8B;
-fx-light-text-color: white;
/* A bright blue for highlighting/accenting objects. For example: selected
* text; selected items in menus, lists, trees, and tables; progress bars */
-fx-accent: #3399FF;
/* A bright blue for the focus indicator of objects. Typically used as the
* first color in -fx-background-color for the "focused" pseudo-class. Also
* typically used with insets of -1.4 to provide a glowing effect.
*/
-fx-focus-color: #3399FF;
/* The color that is used in styling controls. The default value is based
* on -fx-base, but is changed by pseudoclasses to change the base color.
* For example, the "hover" pseudoclass will typically set -fx-color to
* -fx-hover-base (see below) and the "armed" pseudoclass will typically
* set -fx-color to -fx-pressed-base.
*/
-fx-color: -fx-base;
/* The opacity level to use for the "disabled" pseudoclass.
*/
-fx-disabled-opacity: 0.6;
/* Chart Color Palette */
CHART_COLOR_1: #A1CD5f;
CHART_COLOR_2: #C75050;
CHART_COLOR_3: #3399FF;
CHART_COLOR_4: #FAEA77;
CHART_COLOR_5: #FA9E78;
CHART_COLOR_6: #F47BF8;
CHART_COLOR_7: #c84164;
CHART_COLOR_8: #888888;
/***************************************************************************
* *
* Colors that are derived from the main color palette. *
* *
**************************************************************************/
/* The color to use for -fx-text-fill when text is to be painted on top of
* a background filled with the -fx-background color.
*/
-fx-text-background-color: -fx-dark-text-color;
/* A little darker than -fx-color and used to draw boxes around objects such
* as progress bars, scroll bars, scroll panes, trees, tables, and lists.
*/
-fx-box-border: #ACACAC;
/* Darker than -fx-background and used to draw boxes around text boxes and
* password boxes.
*/
-fx-text-box-border: #ACACAC;
/* A gradient that goes from a little darker than -fx-color on the top to
* even more darker than -fx-color on the bottom. Typically is the second
* color in the -fx-background-color list as the small thin border around
* a control. It is typically the same size as the control (i.e., insets
* are 0).
*/
-fx-outer-border: derive(-fx-color,-23%);
/* A gradient that goes from a bit lighter than -fx-color on the top to
* a little darker at the bottom. Typically is the third color in the
* -fx-background-color list as a thin highlight inside the outer border.
* Insets are typically 1.
*/
-fx-inner-border: linear-gradient(to bottom, derive(-fx-color,75%), derive(-fx-color,2%));
/* A gradient that goes from a little lighter than -fx-color at the top to
* a little darker than -fx-color at the bottom and is used to fill the
* body of many controls such as buttons. Typically is the fourth color
* in the -fx-background-color list and represents main body of the control.
* Insets are typically 2.
*/
-fx-body-color: linear-gradient(to bottom, derive(-fx-color,10%) ,derive(-fx-color,-6%));
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-base, -fx-color, and -fx-body-color.
*/
-fx-text-base-color: -fx-dark-text-color;
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-control-inner-background.
*/
-fx-text-inner-color: -fx-dark-text-color;
/* Background for items in list like things such as menus, lists, trees,
* and tables.
*
* TODO: it seems like this should be based upon -fx-accent and we should
* remove the setting -fx-background in all the sections that use
* -fx-selection-bar.
*/
-fx-selection-bar: #3399FF;
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-selection-bar.
*
* TODO: it seems like this should be derived from -fx-selection-bar.
*/
-fx-selection-bar-text: -fx-light-text-color;
/* These are needed for Popup */
-fx-background-color: inherit;
-fx-background-radius: inherit;
-fx-background-insets: inherit;
-fx-padding: inherit;
-fx-cell-focus-inner-border: -fx-selection-bar;
/***************************************************************************
* *
* Set the default background color for the scene *
* *
**************************************************************************/
-fx-background-color: -fx-background;
}
/*******************************************************************************
* *
* Common Styles *
* *
* These are styles that give a standard look to a whole range of controls *
* *
******************************************************************************/
/* ==== BUTTON LIKE THINGS ============================================== */
.button,
.toggle-button,
.menu-button,
.choice-box,
.color-picker.split-button > .color-picker-label,
.combo-box-base,
.combo-box-base > .arrow-button {
-fx-background-color: -fx-box-border, linear-gradient(to bottom, #F0F0F0 0%, #E5E5E5 100%);
-fx-background-insets: 0, 1;
-fx-background-radius: 0, 0;
-fx-padding: 0.1em 0.6em 0.1em 0.6em;
-fx-text-fill: -fx-dark-text-color;
-fx-alignment: CENTER;
-fx-border-color: transparent;
-fx-border-insets: 2px;
}
.button:hover,
.toggle-button:hover,
.menu-button:hover,
.split-menu-button > .label:hover,
.split-menu-button > .arrow-button:hover,
.slider .thumb:hover,
.choice-box:hover,
.color-picker.split-button > .arrow-button:hover,
.color-picker.split-button > .color-picker-label:hover,
.combo-box-base:hover,
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:hover {
-fx-color: -fx-base;
}
.button:armed,
.button:default:armed,
.toggle-button:armed,
.menu-button:armed,
.split-menu-button:armed > .label,
.split-menu-button > .arrow-button:pressed,
.split-menu-button:showing > .arrow-button,
.slider .thumb:pressed,
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:pressed {
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #DAECFC 0%, #C4E0FC 100%);
}
.button:focused,
.toggle-button:focused,
.menu-button:focused,
.choice-box:focused,
.color-picker.split-button:focused > .color-picker-label {
-fx-border-color: black;
-fx-border-insets: 2px;
-fx-border-style: dotted inside;
}
/* ==== DISABLED THINGS ================================================= */
.button:disabled,
.toggle-button:disabled,
.hyperlink:disabled,
.menu-button:disabled,
.split-menu-button:disabled,
.slider:disabled,
.scroll-pane:disabled,
.progress-bar:disabled,
.progress-indicator:disabled,
.text-input:disabled,
.choice-box:disabled,
.combo-box-base:disabled,
.list-view:disabled,
.tree-view:disabled,
.table-view:disabled,
.tree-table-view:disabled,
.tab-pane:disabled,
.tab-pane > .tab-header-area > .headers-region > .tab:disabled {
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
-fx-text-fill: -fx-mid-text-color;
}
/* ==== MNEMONIC THINGS ================================================= */
.button:show-mnemonics .mnemonic-underline,
.toggle-button:show-mnemonics .mnemonic-underline,
.hyperlink:show-mnemonics > .mnemonic-underline,
.split-menu-button:show-mnemonics > .mnemonic-underline,
.menu-button:show-mnemonics > .mnemonic-underline {
-fx-stroke: -fx-text-base-color;
}
/* ==== ARROWS ========================================================== */
.combo-box-base > .arrow-button > .arrow {
-fx-background-color: #606060;
-fx-background-insets: 0;
-fx-padding: 2px 4px 2px 4px;
-fx-shape: "M 0 0 h 7 l -3.5 4 z";
}
/* ==== CHOICE BOX LIKE THINGS ========================================== */
.combo-box-base {
-fx-padding: 0;
}
/* ==== BOX LIKE THINGS ================================================= */
.scroll-pane,
.split-pane,
.list-view,
.tree-view,
.table-view,
.tree-table-view {
-fx-background-color: -fx-box-border, -fx-control-inner-background;
-fx-background-insets: 0, 1;
-fx-padding: 1;
}
/*******************************************************************************
* *
* Label *
* *
******************************************************************************/
.label {
-fx-text-fill: -fx-text-background-color;
}
/*******************************************************************************
* *
* Button & ToggleButton *
* *
******************************************************************************/
/* ==== DEFAULT ========================================================= */
.button:default {
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #F0F0F0 0%, #E5E5E5 100%);
}
.button:default:disabled {
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
-fx-text-fill: -fx-mid-text-color;
}
/*******************************************************************************
* *
* CheckBox *
* *
******************************************************************************/
/* TODO win L&F */
.check-box {
-fx-label-padding: 0 0 0 3px;
-fx-text-fill: -fx-text-background-color;
}
.check-box > .box {
-fx-padding: 1px;
-fx-border-color: -fx-box-border;
-fx-background-color: -fx-control-inner-background;
}
.check-box:hover > .box,
.check-box:armed > .box {
-fx-border-color: -fx-focus-color;
}
.check-box > .box > .mark {
-fx-background-color: transparent;
-fx-padding: 4px;
-fx-shape: "M-1,4, L-1,5.5 L3.5,8.5 L9,0 L9,-1 L7,-1 L3,6 L1,4 Z";
}
.check-box:selected > .box > .mark {
-fx-background-color: black;
}
/*******************************************************************************
* *
* ToolBar *
* *
******************************************************************************/
.tool-bar:horizontal {
-fx-background-color: -fx-box-border, -fx-background;
-fx-background-insets: 0;
-fx-padding: 0.4em;
-fx-spacing: 0.2em;
-fx-alignment: CENTER_LEFT;
}
.tool-bar:horizontal > .container > .separator {
-fx-orientation: vertical;
}
/*******************************************************************************
* *
* ScrollBar *
* *
******************************************************************************/
.scroll-bar:horizontal,
.scroll-bar:vertical {
-fx-background-color: -fx-base;
}
.scroll-bar:disabled {
-fx-opacity: -fx-disabled-opacity;
}
.scroll-bar > .thumb {
-fx-background-color: #CDCDCD;
}
.scroll-bar > .thumb:hover {
-fx-background-color: #A6A6A6;
}
.scroll-bar > .increment-button,
.scroll-bar > .decrement-button {
-fx-background-color: transparent;
-fx-color: transparent;
}
.scroll-bar:horizontal > .increment-button,
.scroll-bar:horizontal > .decrement-button {
-fx-padding: 5px 5px;
}
.scroll-bar:vertical > .increment-button,
.scroll-bar:vertical > .decrement-button {
-fx-padding: 5px 5px;
}
.scroll-bar > .increment-button,
.scroll-bar > .decrement-button {
-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-body-color;
-fx-background-insets: 0, 1, 2;
-fx-color: transparent;
-fx-padding: 3px;
}
.scroll-bar > .increment-button > .increment-arrow,
.scroll-bar > .decrement-button > .decrement-arrow {
-fx-background-color: #606060;
}
.scroll-bar > .increment-button:hover > .increment-arrow,
.scroll-bar > .decrement-button:hover > .decrement-arrow {
-fx-background-color: #606060;
}
.scroll-bar > .increment-button:pressed > .increment-arrow,
.scroll-bar > .decrement-button:pressed > .decrement-arrow {
-fx-background-color: #606060;
}
.scroll-bar:horizontal > .increment-button > .increment-arrow {
-fx-padding: 9 7 0 0;
-fx-shape: "M0.315,1.457l1.414-1.414L5.686,4L1.729,7.957L0.315,6.543L2.857,4L0.315,1.457z";
}
.scroll-bar:vertical > .increment-button > .increment-arrow {
-fx-padding: 7 9 0 0 ;
-fx-shape: "M6.543,0.315l1.414,1.414L4,5.686L0.043,1.729l1.414-1.414L4,2.858L6.543,0.315z";
}
.scroll-bar:horizontal > .decrement-button > .decrement-arrow {
-fx-padding: 9 7 0 0;
-fx-shape: "M5.686,6.543L4.271,7.957L0.314,4l3.957-3.957l1.414,1.414L3.143,4L5.686,6.543z";
}
.scroll-bar:vertical > .decrement-button > .decrement-arrow {
-fx-padding: 7 9 0 0;
-fx-shape: "M1.457,5.563L0.042,4.149L4,0.193l3.957,3.957L6.543,5.563L4,3.021L1.457,5.563z";
}
/*******************************************************************************
* *
* Separator *
* *
******************************************************************************/
.separator:horizontal .line {
-fx-border-color: -fx-text-box-border transparent transparent transparent;
-fx-border-insets: 0, 1 0 0 0;
}
.separator:vertical .line {
-fx-border-color: transparent transparent transparent -fx-text-box-border;
-fx-border-width: 3, 1;
-fx-border-insets: 0, 0 0 0 1;
}
/*******************************************************************************
* *
* ProgressIndicator *
* *
******************************************************************************/
.progress-indicator {
-fx-indeterminate-segment-count: 12;
-fx-spin-enabled: true;
}
.progress-indicator > .determinate-indicator > .indicator {
-fx-background-color: -fx-box-border,
radial-gradient(center 50% 50%, radius 50%, -fx-control-inner-background 70%, derive(-fx-control-inner-background, -9%) 100%),
-fx-control-inner-background;
-fx-background-insets: 0, 1, 5 2 1 2;
-fx-padding: 1;
}
.progress-indicator > .determinate-indicator > .progress {
-fx-background-color: -fx-accent;
-fx-background-insets: 2;
-fx-padding: 1em; /* 9 */
}
/* TODO: scaling the shape seems to make it disappear */
.progress-indicator > .determinate-indicator > .tick {
-fx-background-color: white;
-fx-background-insets: 0;
-fx-padding: 0.416667em; /* 5 */
-fx-shape: "M-0.25,6.083c0.843-0.758,4.583,4.833,5.75,4.833S14.5-1.5,15.917-0.917c1.292,0.532-8.75,17.083-10.5,17.083C3,16.167-1.083,6.833-0.25,6.083z";
-fx-scale-shape: false;
}
.progress-indicator:indeterminate > .spinner {
-fx-padding: 0.833333em; /* 10 */
}
.progress-indicator > .percentage {
-fx-font-size: 0.916667em; /* 11pt - 1 less than the default font */
-fx-fill: -fx-text-background-color;
}
.progress-indicator:indeterminate .segment {
-fx-background-color: -fx-accent;
}
.progress-indicator:indeterminate .segment0 {
-fx-shape:"M10,0C9.998,0,9.995,0,9.992,0C9.991,0,9.991,0,9.99,0C9.988,0,9.986,0,9.985,0S9.982,0,9.981,0S9.979,0,9.978,0S9.975,0,9.974,0S9.972,0,9.971,0C9.969,0,9.968,0,9.966,0H9.965c-0.007,0-0.014,0-0.02,0C9.944,0,9.944,0,9.944,0C9.941,0,9.939,0,9.937,0c-0.77,0.007-1.389,0.634-1.384,1.404C8.557,2.176,9.185,2.8,9.956,2.8c0.001,0,0.003,0,0.004,0H10c0.773-0.002,1.4-0.63,1.4-1.404c0-0.77-0.622-1.393-1.392-1.396C10.006,0,10.003,0,10,0L10,0z";
}
.progress-indicator:indeterminate .segment1 {
-fx-shape:"M5.688,1.156c-0.236,0-0.476,0.06-0.696,0.187C4.98,1.349,4.969,1.356,4.958,1.363c0,0-0.001,0-0.001,0C4.955,1.364,4.954,1.365,4.952,1.366c-0.001,0-0.002,0.001-0.004,0.002c0,0,0,0-0.001,0C4.944,1.371,4.94,1.373,4.936,1.375c0,0,0,0-0.001,0C4.933,1.377,4.931,1.378,4.929,1.38C4.267,1.772,4.046,2.624,4.438,3.288c0.261,0.444,0.73,0.692,1.212,0.692c0.24,0,0.484-0.062,0.706-0.192l0.034-0.02C7.058,3.378,7.283,2.52,6.896,1.851C6.636,1.405,6.168,1.156,5.688,1.156L5.688,1.156z";
}
.progress-indicator:indeterminate .segment2 {
-fx-shape:"M2.534,4.326c-0.482,0-0.951,0.25-1.209,0.697C1.323,5.027,1.321,5.029,1.32,5.031l0,0C1.319,5.033,1.318,5.034,1.317,5.036S1.315,5.039,1.314,5.04c0,0.001,0,0.002-0.001,0.002C1.312,5.044,1.311,5.046,1.31,5.048c0,0,0,0,0,0.001C1.309,5.051,1.308,5.053,1.307,5.055C1.302,5.063,1.297,5.071,1.292,5.079l0,0C1.291,5.082,1.29,5.084,1.288,5.087c-0.376,0.67-0.141,1.519,0.529,1.898c0.218,0.123,0.456,0.182,0.69,0.182c0.488,0,0.963-0.255,1.222-0.708l0.02-0.035c0.383-0.671,0.149-1.527-0.521-1.912C3.008,4.386,2.769,4.326,2.534,4.326L2.534,4.326z";
}
.progress-indicator:indeterminate .segment3 {
-fx-shape:"M1.396,8.648c-0.002,0-0.005,0-0.008,0C0.619,8.652-0.001,9.278,0,10.047c0,0.002,0,0.006,0,0.008l0,0c0,0.019,0,0.037,0,0.056c0,0.001,0,0.002,0,0.003s0,0.003,0,0.004c0.01,0.765,0.632,1.378,1.396,1.378c0.005,0,0.01,0,0.015,0c0.773-0.009,1.395-0.642,1.389-1.415v-0.04C2.794,9.27,2.166,8.648,1.396,8.648L1.396,8.648z";
}
.progress-indicator:indeterminate .segment4 {
-fx-shape:"M2.579,12.955c-0.242,0-0.487,0.062-0.71,0.194c-0.664,0.391-0.885,1.242-0.499,1.906c0.013,0.021,0.025,0.043,0.038,0.063c0.262,0.436,0.724,0.678,1.197,0.678c0.243,0,0.49-0.063,0.714-0.197c0.665-0.396,0.883-1.257,0.489-1.922l-0.02-0.034C3.526,13.201,3.059,12.955,2.579,12.955L2.579,12.955z";
}
.progress-indicator:indeterminate .segment5 {
-fx-shape:"M5.772,16.09c-0.489,0-0.965,0.257-1.223,0.712c-0.38,0.671-0.146,1.52,0.523,1.901c0.002,0.001,0.004,0.002,0.007,0.004h0c0.004,0.002,0.008,0.004,0.012,0.007c0,0,0,0,0.001,0c0.001,0.001,0.002,0.002,0.004,0.002c0.001,0.001,0.003,0.002,0.004,0.003c0,0,0.001,0,0.001,0.001c0.012,0.006,0.023,0.013,0.035,0.019c0.214,0.119,0.446,0.176,0.675,0.176c0.489,0,0.963-0.258,1.22-0.716c0.377-0.675,0.137-1.529-0.537-1.908l-0.035-0.02C6.242,16.149,6.006,16.09,5.772,16.09L5.772,16.09z";
}
.progress-indicator:indeterminate .segment6 {
-fx-shape:"M10.14,17.198c-0.006,0-0.013,0-0.02,0h-0.039c-0.773,0.011-1.394,0.646-1.385,1.419c0.008,0.767,0.631,1.382,1.396,1.382c0.003,0,0.006,0,0.009-0.001c0.024,0,0.051,0,0.075-0.001c0.769-0.016,1.38-0.648,1.367-1.418C11.53,17.813,10.904,17.198,10.14,17.198L10.14,17.198z";
}
.progress-indicator:indeterminate .segment7 {
-fx-shape:"M14.433,15.97c-0.245,0-0.494,0.064-0.72,0.2l-0.034,0.021c-0.663,0.397-0.88,1.258-0.483,1.922c0.261,0.439,0.725,0.683,1.2,0.683c0.24,0,0.484-0.062,0.707-0.194c0.022-0.013,0.044-0.025,0.065-0.039c0.656-0.399,0.866-1.254,0.469-1.913C15.373,16.212,14.909,15.97,14.433,15.97L14.433,15.97z";
}
.progress-indicator:indeterminate .segment8 {
-fx-shape:"M17.539,12.748c-0.493,0-0.973,0.261-1.229,0.723l-0.02,0.034c-0.376,0.676-0.133,1.53,0.542,1.907c0.216,0.121,0.45,0.178,0.681,0.178c0.487,0,0.96-0.256,1.217-0.71c0.003-0.006,0.007-0.012,0.01-0.019c0.007-0.013,0.015-0.025,0.021-0.038l0,0c0.002-0.003,0.003-0.005,0.004-0.008c0.369-0.675,0.124-1.521-0.55-1.893C18.001,12.805,17.769,12.748,17.539,12.748L17.539,12.748z";
}
.progress-indicator:indeterminate .segment9 {
-fx-shape:"M18.603,8.408c-0.011,0-0.021,0-0.031,0c-0.773,0.018-1.388,0.657-1.373,1.431l0.001,0.04c0.015,0.765,0.641,1.377,1.403,1.377c0.008,0,0.016,0,0.023,0c0.77-0.013,1.383-0.646,1.373-1.414c0-0.003,0-0.006,0-0.009l0,0c-0.001-0.019-0.001-0.037-0.001-0.055c0-0.001,0-0.001-0.001-0.002c0-0.002,0-0.004,0-0.006C19.979,9.012,19.358,8.408,18.603,8.408L18.603,8.408z";
}
.progress-indicator:indeterminate .segment10 {
-fx-shape:"M17.345,4.121c-0.248,0-0.5,0.066-0.728,0.205c-0.659,0.403-0.869,1.266-0.468,1.927l0.021,0.034c0.265,0.435,0.728,0.675,1.202,0.675c0.247,0,0.497-0.065,0.724-0.202c0.659-0.397,0.871-1.252,0.477-1.912c-0.007-0.011-0.013-0.021-0.02-0.032c-0.001-0.002-0.002-0.003-0.002-0.004c-0.001,0-0.001-0.001-0.001-0.002c-0.004-0.005-0.008-0.011-0.011-0.017c0-0.001,0-0.001-0.001-0.001c-0.001-0.002-0.002-0.004-0.004-0.007C18.271,4.358,17.813,4.121,17.345,4.121L17.345,4.121z";
}
.progress-indicator:indeterminate .segment11 {
-fx-shape:"M14.104,1.039c-0.494,0-0.974,0.264-1.227,0.729c-0.37,0.679-0.12,1.531,0.559,1.903l0.034,0.019c0.214,0.117,0.445,0.173,0.673,0.173c0.495,0,0.976-0.262,1.231-0.726c0.371-0.674,0.129-1.519-0.542-1.894c-0.012-0.006-0.024-0.013-0.036-0.02c-0.007-0.004-0.014-0.008-0.021-0.012c-0.003-0.001-0.006-0.003-0.009-0.005C14.556,1.094,14.329,1.039,14.104,1.039L14.104,1.039z";
}
/*******************************************************************************
* *
* Text COMMON *
* *
******************************************************************************/
.text-input {
-fx-text-fill: -fx-dark-text-color;
-fx-highlight-fill: derive(-fx-control-inner-background,-20%);
-fx-highlight-text-fill: -fx-text-inner-color;
-fx-prompt-text-fill: -fx-control-inner-background;
-fx-border-color: -fx-text-box-border;
-fx-border-width: 1px;
-fx-background-color: #FFFFFF;
-fx-background-insets: 0;
-fx-background-radius: 0;
-fx-cursor: text;
-fx-padding: 2px;
}
.text-input:focused {
-fx-highlight-fill: -fx-accent;
-fx-border-color: -fx-focus-color;
-fx-border-width: 1px;
-fx-background-color: -fx-focus-color, #FFFFFF;
-fx-background-insets: 0, 1;
-fx-prompt-text-fill: -fx-control-inner-background;
}
/*******************************************************************************
* *
* PopupMenu *
* *
******************************************************************************/
.context-menu {
-fx-background-color: derive(-fx-background, -30%), -fx-background;
-fx-background-insets: 0, 1;
-fx-padding: 1px;
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.2), 2.0, 0.0, 3.0, 3.0);
}
.context-menu > .separator {
-fx-padding: 0.0em 0.333333em 0.0em 0.333333em; /* 0 4 0 4 */
}
/*******************************************************************************
* *
* MenuItem *
* *
******************************************************************************/
.menu-item {
-fx-background-color: transparent;
-fx-background-insets:0.0;
-fx-padding:0.2em 1em 0.2em 1em;
-fx-border-width: 0.0 0.0 0.0 0.0;
-fx-border-color:transparent;
}
.menu-item > .left-container {
-fx-padding: 0.458em 0.791em 0.458em 0.458em;
}
.menu-item > .graphic-container {
-fx-padding: 0em 0.333em 0em 0em;
}
.menu-item >.label {
-fx-padding: 0em 0.5em 0em 0em;
-fx-text-fill: -fx-text-base-color;
}
.menu-item:disabled > .label {
-fx-opacity: -fx-disabled-opacity;
}
.menu-item:focused {
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #DAECFC 0%, #C4E0FC 100%);
-fx-background-insets: 0, 1;
}
.menu-item > .right-container {
-fx-padding: 0em 0em 0em 0.5em;
}
.menu-item:show-mnemonics > .mnemonic-underline {
-fx-stroke: -fx-text-fill;
}
.menu > .right-container > .arrow {
-fx-padding: 0.458em 0.167em 0.458em 0.167em; /* 4.5 2 4.5 2 */
-fx-background-color: -fx-color;
-fx-shape: "M0,-4L4,0L0,4Z";
-fx-scale-shape: false;
}
.menu:selected > .right-container > .arrow {
-fx-background-color: white;
}
.menu-item:disabled {
-fx-opacity: -fx-disabled-opacity;
}
/*******************************************************************************
* *
* ComboBox *
* *
******************************************************************************/
/* Customie the ListCell that appears in the ComboBox button itself */
.combo-box > .list-cell {
-fx-background: transparent;
-fx-background-color: transparent;
-fx-text-fill: -fx-text-base-color;
-fx-padding: 0.1em;
}
.combo-box-base > .arrow-button {
-fx-background-color: transparent;
-fx-padding: 0 0.1em 0 0.1em;
}
.combo-box-popup > .list-view {
-fx-background-color: #606060, white;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.2), 2.0, 0.0, 3.0, 3.0);
}
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell {
-fx-background-color: transparent;
-fx-padding:0.1em;
-fx-border-color:transparent;
}
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected,
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #DAECFC 0%, #C4E0FC 100%);
-fx-background-insets: 0, 1;
}
/*******************************************************************************
* *
* SplitPane *
* *
******************************************************************************/
.split-pane > .split-pane-divider {
-fx-padding: 0 0.25em 0 0.25em; /* 0 3 0 3 */
}
/*******************************************************************************
* *
* ListView and ListCell *
* *
******************************************************************************/
.list-view > .virtual-flow > .scroll-bar:vertical{
-fx-background-insets: 0, 0 0 0 1;
-fx-padding: -1 -1 -1 0;
}
.list-view > .virtual-flow > .scroll-bar:horizontal{
-fx-background-insets: 0, 1 0 0 0;
-fx-padding: 0 -1 -1 -1;
}
.list-view > .virtual-flow > .corner {
-fx-background-color: -fx-box-border, -fx-base;
-fx-background-insets: 0, 1 0 0 1;
}
.list-cell {
-fx-background-color: -fx-control-inner-background;
-fx-padding: 0.8em 0.5em 0.8em 0.5em;
-fx-text-fill: -fx-text-inner-color;
-fx-opacity: 1;
}
.list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:focused {
-fx-background-color: -fx-focus-color, -fx-cell-focus-inner-border, -fx-control-inner-background;
-fx-background-insets: 0, 1, 2;
}
.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected {
-fx-background-color: #DEDEDE, #F7F7F7;
-fx-background-insets: 0, 1;
}
.list-cell:filled:hover {
-fx-background-color: #F7F7F7;
}
/*******************************************************************************
* *
* Tooltip *
* *
******************************************************************************/
.tooltip {
-fx-background-color: -fx-background;
-fx-padding: 0.2em 0.4em 0.2em 0.4em;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 2, 0, 0, 0);
-fx-font-size: 0.8em;
}
/*******************************************************************************
* *
* Charts *
* *
******************************************************************************/
.chart {
-fx-padding: 5px;
}
.chart-content {
-fx-padding: 10px;
}
.chart-title {
-fx-font-size: 1.4em;
}
.chart-legend {
-fx-background-color: linear-gradient(to bottom, derive(-fx-background, -10%), derive(-fx-background, -5%)),
linear-gradient(from 0px 0px to 0px 5px, derive(-fx-background, -5%), derive(-fx-background, 20%));
-fx-background-insets: 0,1;
-fx-background-radius: 6,5;
-fx-padding: 6px;
}
/*******************************************************************************
* *
* Axis *
* *
******************************************************************************/
.axis {
AXIS_COLOR: derive(-fx-background,-20%);
-fx-tick-label-font-size: 0.833333em; /* 10px */
-fx-tick-label-fill: derive(-fx-text-background-color, 30%);
}
.axis:top {
-fx-border-color: transparent transparent AXIS_COLOR transparent;
}
.axis:right {
-fx-border-color: transparent transparent transparent AXIS_COLOR;
}
.axis:bottom {
-fx-border-color: AXIS_COLOR transparent transparent transparent;
}
.axis:left {
-fx-border-color: transparent AXIS_COLOR transparent transparent;
}
.axis-tick-mark,
.axis-minor-tick-mark {
-fx-fill: null;
-fx-stroke: AXIS_COLOR;
}
/*******************************************************************************
* *
* ChartPlot *
* *
******************************************************************************/
.chart-vertical-grid-lines {
-fx-stroke: derive(-fx-background,-10%);
-fx-stroke-dash-array: 0.25em, 0.25em;
}
.chart-horizontal-grid-lines {
-fx-stroke: derive(-fx-background,-10%);
}
.chart-alternative-column-fill {
-fx-fill: null;
-fx-stroke: null;
}
.chart-alternative-row-fill {
-fx-fill: null;
-fx-stroke: null;
}
.chart-vertical-zero-line,
.chart-horizontal-zero-line {
-fx-stroke: derive(-fx-text-background-color, 40%);
}
/*******************************************************************************
* *
* LineChart *
* *
******************************************************************************/
.chart-line-symbol {
-fx-background-color: #f9d900, white;
-fx-background-insets: 0, 2;
-fx-background-radius: 5px;
-fx-padding: 5px;
}
.chart-series-line {
-fx-stroke: #f9d900;
-fx-stroke-width: 3px;
/*-fx-effect: dropshadow( two-pass-box , rgba(0,0,0,0.3) , 8, 0.0 , 0 , 3 );*/
}
.default-color0.chart-line-symbol { -fx-background-color: CHART_COLOR_1, white; }
.default-color1.chart-line-symbol { -fx-background-color: CHART_COLOR_2, white; }
.default-color2.chart-line-symbol { -fx-background-color: CHART_COLOR_3, white; }
.default-color3.chart-line-symbol { -fx-background-color: CHART_COLOR_4, white; }
.default-color4.chart-line-symbol { -fx-background-color: CHART_COLOR_5, white; }
.default-color5.chart-line-symbol { -fx-background-color: CHART_COLOR_6, white; }
.default-color6.chart-line-symbol { -fx-background-color: CHART_COLOR_7, white; }
.default-color7.chart-line-symbol { -fx-background-color: CHART_COLOR_8, white; }
.default-color0.chart-series-line { -fx-stroke: CHART_COLOR_1; }
.default-color1.chart-series-line { -fx-stroke: CHART_COLOR_2; }
.default-color2.chart-series-line { -fx-stroke: CHART_COLOR_3; }
.default-color3.chart-series-line { -fx-stroke: CHART_COLOR_4; }
.default-color4.chart-series-line { -fx-stroke: CHART_COLOR_5; }
.default-color5.chart-series-line { -fx-stroke: CHART_COLOR_6; }
.default-color6.chart-series-line { -fx-stroke: CHART_COLOR_7; }
.default-color7.chart-series-line { -fx-stroke: CHART_COLOR_8; }
/*******************************************************************************
* *
* Combinations *
* *
* This section is for special handling of when one control is nested inside *
* another control. There are many cases where we would end up with ugly *
* double borders that are fixed here. *
* *
******************************************************************************/
.split-pane > * > .table-view { -fx-padding: 0px; }
.split-pane > * > .list-view { -fx-padding: 0px; }
.split-pane > * > .tree-view { -fx-padding: 0px; }
.split-pane > * > .scroll-pane { -fx-padding: 0px; }
.split-pane > * > .split-pane {
-fx-background-insets: 0, 0;
-fx-padding: 0;
}
/* ############################################################################
# Workaround for RT-27627 #
############################################################################ */
.choice-box > .open-button > .arrow { doh: true; }
.split-menu-button:openvertically > .arrow-button > .arrow { doh: true; }
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button > .arrow { doh: true; }
.tree-table-view { doh: true; }
.tree-table-view:focused { doh: true; }
.tree-table-view > .virtual-flow > .scroll-bar:vertical { doh: true; }
.tree-table-view > .virtual-flow > .scroll-bar:horizontal { doh: true; }
.tree-table-view > .virtual-flow > .corner { doh: true; }
.tree-table-row-cell:focused { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected > .tree-table-cell { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected { doh: true; }
.tree-table-view:row-selection:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected:hover{ doh: true; }
.tree-table-row-cell:filled:selected:focused { doh: true; }
.tree-table-row-cell:filled:selected { doh: true; }
.tree-table-row-cell:selected:disabled { doh: true; }
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:hover { doh: true; }
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:hover { doh: true; }
.tree-table-view:constrained-resize > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:last-visible { doh: true; }
.tree-table-view:constrained-resize > .column-header:last-visible { doh: true; }
.tree-table-view:constrained-resize .filler { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:focused { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled .tree-table-cell:focused:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:selected:hover{ doh: true; }
.tree-table-row-cell:filled > .tree-table-cell:selected:focused { doh: true; }
.tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
.tree-table-cell:selected:disabled { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:hover { doh: true; }
.tree-table-view .column-resize-line { doh: true; }
.tree-table-view > .column-header-background { doh: true; }
.tree-table-view .column-header { doh: true; }
.tree-table-view .filler { doh: true; }
.tree-table-view .column-header .sort-order{ doh: true; }
.tree-table-view > .column-header-background > .show-hide-columns-button{ doh: true; }
.tree-table-view .show-hide-column-image { doh: true; }
.tree-table-view .column-drag-header { doh: true; }
.tree-table-view .column-overlay { doh: true; }
.tree-table-view /*> column-header-background > nested-column-header >*/ .arrow { doh: true; }
.tree-table-view .empty-table { doh: true; }
.axis-minor-tick-mark { doh: true; }
.chart-horizontal-zero-line { doh: true; }
.stacked-bar-chart:horizontal .chart-bar { doh: true; }

View File

@@ -44,6 +44,9 @@
<!-- Row 3 -->
<Button fx:id="okButton" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" text="%initialize.button.ok" prefWidth="150.0" onAction="#initializeVault" focusTraversable="false" disable="true" />
<!-- Row 4 -->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>

View File

@@ -14,17 +14,27 @@
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ContextMenu?>
<?import javafx.scene.control.MenuItem?>
<HBox fx:id="rootPane" prefHeight="400.0" prefWidth="600.0" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
<fx:define>
<fx:include fx:id="welcomeView" source="welcome.fxml" />
<ContextMenu fx:id="directoryContextMenu">
<items>
<MenuItem text="%main.directoryList.contextMenu.remove" onAction="#didClickRemoveSelectedEntry" />
<!-- TODO: -->
<MenuItem text="%main.directoryList.contextMenu.addUser" disable="true" />
<MenuItem text="%main.directoryList.contextMenu.changePassword" disable="true" />
</items>
</ContextMenu>
</fx:define>
<children>
<VBox prefWidth="200.0">
<children>
<ListView fx:id="directoryList" VBox.vgrow="ALWAYS" />
<ListView fx:id="directoryList" VBox.vgrow="ALWAYS" focusTraversable="false" />
<ToolBar VBox.vgrow="NEVER">
<items>
<Button text="+" onAction="#didClickAddDirectory" />
@@ -34,7 +44,7 @@
</VBox>
<Pane fx:id="contentPane">
<children>
<fx:reference source="welcomeView"/>
<fx:reference source="welcomeView" />
</children>
</Pane>
</children>

View File

@@ -15,6 +15,8 @@
<?import java.lang.String?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.control.CheckBox?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockController" xmlns:fx="http://javafx.com/fxml">
@@ -37,7 +39,14 @@
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 2 -->
<Button text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton" focusTraversable="false"/>
<Label text="%unlock.label.checkIntegrity" GridPane.rowIndex="2" GridPane.columnIndex="0" />
<CheckBox fx:id="checkIntegrity" wrapText="true" text="%unlock.checkbox.checkIntegrity" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 3 -->
<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton"/>
<!-- Row 4-->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />

View File

@@ -14,6 +14,8 @@
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.chart.LineChart?>
<?import javafx.scene.chart.NumberAxis?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockedController" xmlns:fx="http://javafx.com/fxml">
@@ -31,7 +33,13 @@
<Label fx:id="messageLabel" GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" />
<!-- Row 1 -->
<Button text="%unlocked.button.lock" defaultButton="true" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#closeVault" focusTraversable="false"/>
<Button text="%unlocked.button.lock" defaultButton="true" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickCloseVault" focusTraversable="false"/>
<!-- Row 2 -->
<LineChart fx:id="ioGraph" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" animated="false" createSymbols="false" prefHeight="300.0" legendVisible="true" legendSide="BOTTOM" verticalZeroLineVisible="false" verticalGridLinesVisible="false" horizontalGridLinesVisible="true">
<xAxis><NumberAxis fx:id="xAxis" forceZeroInRange="false" tickMarkVisible="false" minorTickVisible="false" tickLabelsVisible="false" autoRanging="false"/></xAxis>
<yAxis><NumberAxis label="%unlocked.ioGraph.yAxis.label" autoRanging="true" forceZeroInRange="true" /></yAxis>
</LineChart>
</children>
</GridPane>

View File

@@ -0,0 +1,33 @@
<?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
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<?import javafx.scene.shape.Arc?>
<?import javafx.scene.shape.QuadCurve?>
<?import javafx.scene.shape.Path?>
<?import javafx.scene.shape.Line?>
<AnchorPane xmlns:fx="http://javafx.com/fxml">
<children>
<Label AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="50.0" style="-fx-font-size: 1.5em;" text="%welcome.welcomeLabel"/>
<Label AnchorPane.leftAnchor="120.0" AnchorPane.topAnchor="280.0" text="%welcome.addButtonInstructionLabel"/>
<QuadCurve AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="300.0" startX="200.0" startY="0.0" endX="0.0" endY="80.0" controlX="180.0" controlY="80.0" fill="TRANSPARENT" stroke="BLACK" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="370.0" startX="0.0" endX="10.0" startY="10.0" endY="0.0" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="380.0" startX="0.0" endX="10.0" startY="0.0" endY="10.0" strokeWidth="2.0"/>
</children>
</AnchorPane>

View File

@@ -9,9 +9,15 @@
app.name=Cryptomator
# main.fxml
main.directoryList.contextMenu.remove=Remove from list
main.directoryList.contextMenu.addUser=Add user
main.directoryList.contextMenu.changePassword=Change password
# welcome.fxml
welcome.welcomeLabel=Welcome to Cryptomator
welcome.addButtonInstructionLabel=Start by adding a new vault :-)
# initialize.fxml
@@ -19,14 +25,15 @@ initialize.label.username=Username
initialize.label.password=Password
initialize.label.retypePassword=Retype password
initialize.button.ok=Create vault
initialize.alert.directoryIsNotEmpty.title=Confirm
initialize.alert.directoryIsNotEmpty.header=The chosen directory is not empty.
initialize.alert.directoryIsNotEmpty.title=The chosen directory is not empty
initialize.alert.directoryIsNotEmpty.content=All existing files inside this directory will get encrypted. Continue?
# unlock.fxml
unlock.label.username=Username
unlock.label.password=Password
unlock.label.checkIntegrity=File integrity
unlock.checkbox.checkIntegrity=Verify checksums (slower, but detects manipulation)
unlock.button.unlock=Unlock vault
unlock.errorMessage.wrongPassword=Wrong password.
unlock.errorMessage.decryptionFailed=Decryption failed.
@@ -37,6 +44,7 @@ unlock.messageLabel.startServerFailed=Starting WebDAV server failed.
# unlocked.fxml
unlocked.messageLabel.runningOnPort=Vault is accessible via WebDAV on local port %d.
unlocked.button.lock=Lock vault
unlocked.ioGraph.yAxis.label=Throughput (MiB/s)
# tray icon

View File

@@ -1,119 +0,0 @@
@CHARSET "US-ASCII";
.root {
-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
}
.text {
-fx-font-smoothing-type: lcd;
}
.button,
.combo-box {
-fx-border-color: #888888;
-fx-background-insets: 0.0, 1.0;
-fx-background-radius: 4.0, 4.0;
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
}
.text-field {
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
}
.button.green,
.button.red,
.split-menu-button.green,
.split-menu-button.red {
-fx-background-radius: 3.0;
-fx-background-color: #FFFFFF;
-fx-background-insets: 1px 1px 1px 1px;
}
.button.green,
.button.red,
.split-menu-button.green > .label,
.split-menu-button.red > .label {
-fx-text-fill: #FFF;
-fx-alignment: CENTER;
-fx-font-weight: bold;
-fx-font-family: "lucida-grande";
}
.split-menu-button.green > .arrow-button > .arrow,
.split-menu-button.red > .arrow-button > .arrow {
-fx-background-color: #FFF;
}
.button.green,
.split-menu-button.green > .label,
.split-menu-button.green > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #33EE55, #22AA33);
}
.button.green:hover,
.split-menu-button.green > .label:hover,
.split-menu-button.green > .arrow-button:hover {
-fx-background-color: linear-gradient(to bottom, #33EE55, #118822);
}
.button.green:armed,
.split-menu-button.green:armed > .label,
.split-menu-button.green > .arrow-button:pressed,
.split-menu-button.green:showing > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #118822, #22AA33 20%, #33EE55);
}
.button.green:disabled,
.split-menu-button.green:disabled,
.split-menu-button.green:disabled > .label,
.split-menu-button.green:disabled > .arrow-button {
-fx-background-color: #22AA33;
}
.button.red,
.split-menu-button.red > .label,
.split-menu-button.red > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #EE5533, #AA3322);
}
.button.red:hover,
.split-menu-button.red > .label:hover,
.split-menu-button.red > .arrow-button:hover {
-fx-background-color: linear-gradient(to bottom, #EE5533, #882211);
}
.button.red:armed,
.split-menu-button.red:armed > .label,
.split-menu-button.red > .arrow-button:pressed,
.split-menu-button.red:showing > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #882211, #AA3322 20%, #EE5533);
}
.button.red:disabled,
.split-menu-button.red:disabled,
.split-menu-button.red:disabled > .label,
.split-menu-button.red:disabled > .arrow-button {
-fx-background-color: #AA3322;
}
.split-menu-button .menu-item:focused {
-fx-background-color: #CCC;
}
.split-menu-button .menu-item .label {
-fx-text-fill: #000000;
}
.text-field {
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
-fx-border-color: #888888;
-fx-background-color: #FFFFFF;
-fx-padding: 4 2 4 2;
}
.text-field:focused {
-fx-background-color: #FFFFFF;
}

View File

@@ -1,26 +0,0 @@
<?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
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<AnchorPane xmlns:fx="http://javafx.com/fxml">
<children>
<Label fx:id="messageLabel" AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="50.0" text="%welcome.welcomeLabel"/>
</children>
</AnchorPane>