mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 16:51:28 +00:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b49eb82f38 | ||
|
|
523f38c69e | ||
|
|
3cd3012a05 | ||
|
|
3ff8d6bc19 | ||
|
|
7ce6ed6abb | ||
|
|
be0b4859e3 | ||
|
|
760b2c028f | ||
|
|
deb10c1256 | ||
|
|
b6b3360325 | ||
|
|
2e67910a60 | ||
|
|
e19cf1c942 | ||
|
|
55e758315d | ||
|
|
75fe462eb3 | ||
|
|
0e288f0c84 | ||
|
|
3f2ef3a83a | ||
|
|
e90e001718 | ||
|
|
1f8d4c5846 | ||
|
|
d9253be888 | ||
|
|
2d9fc0a8d8 | ||
|
|
1a076d9c1b | ||
|
|
9fe135ef0f | ||
|
|
4cb9da7252 | ||
|
|
ebea3dae65 | ||
|
|
d8c9279f6f | ||
|
|
4f91adb822 | ||
|
|
cc35430dee | ||
|
|
f057fb0e8e | ||
|
|
f4c7dc1bbd | ||
|
|
5bbaf62c67 | ||
|
|
3f32e4ee4b | ||
|
|
be5cf287c8 | ||
|
|
71892108b3 | ||
|
|
1770bab699 | ||
|
|
1d05e878ab | ||
|
|
f76091ddc0 | ||
|
|
6dff296872 | ||
|
|
6d98442f7e | ||
|
|
3cdda99c67 | ||
|
|
6b45d62aa1 | ||
|
|
b7f3f00ce2 | ||
|
|
dbadf54893 | ||
|
|
38a0cfb2eb | ||
|
|
7d6d061d95 | ||
|
|
c743fa8bdc | ||
|
|
8c2fe14e41 | ||
|
|
ac4f10ce93 | ||
|
|
4f15645bf9 | ||
|
|
c1f4ab6ada | ||
|
|
fd54393f36 | ||
|
|
a2c3b38a75 | ||
|
|
2fb35c59d4 | ||
|
|
afc62656bf | ||
|
|
9c8e4fbf3b | ||
|
|
470a609938 | ||
|
|
863b2ec423 | ||
|
|
d0a420d6c0 | ||
|
|
51e2e94ca9 | ||
|
|
d7efd7fc2f | ||
|
|
db36cfa22e |
20
LICENSE
20
LICENSE
@@ -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.
|
||||
202
LICENSES/Apache-2.0-License.txt
Normal file
202
LICENSES/Apache-2.0-License.txt
Normal 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
12
LICENSES/BSD-License.txt
Normal 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.
|
||||
21
LICENSES/MIT-X-Consortium-License.txt
Normal file
21
LICENSES/MIT-X-Consortium-License.txt
Normal 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
96
NOTICE.md
Normal 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.
|
||||
17
README.md
17
README.md
@@ -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.
|
||||
|
||||
[](https://travis-ci.org/totalvoidness/cryptomator)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,4 +6,8 @@ public class DecryptFailedException extends StorageCryptingException {
|
||||
public DecryptFailedException(Throwable t) {
|
||||
super("Decryption failed.", t);
|
||||
}
|
||||
|
||||
public DecryptFailedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
42
main/pom.xml
42
main/pom.xml
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
175
main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java
Normal file
175
main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java
Normal 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<String> futureBookName = runOnBackgroundThread(restResource::getBookName);
|
||||
*
|
||||
* // when done, update text label:
|
||||
* runOnMainThreadWhenFinished(futureBookName, (bookName) -> {
|
||||
* myLabel.setText(bookName);
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* <strong>Example use (exception-aware):</strong>
|
||||
*
|
||||
* <pre>
|
||||
* // get some string from a remote server:
|
||||
* Future<String> futureBookName = runOnBackgroundThread(restResource::getBookName);
|
||||
*
|
||||
* // when done, update text label:
|
||||
* runOnMainThreadWhenFinished(futureBookName, (bookName) -> {
|
||||
* myLabel.setText(bookName);
|
||||
* }, (exception) -> {
|
||||
* myLabel.setText("An exception occured: " + 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<String> futureBookName1 = runOnBackgroundThread(restResource::getBookName);
|
||||
*
|
||||
* Future<String> futureBookName2 = runOnBackgroundThread(() -> {
|
||||
* 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<?> futureDone1 = runOnBackgroundThread(this::doSomeComplexCalculation);
|
||||
*
|
||||
* Future<?> futureDone2 = runOnBackgroundThread(() -> {
|
||||
* 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) -> {
|
||||
* 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) -> {
|
||||
* myLabel.setText(bookName);
|
||||
* }, (exception) -> {
|
||||
* myLabel.setText("An exception occured: " + 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
apple.applescript.AppleScriptEngineFactory
|
||||
2977
main/ui/src/main/resources/css/linux_theme.css
Normal file
2977
main/ui/src/main/resources/css/linux_theme.css
Normal file
File diff suppressed because it is too large
Load Diff
906
main/ui/src/main/resources/css/mac_theme.css
Normal file
906
main/ui/src/main/resources/css/mac_theme.css
Normal 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; }
|
||||
891
main/ui/src/main/resources/css/win_theme.css
Normal file
891
main/ui/src/main/resources/css/win_theme.css
Normal 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; }
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
@@ -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>
|
||||
|
||||
33
main/ui/src/main/resources/fxml/welcome.fxml
Normal file
33
main/ui/src/main/resources/fxml/welcome.fxml
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user