Compare commits
197 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e18a11886 | ||
|
|
386059a238 | ||
|
|
b4ab09b3aa | ||
|
|
289ac55ccd | ||
|
|
b5160cddb9 | ||
|
|
1a81b3a781 | ||
|
|
b6a5db5797 | ||
|
|
aaf98c4fb9 | ||
|
|
55d1ffe703 | ||
|
|
5fefa3c6d4 | ||
|
|
b404e52670 | ||
|
|
44475fa3f1 | ||
|
|
f430f3c579 | ||
|
|
3efa23987f | ||
|
|
1dce871354 | ||
|
|
d919c727cf | ||
|
|
b691e374eb | ||
|
|
ca88e05849 | ||
|
|
104c3b64f6 | ||
|
|
1bef4e786d | ||
|
|
c1f32105d8 | ||
|
|
09b4130c3e | ||
|
|
6d1e0fe609 | ||
|
|
e65c84ca1d | ||
|
|
095f60ec03 | ||
|
|
485df3aa71 | ||
|
|
6b073c1499 | ||
|
|
71983cc3a8 | ||
|
|
db2297d2f1 | ||
|
|
38ab167fa4 | ||
|
|
f87e8f55f1 | ||
|
|
a4e6365e0b | ||
|
|
06034fd95b | ||
|
|
571fee9524 | ||
|
|
af9deffa6d | ||
|
|
48b319ec99 | ||
|
|
9ea9cb6eb2 | ||
|
|
301ba9cdb7 | ||
|
|
740c4c2ba9 | ||
|
|
18e7dcd91f | ||
|
|
95133152f9 | ||
|
|
4cd243e32a | ||
|
|
f454f48248 | ||
|
|
ad3801b223 | ||
|
|
3f946d1c82 | ||
|
|
ecb178d5b2 | ||
|
|
ed7dc60f5e | ||
|
|
6bbfacd794 | ||
|
|
5a06d01ef5 | ||
|
|
aac9ead633 | ||
|
|
cdcc1626ce | ||
|
|
738d2dfc34 | ||
|
|
9771c6d1e7 | ||
|
|
bc0a26b0ad | ||
|
|
7349ef754e | ||
|
|
e8e80f306b | ||
|
|
e1ce400bcd | ||
|
|
8c4d5a9614 | ||
|
|
93a87c86a4 | ||
|
|
685e347524 | ||
|
|
9d2d847727 | ||
|
|
a00086ff2d | ||
|
|
d76154c8d1 | ||
|
|
bc76ab285d | ||
|
|
0d3a5b4e70 | ||
|
|
48f544ef91 | ||
|
|
45cf87d089 | ||
|
|
d7186bb2dd | ||
|
|
85f3487cf0 | ||
|
|
4a754d6a6c | ||
|
|
abf9920caf | ||
|
|
dd2863da5b | ||
|
|
d43396bcfb | ||
|
|
b6383f49b1 | ||
|
|
c5b241a68a | ||
|
|
00a39c80cb | ||
|
|
8d8fe74d3a | ||
|
|
e767436f5d | ||
|
|
03cdf1fdc9 | ||
|
|
49646aae41 | ||
|
|
f3aa636b8b | ||
|
|
c73f18e3b8 | ||
|
|
5f40ce50e7 | ||
|
|
744f9db958 | ||
|
|
111ee99ae1 | ||
|
|
7d81ff3b43 | ||
|
|
00a2c6c5ae | ||
|
|
587c45ee63 | ||
|
|
3d3cb7bb86 | ||
|
|
0e3513e86d | ||
|
|
8845efb983 | ||
|
|
88f81d2682 | ||
|
|
58d500baaf | ||
|
|
103ea9047f | ||
|
|
f4b07b9807 | ||
|
|
6a3b4d486d | ||
|
|
13bcde318b | ||
|
|
242486c0b1 | ||
|
|
ea9c8eee83 | ||
|
|
0d969432c2 | ||
|
|
be369b480b | ||
|
|
4cf872f916 | ||
|
|
3d3c36b66f | ||
|
|
54c2afe3d1 | ||
|
|
3c71878b6b | ||
|
|
f36a61df1c | ||
|
|
1642aa4688 | ||
|
|
6f9b16a7dc | ||
|
|
66ed9126de | ||
|
|
a07efc5209 | ||
|
|
bbeeb79812 | ||
|
|
4d08e9d72b | ||
|
|
040f260bf0 | ||
|
|
cdf9c28a38 | ||
|
|
a6972f62f2 | ||
|
|
1db32470b1 | ||
|
|
ed022412fe | ||
|
|
a2356b62c7 | ||
|
|
9aa6117fb0 | ||
|
|
b9b85a58ac | ||
|
|
9024465d6c | ||
|
|
f22142a876 | ||
|
|
652c4cbafb | ||
|
|
188a13b202 | ||
|
|
75c21b4c9b | ||
|
|
c7ecd612c9 | ||
|
|
3f8f0b1fa7 | ||
|
|
2b4b359adb | ||
|
|
0562a909f9 | ||
|
|
c10d80de18 | ||
|
|
05abea0508 | ||
|
|
d19ffc327b | ||
|
|
a042c14fb9 | ||
|
|
a4be81267e | ||
|
|
c1dd902a10 | ||
|
|
0994e7bb39 | ||
|
|
1f3b91f187 | ||
|
|
e883a04577 | ||
|
|
1dd8a28a9d | ||
|
|
39df98ea3c | ||
|
|
2849e39e85 | ||
|
|
9433c22d7f | ||
|
|
5bd38d31bf | ||
|
|
63f64fae03 | ||
|
|
e321994c35 | ||
|
|
f86b27d62f | ||
|
|
cba8bbefc5 | ||
|
|
507e21f8a3 | ||
|
|
676cb10ef0 | ||
|
|
3b3aa4107b | ||
|
|
7edd303f2e | ||
|
|
ea3384d189 | ||
|
|
b2be41e39b | ||
|
|
f1d125bf8d | ||
|
|
028f6ea824 | ||
|
|
30dc8eecb1 | ||
|
|
4d979c26f6 | ||
|
|
4776dbf603 | ||
|
|
0b5e4469b4 | ||
|
|
8ba89a3bf5 | ||
|
|
b68cf71494 | ||
|
|
5569ecbfc7 | ||
|
|
19bc1ed569 | ||
|
|
5aaee7bbf6 | ||
|
|
3187520797 | ||
|
|
bcee1e0d12 | ||
|
|
9fdd2f339c | ||
|
|
ebdf37ed63 | ||
|
|
09c26f5e86 | ||
|
|
def70c5891 | ||
|
|
11396b71e6 | ||
|
|
05ec9b574e | ||
|
|
efac770915 | ||
|
|
f29bcc447c | ||
|
|
5e0ebab587 | ||
|
|
751dbe6b7e | ||
|
|
a72f8ba8ab | ||
|
|
999285617d | ||
|
|
addf488b26 | ||
|
|
cd5e878a26 | ||
|
|
0a671aa9bc | ||
|
|
8cc445a12a | ||
|
|
432beb2a17 | ||
|
|
9fd271ad7b | ||
|
|
72b1ff78c3 | ||
|
|
edfd264e47 | ||
|
|
0cfc3fb7f7 | ||
|
|
ecf29a91b8 | ||
|
|
38884c6dfd | ||
|
|
7813a11381 | ||
|
|
d774546bf8 | ||
|
|
0b64c7ce25 | ||
|
|
0aef60efc4 | ||
|
|
f0fa4fcf3d | ||
|
|
8bfdad38b9 | ||
|
|
19ea81f0e5 | ||
|
|
5e6f343e68 |
22
.travis.yml
@@ -1,4 +1,22 @@
|
||||
language: java
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
script: mvn -fmain/pom.xml clean package
|
||||
- oraclejdk8
|
||||
before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip -o /tmp/policy.zip && sudo unzip -j -o /tmp/policy.zip *.jar -d `jdk_switcher home oraclejdk8`/jre/lib/security && rm /tmp/policy.zip"
|
||||
script: mvn -fmain/pom.xml -Puber-jar clean package
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://webhooks.gitter.im/e/7d429ab35361726e26f2
|
||||
on_success: change
|
||||
on_failure: always
|
||||
on_start: false
|
||||
deploy:
|
||||
provider: releases
|
||||
prerelease: true
|
||||
api_key:
|
||||
secure: ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk=
|
||||
file: main/uber-jar/target/Cryptomator-$TRAVIS_TAG.jar
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: cryptomator/cryptomator
|
||||
tags: true
|
||||
|
||||
97
NOTICE.md
@@ -4,53 +4,14 @@ Copyright (c) 2014, Sebastian Stenzel
|
||||
Cryptomator is licensed under the MIT license. The details can be found in the accompanying license file.
|
||||
|
||||
## Third party softwares
|
||||
|
||||
Cryptomator uses third party softwares that may be licensed under different licenses.
|
||||
|
||||
### AquaFX
|
||||
The ProgressIndicator in ui/src/main/resource/css/mac_theme.css contains code from the AquaFX project.
|
||||
|
||||
### Jackson
|
||||
Jackson is a high-performance, Free/Open Source JSON processing library.
|
||||
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
|
||||
been in development since 2007.
|
||||
It is currently developed by a community of developers, as well as supported
|
||||
commercially by FasterXML.com.
|
||||
Copyright 2013 Claudine Zillmann (http://aquafx-project.com/)
|
||||
|
||||
**Licensing:** Jackson core and extension components may licensed under different licenses.
|
||||
To find the details that apply to this artifact see the accompanying Apache 2.0 license file.
|
||||
For more information, including possible other licensing options, contact
|
||||
FasterXML.com (http://fasterxml.com).
|
||||
|
||||
**Credits:** A list of contributors may be found from CREDITS file, which is included
|
||||
in some artifacts (usually source distributions); but is always available
|
||||
from the source code management (SCM) system project uses.
|
||||
|
||||
|
||||
### Jetty
|
||||
Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
|
||||
|
||||
All rights reserved. This program and the accompanying materials
|
||||
are made available under the terms of the Eclipse Public License v1.0
|
||||
and Apache License v2.0 which accompanies this distribution.
|
||||
|
||||
The UnixCrypt.java code implements the one way cryptography used by
|
||||
Unix systems for simple password protection. Copyright 1996 Aki Yoshida,
|
||||
modified April 2001 by Iris Van den Broeke, Daniel Deville.
|
||||
Permission to use, copy, modify and distribute UnixCrypt
|
||||
for non-commercial or commercial purposes and without fee is
|
||||
granted provided that the copyright notice appears in all copies.
|
||||
|
||||
|
||||
### Jackrabbit WebDAV Library
|
||||
Copyright 2004-2014 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
Based on source code originally developed by Day Software (http://www.day.com/).
|
||||
|
||||
### Apache Jakarta HttpClient
|
||||
Copyright 1999-2007 The Apache Software Foundation
|
||||
|
||||
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
|
||||
Licensed under the accompanying BSD license file.
|
||||
|
||||
### Apache Commons Collections
|
||||
Copyright 2001-2013 The Apache Software Foundation
|
||||
@@ -83,6 +44,17 @@ Copyright (c) 2013, ControlsFX
|
||||
|
||||
Licensed under the accompanying BSD license file.
|
||||
|
||||
### Dagger 2
|
||||
Copyright 2014 Google, Inc.
|
||||
Copyright 2012 Square, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0
|
||||
|
||||
### Apache Jakarta HttpClient
|
||||
Copyright 1999-2007 The Apache Software Foundation
|
||||
|
||||
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
### Apache Log4j
|
||||
Copyright 1999-2012 Apache Software Foundation
|
||||
|
||||
@@ -90,7 +62,44 @@ This product includes software developed at The Apache Software Foundation (http
|
||||
|
||||
ResolverUtil.java Copyright 2005-2006 Tim Fennell
|
||||
|
||||
### Jackrabbit WebDAV Library
|
||||
Copyright 2004-2014 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
Based on source code originally developed by Day Software (http://www.day.com/).
|
||||
|
||||
### Jackson
|
||||
Jackson is a high-performance, Free/Open Source JSON processing library.
|
||||
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
|
||||
been in development since 2007.
|
||||
It is currently developed by a community of developers, as well as supported
|
||||
commercially by FasterXML.com.
|
||||
|
||||
**Licensing:** Jackson core and extension components may licensed under different licenses.
|
||||
To find the details that apply to this artifact see the accompanying Apache 2.0 license file.
|
||||
For more information, including possible other licensing options, contact
|
||||
FasterXML.com (http://fasterxml.com).
|
||||
|
||||
**Credits:** A list of contributors may be found from CREDITS file, which is included
|
||||
in some artifacts (usually source distributions); but is always available
|
||||
from the source code management (SCM) system project uses.
|
||||
|
||||
### Jetty
|
||||
Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
|
||||
|
||||
All rights reserved. This program and the accompanying materials
|
||||
are made available under the terms of the Eclipse Public License v1.0
|
||||
and Apache License v2.0 which accompanies this distribution.
|
||||
|
||||
The UnixCrypt.java code implements the one way cryptography used by
|
||||
Unix systems for simple password protection. Copyright 1996 Aki Yoshida,
|
||||
modified April 2001 by Iris Van den Broeke, Daniel Deville.
|
||||
Permission to use, copy, modify and distribute UnixCrypt
|
||||
for non-commercial or commercial purposes and without fee is
|
||||
granted provided that the copyright notice appears in all copies.
|
||||
|
||||
### JUnit
|
||||
Copyright (c) 2000-2006, www.hamcrest.org
|
||||
|
||||
Licensed under the accompanying BSD license file.
|
||||
Licensed under the accompanying BSD license file.
|
||||
|
||||
62
README.md
@@ -1,47 +1,55 @@
|
||||
Cryptomator
|
||||
====================
|
||||
|
||||
Multiplatform transparent client-side encryption of your files in the cloud. You need Java 8 in order to run the application. Get the runtime environment here: http://www.oracle.com/technetwork/java/javase/downloads/index.html
|
||||
[](https://travis-ci.org/cryptomator/cryptomator)
|
||||
[](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://flattr.com/submit/auto?user_id=totalvoidness&url=https%3A%2F%2Fgithub.com%2Ftotalvoidness%2Fcryptomator&title=Cryptomator&language=en_GB&tags=github&category=software)
|
||||
|
||||
If you want to take a look at the current beta version, go ahead and download [Cryptomator.dmg](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.dmg), [Cryptomator.exe](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.exe) or [Cryptomator.jar](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.jar).
|
||||
Multiplatform transparent client-side encryption of your files in the cloud.
|
||||
|
||||
If you want to take a look at the current beta version, go ahead and get your copy of cryptomator on [Cryptomator.org](https://cryptomator.org) or clone and build Cryptomator using Maven (instructions below).
|
||||
|
||||
## Features
|
||||
- Totally transparent: Just work on the encrypted volume, as if it was an USB drive
|
||||
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory
|
||||
- Totally transparent: Just work on the encrypted volume, as if it was an USB flash drive
|
||||
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory.
|
||||
- In fact it works with any directory. You can use it to encrypt as many folders as you like
|
||||
- AES encryption with up to 256 bit key length
|
||||
- AES encryption with 256-bit key length
|
||||
- Client-side. No accounts, no data shared with any online service
|
||||
- Filenames get encrypted too
|
||||
- No need to provide credentials for any 3rd party service
|
||||
- Open Source means: No backdoors. Control is better than trust
|
||||
- Use as many encrypted folders in your dropbox as you want. Each having individual passwords
|
||||
- Use as many encrypted folders in your Dropbox as you want. Each having individual passwords
|
||||
- No commercial interest, no government agency, no wasted taxpayers' money ;-)
|
||||
|
||||
## Security
|
||||
- Default key length is 256 bit (falls back to 128 bit, if JCE isn't installed)
|
||||
- Scrypt key generation
|
||||
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
|
||||
### Privacy
|
||||
- 256 bit keys (unlimited strength policy bundled with native binaries - 128-bit elsewhere)
|
||||
- Scrypt key derivation
|
||||
- Cryptographically secure random numbers for salts, IVs and the master key of course
|
||||
- Sensitive data is swiped from the heap asap
|
||||
- Lightweight: Complexity kills security
|
||||
- Lightweight: [Complexity kills security](https://www.schneier.com/essays/archives/1999/11/a_plea_for_simplicit.html)
|
||||
|
||||
## Consistency
|
||||
- I/O operations are transactional and atomic, if the file systems supports it
|
||||
- ~~Metadata is stored per-folder, so it's not a SPOF~~
|
||||
- *NEW:* No Metadata at all. Encrypted files can be decrypted even on completely shuffled file systems (if their contents are undamaged).
|
||||
### Consistency
|
||||
- HMAC over file contents to recognize changed ciphertext before decryption
|
||||
- I/O operations are transactional and atomic, if the file systems support it
|
||||
- Each file contains all information needed for decryption (except for the key of course). No common metadata means no [SPOF](http://en.wikipedia.org/wiki/Single_point_of_failure)
|
||||
|
||||
## Dependencies
|
||||
- Java 8
|
||||
- see pom.xml ;-)
|
||||
## Building
|
||||
|
||||
## TODO
|
||||
#### Dependencies
|
||||
* Java 8
|
||||
* Maven 3
|
||||
* Optional: OS-dependent build tools for native packaging
|
||||
* Optional: JCE unlimited strength policy files (needed for 256-bit keys)
|
||||
|
||||
### UI
|
||||
- Native L&F
|
||||
- Drive icons in WebDAV volumes
|
||||
- Change password functionality
|
||||
- Better explanations on UI
|
||||
#### Building on Debian-based OS
|
||||
```bash
|
||||
apt-get install oracle-java8-installer oracle-java8-unlimited-jce-policy fakeroot maven git
|
||||
git clone https://github.com/cryptomator/cryptomator.git
|
||||
cd cryptomator/main
|
||||
git checkout 0.7.1
|
||||
mvn clean install -Pdebian
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the MIT X Consortium license license. See the LICENSE file for more info.
|
||||
|
||||
[](https://travis-ci.org/totalvoidness/cryptomator)
|
||||
Distributed under the MIT X Consortium license. See the LICENSE file for more info.
|
||||
|
||||
@@ -12,16 +12,14 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>core</artifactId>
|
||||
<name>Cryptomator core I/O module</name>
|
||||
<name>Cryptomator WebDAV and I/O module</name>
|
||||
|
||||
<properties>
|
||||
<jetty.version>9.2.5.v20141112</jetty.version>
|
||||
<jackrabbit.version>2.9.0</jackrabbit.version>
|
||||
<commons.transaction.version>1.2</commons.transaction.version>
|
||||
<jta.version>1.1</jta.version>
|
||||
<jetty.version>9.3.3.v20150827</jetty.version>
|
||||
<jackrabbit.version>2.11.0</jackrabbit.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -29,6 +27,11 @@
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>crypto-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>crypto-aes</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Jetty (Servlet Container) -->
|
||||
<dependency>
|
||||
@@ -41,6 +44,11 @@
|
||||
<artifactId>jetty-webapp</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-httpclient</groupId>
|
||||
<artifactId>commons-httpclient</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackrabbit -->
|
||||
<dependency>
|
||||
@@ -49,6 +57,12 @@
|
||||
<version>${jackrabbit.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- I/O -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
@@ -58,9 +72,11 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.cryptomator.files;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
|
||||
public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements CryptorIOSupport {
|
||||
|
||||
private final Path rootDir;
|
||||
private final Cryptor cryptor;
|
||||
private final EncryptionDecider encryptionDecider;
|
||||
private Path currentDir;
|
||||
|
||||
public EncryptingFileVisitor(Path rootDir, Cryptor cryptor, EncryptionDecider encryptionDecider) {
|
||||
this.rootDir = rootDir;
|
||||
this.cryptor = cryptor;
|
||||
this.encryptionDecider = encryptionDecider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
if (rootDir.equals(dir) || encryptionDecider.shouldEncrypt(dir)) {
|
||||
this.currentDir = dir;
|
||||
return FileVisitResult.CONTINUE;
|
||||
} else {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path plaintextFile, BasicFileAttributes attrs) throws IOException {
|
||||
if (encryptionDecider.shouldEncrypt(plaintextFile)) {
|
||||
final String plaintextName = plaintextFile.getFileName().toString();
|
||||
final String encryptedName = cryptor.encryptPath(plaintextName, '/', '/', this);
|
||||
final Path encryptedPath = plaintextFile.resolveSibling(encryptedName);
|
||||
final InputStream plaintextIn = Files.newInputStream(plaintextFile, StandardOpenOption.READ);
|
||||
final SeekableByteChannel ciphertextOut = Files.newByteChannel(encryptedPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
|
||||
cryptor.encryptFile(plaintextIn, ciphertextOut);
|
||||
Files.delete(plaintextFile);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
if (encryptionDecider.shouldEncrypt(dir)) {
|
||||
final String plaintext = dir.getFileName().toString();
|
||||
final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this);
|
||||
final Path newPath = dir.resolveSibling(encrypted);
|
||||
Files.move(dir, newPath, StandardCopyOption.ATOMIC_MOVE);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writePathSpecificMetadata(String metadataFile, byte[] encryptedMetadata) throws IOException {
|
||||
final Path path = currentDir.resolve(metadataFile);
|
||||
Files.write(path, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readPathSpecificMetadata(String metadataFile) throws IOException {
|
||||
final Path path = currentDir.resolve(metadataFile);
|
||||
return Files.readAllBytes(path);
|
||||
}
|
||||
|
||||
/* callback */
|
||||
|
||||
public interface EncryptionDecider {
|
||||
boolean shouldEncrypt(Path path);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,16 +8,25 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.jackrabbit.WebDavServlet;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.eclipse.jetty.util.thread.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
@@ -26,45 +35,39 @@ import org.slf4j.LoggerFactory;
|
||||
public final class WebDavServer {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class);
|
||||
private static final String LOCALHOST = "::1";
|
||||
private static final String LOCALHOST = SystemUtils.IS_OS_WINDOWS ? "::1" : "localhost";
|
||||
private static final int MAX_PENDING_REQUESTS = 200;
|
||||
private static final int MAX_THREADS = 200;
|
||||
private static final int MIN_THREADS = 4;
|
||||
private static final int THREAD_IDLE_SECONDS = 20;
|
||||
private final Server server;
|
||||
private int port;
|
||||
private final ServerConnector localConnector;
|
||||
private final ContextHandlerCollection servletCollection;
|
||||
|
||||
public WebDavServer() {
|
||||
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
|
||||
final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
|
||||
server = new Server(tp);
|
||||
localConnector = new ServerConnector(server);
|
||||
localConnector.setHost(LOCALHOST);
|
||||
servletCollection = new ContextHandlerCollection();
|
||||
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, "/", ServletContextHandler.NO_SESSIONS);
|
||||
final ServletHolder servlet = new ServletHolder(WindowsSucksServlet.class);
|
||||
servletContext.addServlet(servlet, "/");
|
||||
}
|
||||
|
||||
server.setConnectors(new Connector[] {localConnector});
|
||||
server.setHandler(servletCollection);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param workDir Path of encrypted folder.
|
||||
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
|
||||
* @return <code>true</code> upon success
|
||||
*/
|
||||
public synchronized boolean start(final String workDir, final boolean checkFileIntegrity, final Cryptor cryptor) {
|
||||
final ServerConnector connector = new ServerConnector(server);
|
||||
connector.setHost(LOCALHOST);
|
||||
|
||||
final String contextPath = "/";
|
||||
final String servletPathSpec = "/*";
|
||||
|
||||
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
|
||||
context.addServlet(getWebDavServletHolder(workDir, contextPath, checkFileIntegrity, cryptor), servletPathSpec);
|
||||
context.setContextPath(contextPath);
|
||||
server.setHandler(context);
|
||||
|
||||
public synchronized void start() {
|
||||
try {
|
||||
server.setConnectors(new Connector[] {connector});
|
||||
server.start();
|
||||
port = connector.getLocalPort();
|
||||
return true;
|
||||
LOG.info("Cryptomator is running on port {}", getPort());
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Server couldn't be started", ex);
|
||||
return false;
|
||||
throw new RuntimeException("Server couldn't be started", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,26 +75,100 @@ public final class WebDavServer {
|
||||
return server.isRunning();
|
||||
}
|
||||
|
||||
public synchronized boolean stop() {
|
||||
public synchronized void stop() {
|
||||
try {
|
||||
server.stop();
|
||||
port = 0;
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Server couldn't be stopped", ex);
|
||||
}
|
||||
return server.isStopped();
|
||||
}
|
||||
|
||||
private ServletHolder getWebDavServletHolder(final String workDir, final String contextPath, final boolean checkFileIntegrity, final Cryptor cryptor) {
|
||||
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor));
|
||||
/**
|
||||
* @param workDir Path of encrypted folder.
|
||||
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
|
||||
* @param failingMacCollection A (observable, thread-safe) collection, to which the names of resources are written, whose MAC authentication fails.
|
||||
* @param name The name of the folder. Must be non-empty and only contain any of _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
||||
* @return servlet
|
||||
*/
|
||||
public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection, final String name) {
|
||||
try {
|
||||
if (StringUtils.isEmpty(name)) {
|
||||
throw new IllegalArgumentException("name empty");
|
||||
}
|
||||
if (!StringUtils.containsOnly(name, "_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")) {
|
||||
throw new IllegalArgumentException("name contains illegal characters: " + name);
|
||||
}
|
||||
final URI uri = new URI(null, null, localConnector.getHost(), localConnector.getLocalPort(), "/" + UUID.randomUUID().toString() + "/" + name, null, null);
|
||||
|
||||
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, uri.getRawPath(), ServletContextHandler.SESSIONS);
|
||||
final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor, failingMacCollection, whitelistedResourceCollection);
|
||||
servletContext.addServlet(servlet, "/*");
|
||||
|
||||
servletCollection.mapContexts();
|
||||
|
||||
LOG.debug("{} available on http:{}", workDir, uri.getRawSchemeSpecificPart());
|
||||
return new ServletLifeCycleAdapter(servletContext, uri);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalStateException("Invalid hard-coded URI components.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
|
||||
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection, whitelistedResourceCollection));
|
||||
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
|
||||
result.setInitParameter(WebDavServlet.CFG_HTTP_ROOT, contextPath);
|
||||
result.setInitParameter(WebDavServlet.CFG_CHECK_FILE_INTEGRITY, Boolean.toString(checkFileIntegrity));
|
||||
return result;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
return localConnector.getLocalPort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes implementation-specific methods to other modules.
|
||||
*/
|
||||
public class ServletLifeCycleAdapter implements AutoCloseable {
|
||||
|
||||
private final LifeCycle lifecycle;
|
||||
private final URI servletUri;
|
||||
|
||||
private ServletLifeCycleAdapter(LifeCycle lifecycle, URI servletUri) {
|
||||
this.lifecycle = lifecycle;
|
||||
this.servletUri = servletUri;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return lifecycle.isRunning();
|
||||
}
|
||||
|
||||
public boolean start() {
|
||||
try {
|
||||
lifecycle.start();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to start", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean stop() {
|
||||
try {
|
||||
lifecycle.stop();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to stop", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public URI getServletUri() {
|
||||
return servletUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Windows mount attempts will fail, if not all requests on parent paths of a WebDAV resource get served. This servlet will respond to any
|
||||
* request with status code 200, if the requested resource doesn't match a different servlet.
|
||||
*/
|
||||
public class WindowsSucksServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = -515280795196074354L;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
resp.setStatus(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.cryptomator.webdav.exceptions;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
|
||||
public class DecryptFailedRuntimeException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = -2726689824823439865L;
|
||||
|
||||
public DecryptFailedRuntimeException(DecryptFailedException cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return getCause().getMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLocalizedMessage() {
|
||||
return getCause().getLocalizedMessage();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.exceptions;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class IORuntimeException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = -4713080133052143303L;
|
||||
|
||||
public IORuntimeException(IOException ioException) {
|
||||
super(ioException);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return getCause().getMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLocalizedMessage() {
|
||||
return getCause().getLocalizedMessage();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,22 +6,23 @@
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.BasicFileAttributeView;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
@@ -35,9 +36,9 @@ import org.apache.jackrabbit.webdav.property.DavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertySet;
|
||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.PropEntry;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -45,22 +46,34 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class);
|
||||
private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
|
||||
private static final String[] DAV_CREATIONDATE_PROPNAMES = {DavPropertyName.CREATIONDATE.getName(), "Win32CreationTime"};
|
||||
private static final String[] DAV_MODIFIEDDATE_PROPNAMES = {DavPropertyName.GETLASTMODIFIED.getName(), "Win32LastModifiedTime"};
|
||||
|
||||
protected final DavResourceFactory factory;
|
||||
protected final CryptoResourceFactory factory;
|
||||
protected final DavResourceLocator locator;
|
||||
protected final DavSession session;
|
||||
protected final LockManager lockManager;
|
||||
protected final Cryptor cryptor;
|
||||
protected final Path filePath;
|
||||
protected final DavPropertySet properties;
|
||||
|
||||
protected AbstractEncryptedNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
protected AbstractEncryptedNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath) {
|
||||
this.factory = factory;
|
||||
this.locator = locator;
|
||||
this.session = session;
|
||||
this.lockManager = lockManager;
|
||||
this.cryptor = cryptor;
|
||||
this.filePath = filePath;
|
||||
this.properties = new DavPropertySet();
|
||||
this.determineProperties();
|
||||
if (filePath != null && Files.exists(filePath)) {
|
||||
try {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + filePath.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,8 +88,7 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
return Files.exists(path);
|
||||
return Files.exists(filePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,16 +119,13 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
@Override
|
||||
public long getModificationTime() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
try {
|
||||
return Files.getLastModifiedTime(path).toMillis();
|
||||
return Files.getLastModifiedTime(filePath).toMillis();
|
||||
} catch (IOException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void determineProperties();
|
||||
|
||||
@Override
|
||||
public DavPropertyName[] getPropertyNames() {
|
||||
return getProperties().getPropertyNames();
|
||||
@@ -136,25 +145,27 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
public void setProperty(DavProperty<?> property) throws DavException {
|
||||
getProperties().add(property);
|
||||
|
||||
LOG.info("Set property {}", property.getName());
|
||||
|
||||
try {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (DavPropertyName.CREATIONDATE.equals(property.getName()) && property.getValue() instanceof String) {
|
||||
final String createDateStr = (String) property.getValue();
|
||||
final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr);
|
||||
final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(null, null, createTime);
|
||||
LOG.info("Updating Creation Date: {}", createTime.toString());
|
||||
} else if (DavPropertyName.GETLASTMODIFIED.equals(property.getName()) && property.getValue() instanceof String) {
|
||||
final String lastModifiedTimeStr = (String) property.getValue();
|
||||
final FileTime lastModifiedTime = FileTimeUtils.fromRfc1123String(lastModifiedTimeStr);
|
||||
final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(lastModifiedTime, null, null);
|
||||
LOG.info("Updating Last Modified Date: {}", lastModifiedTime.toString());
|
||||
LOG.trace("Set property {}", property.getName());
|
||||
|
||||
final String namespacelessPropertyName = property.getName().getName();
|
||||
if (Files.exists(filePath)) {
|
||||
try {
|
||||
if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
|
||||
final String createDateStr = (String) property.getValue();
|
||||
final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr);
|
||||
final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(null, null, createTime);
|
||||
LOG.debug("Updating Creation Date: {}", createTime.toString());
|
||||
} else if (Arrays.asList(DAV_MODIFIEDDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
|
||||
final String lastModifiedTimeStr = (String) property.getValue();
|
||||
final FileTime lastModifiedTime = FileTimeUtils.fromRfc1123String(lastModifiedTimeStr);
|
||||
final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(lastModifiedTime, null, null);
|
||||
LOG.debug("Updating Last Modified Date: {}", lastModifiedTime.toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +197,7 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String parentResource = FilenameUtils.getPath(locator.getResourcePath());
|
||||
final String parentResource = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(locator.getResourcePath()), "/");
|
||||
final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
|
||||
try {
|
||||
return getFactory().createResource(parentLocator, session);
|
||||
@@ -196,49 +207,37 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(DavResource dest) throws DavException {
|
||||
final Path src = ResourcePathUtils.getPhysicalPath(this);
|
||||
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
|
||||
try {
|
||||
// check for conflicts:
|
||||
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
|
||||
throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
|
||||
}
|
||||
|
||||
// move:
|
||||
public final void move(DavResource dest) throws DavException {
|
||||
if (dest instanceof AbstractEncryptedNode) {
|
||||
try {
|
||||
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
|
||||
this.move((AbstractEncryptedNode) dest);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error moving file from " + this.getResourcePath() + " to " + dest.getResourcePath());
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error moving file from " + src.toString() + " to " + dst.toString());
|
||||
throw new IORuntimeException(e);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void move(AbstractEncryptedNode dest) throws DavException, IOException;
|
||||
|
||||
@Override
|
||||
public void copy(DavResource dest, boolean shallow) throws DavException {
|
||||
final Path src = ResourcePathUtils.getPhysicalPath(this);
|
||||
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
|
||||
try {
|
||||
// check for conflicts:
|
||||
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
|
||||
throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
|
||||
}
|
||||
|
||||
// copy:
|
||||
public final void copy(DavResource dest, boolean shallow) throws DavException {
|
||||
if (dest instanceof AbstractEncryptedNode) {
|
||||
try {
|
||||
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
this.copy((AbstractEncryptedNode) dest, shallow);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error copying file from " + this.getResourcePath() + " to " + dest.getResourcePath());
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error copying file from " + src.toString() + " to " + dst.toString());
|
||||
throw new IORuntimeException(e);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException;
|
||||
|
||||
@Override
|
||||
public boolean isLockable(Type type, Scope scope) {
|
||||
return true;
|
||||
@@ -257,7 +256,11 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
@Override
|
||||
public ActiveLock[] getLocks() {
|
||||
final ActiveLock exclusiveWriteLock = getLock(Type.WRITE, Scope.EXCLUSIVE);
|
||||
return new ActiveLock[] {exclusiveWriteLock};
|
||||
if (exclusiveWriteLock != null) {
|
||||
return new ActiveLock[] {exclusiveWriteLock};
|
||||
} else {
|
||||
return new ActiveLock[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -281,7 +284,7 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceFactory getFactory() {
|
||||
public CryptoResourceFactory getFactory() {
|
||||
return factory;
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.AbstractDualBidiMap;
|
||||
import org.apache.commons.collections4.map.LRUMap;
|
||||
|
||||
final class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
|
||||
|
||||
BidiLRUMap(int maxSize) {
|
||||
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
|
||||
}
|
||||
|
||||
protected BidiLRUMap(final Map<K, V> normalMap, final Map<V, K> reverseMap, final BidiMap<V, K> inverseBidiMap) {
|
||||
super(normalMap, reverseMap, inverseBidiMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BidiMap<V, K> createBidiMap(Map<V, K> normalMap, Map<K, V> reverseMap, BidiMap<K, V> inverseMap) {
|
||||
return new BidiLRUMap<V, K>(normalMap, reverseMap, inverseMap);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.jackrabbit.webdav.DavLocatorFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.util.EncodeUtil;
|
||||
|
||||
public class CleartextLocatorFactory implements DavLocatorFactory {
|
||||
|
||||
private final String pathPrefix;
|
||||
|
||||
public CleartextLocatorFactory(String pathPrefix) {
|
||||
this.pathPrefix = StringUtils.removeEnd(pathPrefix, "/");
|
||||
}
|
||||
|
||||
// resourcePath == repositoryPath. No encryption here.
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String href) {
|
||||
final String fullPrefix = StringUtils.removeEnd(prefix, "/");
|
||||
final String relativeHref = StringUtils.removeStart(href, fullPrefix);
|
||||
|
||||
final String relativeCleartextPath = EncodeUtil.unescape(relativeHref);
|
||||
assert relativeCleartextPath.startsWith("/");
|
||||
return new CleartextLocator(relativeCleartextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
|
||||
assert resourcePath.startsWith("/");
|
||||
return new CleartextLocator(resourcePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
|
||||
assert path.startsWith("/");
|
||||
return new CleartextLocator(path);
|
||||
}
|
||||
|
||||
private class CleartextLocator implements DavResourceLocator {
|
||||
|
||||
private final String relativeCleartextPath;
|
||||
|
||||
private CleartextLocator(String relativeCleartextPath) {
|
||||
this.relativeCleartextPath = StringUtils.prependIfMissing(FilenameUtils.normalizeNoEndSeparator(relativeCleartextPath, true), "/");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return pathPrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResourcePath() {
|
||||
return relativeCleartextPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWorkspacePath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWorkspaceName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameWorkspace(DavResourceLocator locator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameWorkspace(String workspaceName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHref(boolean isCollection) {
|
||||
final String encodedResourcePath = EncodeUtil.escapePath(relativeCleartextPath);
|
||||
if (isRootLocation()) {
|
||||
return pathPrefix + "/";
|
||||
} else if (isCollection) {
|
||||
return pathPrefix + encodedResourcePath + "/";
|
||||
} else {
|
||||
return pathPrefix + encodedResourcePath;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRootLocation() {
|
||||
return "/".equals(relativeCleartextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavLocatorFactory getFactory() {
|
||||
return CleartextLocatorFactory.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRepositoryPath() {
|
||||
return relativeCleartextPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Locator: " + relativeCleartextPath + " (Prefix: " + pathPrefix + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return relativeCleartextPath.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof CleartextLocator) {
|
||||
final CleartextLocator other = (CleartextLocator) obj;
|
||||
return relativeCleartextPath == null && other.relativeCleartextPath == null || relativeCleartextPath.equals(other.relativeCleartextPath);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavMethods;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletRequest;
|
||||
import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
||||
public class CryptoResourceFactory implements DavResourceFactory, FileConstants {
|
||||
|
||||
private static final String RANGE_BYTE_PREFIX = "bytes=";
|
||||
private static final char RANGE_SET_SEP = ',';
|
||||
private static final char RANGE_SEP = '-';
|
||||
|
||||
private final LockManager lockManager = new SimpleLockManager();
|
||||
private final Cryptor cryptor;
|
||||
private final CryptoWarningHandler cryptoWarningHandler;
|
||||
private final Path dataRoot;
|
||||
private final FilenameTranslator filenameTranslator;
|
||||
|
||||
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, String vaultRoot) {
|
||||
Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot);
|
||||
this.cryptor = cryptor;
|
||||
this.cryptoWarningHandler = cryptoWarningHandler;
|
||||
this.dataRoot = vaultRootPath.resolve("d");
|
||||
this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
|
||||
if (locator.isRootLocation()) {
|
||||
return createRootDirectory(locator, request.getDavSession());
|
||||
}
|
||||
|
||||
try {
|
||||
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
|
||||
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
final String ifRangeHeader = request.getHeader(HttpHeader.IF_RANGE.asString());
|
||||
if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
|
||||
// DIRECTORY
|
||||
return createDirectory(locator, request.getDavSession(), dirFilePath);
|
||||
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
|
||||
// FILE RANGE
|
||||
final Pair<String, String> requestRange = getRequestRange(rangeHeader);
|
||||
response.setStatus(DavServletResponse.SC_PARTIAL_CONTENT);
|
||||
return createFilePart(locator, request.getDavSession(), requestRange, filePath);
|
||||
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && !isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
|
||||
// FULL FILE (if-range not fulfilled)
|
||||
return createFile(locator, request.getDavSession(), filePath);
|
||||
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && !isRangeSatisfiable(rangeHeader)) {
|
||||
// FULL FILE (unsatisfiable range)
|
||||
response.setStatus(DavServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
final EncryptedFile file = createFile(locator, request.getDavSession(), filePath);
|
||||
response.addHeader(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + file.getContentLength());
|
||||
return file;
|
||||
} else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
|
||||
// FULL FILE (as requested)
|
||||
return createFile(locator, request.getDavSession(), filePath);
|
||||
}
|
||||
} catch (NonExistingParentException e) {
|
||||
// return non-existing
|
||||
}
|
||||
return createNonExisting(locator, request.getDavSession());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
|
||||
if (locator.isRootLocation()) {
|
||||
return createRootDirectory(locator, session);
|
||||
}
|
||||
|
||||
try {
|
||||
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
|
||||
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
|
||||
if (Files.exists(dirFilePath)) {
|
||||
return createDirectory(locator, session, dirFilePath);
|
||||
} else if (Files.exists(filePath)) {
|
||||
return createFile(locator, session, filePath);
|
||||
}
|
||||
} catch (NonExistingParentException e) {
|
||||
// return non-existing
|
||||
}
|
||||
return createNonExisting(locator, session);
|
||||
}
|
||||
|
||||
DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException {
|
||||
return createDirectory(locator, session, existingDirectoryFile);
|
||||
}
|
||||
|
||||
DavResource createChildFileResource(DavResourceLocator locator, DavSession session, Path existingFile) throws DavException {
|
||||
return createFile(locator, session, existingFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if a partial response should be generated according to an If-Range precondition.
|
||||
*/
|
||||
private boolean isIfRangePreconditionFulfilled(String ifRangeHeader, Path filePath) throws DavException {
|
||||
if (ifRangeHeader == null) {
|
||||
// no header set -> fulfilled implicitly
|
||||
return true;
|
||||
} else {
|
||||
try {
|
||||
final FileTime expectedTime = FileTimeUtils.fromRfc1123String(ifRangeHeader);
|
||||
final FileTime actualTime = Files.getLastModifiedTime(filePath);
|
||||
return expectedTime.compareTo(actualTime) == 0;
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Unsupported If-Range header: " + ifRangeHeader);
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if and only if exactly one byte range has been requested.
|
||||
*/
|
||||
private boolean isRangeSatisfiable(String rangeHeader) {
|
||||
assert rangeHeader != null;
|
||||
if (!rangeHeader.startsWith(RANGE_BYTE_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, RANGE_BYTE_PREFIX);
|
||||
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
|
||||
if (byteRanges.length != 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the given range header field, if it is supported. Only headers containing a single byte range are supported.<br/>
|
||||
* <code>
|
||||
* bytes=100-200<br/>
|
||||
* bytes=-500<br/>
|
||||
* bytes=1000-
|
||||
* </code>
|
||||
*
|
||||
* @return Tuple of left and right range.
|
||||
* @throws DavException HTTP statuscode 400 for malformed requests.
|
||||
* @throws IllegalArgumentException If the given rangeHeader is not satisfiable. Check with {@link #isRangeSatisfiable(String)} before.
|
||||
*/
|
||||
private Pair<String, String> getRequestRange(String rangeHeader) throws DavException {
|
||||
assert rangeHeader != null;
|
||||
if (!rangeHeader.startsWith(RANGE_BYTE_PREFIX)) {
|
||||
throw new IllegalArgumentException("Unsatisfiable range. Should have generated 416 resonse.");
|
||||
}
|
||||
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, RANGE_BYTE_PREFIX);
|
||||
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
|
||||
if (byteRanges.length != 1) {
|
||||
throw new IllegalArgumentException("Unsatisfiable range. Should have generated 416 resonse.");
|
||||
}
|
||||
final String byteRange = byteRanges[0];
|
||||
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
|
||||
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
|
||||
throw new DavException(DavServletResponse.SC_BAD_REQUEST, "malformed range header: " + rangeHeader);
|
||||
}
|
||||
return new ImmutablePair<>(bytePos[0], bytePos[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Absolute file path for a given cleartext file resourcePath.
|
||||
* @throws NonExistingParentException If one ancestor of the encrypted path is missing
|
||||
*/
|
||||
Path getEncryptedFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
|
||||
assert relativeCleartextPath.startsWith("/");
|
||||
final String parentCleartextPath = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(relativeCleartextPath), "/");
|
||||
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
|
||||
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||
try {
|
||||
final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||
return parent.resolve(encryptedFilename);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Absolute file path for a given cleartext file resourcePath.
|
||||
* @throws NonExistingParentException If one ancestor of the encrypted path is missing
|
||||
*/
|
||||
Path getEncryptedDirectoryFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
|
||||
assert relativeCleartextPath.startsWith("/");
|
||||
final String parentCleartextPath = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(relativeCleartextPath), "/");
|
||||
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
|
||||
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||
try {
|
||||
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
|
||||
return parent.resolve(encryptedFilename);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param createNonExisting if <code>false</code>, a {@link NonExistingParentException} will be thrown for missing ancestors.
|
||||
* @return Absolute directory path for a given cleartext directory resourcePath.
|
||||
* @throws NonExistingParentException if one ancestor directory is missing.
|
||||
*/
|
||||
private Path getEncryptedDirectoryPath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
|
||||
assert relativeCleartextPath.startsWith("/");
|
||||
assert "/".equals(relativeCleartextPath) || !relativeCleartextPath.endsWith("/");
|
||||
try {
|
||||
final Path result;
|
||||
if ("/".equals(relativeCleartextPath)) {
|
||||
// root level
|
||||
final String fixedRootDirectory = cryptor.encryptDirectoryPath("", FileSystems.getDefault().getSeparator());
|
||||
result = dataRoot.resolve(fixedRootDirectory);
|
||||
} else {
|
||||
final String parentCleartextPath = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(relativeCleartextPath), "/");
|
||||
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
|
||||
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
|
||||
final Path directoryFile = parent.resolve(encryptedFilename);
|
||||
if (!createNonExisting && !Files.exists(directoryFile)) {
|
||||
throw new NonExistingParentException();
|
||||
}
|
||||
final String directoryId = filenameTranslator.getDirectoryId(directoryFile, true);
|
||||
final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
|
||||
result = dataRoot.resolve(directory);
|
||||
}
|
||||
Files.createDirectories(result);
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, Path filePath) {
|
||||
return new EncryptedFilePart(this, locator, session, requestRange, lockManager, cryptor, cryptoWarningHandler, filePath);
|
||||
}
|
||||
|
||||
private EncryptedFile createFile(DavResourceLocator locator, DavSession session, Path filePath) {
|
||||
return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
|
||||
}
|
||||
|
||||
private EncryptedDir createRootDirectory(DavResourceLocator locator, DavSession session) throws DavException {
|
||||
final Path rootFile = dataRoot.resolve(ROOT_FILE);
|
||||
final Path rootDir = filenameTranslator.getEncryptedDirectoryPath("");
|
||||
try {
|
||||
// make sure, root dir always exists.
|
||||
// create dir first (because it fails silently, if alreay existing)
|
||||
Files.createDirectories(rootDir);
|
||||
Files.createFile(rootFile);
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
// no-op
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
return createDirectory(locator, session, dataRoot.resolve(ROOT_FILE));
|
||||
}
|
||||
|
||||
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path filePath) {
|
||||
return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, filePath);
|
||||
}
|
||||
|
||||
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
|
||||
return new NonExistingNode(this, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
static class NonExistingParentException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 4421121746624627094L;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
class CryptoWarningHandler {
|
||||
|
||||
private final Collection<String> resourcesWithInvalidMac;
|
||||
private final Collection<String> whitelistedResources;
|
||||
|
||||
public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac, Collection<String> whitelistedResources) {
|
||||
this.resourcesWithInvalidMac = resourcesWithInvalidMac;
|
||||
this.whitelistedResources = whitelistedResources;
|
||||
}
|
||||
|
||||
public void macAuthFailed(String resourcePath) {
|
||||
// collection might be a list, but we don't want duplicates:
|
||||
if (!resourcesWithInvalidMac.contains(resourcePath)) {
|
||||
resourcesWithInvalidMac.add(resourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean ignoreMac(String resourcePath) {
|
||||
return whitelistedResources.contains(resourcePath);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.jackrabbit.webdav.AbstractLocatorFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.SensitiveDataSwipeListener;
|
||||
|
||||
class DavLocatorFactoryImpl extends AbstractLocatorFactory implements SensitiveDataSwipeListener, CryptorIOSupport {
|
||||
|
||||
private static final int MAX_CACHED_PATHS = 10000;
|
||||
private final Path fsRoot;
|
||||
private final Cryptor cryptor;
|
||||
private final BidiMap<String, String> pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // <decryptedPath, encryptedPath>
|
||||
|
||||
DavLocatorFactoryImpl(String fsRoot, String httpRoot, Cryptor cryptor) {
|
||||
super(httpRoot);
|
||||
this.fsRoot = FileSystems.getDefault().getPath(fsRoot);
|
||||
this.cryptor = cryptor;
|
||||
cryptor.addSensitiveDataSwipeListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Encrypted absolute paths on the file system.
|
||||
*/
|
||||
@Override
|
||||
protected String getRepositoryPath(String resourcePath, String wspPath) {
|
||||
String encryptedPath = pathCache.get(resourcePath);
|
||||
if (encryptedPath == null) {
|
||||
encryptedPath = encryptRepositoryPath(resourcePath);
|
||||
pathCache.put(resourcePath, encryptedPath);
|
||||
}
|
||||
return encryptedPath;
|
||||
}
|
||||
|
||||
private String encryptRepositoryPath(String resourcePath) {
|
||||
if (resourcePath == null) {
|
||||
return fsRoot.toString();
|
||||
}
|
||||
final String encryptedRepoPath = cryptor.encryptPath(resourcePath, FileSystems.getDefault().getSeparator().charAt(0), '/', this);
|
||||
return fsRoot.resolve(encryptedRepoPath).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Decrypted path for use in URIs.
|
||||
*/
|
||||
@Override
|
||||
protected String getResourcePath(String repositoryPath, String wspPath) {
|
||||
String decryptedPath = pathCache.getKey(repositoryPath);
|
||||
if (decryptedPath == null) {
|
||||
decryptedPath = decryptResourcePath(repositoryPath);
|
||||
pathCache.put(decryptedPath, repositoryPath);
|
||||
}
|
||||
return decryptedPath;
|
||||
}
|
||||
|
||||
private String decryptResourcePath(String repositoryPath) {
|
||||
final Path absRepoPath = FileSystems.getDefault().getPath(repositoryPath);
|
||||
if (fsRoot.equals(absRepoPath)) {
|
||||
return null;
|
||||
} else {
|
||||
final Path relativeRepositoryPath = fsRoot.relativize(absRepoPath);
|
||||
final String resourcePath = cryptor.decryptPath(relativeRepositoryPath.toString(), FileSystems.getDefault().getSeparator().charAt(0), '/', this);
|
||||
return resourcePath;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
|
||||
// we don't support workspaces
|
||||
return super.createResourceLocator(prefix, "", path, isResourcePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
|
||||
// we don't support workspaces
|
||||
return super.createResourceLocator(prefix, "", resourcePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void swipeSensitiveData() {
|
||||
pathCache.clear();
|
||||
}
|
||||
|
||||
/* Cryptor I/O Support */
|
||||
|
||||
@Override
|
||||
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException {
|
||||
final Path metaDataFile = fsRoot.resolve(encryptedPath);
|
||||
Files.write(metaDataFile, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readPathSpecificMetadata(String encryptedPath) throws IOException {
|
||||
final Path metaDataFile = fsRoot.resolve(encryptedPath);
|
||||
if (!Files.isReadable(metaDataFile)) {
|
||||
return null;
|
||||
} else {
|
||||
return Files.readAllBytes(metaDataFile);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavMethods;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletRequest;
|
||||
import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.EncryptedDir;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFile;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFilePart;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.NonExistingNode;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.ResourcePathUtils;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
||||
class DavResourceFactoryImpl implements DavResourceFactory {
|
||||
|
||||
private final LockManager lockManager = new SimpleLockManager();
|
||||
private final Cryptor cryptor;
|
||||
private final boolean checkFileIntegrity;
|
||||
|
||||
DavResourceFactoryImpl(Cryptor cryptor, boolean checkFileIntegrity) {
|
||||
this.cryptor = cryptor;
|
||||
this.checkFileIntegrity = checkFileIntegrity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(locator);
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
|
||||
if (Files.isRegularFile(path) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
|
||||
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
|
||||
return createFilePart(locator, request.getDavSession(), request);
|
||||
} else if (Files.isRegularFile(path) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
|
||||
return createFile(locator, request.getDavSession());
|
||||
} else if (Files.isDirectory(path) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
|
||||
return createDirectory(locator, request.getDavSession());
|
||||
} else {
|
||||
return createNonExisting(locator, request.getDavSession());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(locator);
|
||||
|
||||
if (Files.isRegularFile(path)) {
|
||||
return createFile(locator, session);
|
||||
} else if (Files.isDirectory(path)) {
|
||||
return createDirectory(locator, session);
|
||||
} else {
|
||||
return createNonExisting(locator, session);
|
||||
}
|
||||
}
|
||||
|
||||
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
|
||||
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, checkFileIntegrity);
|
||||
}
|
||||
|
||||
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
|
||||
return new EncryptedFile(this, locator, session, lockManager, cryptor, checkFileIntegrity);
|
||||
}
|
||||
|
||||
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session) {
|
||||
return new EncryptedDir(this, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
|
||||
return new NonExistingNode(this, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.io.InputContext;
|
||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.ActiveLock;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.ResourceType;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.webdav.exceptions.DavRuntimeException;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
|
||||
private final FilenameTranslator filenameTranslator;
|
||||
private String directoryId;
|
||||
private Path directoryPath;
|
||||
|
||||
public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path filePath) {
|
||||
super(factory, locator, session, lockManager, cryptor, filePath);
|
||||
this.filenameTranslator = filenameTranslator;
|
||||
properties.add(new ResourceType(ResourceType.COLLECTION));
|
||||
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Path or <code>null</code>, if directory does not yet exist.
|
||||
*/
|
||||
protected synchronized String getDirectoryId() {
|
||||
if (directoryId == null) {
|
||||
try {
|
||||
directoryId = filenameTranslator.getDirectoryId(filePath, false);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
return directoryId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Path or <code>null</code>, if directory does not yet exist.
|
||||
*/
|
||||
private synchronized Path getDirectoryPath() {
|
||||
if (directoryPath == null) {
|
||||
final String dirId = getDirectoryId();
|
||||
if (dirId != null) {
|
||||
directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
|
||||
}
|
||||
}
|
||||
return directoryPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return Files.exists(filePath) && Files.exists(getDirectoryPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getModificationTime() {
|
||||
try {
|
||||
final Path dirPath = getDirectoryPath();
|
||||
if (dirPath == null) {
|
||||
return -1;
|
||||
} else {
|
||||
return Files.getLastModifiedTime(dirPath).toMillis();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
if (resource instanceof AbstractEncryptedNode) {
|
||||
addMember((AbstractEncryptedNode) resource, inputContext);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported resource type: " + resource.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void addMember(AbstractEncryptedNode childResource, InputContext inputContext) throws DavException {
|
||||
if (childResource.isCollection()) {
|
||||
this.addMemberDir(childResource.getLocator(), inputContext);
|
||||
} else {
|
||||
this.addMemberFile(childResource.getLocator(), inputContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMemberDir(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
|
||||
final Path dirPath = getDirectoryPath();
|
||||
if (dirPath == null) {
|
||||
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
try {
|
||||
final String cleartextDirName = FilenameUtils.getName(childLocator.getResourcePath());
|
||||
final String ciphertextDirName = filenameTranslator.getEncryptedDirFileName(cleartextDirName);
|
||||
final Path dirFilePath = dirPath.resolve(ciphertextDirName);
|
||||
final String directoryId = filenameTranslator.getDirectoryId(dirFilePath, true);
|
||||
final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
|
||||
Files.createDirectories(directoryPath);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMemberFile(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
|
||||
final Path dirPath = getDirectoryPath();
|
||||
if (dirPath == null) {
|
||||
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
try {
|
||||
final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath());
|
||||
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||
final Path filePath = dirPath.resolve(ciphertextFilename);
|
||||
final Path tmpFilePath = Files.createTempFile(dirPath, null, null);
|
||||
// encrypt to tmp file:
|
||||
try (final FileChannel c = FileChannel.open(tmpFilePath, StandardOpenOption.WRITE, StandardOpenOption.DSYNC)) {
|
||||
cryptor.encryptFile(inputContext.getInputStream(), c);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (EncryptFailedException e) {
|
||||
LOG.error("Encryption failed for unknown reasons.", e);
|
||||
throw new IllegalStateException("Encryption failed for unknown reasons.", e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(inputContext.getInputStream());
|
||||
}
|
||||
// mv tmp to target file:
|
||||
try {
|
||||
Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
Files.setLastModifiedTime(filePath, FileTime.from(Instant.now()));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create file.", e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
final Path dirPath = getDirectoryPath();
|
||||
if (dirPath == null) {
|
||||
throw new DavRuntimeException(new DavException(DavServletResponse.SC_NOT_FOUND));
|
||||
}
|
||||
|
||||
try (final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dirPath, DIRECTORY_CONTENT_FILTER)) {
|
||||
final List<DavResource> result = new ArrayList<>();
|
||||
for (final Path childPath : directoryStream) {
|
||||
try {
|
||||
final String cleartextFilename = filenameTranslator.getCleartextFilename(childPath.getFileName().toString());
|
||||
final String cleartextFilepath = locator.isRootLocation() ? '/' + cleartextFilename : locator.getResourcePath() + '/' + cleartextFilename;
|
||||
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), cleartextFilepath);
|
||||
final DavResource resource;
|
||||
if (StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), DIR_EXT)) {
|
||||
resource = factory.createChildDirectoryResource(childLocator, session, childPath);
|
||||
} else {
|
||||
assert StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), FILE_EXT);
|
||||
resource = factory.createChildFileResource(childLocator, session, childPath);
|
||||
}
|
||||
if (resource.exists()) {
|
||||
result.add(resource);
|
||||
}
|
||||
} catch (DecryptFailedException e) {
|
||||
LOG.warn("Decryption of resource failed: " + childPath);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return new DavResourceIteratorImpl(result);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Exception during getMembers.", e);
|
||||
throw new UncheckedIOException(e);
|
||||
} catch (DavException e) {
|
||||
LOG.error("Exception during getMembers.", e);
|
||||
throw new DavRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
if (member instanceof AbstractEncryptedNode) {
|
||||
removeMember((AbstractEncryptedNode) member);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported resource type: " + member.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void removeMember(AbstractEncryptedNode member) throws DavException {
|
||||
final Path dirPath = getDirectoryPath();
|
||||
if (dirPath == null) {
|
||||
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
// https://tools.ietf.org/html/rfc4918#section-9.6
|
||||
// we must unlock anything we want to delete:
|
||||
for (ActiveLock lock : member.getLocks()) {
|
||||
member.unlock(lock.getToken());
|
||||
}
|
||||
// now we can delete the file or directory:
|
||||
try {
|
||||
final String cleartextFilename = FilenameUtils.getName(member.getResourcePath());
|
||||
if (member instanceof EncryptedDir) {
|
||||
final EncryptedDir subDir = (EncryptedDir) member;
|
||||
deleteSubDirectory(subDir);
|
||||
} else {
|
||||
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||
final Path memberPath = dirPath.resolve(ciphertextFilename);
|
||||
Files.deleteIfExists(memberPath);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// no-op
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(AbstractEncryptedNode dest) throws DavException, IOException {
|
||||
// when moving a directory we only need to move the file (actual dir is ID-dependent and won't change)
|
||||
final Path srcPath = filePath;
|
||||
final Path dstPath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstPath = ((NonExistingNode) dest).materializeDirFilePath();
|
||||
} else {
|
||||
dstPath = dest.filePath;
|
||||
}
|
||||
|
||||
// move:
|
||||
Files.createDirectories(dstPath.getParent());
|
||||
try {
|
||||
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
|
||||
final Path dstDirFilePath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstDirFilePath = ((NonExistingNode) dest).materializeDirFilePath();
|
||||
} else {
|
||||
dstDirFilePath = dest.filePath;
|
||||
}
|
||||
|
||||
// copy dirFile:
|
||||
final String srcDirId = getDirectoryId();
|
||||
if (srcDirId == null) {
|
||||
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
final String dstDirId = UUID.randomUUID().toString();
|
||||
try (final FileChannel c = FileChannel.open(dstDirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
|
||||
SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
|
||||
c.write(ByteBuffer.wrap(dstDirId.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
// copy actual dir:
|
||||
if (!shallow) {
|
||||
copyDirectoryContents(srcDirId, dstDirId);
|
||||
} else {
|
||||
final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
|
||||
Files.createDirectories(dstDirPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyDirectoryContents(String srcDirId, String dstDirId) throws IOException {
|
||||
final Path srcDirPath = filenameTranslator.getEncryptedDirectoryPath(srcDirId);
|
||||
final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
|
||||
Files.createDirectories(dstDirPath);
|
||||
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(srcDirPath, DIRECTORY_CONTENT_FILTER);
|
||||
for (final Path srcChildPath : directoryStream) {
|
||||
final String childName = srcChildPath.getFileName().toString();
|
||||
final Path dstChildPath = dstDirPath.resolve(childName);
|
||||
if (StringUtils.endsWithIgnoreCase(childName, FILE_EXT)) {
|
||||
try {
|
||||
Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} else if (StringUtils.endsWithIgnoreCase(childName, DIR_EXT)) {
|
||||
final String srcSubdirId = filenameTranslator.getDirectoryId(srcChildPath, false);
|
||||
final String dstSubdirId = filenameTranslator.getDirectoryId(dstChildPath, true);
|
||||
copyDirectoryContents(srcSubdirId, dstSubdirId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a given directory recursively by resolving subdirectories using their directory files.
|
||||
*/
|
||||
private void deleteSubDirectory(final EncryptedDir subDir) throws IOException {
|
||||
final Path subDirPath = subDir.getDirectoryPath();
|
||||
filenameTranslator.uncacheDirectoryId(subDir.filePath);
|
||||
Files.delete(subDir.filePath);
|
||||
final LinkedTransferQueue<Path> queue = new LinkedTransferQueue<>();
|
||||
queue.put(subDirPath);
|
||||
Path dir;
|
||||
while ((dir = queue.poll()) != null) {
|
||||
if (Files.exists(dir)) {
|
||||
Files.walkFileTree(dir, new RecursiveDirectoryDeletingVisitor(queue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all files it visits and enqueues subdirectories into a given {@link Queue} for deletion, too.
|
||||
*
|
||||
* If its parent directory is empty after deleting, it will get deleted, too.
|
||||
*/
|
||||
private class RecursiveDirectoryDeletingVisitor extends SimpleFileVisitor<Path> {
|
||||
|
||||
private final Queue<Path> directories;
|
||||
|
||||
private RecursiveDirectoryDeletingVisitor(Queue<Path> directories) {
|
||||
this.directories = directories;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
|
||||
if (file.toString().endsWith(DIR_EXT)) {
|
||||
final String directoryId = filenameTranslator.getDirectoryId(file, false);
|
||||
final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
|
||||
directories.add(directoryPath);
|
||||
filenameTranslator.uncacheDirectoryId(file);
|
||||
}
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
// first check, if we're the only remaining child:
|
||||
boolean hasSiblings = false;
|
||||
try (final DirectoryStream<Path> siblings = Files.newDirectoryStream(dir.getParent())) {
|
||||
for (Path sibling : siblings) {
|
||||
if (!dir.getFileName().equals(sibling.getFileName())) {
|
||||
hasSiblings = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// delete our current directory:
|
||||
Files.delete(dir);
|
||||
// if we have siblings, we still need our parent. Otherwise delete it, too:
|
||||
if (!hasSiblings) {
|
||||
Files.delete(dir.getParent());
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
|
||||
LOG.error("Failed to delete file " + file.toString(), exc);
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.io.InputContext;
|
||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpHeaderValue;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
|
||||
|
||||
protected final CryptoWarningHandler cryptoWarningHandler;
|
||||
protected final Long contentLength;
|
||||
|
||||
public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) {
|
||||
super(factory, locator, session, lockManager, cryptor, filePath);
|
||||
if (filePath == null) {
|
||||
throw new IllegalArgumentException("filePath must not be null");
|
||||
}
|
||||
this.cryptoWarningHandler = cryptoWarningHandler;
|
||||
Long contentLength = null;
|
||||
if (Files.isRegularFile(filePath)) {
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
|
||||
contentLength = cryptor.decryptedContentLength(c);
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
|
||||
if (contentLength > RANGE_REQUEST_LOWER_LIMIT) {
|
||||
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
|
||||
}
|
||||
} catch (OverlappingFileLockException e) {
|
||||
// file header currently locked, report -1 for unknown size.
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, -1l));
|
||||
} catch (MacAuthenticationFailedException e) {
|
||||
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
|
||||
// don't add content length DAV property
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error reading filesize " + filePath.toString(), e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
|
||||
public Long getContentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
throw new UnsupportedOperationException("Can not add member to file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
throw new UnsupportedOperationException("Can not list members of file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
throw new UnsupportedOperationException("Can not remove member to file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
if (Files.isRegularFile(filePath)) {
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
|
||||
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
|
||||
final Long contentLength = cryptor.decryptedContentLength(c);
|
||||
if (contentLength != null) {
|
||||
outputContext.setContentLength(contentLength);
|
||||
}
|
||||
if (outputContext.hasStream()) {
|
||||
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
|
||||
cryptor.decryptFile(c, outputContext.getOutputStream(), authenticate);
|
||||
outputContext.getOutputStream().flush();
|
||||
}
|
||||
|
||||
} catch (EOFException e) {
|
||||
LOG.warn("Unexpected end of stream (possibly client hung up).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(AbstractEncryptedNode dest) throws DavException, IOException {
|
||||
final Path srcPath = filePath;
|
||||
final Path dstPath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstPath = ((NonExistingNode) dest).materializeFilePath();
|
||||
} else {
|
||||
dstPath = dest.filePath;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
|
||||
final Path srcPath = filePath;
|
||||
final Path dstPath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstPath = ((NonExistingNode) dest).materializeFilePath();
|
||||
} else {
|
||||
dstPath = dest.filePath;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Delivers only the requested range of bytes from a file.
|
||||
*
|
||||
* @see {@link https://tools.ietf.org/html/rfc7233#section-4}
|
||||
*/
|
||||
class EncryptedFilePart extends EncryptedFile {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
|
||||
|
||||
private final Pair<Long, Long> range;
|
||||
|
||||
public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, LockManager lockManager, Cryptor cryptor,
|
||||
CryptoWarningHandler cryptoWarningHandler, Path filePath) {
|
||||
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
|
||||
|
||||
try {
|
||||
final Long lower = requestRange.getLeft().isEmpty() ? null : Long.valueOf(requestRange.getLeft());
|
||||
final Long upper = requestRange.getRight().isEmpty() ? null : Long.valueOf(requestRange.getRight());
|
||||
if (lower == null) {
|
||||
range = new ImmutablePair<Long, Long>(contentLength - upper, contentLength - 1);
|
||||
} else if (upper == null) {
|
||||
range = new ImmutablePair<Long, Long>(lower, contentLength - 1);
|
||||
} else {
|
||||
range = new ImmutablePair<Long, Long>(lower, Math.min(upper, contentLength - 1));
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid byte range: " + requestRange, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
assert Files.isRegularFile(filePath);
|
||||
assert contentLength != null;
|
||||
|
||||
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
|
||||
if (rangeLength <= 0 || range.getLeft() > contentLength - 1) {
|
||||
// unsatisfiable content range:
|
||||
outputContext.setContentLength(0);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength);
|
||||
LOG.debug("Requested content range unsatisfiable: " + getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
|
||||
return;
|
||||
} else {
|
||||
outputContext.setContentLength(rangeLength);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
|
||||
}
|
||||
|
||||
assert range.getLeft() > 0;
|
||||
assert range.getLeft() < contentLength;
|
||||
assert range.getRight() < contentLength;
|
||||
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ)) {
|
||||
if (outputContext.hasStream()) {
|
||||
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
|
||||
cryptor.decryptRange(c, outputContext.getOutputStream(), range.getLeft(), rangeLength, authenticate);
|
||||
outputContext.getOutputStream().flush();
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.trace("Unexpected end of stream during delivery of partial content (client hung up).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
|
||||
return String.format("bytes %d-%d/%d", firstByte, lastByte, completeLength);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
interface FileConstants {
|
||||
|
||||
/**
|
||||
* Number of bytes in the file header.
|
||||
*/
|
||||
long FILE_HEADER_LENGTH = 104;
|
||||
|
||||
/**
|
||||
* Allow range requests for files > 32MiB.
|
||||
*/
|
||||
long RANGE_REQUEST_LOWER_LIMIT = 32 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Maximum path length on some file systems or cloud storage providers is restricted.<br/>
|
||||
* Parent folder path uses up to 58 chars (sha256 -> 32 bytes base32 encoded to 56 bytes + two slashes). That in mind we don't want the total path to be longer than 255 chars.<br/>
|
||||
* 128 chars would be enought for up to 80 plaintext chars. Also we need up to 9 chars for our file extension. So lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
|
||||
*/
|
||||
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 137;
|
||||
|
||||
/**
|
||||
* Dummy file, on which file attributes can be stored for the root directory.
|
||||
*/
|
||||
String ROOT_FILE = "root";
|
||||
|
||||
/**
|
||||
* For encrypted directory names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String DIR_EXT = ".dir";
|
||||
|
||||
/**
|
||||
* For encrypted direcotry names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String LONG_DIR_EXT = ".lng.dir";
|
||||
|
||||
/**
|
||||
* For encrypted file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String FILE_EXT = ".file";
|
||||
|
||||
/**
|
||||
* For encrypted file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String LONG_FILE_EXT = ".lng.file";
|
||||
|
||||
/**
|
||||
* Length of prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
|
||||
*/
|
||||
int LONG_NAME_PREFIX_LENGTH = 8;
|
||||
|
||||
/**
|
||||
* Matches valid encrypted filenames (both normal and long filenames - see {@link #ENCRYPTED_FILENAME_LENGTH_LIMIT}).
|
||||
*/
|
||||
PathMatcher ENCRYPTED_FILE_MATCHER = new PathMatcher() {
|
||||
|
||||
private final Pattern BASIC_NAME_PATTERN = Pattern.compile("^[a-z2-7]+=*$", Pattern.CASE_INSENSITIVE);
|
||||
private final Pattern LONG_NAME_PATTERN = Pattern.compile("^[a-z2-7]{8}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@Override
|
||||
public boolean matches(Path path) {
|
||||
final String filename = path.getFileName().toString();
|
||||
if (StringUtils.endsWithIgnoreCase(filename, LONG_FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_FILE_EXT);
|
||||
return LONG_NAME_PATTERN.matcher(basename).matches();
|
||||
} else if (StringUtils.endsWithIgnoreCase(filename, FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, FILE_EXT);
|
||||
return BASIC_NAME_PATTERN.matcher(basename).matches();
|
||||
} else if (StringUtils.endsWithIgnoreCase(filename, LONG_DIR_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_DIR_EXT);
|
||||
return LONG_NAME_PATTERN.matcher(basename).matches();
|
||||
} else if (StringUtils.endsWithIgnoreCase(filename, DIR_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, DIR_EXT);
|
||||
return BASIC_NAME_PATTERN.matcher(basename).matches();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter to determine files of interest in encrypted directory. Based on {@link #ENCRYPTED_FILE_MATCHER}.
|
||||
*/
|
||||
Filter<Path> DIRECTORY_CONTENT_FILTER = new Filter<Path>() {
|
||||
@Override
|
||||
public boolean accept(Path entry) throws IOException {
|
||||
return ENCRYPTED_FILE_MATCHER.matches(entry);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
@@ -0,0 +1,234 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||
import org.apache.commons.collections4.map.LRUMap;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
class FilenameTranslator implements FileConstants {
|
||||
|
||||
private static final int MAX_CACHED_DIRECTORY_IDS = 5000;
|
||||
private static final int MAX_CACHED_METADATA_FILES = 1000;
|
||||
|
||||
private final Cryptor cryptor;
|
||||
private final Path dataRoot;
|
||||
private final Path metadataRoot;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final Map<Pair<Path, FileTime>, String> directoryIdCache = new LRUMap<>(MAX_CACHED_DIRECTORY_IDS); // <directoryFile, directoryId>
|
||||
private final Map<Pair<Path, FileTime>, LongFilenameMetadata> metadataCache = new LRUMap<>(MAX_CACHED_METADATA_FILES); // <metadataFile, metadata>
|
||||
|
||||
public FilenameTranslator(Cryptor cryptor, Path vaultRoot) {
|
||||
this.cryptor = cryptor;
|
||||
this.dataRoot = vaultRoot.resolve("d");
|
||||
this.metadataRoot = vaultRoot.resolve("m");
|
||||
}
|
||||
|
||||
/* file and directory name en/decryption */
|
||||
|
||||
public String getDirectoryId(Path directoryFile, boolean createIfNonexisting) throws IOException {
|
||||
try {
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(directoryFile, Files.getLastModifiedTime(directoryFile));
|
||||
String directoryId = directoryIdCache.get(key);
|
||||
if (directoryId == null) {
|
||||
directoryId = new String(readAllBytesAtomically(directoryFile), StandardCharsets.UTF_8);
|
||||
directoryIdCache.put(key, directoryId);
|
||||
}
|
||||
return directoryId;
|
||||
} catch (FileNotFoundException | NoSuchFileException e) {
|
||||
if (createIfNonexisting) {
|
||||
final String directoryId = UUID.randomUUID().toString();
|
||||
writeAllBytesAtomically(directoryFile, directoryId.getBytes(StandardCharsets.UTF_8));
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(directoryFile, Files.getLastModifiedTime(directoryFile));
|
||||
directoryIdCache.put(key, directoryId);
|
||||
return directoryId;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* to be called when a directory gets deleted, so the corresponding directory id is not longer cached.
|
||||
*/
|
||||
public void uncacheDirectoryId(Path directoryFile) throws IOException {
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(directoryFile, Files.getLastModifiedTime(directoryFile));
|
||||
directoryIdCache.remove(key);
|
||||
}
|
||||
|
||||
public Path getEncryptedDirectoryPath(String directoryId) {
|
||||
final String encrypted = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
|
||||
return dataRoot.resolve(encrypted);
|
||||
}
|
||||
|
||||
public String getEncryptedFilename(String cleartextFilename) throws IOException {
|
||||
return getEncryptedFilename(cleartextFilename, FILE_EXT, LONG_FILE_EXT);
|
||||
}
|
||||
|
||||
public String getEncryptedDirFileName(String cleartextDirName) throws IOException {
|
||||
return getEncryptedFilename(cleartextDirName, DIR_EXT, LONG_DIR_EXT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption will blow up the filename length due to aes block sizes, IVs and base32 encoding. The result may be too long for some old file systems.<br/>
|
||||
* This means that we need a workaround for filenames longer than the limit defined in {@link FileConstants#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
|
||||
* <br/>
|
||||
* For filenames longer than this limit we use a metadata file containing the full encrypted paths. For the actual filename a unique alternative is created by concatenating the metadata filename
|
||||
* and a unique id.
|
||||
*/
|
||||
private String getEncryptedFilename(String cleartextFilename, String basicExt, String longExt) throws IOException {
|
||||
final String ivAndCiphertext = cryptor.encryptFilename(cleartextFilename);
|
||||
if (ivAndCiphertext.length() + basicExt.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
|
||||
final String metadataGroup = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
|
||||
final String longFilename = metadataGroup + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + longExt;
|
||||
this.writeMetadata(metadataGroup, metadata);
|
||||
return longFilename;
|
||||
} else {
|
||||
return ivAndCiphertext + basicExt;
|
||||
}
|
||||
}
|
||||
|
||||
public String getCleartextFilename(String encryptedFilename) throws DecryptFailedException, IOException {
|
||||
final String ciphertext;
|
||||
if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_FILE_EXT);
|
||||
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
|
||||
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
|
||||
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
|
||||
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, FILE_EXT)) {
|
||||
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, FILE_EXT);
|
||||
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_DIR_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_DIR_EXT);
|
||||
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
|
||||
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
|
||||
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
|
||||
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, DIR_EXT)) {
|
||||
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, DIR_EXT);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported path component: " + encryptedFilename);
|
||||
}
|
||||
return cryptor.decryptFilename(ciphertext);
|
||||
}
|
||||
|
||||
/* Locked I/O */
|
||||
|
||||
private void writeAllBytesAtomically(Path path, byte[] bytes) throws IOException {
|
||||
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
|
||||
final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
|
||||
c.write(ByteBuffer.wrap(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] readAllBytesAtomically(Path path) throws IOException {
|
||||
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.DSYNC); final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
|
||||
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
|
||||
c.read(buffer);
|
||||
return buffer.array();
|
||||
}
|
||||
}
|
||||
|
||||
/* Long name metadata files */
|
||||
|
||||
private void writeMetadata(String metadataGroup, LongFilenameMetadata metadata) throws IOException {
|
||||
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
|
||||
Files.createDirectories(metadataDir);
|
||||
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
|
||||
|
||||
// evict previously cached entries:
|
||||
try {
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
|
||||
metadataCache.remove(key);
|
||||
} catch (FileNotFoundException | NoSuchFileException e) {
|
||||
// didn't exist yet? then we don't need to do anything anyway.
|
||||
}
|
||||
|
||||
// write:
|
||||
final byte[] metadataContent = objectMapper.writeValueAsBytes(metadata);
|
||||
writeAllBytesAtomically(metadataFile, metadataContent);
|
||||
|
||||
// add to cache:
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
|
||||
metadataCache.put(key, metadata);
|
||||
}
|
||||
|
||||
private LongFilenameMetadata readMetadata(String metadataGroup) throws IOException {
|
||||
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
|
||||
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
|
||||
try {
|
||||
// use cached metadata, if possible:
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
|
||||
LongFilenameMetadata metadata = metadataCache.get(key);
|
||||
// else read from filesystem:
|
||||
if (metadata == null) {
|
||||
final byte[] metadataContent = readAllBytesAtomically(metadataFile);
|
||||
metadata = objectMapper.readValue(metadataContent, LongFilenameMetadata.class);
|
||||
metadataCache.put(key, metadata);
|
||||
}
|
||||
return metadata;
|
||||
} catch (FileNotFoundException | NoSuchFileException e) {
|
||||
// not yet existing:
|
||||
return new LongFilenameMetadata();
|
||||
}
|
||||
}
|
||||
|
||||
private static class LongFilenameMetadata implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 6214509403824421320L;
|
||||
|
||||
@JsonDeserialize(as = DualHashBidiMap.class)
|
||||
private BidiMap<UUID, String> encryptedFilenames = new DualHashBidiMap<>();
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public synchronized String getEncryptedFilenameForUUID(final UUID uuid) {
|
||||
return encryptedFilenames.get(uuid);
|
||||
}
|
||||
|
||||
public synchronized UUID getOrCreateUuidForEncryptedFilename(String encryptedFilename) {
|
||||
UUID uuid = encryptedFilenames.getKey(encryptedFilename);
|
||||
if (uuid == null) {
|
||||
uuid = UUID.randomUUID();
|
||||
encryptedFilenames.put(uuid, encryptedFilename);
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
// used by jackson
|
||||
@SuppressWarnings("unused")
|
||||
public BidiMap<UUID, String> getEncryptedFilenames() {
|
||||
return encryptedFilenames;
|
||||
}
|
||||
|
||||
// used by jackson
|
||||
@SuppressWarnings("unused")
|
||||
public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
|
||||
this.encryptedFilenames = encryptedFilenames;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import org.apache.jackrabbit.webdav.property.AbstractDavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
@@ -6,25 +6,27 @@
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.io.InputContext;
|
||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.property.DavProperty;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.jackrabbit.CryptoResourceFactory.NonExistingParentException;
|
||||
|
||||
public class NonExistingNode extends AbstractEncryptedNode {
|
||||
class NonExistingNode extends AbstractEncryptedNode {
|
||||
|
||||
public NonExistingNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -34,7 +36,12 @@ public class NonExistingNode extends AbstractEncryptedNode {
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getModificationTime() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,8 +65,40 @@ public class NonExistingNode extends AbstractEncryptedNode {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineProperties() {
|
||||
// do nothing.
|
||||
public void move(AbstractEncryptedNode destination) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(AbstractEncryptedNode destination, boolean shallow) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(DavProperty<?> property) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return lazily resolved file path, e.g. needed during MOVE operations.
|
||||
*/
|
||||
public Path materializeFilePath() {
|
||||
try {
|
||||
return factory.getEncryptedFilePath(locator.getResourcePath(), true);
|
||||
} catch (NonExistingParentException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return lazily resolved directory file path, e.g. needed during MOVE operations.
|
||||
*/
|
||||
public Path materializeDirFilePath() {
|
||||
try {
|
||||
return factory.getEncryptedDirectoryFilePath(locator.getResourcePath(), true);
|
||||
} catch (NonExistingParentException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.channels.NonReadableChannelException;
|
||||
import java.nio.channels.NonWritableChannelException;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Instances of this class wrap a file lock, that is created upon construction and destroyed by {@link #close()}.
|
||||
*
|
||||
* If the construction fails (e.g. if the file system does not support locks) no exception will be thrown and no lock is created.
|
||||
*/
|
||||
class SilentlyFailingFileLock implements AutoCloseable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SilentlyFailingFileLock.class);
|
||||
|
||||
private final FileLock lock;
|
||||
|
||||
/**
|
||||
* Invokes #SilentlyFailingFileLock(FileChannel, long, long, boolean) with a position of 0 and a size of {@link Long#MAX_VALUE}.
|
||||
*/
|
||||
SilentlyFailingFileLock(FileChannel channel, boolean shared) {
|
||||
this(channel, 0L, Long.MAX_VALUE, shared);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NonReadableChannelException If shared is true this channel was not opened for reading
|
||||
* @throws NonWritableChannelException If shared is false but this channel was not opened for writing
|
||||
* @see FileChannel#lock(long, long, boolean)
|
||||
*/
|
||||
SilentlyFailingFileLock(FileChannel channel, long position, long size, boolean shared) {
|
||||
FileLock lock = null;
|
||||
try {
|
||||
lock = channel.tryLock(position, size, shared);
|
||||
} catch (IOException | OverlappingFileLockException e) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.trace("Unable to lock file.");
|
||||
}
|
||||
} finally {
|
||||
this.lock = lock;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (lock != null) {
|
||||
lock.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,45 +8,50 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavLocatorFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavSessionProvider;
|
||||
import org.apache.jackrabbit.webdav.WebdavRequest;
|
||||
import org.apache.jackrabbit.webdav.WebdavResponse;
|
||||
import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class WebDavServlet extends AbstractWebdavServlet {
|
||||
|
||||
private static final long serialVersionUID = 7965170007048673022L;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebDavServlet.class);
|
||||
public static final String CFG_FS_ROOT = "cfg.fs.root";
|
||||
public static final String CFG_HTTP_ROOT = "cfg.http.root";
|
||||
public static final String CFG_CHECK_FILE_INTEGRITY = "cfg.checkFileIntegrity";
|
||||
private DavSessionProvider davSessionProvider;
|
||||
private DavLocatorFactory davLocatorFactory;
|
||||
private DavResourceFactory davResourceFactory;
|
||||
private final Cryptor cryptor;
|
||||
private final CryptoWarningHandler cryptoWarningHandler;
|
||||
|
||||
public WebDavServlet(final Cryptor cryptor) {
|
||||
public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
|
||||
super();
|
||||
this.cryptor = cryptor;
|
||||
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection, whitelistedResourceCollection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ServletConfig config) throws ServletException {
|
||||
super.init(config);
|
||||
|
||||
davSessionProvider = new DavSessionProviderImpl();
|
||||
|
||||
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
|
||||
final String httpRoot = config.getInitParameter(CFG_HTTP_ROOT);
|
||||
final boolean checkFileIntegrity = Boolean.parseBoolean(config.getInitParameter(CFG_CHECK_FILE_INTEGRITY));
|
||||
this.davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, httpRoot, cryptor);
|
||||
|
||||
this.davResourceFactory = new DavResourceFactoryImpl(cryptor, checkFileIntegrity);
|
||||
davSessionProvider = new DavSessionProviderImpl();
|
||||
davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath());
|
||||
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, fsRoot);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -84,4 +89,30 @@ public class WebDavServlet extends AbstractWebdavServlet {
|
||||
this.davResourceFactory = resourceFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPut(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
|
||||
long t0 = System.nanoTime();
|
||||
super.doPut(request, response, resource);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
long t1 = System.nanoTime();
|
||||
LOG.trace("PUT TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
|
||||
long t0 = System.nanoTime();
|
||||
try {
|
||||
super.doGet(request, response, resource);
|
||||
} catch (MacAuthenticationFailedException e) {
|
||||
LOG.warn("File integrity violation for " + resource.getLocator().getResourcePath());
|
||||
cryptoWarningHandler.macAuthFailed(resource.getLocator().getResourcePath());
|
||||
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
||||
}
|
||||
if (LOG.isDebugEnabled()) {
|
||||
long t1 = System.nanoTime();
|
||||
LOG.trace("GET TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.io.InputContext;
|
||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.ResourceType;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.exceptions.DavRuntimeException;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class EncryptedDir extends AbstractEncryptedNode {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
|
||||
|
||||
public EncryptedDir(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
if (resource.isCollection()) {
|
||||
this.addMemberDir(resource, inputContext);
|
||||
} else {
|
||||
this.addMemberFile(resource, inputContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException {
|
||||
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
|
||||
try {
|
||||
Files.createDirectories(childPath);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create subdirectory.", e);
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException {
|
||||
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
|
||||
SeekableByteChannel channel = null;
|
||||
try {
|
||||
channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
|
||||
cryptor.encryptFile(inputContext.getInputStream(), channel);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create file.", e);
|
||||
throw new IORuntimeException(e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(channel);
|
||||
IOUtils.closeQuietly(inputContext.getInputStream());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
final Path dir = ResourcePathUtils.getPhysicalPath(this);
|
||||
try {
|
||||
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
|
||||
final List<DavResource> result = new ArrayList<>();
|
||||
|
||||
for (final Path childPath : directoryStream) {
|
||||
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), childPath.toString(), false);
|
||||
final DavResource resource = factory.createResource(childLocator, session);
|
||||
result.add(resource);
|
||||
}
|
||||
return new DavResourceIteratorImpl(result);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Exception during getMembers.", e);
|
||||
throw new IORuntimeException(e);
|
||||
} catch (DavException e) {
|
||||
LOG.error("Exception during getMembers.", e);
|
||||
throw new DavRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
|
||||
try {
|
||||
Files.walkFileTree(memberPath, new DeletingFileVisitor());
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineProperties() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
properties.add(new ResourceType(ResourceType.COLLECTION));
|
||||
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
|
||||
if (Files.exists(path)) {
|
||||
try {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + path.toString(), e);
|
||||
// don't add any further properties
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all files and folders, it visits.
|
||||
*/
|
||||
private static class DeletingFileVisitor extends SimpleFileVisitor<Path> {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
|
||||
if (attributes.isRegularFile()) {
|
||||
Files.delete(file);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
|
||||
LOG.error("Failed to delete file " + file.toString(), exc);
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.io.InputContext;
|
||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpHeaderValue;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class EncryptedFile extends AbstractEncryptedNode {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
|
||||
|
||||
protected final boolean checkIntegrity;
|
||||
|
||||
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, boolean checkIntegrity) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
this.checkIntegrity = checkIntegrity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
throw new UnsupportedOperationException("Can not add member to file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
throw new UnsupportedOperationException("Can not list members of file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
throw new UnsupportedOperationException("Can not remove member to file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.exists(path)) {
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
|
||||
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
|
||||
SeekableByteChannel channel = null;
|
||||
try {
|
||||
channel = Files.newByteChannel(path, StandardOpenOption.READ);
|
||||
if (checkIntegrity && !cryptor.authenticateContent(channel)) {
|
||||
throw new DecryptFailedException("File content compromised: " + path.toString());
|
||||
}
|
||||
outputContext.setContentLength(cryptor.decryptedContentLength(channel));
|
||||
if (outputContext.hasStream()) {
|
||||
cryptor.decryptedFile(channel, outputContext.getOutputStream());
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
LOG.warn("Unexpected end of stream (possibly client hung up).");
|
||||
} catch (DecryptFailedException e) {
|
||||
throw new IOException("Error decrypting file " + path.toString(), e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineProperties() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.exists(path)) {
|
||||
SeekableByteChannel channel = null;
|
||||
try {
|
||||
channel = Files.newByteChannel(path, StandardOpenOption.READ);
|
||||
final Long contentLength = cryptor.decryptedContentLength(channel);
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
|
||||
|
||||
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + path.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.MutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletRequest;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Delivers only the requested range of bytes from a file.
|
||||
*
|
||||
* @see {@link https://tools.ietf.org/html/rfc7233#section-4}
|
||||
*/
|
||||
public class EncryptedFilePart extends EncryptedFile {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
|
||||
private static final String BYTE_UNIT_PREFIX = "bytes=";
|
||||
private static final char RANGE_SET_SEP = ',';
|
||||
private static final char RANGE_SEP = '-';
|
||||
|
||||
/**
|
||||
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
|
||||
*/
|
||||
private static final Long SUFFIX_BYTE_RANGE_LOWER = -1L;
|
||||
|
||||
/**
|
||||
* e.g. range 500- (gets all bytes from 500) -> (500, MAX_LONG)
|
||||
*/
|
||||
private static final Long SUFFIX_BYTE_RANGE_UPPER = Long.MAX_VALUE;
|
||||
|
||||
private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
|
||||
|
||||
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, boolean checkIntegrity) {
|
||||
super(factory, locator, session, lockManager, cryptor, checkIntegrity);
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
if (rangeHeader == null) {
|
||||
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
|
||||
}
|
||||
determineByteRanges(rangeHeader);
|
||||
}
|
||||
|
||||
private void determineByteRanges(String rangeHeader) {
|
||||
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, BYTE_UNIT_PREFIX);
|
||||
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
|
||||
if (byteRanges.length == 0) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
for (final String byteRange : byteRanges) {
|
||||
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
|
||||
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
final Long lower = bytePos[0].isEmpty() ? SUFFIX_BYTE_RANGE_LOWER : Long.valueOf(bytePos[0]);
|
||||
final Long upper = bytePos[1].isEmpty() ? SUFFIX_BYTE_RANGE_UPPER : Long.valueOf(bytePos[1]);
|
||||
if (lower > upper) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
requestedContentRanges.add(new ImmutablePair<Long, Long>(lower, upper));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return One range, that spans all requested ranges.
|
||||
*/
|
||||
private Pair<Long, Long> getUnionRange(Long fileSize) {
|
||||
final long lastByte = fileSize - 1;
|
||||
final MutablePair<Long, Long> result = new MutablePair<Long, Long>();
|
||||
for (Pair<Long, Long> range : requestedContentRanges) {
|
||||
final long left;
|
||||
final long right;
|
||||
if (SUFFIX_BYTE_RANGE_LOWER.equals(range.getLeft())) {
|
||||
left = lastByte - range.getRight();
|
||||
right = lastByte;
|
||||
} else if (SUFFIX_BYTE_RANGE_UPPER.equals(range.getRight())) {
|
||||
left = range.getLeft();
|
||||
right = lastByte;
|
||||
} else {
|
||||
left = range.getLeft();
|
||||
right = range.getRight();
|
||||
}
|
||||
if (result.getLeft() == null || left < result.getLeft()) {
|
||||
result.setLeft(left);
|
||||
}
|
||||
if (result.getRight() == null || right > result.getRight()) {
|
||||
result.setRight(right);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.exists(path)) {
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
|
||||
SeekableByteChannel channel = null;
|
||||
try {
|
||||
channel = Files.newByteChannel(path, StandardOpenOption.READ);
|
||||
if (checkIntegrity && !cryptor.authenticateContent(channel)) {
|
||||
throw new DecryptFailedException("File content compromised: " + path.toString());
|
||||
}
|
||||
final Long fileSize = cryptor.decryptedContentLength(channel);
|
||||
final Pair<Long, Long> range = getUnionRange(fileSize);
|
||||
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
||||
outputContext.setContentLength(rangeLength);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
|
||||
if (outputContext.hasStream()) {
|
||||
cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
|
||||
}
|
||||
} catch (DecryptFailedException e) {
|
||||
throw new IOException("Error decrypting file " + path.toString(), e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
|
||||
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
|
||||
public final class ResourcePathUtils {
|
||||
|
||||
private ResourcePathUtils() {
|
||||
throw new IllegalStateException("not instantiable");
|
||||
}
|
||||
|
||||
public static Path getPhysicalPath(DavResource resource) {
|
||||
return getPhysicalPath(resource.getLocator());
|
||||
}
|
||||
|
||||
public static Path getPhysicalPath(DavResourceLocator locator) {
|
||||
return FileSystems.getDefault().getPath(locator.getRepositoryPath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinTask;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.apache.commons.httpclient.HttpClient;
|
||||
import org.apache.commons.httpclient.HttpMethod;
|
||||
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
|
||||
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
|
||||
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
|
||||
import org.apache.commons.httpclient.methods.GetMethod;
|
||||
import org.apache.commons.httpclient.methods.PutMethod;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.cryptomator.crypto.aes256.Aes256Cryptor;
|
||||
import org.cryptomator.webdav.WebDavServer;
|
||||
import org.cryptomator.webdav.WebDavServer.ServletLifeCycleAdapter;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
|
||||
public class RangeRequestTest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RangeRequestTest.class);
|
||||
private static final Aes256Cryptor CRYPTOR = new Aes256Cryptor();
|
||||
private static final WebDavServer SERVER = new WebDavServer();
|
||||
private static final File TMP_VAULT = Files.createTempDir();
|
||||
private static ServletLifeCycleAdapter SERVLET;
|
||||
private static URI VAULT_BASE_URI;
|
||||
|
||||
@BeforeClass
|
||||
public static void startServer() throws URISyntaxException {
|
||||
SERVER.start();
|
||||
SERVLET = SERVER.createServlet(TMP_VAULT.toPath(), CRYPTOR, new ArrayList<String>(), new ArrayList<String>(), "JUnitTestVault");
|
||||
SERVLET.start();
|
||||
VAULT_BASE_URI = new URI("http", SERVLET.getServletUri().getSchemeSpecificPart() + "/", null);
|
||||
Assert.assertTrue(SERVLET.isRunning());
|
||||
Assert.assertNotNull(VAULT_BASE_URI);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void stopServer() {
|
||||
SERVLET.stop();
|
||||
SERVER.stop();
|
||||
FileUtils.deleteQuietly(TMP_VAULT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFullFileDecryption() throws IOException, URISyntaxException {
|
||||
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "fullFileDecryptionTestFile.txt");
|
||||
final HttpClient client = new HttpClient();
|
||||
|
||||
// prepare 64MiB test data:
|
||||
final byte[] plaintextData = new byte[16777216 * Integer.BYTES];
|
||||
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
|
||||
for (int i = 0; i < 16777216; i++) {
|
||||
bbIn.putInt(i);
|
||||
}
|
||||
final InputStream plaintextDataInputStream = new ByteArrayInputStream(plaintextData);
|
||||
|
||||
// put request:
|
||||
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
|
||||
putMethod.setRequestEntity(new ByteArrayRequestEntity(plaintextData));
|
||||
final int putResponse = client.executeMethod(putMethod);
|
||||
putMethod.releaseConnection();
|
||||
Assert.assertEquals(201, putResponse);
|
||||
|
||||
// get request:
|
||||
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
|
||||
final int statusCode = client.executeMethod(getMethod);
|
||||
Assert.assertEquals(200, statusCode);
|
||||
// final byte[] received = new byte[plaintextData.length];
|
||||
// IOUtils.read(getMethod.getResponseBodyAsStream(), received);
|
||||
// Assert.assertArrayEquals(plaintextData, received);
|
||||
Assert.assertTrue(IOUtils.contentEquals(plaintextDataInputStream, getMethod.getResponseBodyAsStream()));
|
||||
getMethod.releaseConnection();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncRangeRequests() throws IOException, URISyntaxException, InterruptedException {
|
||||
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "asyncRangeRequestTestFile.txt");
|
||||
|
||||
final MultiThreadedHttpConnectionManager cm = new MultiThreadedHttpConnectionManager();
|
||||
cm.getParams().setDefaultMaxConnectionsPerHost(50);
|
||||
final HttpClient client = new HttpClient(cm);
|
||||
|
||||
// prepare 8MiB test data:
|
||||
final byte[] plaintextData = new byte[2097152 * Integer.BYTES];
|
||||
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
|
||||
for (int i = 0; i < 2097152; i++) {
|
||||
bbIn.putInt(i);
|
||||
}
|
||||
|
||||
// put request:
|
||||
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
|
||||
putMethod.setRequestEntity(new ByteArrayRequestEntity(plaintextData));
|
||||
final int putResponse = client.executeMethod(putMethod);
|
||||
putMethod.releaseConnection();
|
||||
Assert.assertEquals(201, putResponse);
|
||||
|
||||
// multiple async range requests:
|
||||
final List<ForkJoinTask<?>> tasks = new ArrayList<>();
|
||||
final Random generator = new Random(System.currentTimeMillis());
|
||||
|
||||
final AtomicBoolean success = new AtomicBoolean(true);
|
||||
|
||||
// 10 full interrupted requests:
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
|
||||
try {
|
||||
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
|
||||
final int statusCode = client.executeMethod(getMethod);
|
||||
if (statusCode != 200) {
|
||||
LOG.error("Invalid status code for interrupted full request");
|
||||
success.set(false);
|
||||
}
|
||||
getMethod.getResponseBodyAsStream().read();
|
||||
getMethod.getResponseBodyAsStream().close();
|
||||
getMethod.releaseConnection();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
tasks.add(task);
|
||||
}
|
||||
|
||||
// 50 crappy interrupted range requests:
|
||||
for (int i = 0; i < 50; i++) {
|
||||
final int lower = generator.nextInt(plaintextData.length);
|
||||
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
|
||||
try {
|
||||
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
|
||||
getMethod.addRequestHeader("Range", "bytes=" + lower + "-");
|
||||
final int statusCode = client.executeMethod(getMethod);
|
||||
if (statusCode != 206) {
|
||||
LOG.error("Invalid status code for interrupted range request");
|
||||
success.set(false);
|
||||
}
|
||||
getMethod.getResponseBodyAsStream().read();
|
||||
getMethod.getResponseBodyAsStream().close();
|
||||
getMethod.releaseConnection();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
tasks.add(task);
|
||||
}
|
||||
|
||||
// 50 normal open range requests:
|
||||
for (int i = 0; i < 50; i++) {
|
||||
final int lower = generator.nextInt(plaintextData.length - 512);
|
||||
final int upper = plaintextData.length - 1;
|
||||
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
|
||||
try {
|
||||
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
|
||||
getMethod.addRequestHeader("Range", "bytes=" + lower + "-");
|
||||
final byte[] expected = Arrays.copyOfRange(plaintextData, lower, upper + 1);
|
||||
final int statusCode = client.executeMethod(getMethod);
|
||||
final byte[] responseBody = new byte[upper - lower + 10];
|
||||
final int bytesRead = IOUtils.read(getMethod.getResponseBodyAsStream(), responseBody);
|
||||
getMethod.releaseConnection();
|
||||
if (statusCode != 206) {
|
||||
LOG.error("Invalid status code for open range request");
|
||||
success.set(false);
|
||||
} else if (upper - lower + 1 != bytesRead) {
|
||||
LOG.error("Invalid response length for open range request");
|
||||
success.set(false);
|
||||
} else if (!Arrays.equals(expected, Arrays.copyOfRange(responseBody, 0, bytesRead))) {
|
||||
LOG.error("Invalid response body for open range request");
|
||||
success.set(false);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
tasks.add(task);
|
||||
}
|
||||
|
||||
// 200 normal closed range requests:
|
||||
for (int i = 0; i < 200; i++) {
|
||||
final int pos1 = generator.nextInt(plaintextData.length - 512);
|
||||
final int pos2 = pos1 + 512;
|
||||
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
|
||||
try {
|
||||
final int lower = Math.min(pos1, pos2);
|
||||
final int upper = Math.max(pos1, pos2);
|
||||
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
|
||||
getMethod.addRequestHeader("Range", "bytes=" + lower + "-" + upper);
|
||||
final byte[] expected = Arrays.copyOfRange(plaintextData, lower, upper + 1);
|
||||
final int statusCode = client.executeMethod(getMethod);
|
||||
final byte[] responseBody = new byte[upper - lower + 1];
|
||||
final int bytesRead = IOUtils.read(getMethod.getResponseBodyAsStream(), responseBody);
|
||||
getMethod.releaseConnection();
|
||||
if (statusCode != 206) {
|
||||
LOG.error("Invalid status code for closed range request");
|
||||
success.set(false);
|
||||
} else if (upper - lower + 1 != bytesRead) {
|
||||
LOG.error("Invalid response length for closed range request");
|
||||
success.set(false);
|
||||
} else if (!Arrays.equals(expected, Arrays.copyOfRange(responseBody, 0, bytesRead))) {
|
||||
LOG.error("Invalid response body for closed range request");
|
||||
success.set(false);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
tasks.add(task);
|
||||
}
|
||||
|
||||
Collections.shuffle(tasks, generator);
|
||||
|
||||
final ForkJoinPool pool = new ForkJoinPool(4);
|
||||
for (ForkJoinTask<?> task : tasks) {
|
||||
pool.execute(task);
|
||||
}
|
||||
for (ForkJoinTask<?> task : tasks) {
|
||||
task.join();
|
||||
}
|
||||
pool.shutdown();
|
||||
cm.shutdown();
|
||||
|
||||
Assert.assertTrue(success.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnsatisfiableRangeRequest() throws IOException, URISyntaxException {
|
||||
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "unsatisfiableRangeRequestTestFile.txt");
|
||||
final HttpClient client = new HttpClient();
|
||||
|
||||
// prepare file content:
|
||||
final byte[] fileContent = "This is some test file content.".getBytes();
|
||||
|
||||
// put request:
|
||||
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
|
||||
putMethod.setRequestEntity(new ByteArrayRequestEntity(fileContent));
|
||||
final int putResponse = client.executeMethod(putMethod);
|
||||
putMethod.releaseConnection();
|
||||
Assert.assertEquals(201, putResponse);
|
||||
|
||||
// get request:
|
||||
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
|
||||
getMethod.addRequestHeader("Range", "chunks=1-2");
|
||||
final int getResponse = client.executeMethod(getMethod);
|
||||
final byte[] response = new byte[fileContent.length];
|
||||
IOUtils.read(getMethod.getResponseBodyAsStream(), response);
|
||||
getMethod.releaseConnection();
|
||||
Assert.assertEquals(416, getResponse);
|
||||
Assert.assertArrayEquals(fileContent, response);
|
||||
}
|
||||
|
||||
}
|
||||
33
main/core/src/test/resources/log4j2.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
Copyright (c) 2014 Markus Kreusch
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - log4j config for WebDAV unit tests
|
||||
-->
|
||||
<Configuration status="WARN">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||
</Console>
|
||||
<Console name="StdErr" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<!-- show our own debug messages: -->
|
||||
<Logger name="org.cryptomator" level="DEBUG" />
|
||||
<!-- mute dependencies: -->
|
||||
<Root level="INFO">
|
||||
<AppenderRef ref="Console" />
|
||||
<AppenderRef ref="StdErr" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>crypto-aes</artifactId>
|
||||
<name>Cryptomator cryptographic module (AES)</name>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
|
||||
interface AesCryptographicConfiguration {
|
||||
|
||||
/**
|
||||
@@ -26,7 +29,7 @@ interface AesCryptographicConfiguration {
|
||||
int SCRYPT_BLOCK_SIZE = 8;
|
||||
|
||||
/**
|
||||
* Number of bytes of the master key. Should be the maximum possible AES key length to provide best security.
|
||||
* Preferred number of bytes of the master key.
|
||||
*/
|
||||
int PREF_MASTER_KEY_LENGTH_IN_BITS = 256;
|
||||
|
||||
@@ -60,18 +63,18 @@ interface AesCryptographicConfiguration {
|
||||
String AES_KEYWRAP_CIPHER = "AESWrap";
|
||||
|
||||
/**
|
||||
* Cipher specs for file name and file content encryption. Using CTR-mode for random access.
|
||||
* Cipher specs for file content encryption. Using CTR-mode for random access.<br/>
|
||||
*
|
||||
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
|
||||
*/
|
||||
String AES_CTR_CIPHER = "AES/CTR/NoPadding";
|
||||
|
||||
/**
|
||||
* Cipher specs for single block encryption (like file size).
|
||||
* Cipher specs for file header encryption (fixed-length block cipher).<br/>
|
||||
*
|
||||
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl
|
||||
*/
|
||||
String AES_ECB_CIPHER = "AES/ECB/PKCS5Padding";
|
||||
String AES_CBC_CIPHER = "AES/CBC/PKCS5Padding";
|
||||
|
||||
/**
|
||||
* AES block size is 128 bit or 16 bytes.
|
||||
@@ -79,10 +82,13 @@ interface AesCryptographicConfiguration {
|
||||
int AES_BLOCK_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Number of non-zero bytes in the IV used for file name encryption. Less means shorter encrypted filenames, more means higher entropy.
|
||||
* Maximum length is {@value #AES_BLOCK_LENGTH}. Even the shortest base32 (see {@link FileNamingConventions#ENCRYPTED_FILENAME_CODEC})
|
||||
* encoded byte array will need 8 chars. The maximum number of bytes that fit in 8 base32 chars is 5. Thus 5 is the ideal length.
|
||||
* Number of bytes, a content block over which a MAC is calculated consists of.
|
||||
*/
|
||||
int FILE_NAME_IV_LENGTH = 5;
|
||||
int CONTENT_MAC_BLOCK = 32 * 1024;
|
||||
|
||||
/**
|
||||
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
|
||||
*/
|
||||
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.bouncycastle.crypto.BlockCipher;
|
||||
import org.bouncycastle.crypto.CipherParameters;
|
||||
import org.bouncycastle.crypto.Mac;
|
||||
import org.bouncycastle.crypto.engines.AESFastEngine;
|
||||
import org.bouncycastle.crypto.macs.CMac;
|
||||
import org.bouncycastle.crypto.paddings.ISO7816d4Padding;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
|
||||
/**
|
||||
* Implements the RFC 5297 SIV mode.
|
||||
*/
|
||||
final class AesSivCipherUtil {
|
||||
|
||||
private static final byte[] BYTES_ZERO = new byte[16];
|
||||
private static final byte DOUBLING_CONST = (byte) 0x87;
|
||||
|
||||
static byte[] sivEncrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) {
|
||||
final byte[] aesKeyBytes = aesKey.getEncoded();
|
||||
final byte[] macKeyBytes = macKey.getEncoded();
|
||||
if (aesKeyBytes == null || macKeyBytes == null) {
|
||||
throw new IllegalArgumentException("Can't get bytes of given key.");
|
||||
}
|
||||
try {
|
||||
return sivEncrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
|
||||
} catch (InvalidKeyException ex) {
|
||||
throw new IllegalArgumentException(ex);
|
||||
} finally {
|
||||
Arrays.fill(aesKeyBytes, (byte) 0);
|
||||
Arrays.fill(macKeyBytes, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
static byte[] sivEncrypt(byte[] aesKey, byte[] macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException {
|
||||
if (aesKey.length != 16 && aesKey.length != 24 && aesKey.length != 32) {
|
||||
throw new InvalidKeyException("Invalid aesKey length " + aesKey.length);
|
||||
}
|
||||
|
||||
final byte[] iv = s2v(macKey, plaintext, additionalData);
|
||||
|
||||
final int numBlocks = (plaintext.length + 15) / 16;
|
||||
|
||||
// clear out the 31st and 63rd (rightmost) bit:
|
||||
final byte[] ctr = Arrays.copyOf(iv, 16);
|
||||
ctr[8] = (byte) (ctr[8] & 0x7F);
|
||||
ctr[12] = (byte) (ctr[12] & 0x7F);
|
||||
final ByteBuffer ctrBuf = ByteBuffer.wrap(ctr);
|
||||
final long initialCtrVal = ctrBuf.getLong(8);
|
||||
|
||||
final byte[] x = new byte[numBlocks * 16];
|
||||
final BlockCipher aes = new AESFastEngine();
|
||||
aes.init(true, new KeyParameter(aesKey));
|
||||
for (int i = 0; i < numBlocks; i++) {
|
||||
final long ctrVal = initialCtrVal + i;
|
||||
ctrBuf.putLong(8, ctrVal);
|
||||
aes.processBlock(ctrBuf.array(), 0, x, i * 16);
|
||||
aes.reset();
|
||||
}
|
||||
|
||||
final byte[] ciphertext = xor(plaintext, x);
|
||||
|
||||
return ArrayUtils.addAll(iv, ciphertext);
|
||||
}
|
||||
|
||||
static byte[] sivDecrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws DecryptFailedException {
|
||||
final byte[] aesKeyBytes = aesKey.getEncoded();
|
||||
final byte[] macKeyBytes = macKey.getEncoded();
|
||||
if (aesKeyBytes == null || macKeyBytes == null) {
|
||||
throw new IllegalArgumentException("Can't get bytes of given key.");
|
||||
}
|
||||
try {
|
||||
return sivDecrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
|
||||
} catch (InvalidKeyException ex) {
|
||||
throw new IllegalArgumentException(ex);
|
||||
} finally {
|
||||
Arrays.fill(aesKeyBytes, (byte) 0);
|
||||
Arrays.fill(macKeyBytes, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
static byte[] sivDecrypt(byte[] aesKey, byte[] macKey, byte[] ciphertext, byte[]... additionalData) throws DecryptFailedException, InvalidKeyException {
|
||||
if (aesKey.length != 16 && aesKey.length != 24 && aesKey.length != 32) {
|
||||
throw new InvalidKeyException("Invalid aesKey length " + aesKey.length);
|
||||
}
|
||||
|
||||
final byte[] iv = Arrays.copyOf(ciphertext, 16);
|
||||
|
||||
final byte[] actualCiphertext = Arrays.copyOfRange(ciphertext, 16, ciphertext.length);
|
||||
final int numBlocks = (actualCiphertext.length + 15) / 16;
|
||||
|
||||
// clear out the 31st and 63rd (rightmost) bit:
|
||||
final byte[] ctr = Arrays.copyOf(iv, 16);
|
||||
ctr[8] = (byte) (ctr[8] & 0x7F);
|
||||
ctr[12] = (byte) (ctr[12] & 0x7F);
|
||||
final ByteBuffer ctrBuf = ByteBuffer.wrap(ctr);
|
||||
final long initialCtrVal = ctrBuf.getLong(8);
|
||||
|
||||
final byte[] x = new byte[numBlocks * 16];
|
||||
final BlockCipher aes = new AESFastEngine();
|
||||
aes.init(true, new KeyParameter(aesKey));
|
||||
for (int i = 0; i < numBlocks; i++) {
|
||||
final long ctrVal = initialCtrVal + i;
|
||||
ctrBuf.putLong(8, ctrVal);
|
||||
aes.processBlock(ctrBuf.array(), 0, x, i * 16);
|
||||
aes.reset();
|
||||
}
|
||||
|
||||
final byte[] plaintext = xor(actualCiphertext, x);
|
||||
|
||||
final byte[] control = s2v(macKey, plaintext, additionalData);
|
||||
|
||||
if (MessageDigest.isEqual(control, iv)) {
|
||||
return plaintext;
|
||||
} else {
|
||||
throw new DecryptFailedException("Authentication failed");
|
||||
}
|
||||
}
|
||||
|
||||
static byte[] s2v(byte[] macKey, byte[] plaintext, byte[]... additionalData) {
|
||||
final CipherParameters params = new KeyParameter(macKey);
|
||||
final BlockCipher aes = new AESFastEngine();
|
||||
final CMac mac = new CMac(aes);
|
||||
mac.init(params);
|
||||
|
||||
byte[] d = mac(mac, BYTES_ZERO);
|
||||
|
||||
for (byte[] s : additionalData) {
|
||||
d = xor(dbl(d), mac(mac, s));
|
||||
}
|
||||
|
||||
final byte[] t;
|
||||
if (plaintext.length >= 16) {
|
||||
t = xorend(plaintext, d);
|
||||
} else {
|
||||
t = xor(dbl(d), pad(plaintext));
|
||||
}
|
||||
|
||||
return mac(mac, t);
|
||||
}
|
||||
|
||||
private static byte[] mac(Mac mac, byte[] in) {
|
||||
byte[] result = new byte[mac.getMacSize()];
|
||||
mac.update(in, 0, in.length);
|
||||
mac.doFinal(result, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* First bit 1, following bits 0.
|
||||
*/
|
||||
private static byte[] pad(byte[] in) {
|
||||
final byte[] result = Arrays.copyOf(in, 16);
|
||||
new ISO7816d4Padding().addPadding(result, in.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Code taken from {@link org.bouncycastle.crypto.macs.CMac}
|
||||
*/
|
||||
private static int shiftLeft(byte[] block, byte[] output) {
|
||||
int i = block.length;
|
||||
int bit = 0;
|
||||
while (--i >= 0) {
|
||||
int b = block[i] & 0xff;
|
||||
output[i] = (byte) ((b << 1) | bit);
|
||||
bit = (b >>> 7) & 1;
|
||||
}
|
||||
return bit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Code taken from {@link org.bouncycastle.crypto.macs.CMac}
|
||||
*/
|
||||
private static byte[] dbl(byte[] in) {
|
||||
byte[] ret = new byte[in.length];
|
||||
int carry = shiftLeft(in, ret);
|
||||
int xor = 0xff & DOUBLING_CONST;
|
||||
|
||||
/*
|
||||
* NOTE: This construction is an attempt at a constant-time implementation.
|
||||
*/
|
||||
ret[in.length - 1] ^= (xor >>> ((1 - carry) << 3));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static byte[] xor(byte[] in1, byte[] in2) {
|
||||
if (in1 == null || in2 == null || in1.length > in2.length) {
|
||||
throw new IllegalArgumentException("Length of first input must be <= length of second input.");
|
||||
}
|
||||
|
||||
final byte[] result = new byte[in1.length];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = (byte) (in1[i] ^ in2[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] xorend(byte[] in1, byte[] in2) {
|
||||
if (in1 == null || in2 == null || in1.length < in2.length) {
|
||||
throw new IllegalArgumentException("Length of first input must be >= length of second input.");
|
||||
}
|
||||
|
||||
final byte[] result = Arrays.copyOf(in1, in1.length);
|
||||
final int diff = in1.length - in2.length;
|
||||
for (int i = 0; i < in2.length; i++) {
|
||||
result[i + diff] = (byte) (result[i + diff] ^ in2[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
class BlocksData {
|
||||
|
||||
public static final int MAX_NUM_BLOCKS = 128;
|
||||
|
||||
final ByteBuffer buffer;
|
||||
final long startBlockNum;
|
||||
final int numBlocks;
|
||||
|
||||
BlocksData(ByteBuffer buffer, long startBlockNum, int numBlocks) {
|
||||
if (numBlocks > MAX_NUM_BLOCKS) {
|
||||
throw new IllegalArgumentException("Too many blocks to process at once: " + numBlocks);
|
||||
}
|
||||
this.buffer = buffer;
|
||||
this.startBlockNum = startBlockNum;
|
||||
this.numBlocks = numBlocks;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.CryptingException;
|
||||
|
||||
abstract class CryptoWorker implements Callable<Void> {
|
||||
|
||||
static final BlocksData POISON = new BlocksData(ByteBuffer.allocate(0), -1L, 0);
|
||||
|
||||
final Lock lock;
|
||||
final Condition blockDone;
|
||||
final AtomicLong currentBlock;
|
||||
final BlockingQueue<BlocksData> queue;
|
||||
|
||||
public CryptoWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue) {
|
||||
this.lock = lock;
|
||||
this.blockDone = blockDone;
|
||||
this.currentBlock = currentBlock;
|
||||
this.queue = queue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Void call() throws IOException {
|
||||
try {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
final BlocksData blocksData = queue.take();
|
||||
if (blocksData == POISON) {
|
||||
// put poison back in for other threads:
|
||||
break;
|
||||
}
|
||||
final ByteBuffer processedBytes = this.process(blocksData);
|
||||
lock.lock();
|
||||
try {
|
||||
while (currentBlock.get() != blocksData.startBlockNum) {
|
||||
blockDone.await();
|
||||
}
|
||||
assert currentBlock.get() == blocksData.startBlockNum;
|
||||
// yay, its my turn!
|
||||
this.write(processedBytes);
|
||||
// signal worker working on next block:
|
||||
currentBlock.set(blocksData.startBlockNum + blocksData.numBlocks);
|
||||
blockDone.signalAll();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract ByteBuffer process(BlocksData block) throws CryptingException;
|
||||
|
||||
protected abstract void write(ByteBuffer processedBytes) throws IOException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CompletionService;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorCompletionService;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class CryptoWorkerExecutor {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CryptoWorkerExecutor.class);
|
||||
|
||||
private final int numWorkers;
|
||||
private final Lock lock;
|
||||
private final Condition blockDone;
|
||||
private final AtomicLong currentBlock;
|
||||
private final BlockingQueue<BlocksData> inputQueue;
|
||||
private final ExecutorService executorService;
|
||||
private final CompletionService<Void> completionService;
|
||||
private boolean acceptWork;
|
||||
|
||||
/**
|
||||
* Starts as many {@link CryptoWorker} as specified in the constructor, that start working immediately on the items submitted via {@link #offer(BlocksData, long, TimeUnit)}.
|
||||
*/
|
||||
public CryptoWorkerExecutor(int numWorkers, WorkerFactory workerFactory) {
|
||||
this.numWorkers = numWorkers;
|
||||
this.lock = new ReentrantLock();
|
||||
this.blockDone = lock.newCondition();
|
||||
this.currentBlock = new AtomicLong();
|
||||
this.inputQueue = new LinkedBlockingQueue<>(numWorkers * 2); // one cycle read-ahead
|
||||
this.executorService = Executors.newFixedThreadPool(numWorkers);
|
||||
this.completionService = new ExecutorCompletionService<>(executorService);
|
||||
this.acceptWork = true;
|
||||
|
||||
// start workers:
|
||||
for (int i = 0; i < numWorkers; i++) {
|
||||
final CryptoWorker worker = workerFactory.createWorker(lock, blockDone, currentBlock, inputQueue);
|
||||
completionService.submit(worker);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds work to the work queue. On timeout all workers will be shut down.
|
||||
*
|
||||
* @see BlockingQueue#offer(Object, long, TimeUnit)
|
||||
* @return <code>true</code> if the work has been added in time. <code>false</code> in any other case.
|
||||
*/
|
||||
public boolean offer(BlocksData data, long timeout, TimeUnit unit) {
|
||||
if (!acceptWork) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
final boolean success = inputQueue.offer(data, timeout, unit);
|
||||
if (!success) {
|
||||
this.acceptWork = false;
|
||||
inputQueue.clear();
|
||||
poisonWorkers();
|
||||
}
|
||||
return success;
|
||||
} catch (InterruptedException e) {
|
||||
LOG.error("Interrupted thread.", e);
|
||||
executorService.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown of this executor, waiting for all jobs to finish (normally or by throwing exceptions).
|
||||
*
|
||||
* @throws ExecutionException If any of the workers failed.
|
||||
*/
|
||||
public void waitUntilDone() throws ExecutionException {
|
||||
this.acceptWork = false;
|
||||
try {
|
||||
poisonWorkers();
|
||||
// now workers will one after another finish their work, potentially throwing an ExecutionException:
|
||||
for (int i = 0; i < numWorkers; i++) {
|
||||
completionService.take().get();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOG.error("Interrupted thread.", e);
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
// shutdown either after normal decryption or if ANY worker threw an exception:
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private void poisonWorkers() throws InterruptedException {
|
||||
// add enough poison for each worker:
|
||||
for (int i = 0; i < numWorkers; i++) {
|
||||
inputQueue.put(CryptoWorker.POISON);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface WorkerFactory {
|
||||
CryptoWorker createWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> inputQueue);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.Mac;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.CryptingException;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
||||
|
||||
abstract class DecryptWorker extends CryptoWorker implements AesCryptographicConfiguration {
|
||||
|
||||
private final boolean shouldAuthenticate;
|
||||
private final WritableByteChannel out;
|
||||
|
||||
public DecryptWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue, boolean shouldAuthenticate, WritableByteChannel out) {
|
||||
super(lock, blockDone, currentBlock, queue);
|
||||
this.shouldAuthenticate = shouldAuthenticate;
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ByteBuffer process(BlocksData data) throws CryptingException {
|
||||
final Cipher cipher = initCipher(data.startBlockNum);
|
||||
final Mac mac = initMac();
|
||||
|
||||
final ByteBuffer plaintextBuf = ByteBuffer.allocate(cipher.getOutputSize(CONTENT_MAC_BLOCK) * data.numBlocks);
|
||||
|
||||
final ByteBuffer ciphertextBuf = data.buffer.asReadOnlyBuffer();
|
||||
final ByteBuffer macBuf = data.buffer.asReadOnlyBuffer();
|
||||
|
||||
for (long blockNum = data.startBlockNum; blockNum < data.startBlockNum + data.numBlocks; blockNum++) {
|
||||
assert (blockNum - data.startBlockNum) < BlocksData.MAX_NUM_BLOCKS;
|
||||
assert (blockNum - data.startBlockNum) * CONTENT_MAC_BLOCK < Integer.MAX_VALUE;
|
||||
final int pos = (int) (blockNum - data.startBlockNum) * (CONTENT_MAC_BLOCK + mac.getMacLength());
|
||||
ciphertextBuf.limit(Math.min(data.buffer.limit() - mac.getMacLength(), pos + CONTENT_MAC_BLOCK));
|
||||
ciphertextBuf.position(pos);
|
||||
try {
|
||||
macBuf.limit(ciphertextBuf.limit() + mac.getMacLength());
|
||||
macBuf.position(ciphertextBuf.limit());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new DecryptFailedException("Invalid file content, missing MAC.");
|
||||
}
|
||||
if (shouldAuthenticate) {
|
||||
checkMac(mac, blockNum, ciphertextBuf, macBuf);
|
||||
}
|
||||
ciphertextBuf.position(pos);
|
||||
decrypt(cipher, ciphertextBuf, plaintextBuf);
|
||||
}
|
||||
|
||||
plaintextBuf.flip();
|
||||
return plaintextBuf;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void write(ByteBuffer processedBytes) throws IOException {
|
||||
out.write(processedBytes);
|
||||
}
|
||||
|
||||
protected abstract Cipher initCipher(long startBlockNum);
|
||||
|
||||
protected abstract Mac initMac();
|
||||
|
||||
protected abstract void checkMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf, ByteBuffer macBuf) throws MacAuthenticationFailedException;
|
||||
|
||||
protected abstract void decrypt(Cipher cipher, ByteBuffer ciphertextBuf, ByteBuffer plaintextBuf) throws DecryptFailedException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.Mac;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.CryptingException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
|
||||
abstract class EncryptWorker extends CryptoWorker implements AesCryptographicConfiguration {
|
||||
|
||||
private final WritableByteChannel out;
|
||||
|
||||
public EncryptWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue, WritableByteChannel out) {
|
||||
super(lock, blockDone, currentBlock, queue);
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ByteBuffer process(BlocksData data) throws CryptingException {
|
||||
final Cipher cipher = initCipher(data.startBlockNum);
|
||||
final Mac mac = initMac();
|
||||
|
||||
final ByteBuffer ciphertextBuf = ByteBuffer.allocate((cipher.getOutputSize(CONTENT_MAC_BLOCK) + mac.getMacLength()) * data.numBlocks);
|
||||
final ByteBuffer plaintextBuf = data.buffer.asReadOnlyBuffer();
|
||||
|
||||
for (long blockNum = data.startBlockNum; blockNum < data.startBlockNum + data.numBlocks; blockNum++) {
|
||||
final int pos = (int) (blockNum - data.startBlockNum) * CONTENT_MAC_BLOCK;
|
||||
plaintextBuf.limit(Math.min(data.buffer.limit(), pos + CONTENT_MAC_BLOCK));
|
||||
encrypt(cipher, plaintextBuf, ciphertextBuf);
|
||||
final ByteBuffer toMac = ciphertextBuf.asReadOnlyBuffer();
|
||||
toMac.limit(ciphertextBuf.position());
|
||||
toMac.position((int) (blockNum - data.startBlockNum) * (CONTENT_MAC_BLOCK + mac.getMacLength()));
|
||||
ciphertextBuf.put(calcMac(mac, blockNum, toMac));
|
||||
}
|
||||
|
||||
ciphertextBuf.flip();
|
||||
return ciphertextBuf;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void write(ByteBuffer processedBytes) throws IOException {
|
||||
out.write(processedBytes);
|
||||
}
|
||||
|
||||
protected abstract Cipher initCipher(long startBlockNum);
|
||||
|
||||
protected abstract Mac initMac();
|
||||
|
||||
protected abstract byte[] calcMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf);
|
||||
|
||||
protected abstract void encrypt(Cipher cipher, ByteBuffer plaintextBuf, ByteBuffer ciphertextBuf) throws EncryptFailedException;
|
||||
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.PathMatcher;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
|
||||
interface FileNamingConventions {
|
||||
|
||||
/**
|
||||
* Extension of masterkey files inside the root directory of the encrypted storage.
|
||||
*/
|
||||
String MASTERKEY_FILE_EXT = ".masterkey.json";
|
||||
|
||||
/**
|
||||
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
|
||||
*/
|
||||
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
|
||||
|
||||
/**
|
||||
* Maximum length possible on file systems with a filename limit of 255 chars.<br/>
|
||||
* Also we would need a few chars for our file extension, so lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
|
||||
*/
|
||||
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 250;
|
||||
|
||||
/**
|
||||
* For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String BASIC_FILE_EXT = ".aes";
|
||||
|
||||
/**
|
||||
* Prefix in front of the actual encrypted file name used as IV.
|
||||
*/
|
||||
String IV_PREFIX_SEPARATOR = "_";
|
||||
|
||||
/**
|
||||
* For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String LONG_NAME_FILE_EXT = ".lng.aes";
|
||||
|
||||
/**
|
||||
* Prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
|
||||
*/
|
||||
String LONG_NAME_PREFIX_SEPARATOR = "_";
|
||||
|
||||
/**
|
||||
* For metadata files for a certain group of files. The cryptor may decide what files to assign to the same group; hopefully using some
|
||||
* kind of uniform distribution for better load balancing.
|
||||
*/
|
||||
String METADATA_FILE_EXT = ".meta";
|
||||
|
||||
/**
|
||||
* Matches both, {@value #BASIC_FILE_EXT} and {@value #LONG_NAME_FILE_EXT} files.
|
||||
*/
|
||||
PathMatcher ENCRYPTED_FILE_GLOB_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**/*{" + BASIC_FILE_EXT + "," + LONG_NAME_FILE_EXT + "}");
|
||||
|
||||
}
|
||||
@@ -4,10 +4,13 @@ import java.io.Serializable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
|
||||
@JsonPropertyOrder(value = {"scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
|
||||
@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
|
||||
public class KeyFile implements Serializable {
|
||||
|
||||
static final Integer CURRENT_VERSION = 2;
|
||||
private static final long serialVersionUID = 8578363158959619885L;
|
||||
|
||||
private Integer version;
|
||||
private byte[] scryptSalt;
|
||||
private int scryptCostParam;
|
||||
private int scryptBlockSize;
|
||||
@@ -15,6 +18,14 @@ public class KeyFile implements Serializable {
|
||||
private byte[] primaryMasterKey;
|
||||
private byte[] hMacMasterKey;
|
||||
|
||||
public Integer getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(Integer version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public byte[] getScryptSalt() {
|
||||
return scryptSalt;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class LengthLimitingOutputStream extends FilterOutputStream {
|
||||
|
||||
private final long limit;
|
||||
private volatile long bytesWritten;
|
||||
|
||||
public LengthLimitingOutputStream(OutputStream out, long limit) {
|
||||
super(out);
|
||||
this.limit = limit;
|
||||
this.bytesWritten = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
if (bytesWritten < limit) {
|
||||
out.write(b);
|
||||
increaseNumberOfWrittenBytes(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
final long bytesAvailable = limit - bytesWritten;
|
||||
final int adjustedLen = (int) Math.min(len, bytesAvailable);
|
||||
if (adjustedLen > 0) {
|
||||
out.write(b, off, adjustedLen);
|
||||
increaseNumberOfWrittenBytes(adjustedLen);
|
||||
}
|
||||
}
|
||||
|
||||
public long getBytesWritten() {
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
private void increaseNumberOfWrittenBytes(int amount) throws IOException {
|
||||
bytesWritten += amount;
|
||||
if (bytesWritten >= limit) {
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
/**
|
||||
* Not thread-safe!
|
||||
*/
|
||||
public class LengthObfuscatingInputStream extends FilterInputStream {
|
||||
|
||||
private final byte[] padding;
|
||||
private int paddingLength = -1;
|
||||
private long inputBytesRead = 0;
|
||||
private int paddingBytesRead = 0;
|
||||
|
||||
LengthObfuscatingInputStream(InputStream in, byte[] padding) {
|
||||
super(in);
|
||||
this.padding = padding;
|
||||
}
|
||||
|
||||
long getRealInputLength() {
|
||||
return inputBytesRead;
|
||||
}
|
||||
|
||||
private void choosePaddingLengthOnce() {
|
||||
if (paddingLength == -1) {
|
||||
long upperBound = Math.min(Math.max(inputBytesRead / 10, 4096), 16 * 1024 * 1024); // 10% of original bytes (at least 4KiB), but not more than 16MiBs
|
||||
paddingLength = (int) (Math.random() * upperBound);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
final int b = in.read();
|
||||
if (b != -1) {
|
||||
// stream available:
|
||||
inputBytesRead++;
|
||||
return b;
|
||||
} else {
|
||||
choosePaddingLengthOnce();
|
||||
return readFromPadding();
|
||||
}
|
||||
}
|
||||
|
||||
private int readFromPadding() {
|
||||
if (paddingLength == -1) {
|
||||
throw new IllegalStateException("No padding length chosen yet.");
|
||||
}
|
||||
|
||||
if (paddingBytesRead < paddingLength) {
|
||||
// padding available:
|
||||
return padding[paddingBytesRead++ % padding.length];
|
||||
} else {
|
||||
// end of stream AND padding
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
final int bytesRead = IOUtils.read(in, b, off, len); // 0 on EOF
|
||||
inputBytesRead += bytesRead;
|
||||
|
||||
if (bytesRead == len) {
|
||||
return bytesRead;
|
||||
} else if (bytesRead < len) {
|
||||
choosePaddingLengthOnce();
|
||||
final int additionalBytesNeeded = len - bytesRead;
|
||||
final int additionalBytesRead = readFromPadding(b, off + bytesRead, additionalBytesNeeded);
|
||||
return (bytesRead == 0 && additionalBytesRead == 0) ? -1 : bytesRead + additionalBytesRead;
|
||||
} else {
|
||||
// bytesRead > len:
|
||||
throw new IllegalStateException("Read more bytes than requested.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bytes read from padding (0, if fully read)
|
||||
*/
|
||||
private int readFromPadding(byte[] b, int off, int len) {
|
||||
if (len < 0) {
|
||||
throw new IllegalArgumentException("Length must not be negative");
|
||||
}
|
||||
if (paddingLength == -1) {
|
||||
throw new IllegalStateException("No padding length chosen yet.");
|
||||
}
|
||||
|
||||
final int remainingPadding = paddingLength - paddingBytesRead;
|
||||
if (remainingPadding > len) {
|
||||
// padding available:
|
||||
for (int i = 0; i < len; i++) {
|
||||
b[off + i] = padding[paddingBytesRead++ % padding.length];
|
||||
}
|
||||
return len;
|
||||
} else {
|
||||
// partly available:
|
||||
for (int i = 0; i < remainingPadding; i++) {
|
||||
b[off + i] = padding[paddingBytesRead++ % padding.length];
|
||||
}
|
||||
return remainingPadding;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
throw new IOException("Skip not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
if (paddingLength == -1) {
|
||||
// EOF not yet reached; delegate original stream to answer this rather complicated question:
|
||||
return in.available();
|
||||
} else {
|
||||
// EOF already reached, read from remaining padding:
|
||||
return paddingLength - paddingBytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
class LongFilenameMetadata implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 6214509403824421320L;
|
||||
|
||||
@JsonDeserialize(as = DualHashBidiMap.class)
|
||||
private BidiMap<UUID, String> encryptedFilenames = new DualHashBidiMap<>();
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public synchronized String getEncryptedFilenameForUUID(final UUID uuid) {
|
||||
return encryptedFilenames.get(uuid);
|
||||
}
|
||||
|
||||
public synchronized UUID getOrCreateUuidForEncryptedFilename(String encryptedFilename) {
|
||||
UUID uuid = encryptedFilenames.getKey(encryptedFilename);
|
||||
if (uuid == null) {
|
||||
uuid = UUID.randomUUID();
|
||||
encryptedFilenames.put(uuid, encryptedFilename);
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public BidiMap<UUID, String> getEncryptedFilenames() {
|
||||
return encryptedFilenames;
|
||||
}
|
||||
|
||||
public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
|
||||
this.encryptedFilenames = encryptedFilenames;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
||||
/**
|
||||
* Updates a {@link Mac} with the bytes read from this stream.
|
||||
*/
|
||||
class MacInputStream extends FilterInputStream {
|
||||
|
||||
private final Mac mac;
|
||||
|
||||
/**
|
||||
* @param in Stream from which to read contents, which will update the Mac.
|
||||
* @param mac Mac to be updated during writes.
|
||||
*/
|
||||
public MacInputStream(InputStream in, Mac mac) {
|
||||
super(in);
|
||||
this.mac = mac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int b = in.read();
|
||||
mac.update((byte) b);
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int read = in.read(b, off, len);
|
||||
mac.update(b, off, len);
|
||||
return read;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
||||
/**
|
||||
* Updates a {@link Mac} with the bytes written to this stream.
|
||||
*/
|
||||
class MacOutputStream extends FilterOutputStream {
|
||||
|
||||
private final Mac mac;
|
||||
|
||||
/**
|
||||
* @param out Stream to redirect contents to after updating the mac.
|
||||
* @param mac Mac to be updated during writes.
|
||||
*/
|
||||
public MacOutputStream(OutputStream out, Mac mac) {
|
||||
super(out);
|
||||
this.mac = mac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
mac.update((byte) b);
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
mac.update(b, off, len);
|
||||
out.write(b, off, len);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,31 +15,29 @@ import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class Aes256CryptorTest {
|
||||
|
||||
private static final Random TEST_PRNG = new Random();
|
||||
|
||||
@Test
|
||||
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException {
|
||||
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
cryptor.destroy();
|
||||
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor();
|
||||
final InputStream in = new ByteArrayInputStream(out.toByteArray());
|
||||
decryptor.decryptMasterKey(in, pw);
|
||||
|
||||
@@ -48,17 +46,17 @@ public class Aes256CryptorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
cryptor.destroy();
|
||||
IOUtils.closeQuietly(out);
|
||||
|
||||
// all these passwords are expected to fail.
|
||||
final String[] wrongPws = {"a", "as", "asdf", "sdf", "das", "dsa", "foo", "bar", "baz"};
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor();
|
||||
for (final String wrongPw : wrongPws) {
|
||||
final InputStream in = new ByteArrayInputStream(out.toByteArray());
|
||||
try {
|
||||
@@ -72,17 +70,17 @@ public class Aes256CryptorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntegrityAuthentication() throws IOException {
|
||||
@Test(expected = DecryptFailedException.class)
|
||||
public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = "Hello World".getBytes();
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
|
||||
// init cryptor:
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096);
|
||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||
IOUtils.closeQuietly(plaintextIn);
|
||||
@@ -90,11 +88,6 @@ public class Aes256CryptorTest {
|
||||
|
||||
encryptedData.position(0);
|
||||
|
||||
// authenticate unmodified content:
|
||||
final SeekableByteChannel encryptedIn1 = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
final boolean isContentUnmodified1 = cryptor.authenticateContent(encryptedIn1);
|
||||
Assert.assertTrue(isContentUnmodified1);
|
||||
|
||||
// toggle one bit inf first content byte:
|
||||
encryptedData.position(64);
|
||||
final byte fifthByte = encryptedData.get();
|
||||
@@ -103,23 +96,23 @@ public class Aes256CryptorTest {
|
||||
|
||||
encryptedData.position(0);
|
||||
|
||||
// authenticate modified content:
|
||||
final SeekableByteChannel encryptedIn2 = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
final boolean isContentUnmodified2 = cryptor.authenticateContent(encryptedIn2);
|
||||
Assert.assertFalse(isContentUnmodified2);
|
||||
// decrypt modified content (should fail with DecryptFailedException):
|
||||
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
||||
cryptor.decryptFile(encryptedIn, plaintextOut, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = "Hello World".getBytes();
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
|
||||
// init cryptor:
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096 + 32); // header + content + maximum possible size obfuscation padding + 32 bytes mac (per each 32k)
|
||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||
IOUtils.closeQuietly(plaintextIn);
|
||||
@@ -134,7 +127,7 @@ public class Aes256CryptorTest {
|
||||
|
||||
// decrypt:
|
||||
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
||||
final Long numDecryptedBytes = cryptor.decryptedFile(encryptedIn, plaintextOut);
|
||||
final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut, true);
|
||||
IOUtils.closeQuietly(encryptedIn);
|
||||
IOUtils.closeQuietly(plaintextOut);
|
||||
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
|
||||
@@ -145,20 +138,20 @@ public class Aes256CryptorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
|
||||
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
|
||||
// 8MiB test plaintext data:
|
||||
final byte[] plaintextData = new byte[2097152 * Integer.BYTES];
|
||||
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
|
||||
for (int i = 0; i < 65536; i++) {
|
||||
for (int i = 0; i < 2097152; i++) {
|
||||
bbIn.putInt(i);
|
||||
}
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
|
||||
// init cryptor:
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate((int) (104 + plaintextData.length * 1.2));
|
||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||
IOUtils.closeQuietly(plaintextIn);
|
||||
@@ -169,54 +162,43 @@ public class Aes256CryptorTest {
|
||||
// decrypt:
|
||||
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
||||
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 25000 * Integer.BYTES, 30000 * Integer.BYTES);
|
||||
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 260000 * Integer.BYTES, 4000 * Integer.BYTES, true);
|
||||
IOUtils.closeQuietly(encryptedIn);
|
||||
IOUtils.closeQuietly(plaintextOut);
|
||||
Assert.assertTrue(numDecryptedBytes > 0);
|
||||
|
||||
// check decrypted data:
|
||||
final byte[] result = plaintextOut.toByteArray();
|
||||
final byte[] expected = Arrays.copyOfRange(plaintextData, 25000 * Integer.BYTES, 55000 * Integer.BYTES);
|
||||
final byte[] expected = Arrays.copyOfRange(plaintextData, 260000 * Integer.BYTES, 264000 * Integer.BYTES);
|
||||
Assert.assertArrayEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionOfFilenames() throws IOException {
|
||||
final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
public void testEncryptionOfFilenames() throws IOException, DecryptFailedException {
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// short path components
|
||||
// directory paths
|
||||
final String originalPath1 = "foo/bar/baz";
|
||||
final String encryptedPath1a = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
|
||||
final String encryptedPath1b = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
|
||||
final String encryptedPath1a = cryptor.encryptDirectoryPath(originalPath1, "/");
|
||||
final String encryptedPath1b = cryptor.encryptDirectoryPath(originalPath1, "/");
|
||||
Assert.assertEquals(encryptedPath1a, encryptedPath1b);
|
||||
final String decryptedPath1 = cryptor.decryptPath(encryptedPath1a, '/', '/', ioSupportMock);
|
||||
Assert.assertEquals(originalPath1, decryptedPath1);
|
||||
|
||||
// long path components
|
||||
// long file names
|
||||
final String str50chars = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
|
||||
final String originalPath2 = "foo/" + str50chars + str50chars + str50chars + str50chars + str50chars + "/baz";
|
||||
final String encryptedPath2a = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
|
||||
final String encryptedPath2b = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
|
||||
final String originalPath2 = str50chars + str50chars + str50chars + str50chars + str50chars + "_isLongerThan255Chars.txt";
|
||||
final String encryptedPath2a = cryptor.encryptFilename(originalPath2);
|
||||
final String encryptedPath2b = cryptor.encryptFilename(originalPath2);
|
||||
Assert.assertEquals(encryptedPath2a, encryptedPath2b);
|
||||
final String decryptedPath2 = cryptor.decryptPath(encryptedPath2a, '/', '/', ioSupportMock);
|
||||
final String decryptedPath2 = cryptor.decryptFilename(encryptedPath2a);
|
||||
Assert.assertEquals(originalPath2, decryptedPath2);
|
||||
}
|
||||
|
||||
private static class CryptoIOSupportMock implements CryptorIOSupport {
|
||||
|
||||
private final Map<String, byte[]> map = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) {
|
||||
map.put(encryptedPath, encryptedMetadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readPathSpecificMetadata(String encryptedPath) {
|
||||
return map.get(encryptedPath);
|
||||
}
|
||||
|
||||
// block size length file names
|
||||
final String originalPath3 = "aaaabbbbccccdddd";
|
||||
final String encryptedPath3a = cryptor.encryptFilename(originalPath3);
|
||||
final String encryptedPath3b = cryptor.encryptFilename(originalPath3);
|
||||
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
|
||||
final String decryptedPath3 = cryptor.decryptFilename(encryptedPath3a);
|
||||
Assert.assertEquals(originalPath3, decryptedPath3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Official RFC 5297 test vector taken from https://tools.ietf.org/html/rfc5297#appendix-A.1
|
||||
*/
|
||||
public class AesSivCipherUtilTest {
|
||||
|
||||
@Test
|
||||
public void testS2v() throws DecoderException {
|
||||
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
|
||||
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
|
||||
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
|
||||
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
|
||||
|
||||
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
|
||||
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
|
||||
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
|
||||
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
|
||||
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
|
||||
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
|
||||
|
||||
final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
|
||||
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
|
||||
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
|
||||
(byte) 0xdd, (byte) 0xee};
|
||||
|
||||
final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
|
||||
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
|
||||
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
|
||||
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93};
|
||||
|
||||
final byte[] result = AesSivCipherUtil.s2v(macKey, plaintext, ad);
|
||||
Assert.assertArrayEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSivEncrypt() throws InvalidKeyException {
|
||||
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
|
||||
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
|
||||
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
|
||||
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
|
||||
|
||||
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
|
||||
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
|
||||
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
|
||||
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff};
|
||||
|
||||
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
|
||||
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
|
||||
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
|
||||
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
|
||||
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
|
||||
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
|
||||
|
||||
final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
|
||||
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
|
||||
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
|
||||
(byte) 0xdd, (byte) 0xee};
|
||||
|
||||
final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
|
||||
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
|
||||
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
|
||||
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
|
||||
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
|
||||
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
|
||||
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
|
||||
(byte) 0xfe, (byte) 0x5c};
|
||||
|
||||
final byte[] result = AesSivCipherUtil.sivEncrypt(aesKey, macKey, plaintext, ad);
|
||||
Assert.assertArrayEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSivDecrypt() throws DecryptFailedException, InvalidKeyException {
|
||||
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
|
||||
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
|
||||
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
|
||||
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
|
||||
|
||||
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
|
||||
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
|
||||
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
|
||||
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff};
|
||||
|
||||
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
|
||||
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
|
||||
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
|
||||
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
|
||||
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
|
||||
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
|
||||
|
||||
final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
|
||||
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
|
||||
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
|
||||
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
|
||||
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
|
||||
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
|
||||
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
|
||||
(byte) 0xfe, (byte) 0x5c};
|
||||
|
||||
final byte[] expected = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
|
||||
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
|
||||
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
|
||||
(byte) 0xdd, (byte) 0xee};
|
||||
|
||||
final byte[] result = AesSivCipherUtil.sivDecrypt(aesKey, macKey, ciphertext, ad);
|
||||
Assert.assertArrayEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test(expected = DecryptFailedException.class)
|
||||
public void testSivDecryptWithInvalidKey() throws DecryptFailedException, InvalidKeyException {
|
||||
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
|
||||
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
|
||||
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
|
||||
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
|
||||
|
||||
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
|
||||
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
|
||||
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
|
||||
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0x00};
|
||||
|
||||
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
|
||||
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
|
||||
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
|
||||
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
|
||||
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
|
||||
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
|
||||
|
||||
final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
|
||||
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
|
||||
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
|
||||
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
|
||||
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
|
||||
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
|
||||
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
|
||||
(byte) 0xfe, (byte) 0x5c};
|
||||
|
||||
final byte[] expected = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
|
||||
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
|
||||
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
|
||||
(byte) 0xdd, (byte) 0xee};
|
||||
|
||||
final byte[] result = AesSivCipherUtil.sivDecrypt(aesKey, macKey, ciphertext, ad);
|
||||
Assert.assertArrayEquals(expected, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* https://tools.ietf.org/html/rfc5297#appendix-A.2
|
||||
*/
|
||||
@Test
|
||||
public void testNonceBasedAuthenticatedEncryption() throws InvalidKeyException {
|
||||
final byte[] macKey = {(byte) 0x7f, (byte) 0x7e, (byte) 0x7d, (byte) 0x7c, //
|
||||
(byte) 0x7b, (byte) 0x7a, (byte) 0x79, (byte) 0x78, //
|
||||
(byte) 0x77, (byte) 0x76, (byte) 0x75, (byte) 0x74, //
|
||||
(byte) 0x73, (byte) 0x72, (byte) 0x71, (byte) 0x70};
|
||||
|
||||
final byte[] aesKey = {(byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, //
|
||||
(byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, //
|
||||
(byte) 0x48, (byte) 0x49, (byte) 0x4a, (byte) 0x4b, //
|
||||
(byte) 0x4c, (byte) 0x4d, (byte) 0x4e, (byte) 0x4f};
|
||||
|
||||
final byte[] ad1 = {(byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33, //
|
||||
(byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, //
|
||||
(byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb, //
|
||||
(byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff, //
|
||||
(byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, //
|
||||
(byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, //
|
||||
(byte) 0xff, (byte) 0xee, (byte) 0xdd, (byte) 0xcc, //
|
||||
(byte) 0xbb, (byte) 0xaa, (byte) 0x99, (byte) 0x88, //
|
||||
(byte) 0x77, (byte) 0x66, (byte) 0x55, (byte) 0x44, //
|
||||
(byte) 0x33, (byte) 0x22, (byte) 0x11, (byte) 0x00};
|
||||
|
||||
final byte[] ad2 = {(byte) 0x10, (byte) 0x20, (byte) 0x30, (byte) 0x40, //
|
||||
(byte) 0x50, (byte) 0x60, (byte) 0x70, (byte) 0x80, //
|
||||
(byte) 0x90, (byte) 0xa0};
|
||||
|
||||
final byte[] nonce = {(byte) 0x09, (byte) 0xf9, (byte) 0x11, (byte) 0x02, //
|
||||
(byte) 0x9d, (byte) 0x74, (byte) 0xe3, (byte) 0x5b, //
|
||||
(byte) 0xd8, (byte) 0x41, (byte) 0x56, (byte) 0xc5, //
|
||||
(byte) 0x63, (byte) 0x56, (byte) 0x88, (byte) 0xc0};
|
||||
|
||||
final byte[] plaintext = {(byte) 0x74, (byte) 0x68, (byte) 0x69, (byte) 0x73, //
|
||||
(byte) 0x20, (byte) 0x69, (byte) 0x73, (byte) 0x20, //
|
||||
(byte) 0x73, (byte) 0x6f, (byte) 0x6d, (byte) 0x65, //
|
||||
(byte) 0x20, (byte) 0x70, (byte) 0x6c, (byte) 0x61, //
|
||||
(byte) 0x69, (byte) 0x6e, (byte) 0x74, (byte) 0x65, //
|
||||
(byte) 0x78, (byte) 0x74, (byte) 0x20, (byte) 0x74, //
|
||||
(byte) 0x6f, (byte) 0x20, (byte) 0x65, (byte) 0x6e, //
|
||||
(byte) 0x63, (byte) 0x72, (byte) 0x79, (byte) 0x70, //
|
||||
(byte) 0x74, (byte) 0x20, (byte) 0x75, (byte) 0x73, //
|
||||
(byte) 0x69, (byte) 0x6e, (byte) 0x67, (byte) 0x20, //
|
||||
(byte) 0x53, (byte) 0x49, (byte) 0x56, (byte) 0x2d, //
|
||||
(byte) 0x41, (byte) 0x45, (byte) 0x53};
|
||||
|
||||
final byte[] result = AesSivCipherUtil.sivEncrypt(aesKey, macKey, plaintext, ad1, ad2, nonce);
|
||||
|
||||
final byte[] expected = {(byte) 0x7b, (byte) 0xdb, (byte) 0x6e, (byte) 0x3b, //
|
||||
(byte) 0x43, (byte) 0x26, (byte) 0x67, (byte) 0xeb, //
|
||||
(byte) 0x06, (byte) 0xf4, (byte) 0xd1, (byte) 0x4b, //
|
||||
(byte) 0xff, (byte) 0x2f, (byte) 0xbd, (byte) 0x0f, //
|
||||
(byte) 0xcb, (byte) 0x90, (byte) 0x0f, (byte) 0x2f, //
|
||||
(byte) 0xdd, (byte) 0xbe, (byte) 0x40, (byte) 0x43, //
|
||||
(byte) 0x26, (byte) 0x60, (byte) 0x19, (byte) 0x65, //
|
||||
(byte) 0xc8, (byte) 0x89, (byte) 0xbf, (byte) 0x17, //
|
||||
(byte) 0xdb, (byte) 0xa7, (byte) 0x7c, (byte) 0xeb, //
|
||||
(byte) 0x09, (byte) 0x4f, (byte) 0xa6, (byte) 0x63, //
|
||||
(byte) 0xb7, (byte) 0xa3, (byte) 0xf7, (byte) 0x48, //
|
||||
(byte) 0xba, (byte) 0x8a, (byte) 0xf8, (byte) 0x29, //
|
||||
(byte) 0xea, (byte) 0x64, (byte) 0xad, (byte) 0x54, //
|
||||
(byte) 0x4a, (byte) 0x27, (byte) 0x2e, (byte) 0x9c, //
|
||||
(byte) 0x48, (byte) 0x5b, (byte) 0x62, (byte) 0xa3, //
|
||||
(byte) 0xfd, (byte) 0x5c, (byte) 0x0d};
|
||||
|
||||
Assert.assertArrayEquals(expected, result);
|
||||
|
||||
}
|
||||
}
|
||||
33
main/crypto-aes/src/test/resources/log4j2.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
Copyright (c) 2014 Markus Kreusch
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - log4j config for WebDAV unit tests
|
||||
-->
|
||||
<Configuration status="WARN">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||
</Console>
|
||||
<Console name="StdErr" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<!-- show our own debug messages: -->
|
||||
<Logger name="org.cryptomator" level="DEBUG" />
|
||||
<!-- mute dependencies: -->
|
||||
<Root level="INFO">
|
||||
<AppenderRef ref="Console" />
|
||||
<AppenderRef ref="StdErr" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
@@ -12,12 +12,13 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>crypto-api</artifactId>
|
||||
<name>Cryptomator cryptographic module API</name>
|
||||
|
||||
<dependencies>
|
||||
<!-- commons -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
@@ -26,5 +27,9 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,38 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class AbstractCryptor implements Cryptor {
|
||||
|
||||
private final Set<SensitiveDataSwipeListener> swipeListeners = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public final void swipeSensitiveData() {
|
||||
this.swipeSensitiveDataInternal();
|
||||
for (final SensitiveDataSwipeListener sensitiveDataSwipeListener : swipeListeners) {
|
||||
sensitiveDataSwipeListener.swipeSensitiveData();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void swipeSensitiveDataInternal();
|
||||
|
||||
@Override
|
||||
public final void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
this.swipeListeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
this.swipeListeners.remove(listener);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
|
||||
public class AbstractCryptorDecorator implements Cryptor {
|
||||
|
||||
protected final Cryptor cryptor;
|
||||
|
||||
public AbstractCryptorDecorator(Cryptor cryptor) {
|
||||
this.cryptor = cryptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
|
||||
cryptor.encryptMasterKey(out, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException {
|
||||
cryptor.decryptMasterKey(in, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
|
||||
return cryptor.encryptDirectoryPath(cleartextDirectoryId, nativePathSep);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptFilename(String cleartextName) {
|
||||
return cryptor.encryptFilename(cleartextName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
|
||||
return cryptor.decryptFilename(ciphertextName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
|
||||
return cryptor.decryptedContentLength(encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
return cryptor.decryptFile(encryptedFile, plaintextFile, authenticate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length, authenticate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
||||
return cryptor.encryptFile(plaintextFile, encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws DestroyFailedException {
|
||||
cryptor.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return cryptor.isDestroyed();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,17 +12,20 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
|
||||
/**
|
||||
* Provides access to cryptographic functions. All methods are threadsafe.
|
||||
*/
|
||||
public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
public interface Cryptor extends Destroyable {
|
||||
|
||||
/**
|
||||
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
|
||||
@@ -33,57 +36,50 @@ public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
* Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
|
||||
*
|
||||
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
|
||||
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong
|
||||
* password. In this case a DecryptFailedException will be thrown.
|
||||
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
|
||||
* this case Java JCE needs to be installed.
|
||||
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong password. In this case a DecryptFailedException will be thrown.
|
||||
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In this case Java JCE needs to be installed.
|
||||
* @throws UnsupportedVaultException If the masterkey file is too old or too modern.
|
||||
*/
|
||||
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException;
|
||||
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException;
|
||||
|
||||
/**
|
||||
* Encrypts each plaintext path component for its own.
|
||||
* Encrypts a given plaintext path representing a directory structure. See {@link #encryptFilename(String, CryptorMetadataSupport)} for contents inside directories.
|
||||
*
|
||||
* @param cleartextPath A relative path (UTF-8 encoded)
|
||||
* @param encryptedPathSep Path separator char like '/' used on local file system. Must not be null, even if cleartextPath is a sole
|
||||
* file name without any path separators.
|
||||
* @param cleartextPathSep Path separator char like '/' used in webdav URIs. Must not be null, even if cleartextPath is a sole file name
|
||||
* without any path separators.
|
||||
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
|
||||
* @return Encrypted path components concatenated by the given encryptedPathSep. Must not start with encryptedPathSep, unless the
|
||||
* encrypted path is explicitly absolute.
|
||||
* @param cleartextDirectoryId A unique directory id
|
||||
* @param nativePathSep Path separator like "/" used on local file system. Must not be null, even if cleartextPath is a sole file name without any path separators.
|
||||
* @return Encrypted path.
|
||||
*/
|
||||
String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
|
||||
String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep);
|
||||
|
||||
/**
|
||||
* Decrypts each encrypted path component for its own.
|
||||
* Encrypts the name of a file. See {@link #encryptDirectoryPath(String, char)} for parent dir.
|
||||
*
|
||||
* @param encryptedPath A relative path (UTF-8 encoded)
|
||||
* @param encryptedPathSep Path separator char like '/' used on local file system. Must not be null, even if encryptedPath is a sole
|
||||
* file name without any path separators.
|
||||
* @param cleartextPathSep Path separator char like '/' used in webdav URIs. Must not be null, even if encryptedPath is a sole file name
|
||||
* without any path separators.
|
||||
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
|
||||
* @return Decrypted path components concatenated by the given cleartextPathSep. Must not start with cleartextPathSep, unless the
|
||||
* cleartext path is explicitly absolute.
|
||||
* @param cleartextName A plaintext filename without any preceeding directory paths.
|
||||
* @return Encrypted filename.
|
||||
*/
|
||||
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
|
||||
String encryptFilename(String cleartextName);
|
||||
|
||||
/**
|
||||
* @return <code>true</code> If the integrity of the file can be assured.
|
||||
* Decrypts the name of a file.
|
||||
*
|
||||
* @param ciphertextName A ciphertext filename without any preceeding directory paths.
|
||||
* @return Decrypted filename.
|
||||
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
|
||||
*/
|
||||
boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException;
|
||||
String decryptFilename(String ciphertextName) throws DecryptFailedException;
|
||||
|
||||
/**
|
||||
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
|
||||
* @return Content length of the decrypted file or <code>null</code> if unknown.
|
||||
* @throws MacAuthenticationFailedException If the MAC auth failed.
|
||||
*/
|
||||
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException;
|
||||
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException;
|
||||
|
||||
/**
|
||||
* @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
|
||||
* @throws DecryptFailedException If decryption failed
|
||||
*/
|
||||
Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
|
||||
Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException;
|
||||
|
||||
/**
|
||||
* @param pos First byte (inclusive)
|
||||
@@ -91,21 +87,11 @@ public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
* @return Number of decrypted bytes. This might not be equal to the number of bytes requested due to potential overheads.
|
||||
* @throws DecryptFailedException If decryption failed
|
||||
*/
|
||||
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException;
|
||||
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException;
|
||||
|
||||
/**
|
||||
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
|
||||
*/
|
||||
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException;
|
||||
|
||||
/**
|
||||
* @return A filter, that returns <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting
|
||||
* metadata file of the {@link Cryptor}.
|
||||
*/
|
||||
Filter<Path> getPayloadFilesFilter();
|
||||
|
||||
void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
|
||||
|
||||
void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
|
||||
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException;
|
||||
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ public interface CryptorIOSampling {
|
||||
/**
|
||||
* @return Number of encrypted bytes since the last reset.
|
||||
*/
|
||||
Long pollEncryptedBytes(boolean resetCounter);
|
||||
long pollEncryptedBytes(boolean resetCounter);
|
||||
|
||||
/**
|
||||
* @return Number of decrypted bytes since the last reset.
|
||||
*/
|
||||
Long pollDecryptedBytes(boolean resetCounter);
|
||||
long pollDecryptedBytes(boolean resetCounter);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Methods that may be called by the Cryptor when accessing a path.
|
||||
*/
|
||||
public interface CryptorIOSupport {
|
||||
|
||||
/**
|
||||
* Persists encryptedMetadata to the given encryptedPath.
|
||||
*
|
||||
* @param encryptedPath A relative path
|
||||
* @throws IOException
|
||||
*/
|
||||
void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException;
|
||||
|
||||
/**
|
||||
* @return Previously written encryptedMetadata stored at the given encryptedPath or <code>null</code> if no such file exists.
|
||||
*/
|
||||
byte[] readPathSpecificMetadata(String encryptedPath) throws IOException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.AbstractDualBidiMap;
|
||||
import org.apache.commons.collections4.map.LRUMap;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
|
||||
public class PathCachingCryptorDecorator extends AbstractCryptorDecorator {
|
||||
|
||||
private static final int MAX_CACHED_PATHS = 5000;
|
||||
private static final int MAX_CACHED_NAMES = 5000;
|
||||
|
||||
private final Map<String, String> pathCache = new LRUMap<>(MAX_CACHED_PATHS); // <cleartextDirectoryId, ciphertextPath>
|
||||
private final BidiMap<String, String> nameCache = new BidiLRUMap<>(MAX_CACHED_NAMES); // <cleartextName, ciphertextName>
|
||||
|
||||
private PathCachingCryptorDecorator(Cryptor cryptor) {
|
||||
super(cryptor);
|
||||
}
|
||||
|
||||
public static Cryptor decorate(Cryptor cryptor) {
|
||||
return new PathCachingCryptorDecorator(cryptor);
|
||||
}
|
||||
|
||||
/* Cryptor */
|
||||
|
||||
@Override
|
||||
public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
|
||||
return pathCache.computeIfAbsent(cleartextDirectoryId, id -> cryptor.encryptDirectoryPath(id, nativePathSep));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptFilename(String cleartextName) {
|
||||
return nameCache.computeIfAbsent(cleartextName, name -> cryptor.encryptFilename(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
|
||||
String cleartextName = nameCache.getKey(ciphertextName);
|
||||
if (cleartextName == null) {
|
||||
cleartextName = cryptor.decryptFilename(ciphertextName);
|
||||
nameCache.put(cleartextName, ciphertextName);
|
||||
}
|
||||
return cleartextName;
|
||||
}
|
||||
|
||||
private static class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
|
||||
|
||||
BidiLRUMap(int maxSize) {
|
||||
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
|
||||
}
|
||||
|
||||
protected BidiLRUMap(final Map<K, V> normalMap, final Map<V, K> reverseMap, final BidiMap<V, K> inverseBidiMap) {
|
||||
super(normalMap, reverseMap, inverseBidiMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BidiMap<V, K> createBidiMap(Map<V, K> normalMap, Map<K, V> reverseMap, BidiMap<K, V> inverseMap) {
|
||||
return new BidiLRUMap<V, K>(normalMap, reverseMap, inverseMap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
|
||||
/**
|
||||
* Decorates the Cryptor by decorating the In- and OutputStreams used during de-/encryption.
|
||||
*/
|
||||
public class SamplingCryptorDecorator extends AbstractCryptorDecorator implements CryptorIOSampling {
|
||||
|
||||
private final LongAdder encryptedBytes;
|
||||
private final LongAdder decryptedBytes;
|
||||
|
||||
private SamplingCryptorDecorator(Cryptor cryptor) {
|
||||
super(cryptor);
|
||||
encryptedBytes = new LongAdder();
|
||||
decryptedBytes = new LongAdder();
|
||||
}
|
||||
|
||||
public static Cryptor decorate(Cryptor cryptor) {
|
||||
return new SamplingCryptorDecorator(cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long pollEncryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return encryptedBytes.sumThenReset();
|
||||
} else {
|
||||
return encryptedBytes.sum();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long pollDecryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return decryptedBytes.sumThenReset();
|
||||
} else {
|
||||
return decryptedBytes.sum();
|
||||
}
|
||||
}
|
||||
|
||||
/* Cryptor */
|
||||
|
||||
@Override
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptFile(encryptedFile, countingOutputStream, authenticate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptRange(encryptedFile, countingOutputStream, pos, length, authenticate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
||||
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
|
||||
return cryptor.encryptFile(countingInputStream, encryptedFile);
|
||||
}
|
||||
|
||||
private class CountingInputStream extends InputStream {
|
||||
|
||||
private final InputStream in;
|
||||
private final LongAdder counter;
|
||||
|
||||
private CountingInputStream(LongAdder counter, InputStream in) {
|
||||
this.in = in;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int count = in.read();
|
||||
counter.add(count);
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int count = in.read(b, off, len);
|
||||
counter.add(count);
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class CountingOutputStream extends OutputStream {
|
||||
|
||||
private final OutputStream out;
|
||||
private final LongAdder counter;
|
||||
|
||||
private CountingOutputStream(LongAdder counter, OutputStream out) {
|
||||
this.out = out;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
counter.increment();
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
counter.add(len);
|
||||
out.write(b, off, len);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
|
||||
public class SamplingDecorator implements Cryptor, CryptorIOSampling {
|
||||
|
||||
private final Cryptor cryptor;
|
||||
private final AtomicLong encryptedBytes;
|
||||
private final AtomicLong decryptedBytes;
|
||||
|
||||
private SamplingDecorator(Cryptor cryptor) {
|
||||
this.cryptor = cryptor;
|
||||
encryptedBytes = new AtomicLong();
|
||||
decryptedBytes = new AtomicLong();
|
||||
}
|
||||
|
||||
public static Cryptor decorate(Cryptor cryptor) {
|
||||
return new SamplingDecorator(cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void swipeSensitiveData() {
|
||||
cryptor.swipeSensitiveData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long pollEncryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return encryptedBytes.getAndSet(0);
|
||||
} else {
|
||||
return encryptedBytes.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long pollDecryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return decryptedBytes.getAndSet(0);
|
||||
} else {
|
||||
return decryptedBytes.get();
|
||||
}
|
||||
}
|
||||
|
||||
/* Cryptor */
|
||||
|
||||
@Override
|
||||
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
|
||||
cryptor.encryptMasterKey(out, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
|
||||
cryptor.decryptMasterKey(in, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
|
||||
encryptedBytes.addAndGet(StringUtils.length(cleartextPath));
|
||||
return cryptor.encryptPath(cleartextPath, encryptedPathSep, cleartextPathSep, ioSupport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
|
||||
decryptedBytes.addAndGet(StringUtils.length(encryptedPath));
|
||||
return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
|
||||
return cryptor.authenticateContent(encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
|
||||
return cryptor.decryptedContentLength(encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptedFile(encryptedFile, countingInputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
|
||||
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
|
||||
return cryptor.encryptFile(countingInputStream, encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter<Path> getPayloadFilesFilter() {
|
||||
return cryptor.getPayloadFilesFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
cryptor.addSensitiveDataSwipeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
cryptor.removeSensitiveDataSwipeListener(listener);
|
||||
}
|
||||
|
||||
private class CountingInputStream extends InputStream {
|
||||
|
||||
private final InputStream in;
|
||||
private final AtomicLong counter;
|
||||
|
||||
private CountingInputStream(AtomicLong counter, InputStream in) {
|
||||
this.in = in;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int count = in.read();
|
||||
counter.addAndGet(count);
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int count = in.read(b, off, len);
|
||||
counter.addAndGet(count);
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class CountingOutputStream extends OutputStream {
|
||||
|
||||
private final OutputStream out;
|
||||
private final AtomicLong counter;
|
||||
|
||||
private CountingOutputStream(AtomicLong counter, OutputStream out) {
|
||||
this.out = out;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
counter.incrementAndGet();
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
counter.addAndGet(len);
|
||||
out.write(b, off, len);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
public interface SensitiveDataSwipeListener {
|
||||
|
||||
/**
|
||||
* Removes sensitive data from memory. Depending on the data (e.g. for passwords) it might be necessary to overwrite the memory before
|
||||
* freeing the object.
|
||||
*/
|
||||
void swipeSensitiveData();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CryptingException extends IOException {
|
||||
private static final long serialVersionUID = -6622699014483319376L;
|
||||
|
||||
public CryptingException(String string) {
|
||||
super(string);
|
||||
}
|
||||
|
||||
public CryptingException(String string, Throwable t) {
|
||||
super(string, t);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class DecryptFailedException extends StorageCryptingException {
|
||||
public class DecryptFailedException extends CryptingException {
|
||||
private static final long serialVersionUID = -3855673600374897828L;
|
||||
|
||||
public DecryptFailedException(Throwable t) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class EncryptFailedException extends CryptingException {
|
||||
private static final long serialVersionUID = -3855673600374897828L;
|
||||
|
||||
public EncryptFailedException(Throwable t) {
|
||||
super("Encryption failed.", t);
|
||||
}
|
||||
|
||||
public EncryptFailedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class MacAuthenticationFailedException extends DecryptFailedException {
|
||||
|
||||
private static final long serialVersionUID = -5577052361643658772L;
|
||||
|
||||
public MacAuthenticationFailedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class MasterkeyDecryptionException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -6241452734672333206L;
|
||||
|
||||
public MasterkeyDecryptionException(String string) {
|
||||
super(string);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class StorageCryptingException extends Exception {
|
||||
private static final long serialVersionUID = -6622699014483319376L;
|
||||
|
||||
public StorageCryptingException(String string) {
|
||||
super(string);
|
||||
}
|
||||
|
||||
public StorageCryptingException(String string, Throwable t) {
|
||||
super(string, t);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class UnsupportedKeyLengthException extends StorageCryptingException {
|
||||
public class UnsupportedKeyLengthException extends MasterkeyDecryptionException {
|
||||
private static final long serialVersionUID = 8114147446419390179L;
|
||||
|
||||
private final int requestedLength;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class UnsupportedVaultException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -5147549533387945622L;
|
||||
|
||||
private final Integer detectedVersion;
|
||||
private final Integer supportedVersion;
|
||||
|
||||
public UnsupportedVaultException(Integer detectedVersion, Integer supportedVersion) {
|
||||
super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion);
|
||||
this.detectedVersion = detectedVersion;
|
||||
this.supportedVersion = supportedVersion;
|
||||
}
|
||||
|
||||
public Integer getDetectedVersion() {
|
||||
return detectedVersion;
|
||||
}
|
||||
|
||||
public Integer getSupportedVersion() {
|
||||
return supportedVersion;
|
||||
}
|
||||
|
||||
public boolean isVaultOlderThanSoftware() {
|
||||
return detectedVersion == null || detectedVersion < supportedVersion;
|
||||
}
|
||||
|
||||
public boolean isSoftwareOlderThanVault() {
|
||||
return detectedVersion > supportedVersion;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class WrongPasswordException extends StorageCryptingException {
|
||||
public class WrongPasswordException extends MasterkeyDecryptionException {
|
||||
private static final long serialVersionUID = -602047799678568780L;
|
||||
|
||||
public WrongPasswordException() {
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
|
||||
public class SeekableByteChannelInputStream extends InputStream {
|
||||
private final SeekableByteChannel channel;
|
||||
private volatile long markedPos = 0;
|
||||
|
||||
public SeekableByteChannelInputStream(SeekableByteChannel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
final ByteBuffer buffer = ByteBuffer.allocate(1);
|
||||
final int read = channel.read(buffer);
|
||||
if (read == 1) {
|
||||
return buffer.get(0);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
|
||||
return channel.read(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
long available = channel.size() - channel.position();
|
||||
if (available > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
} else {
|
||||
return (int) available;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
final long pos = channel.position();
|
||||
final long max = channel.size();
|
||||
final long maxSkip = max - pos;
|
||||
final long actualSkip = Math.min(n, maxSkip);
|
||||
channel.position(channel.position() + actualSkip);
|
||||
return actualSkip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
channel.close();
|
||||
super.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void mark(int readlimit) {
|
||||
try {
|
||||
markedPos = channel.position();
|
||||
} catch (IOException e) {
|
||||
markedPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
channel.position(markedPos);
|
||||
}
|
||||
|
||||
public synchronized void resetTo(long position) throws IOException {
|
||||
channel.position(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
|
||||
public class SeekableByteChannelOutputStream extends OutputStream {
|
||||
|
||||
private final SeekableByteChannel channel;
|
||||
|
||||
public SeekableByteChannelOutputStream(SeekableByteChannel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
final byte actualByte = (byte) (b & 0x000000FF);
|
||||
final ByteBuffer buffer = ByteBuffer.allocate(1);
|
||||
buffer.put(actualByte);
|
||||
channel.write(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
|
||||
channel.write(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
channel.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SeekableByteChannel#truncate(long)
|
||||
*/
|
||||
public void truncate(long size) throws IOException {
|
||||
channel.truncate(size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SeekableByteChannel#position()
|
||||
*/
|
||||
public long position() throws IOException {
|
||||
return channel.position();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SeekableByteChannel#position(long)
|
||||
*/
|
||||
public void position(long newPosition) throws IOException {
|
||||
channel.position(newPosition);
|
||||
}
|
||||
|
||||
}
|
||||
BIN
main/installer-debian/package/linux/Cryptomator.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
16
main/installer-debian/package/linux/control
Normal file
@@ -0,0 +1,16 @@
|
||||
Package: APPLICATION_PACKAGE
|
||||
Version: APPLICATION_VERSION
|
||||
Section: contrib/utils
|
||||
Maintainer: Sebastian Stenzel <sebastian.stenzel@gmail.com>
|
||||
Homepage: https://cryptomator.org
|
||||
Vcs-Git: https://github.com/totalvoidness/cryptomator.git
|
||||
Vcs-Browser: https://github.com/totalvoidness/cryptomator
|
||||
Priority: optional
|
||||
Architecture: APPLICATION_ARCH
|
||||
Provides: APPLICATION_PACKAGE
|
||||
Installed-Size: APPLICATION_INSTALLED_SIZE
|
||||
Depends: gvfs-bin, gvfs-backends, gvfs-fuse, xdg-utils
|
||||
Description: Multi-platform client-side encryption of your cloud files.
|
||||
Cryptomator provides free client-side AES encryption for your cloud files.
|
||||
Create encrypted vaults, which get mounted as virtual volumes. Whatever
|
||||
you save on one of these volumes will end up encrypted inside your vault.
|
||||
23
main/installer-debian/package/linux/copyright
Normal file
@@ -0,0 +1,23 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: cryptomator
|
||||
Source: <https://github.com/totalvoidness/cryptomator>
|
||||
|
||||
Copyright: 2015 Sebastian Stenzel <sebastian.stenzel@gmail.com> and contributors.
|
||||
License: MIT
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
88
main/installer-debian/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-debian</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator Debian installer</name>
|
||||
|
||||
<properties>
|
||||
<javafx.application.name>Cryptomator</javafx.application.name>
|
||||
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
|
||||
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.7</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>create-deployment-bundle</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
|
||||
|
||||
<!-- Define application to build -->
|
||||
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
|
||||
<!-- Create main application jar -->
|
||||
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
|
||||
<fx:application refid="fxApp" />
|
||||
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
|
||||
</fx:resources>
|
||||
<fx:manifest>
|
||||
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
|
||||
<fx:attribute name="Implementation-Version" value="${project.version}" />
|
||||
</fx:manifest>
|
||||
</fx:jar>
|
||||
|
||||
<!-- Create native package -->
|
||||
<fx:deploy nativeBundles="deb" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
|
||||
<fx:application refid="fxApp"/>
|
||||
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||
<fx:platform javafx="2.2+" j2se="8.0">
|
||||
<fx:property name="logPath" value="~/.Cryptomator/cryptomator.log" />
|
||||
<fx:jvmarg value="-Xmx2048m"/>
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="true" />
|
||||
</fx:deploy>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
BIN
main/installer-osx/package/macosx/Cryptomator-Volume.icns
Normal file
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
BIN
main/installer-osx/package/macosx/Cryptomator.icns
Normal file
102
main/installer-osx/package/macosx/Info.plist
Normal file
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.7.4</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleAllowMixedLocalizations</key>
|
||||
<true/>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>DEPLOY_LAUNCHER_NAME</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>DEPLOY_ICON_FILE</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>DEPLOY_BUNDLE_IDENTIFIER</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>DEPLOY_BUNDLE_NAME</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>DEPLOY_BUNDLE_SHORT_VERSION</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See http://developer.apple.com/library/mac/#releasenotes/General/SubmittingToMacAppStore/_index.html for list of AppStore categories -->
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>DEPLOY_BUNDLE_CATEGORY</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>100</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>DEPLOY_BUNDLE_COPYRIGHT</string>
|
||||
<key>JVMRuntime</key>
|
||||
<string>DEPLOY_JAVA_RUNTIME_NAME</string>
|
||||
<key>JVMMainClassName</key>
|
||||
<string>DEPLOY_LAUNCHER_CLASS</string>
|
||||
<key>JVMAppClasspath</key>
|
||||
<string>DEPLOY_APP_CLASSPATH</string>
|
||||
<key>JVMMainJarName</key>
|
||||
<string>DEPLOY_MAIN_JAR_NAME</string>
|
||||
<key>JVMPreferencesID</key>
|
||||
<string>DEPLOY_PREFERENCES_ID</string>
|
||||
<key>JVMOptions</key>
|
||||
<array>
|
||||
DEPLOY_JVM_OPTIONS
|
||||
</array>
|
||||
<key>JVMUserOptions</key>
|
||||
<dict>
|
||||
DEPLOY_JVM_USER_OPTIONS
|
||||
</dict>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<!-- hide from dock -->
|
||||
<key>LSUIElement</key>
|
||||
<string>1</string>
|
||||
<!-- register .cryptomator bundle extension -->
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>cryptomator</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>Cryptomator.icns</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Cryptomator Vault</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>org.cryptomator.folder</string>
|
||||
</array>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>com.apple.package</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Cryptomator Vault</string>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>Cryptomator.icns</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.cryptomator.folder</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>cryptomator</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
88
main/installer-osx/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-osx</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator Mac OS X installer</name>
|
||||
|
||||
<properties>
|
||||
<javafx.application.name>Cryptomator</javafx.application.name>
|
||||
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
|
||||
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.7</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>create-deployment-bundle</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
|
||||
|
||||
<!-- Define application to build -->
|
||||
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
|
||||
<!-- Create main application jar -->
|
||||
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
|
||||
<fx:application refid="fxApp" />
|
||||
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
|
||||
</fx:resources>
|
||||
<fx:manifest>
|
||||
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
|
||||
<fx:attribute name="Implementation-Version" value="${project.version}" />
|
||||
</fx:manifest>
|
||||
</fx:jar>
|
||||
|
||||
<!-- Create native package -->
|
||||
<fx:deploy nativeBundles="dmg" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
|
||||
<fx:application refid="fxApp"/>
|
||||
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||
<fx:platform javafx="2.2+" j2se="8.0">
|
||||
<fx:property name="logPath" value="~/Library/Logs/Cryptomator/cryptomator.log" />
|
||||
<fx:jvmarg value="-Xmx2048m"/>
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="true" />
|
||||
</fx:deploy>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
After Width: | Height: | Size: 9.6 KiB |
BIN
main/installer-win-portable/package/windows/Cryptomator.ico
Normal file
|
After Width: | Height: | Size: 361 KiB |
74
main/installer-win-portable/package/windows/Cryptomator.iss
Normal file
@@ -0,0 +1,74 @@
|
||||
;This file will be executed next to the application bundle image
|
||||
;I.e. current directory will contain folder APPLICATION_NAME with application files
|
||||
[Setup]
|
||||
AppId={{PRODUCT_APP_IDENTIFIER}}
|
||||
AppName=APPLICATION_NAME
|
||||
AppVersion=APPLICATION_VERSION
|
||||
AppVerName=APPLICATION_NAME APPLICATION_VERSION
|
||||
AppPublisher=APPLICATION_VENDOR
|
||||
AppComments=APPLICATION_COMMENTS
|
||||
AppCopyright=APPLICATION_COPYRIGHT
|
||||
AppPublisherURL=https://cryptomator.org/
|
||||
;AppSupportURL=http://java.com/
|
||||
;AppUpdatesURL=http://java.com/
|
||||
DefaultDirName=APPLICATION_INSTALL_ROOT\APPLICATION_NAME
|
||||
DisableStartupPrompt=Yes
|
||||
DisableDirPage=No
|
||||
DisableProgramGroupPage=Yes
|
||||
DisableReadyPage=Yes
|
||||
DisableFinishedPage=No
|
||||
DisableWelcomePage=Yes
|
||||
DefaultGroupName=APPLICATION_GROUP
|
||||
;Optional License
|
||||
LicenseFile=APPLICATION_LICENSE_FILE
|
||||
;WinXP or above
|
||||
MinVersion=0,5.1
|
||||
OutputBaseFilename=INSTALLER_FILE_NAME
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
PrivilegesRequired=admin
|
||||
SetupIconFile=APPLICATION_NAME\APPLICATION_NAME.ico
|
||||
UninstallDisplayIcon={app}\APPLICATION_NAME.ico
|
||||
UninstallDisplayName=APPLICATION_NAME
|
||||
WizardImageStretch=No
|
||||
WizardSmallImageFile=Cryptomator-setup-icon.bmp
|
||||
WizardImageBackColor=$ffffff
|
||||
ArchitecturesInstallIn64BitMode=ARCHITECTURE_BIT_MODE
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Files]
|
||||
Source: "APPLICATION_NAME\APPLICATION_NAME.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "APPLICATION_NAME\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_MENU_SHORTCUT()
|
||||
Name: "{commondesktop}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_DESKTOP_SHORTCUT()
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\RUN_FILENAME.exe"; Description: "{cm:LaunchProgram,APPLICATION_NAME}"; Flags: nowait postinstall skipifsilent; Check: APPLICATION_NOT_SERVICE()
|
||||
Filename: "{app}\RUN_FILENAME.exe"; Parameters: "-install -svcName ""APPLICATION_NAME"" -svcDesc ""APPLICATION_DESCRIPTION"" -mainExe ""APPLICATION_LAUNCHER_FILENAME"" START_ON_INSTALL RUN_AT_STARTUP"; Check: APPLICATION_SERVICE()
|
||||
|
||||
[UninstallRun]
|
||||
Filename: "{app}\RUN_FILENAME.exe "; Parameters: "-uninstall -svcName APPLICATION_NAME STOP_ON_UNINSTALL"; Check: APPLICATION_SERVICE()
|
||||
|
||||
[Code]
|
||||
function returnTrue(): Boolean;
|
||||
begin
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function returnFalse(): Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
end;
|
||||
|
||||
function InitializeSetup(): Boolean;
|
||||
begin
|
||||
// Possible future improvements:
|
||||
// if version less or same => just launch app
|
||||
// if upgrade => check if same app is running and wait for it to exit
|
||||
// Add pack200/unpack200 support?
|
||||
Result := True;
|
||||
end;
|
||||
88
main/installer-win-portable/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-win-portable</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator (Portable) Windows installer</name>
|
||||
|
||||
<properties>
|
||||
<javafx.application.name>Cryptomator</javafx.application.name>
|
||||
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
|
||||
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.7</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>create-deployment-bundle</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
|
||||
|
||||
<!-- Define application to build -->
|
||||
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
|
||||
<!-- Create main application jar -->
|
||||
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
|
||||
<fx:application refid="fxApp" />
|
||||
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
|
||||
</fx:resources>
|
||||
<fx:manifest>
|
||||
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
|
||||
<fx:attribute name="Implementation-Version" value="${project.version}" />
|
||||
</fx:manifest>
|
||||
</fx:jar>
|
||||
|
||||
<!-- Create native package -->
|
||||
<fx:deploy nativeBundles="exe" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
|
||||
<fx:application refid="fxApp"/>
|
||||
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||
<fx:platform javafx="2.2+" j2se="8.0">
|
||||
<fx:property name="settingsPath" value="./settings.json" />
|
||||
<fx:property name="logPath" value="cryptomator.log" />
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="false" menu="false" shortcut="false" />
|
||||
</fx:deploy>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
BIN
main/installer-win/package/windows/Cryptomator-setup-icon.bmp
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
main/installer-win/package/windows/Cryptomator.ico
Normal file
|
After Width: | Height: | Size: 361 KiB |
80
main/installer-win/package/windows/Cryptomator.iss
Normal file
@@ -0,0 +1,80 @@
|
||||
;This file will be executed next to the application bundle image
|
||||
;I.e. current directory will contain folder APPLICATION_NAME with application files
|
||||
[Setup]
|
||||
AppId={{PRODUCT_APP_IDENTIFIER}}
|
||||
AppName=APPLICATION_NAME
|
||||
AppVersion=APPLICATION_VERSION
|
||||
AppVerName=APPLICATION_NAME APPLICATION_VERSION
|
||||
AppPublisher=APPLICATION_VENDOR
|
||||
AppComments=APPLICATION_COMMENTS
|
||||
AppCopyright=APPLICATION_COPYRIGHT
|
||||
AppPublisherURL=https://cryptomator.org/
|
||||
;AppSupportURL=http://java.com/
|
||||
;AppUpdatesURL=http://java.com/
|
||||
DefaultDirName=APPLICATION_INSTALL_ROOT\APPLICATION_NAME
|
||||
DisableStartupPrompt=Yes
|
||||
DisableDirPage=No
|
||||
DisableProgramGroupPage=Yes
|
||||
DisableReadyPage=Yes
|
||||
DisableFinishedPage=No
|
||||
DisableWelcomePage=Yes
|
||||
DefaultGroupName=APPLICATION_GROUP
|
||||
;Optional License
|
||||
LicenseFile=APPLICATION_LICENSE_FILE
|
||||
;WinXP or above
|
||||
MinVersion=0,5.1
|
||||
OutputBaseFilename=INSTALLER_FILE_NAME
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
PrivilegesRequired=admin
|
||||
SetupIconFile=APPLICATION_NAME\APPLICATION_NAME.ico
|
||||
UninstallDisplayIcon={app}\APPLICATION_NAME.ico
|
||||
UninstallDisplayName=APPLICATION_NAME
|
||||
WizardImageStretch=No
|
||||
WizardSmallImageFile=Cryptomator-setup-icon.bmp
|
||||
WizardImageBackColor=$ffffff
|
||||
ArchitecturesInstallIn64BitMode=ARCHITECTURE_BIT_MODE
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Registry]
|
||||
;Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Internet Settings"; ValueType: dword; ValueName: "AutoDetect"; ValueData: "0"
|
||||
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Services\WebClient\Parameters"; ValueType: dword; ValueName: "FileSizeLimitInBytes"; ValueData: "$ffffffff"
|
||||
|
||||
[Files]
|
||||
Source: "APPLICATION_NAME\APPLICATION_NAME.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "APPLICATION_NAME\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_MENU_SHORTCUT()
|
||||
Name: "{commondesktop}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_DESKTOP_SHORTCUT()
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\RUN_FILENAME.exe"; Description: "{cm:LaunchProgram,APPLICATION_NAME}"; Flags: nowait postinstall skipifsilent; Check: APPLICATION_NOT_SERVICE()
|
||||
Filename: "{app}\RUN_FILENAME.exe"; Parameters: "-install -svcName ""APPLICATION_NAME"" -svcDesc ""APPLICATION_DESCRIPTION"" -mainExe ""APPLICATION_LAUNCHER_FILENAME"" START_ON_INSTALL RUN_AT_STARTUP"; Check: APPLICATION_SERVICE()
|
||||
Filename: "net"; Parameters: "stop webclient"; Description: "Stopping WebClient..."; Flags: waituntilterminated runhidden
|
||||
Filename: "net"; Parameters: "start webclient"; Description: "Restarting WebClient..."; Flags: waituntilterminated runhidden
|
||||
|
||||
[UninstallRun]
|
||||
Filename: "{app}\RUN_FILENAME.exe "; Parameters: "-uninstall -svcName APPLICATION_NAME STOP_ON_UNINSTALL"; Check: APPLICATION_SERVICE()
|
||||
|
||||
[Code]
|
||||
function returnTrue(): Boolean;
|
||||
begin
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function returnFalse(): Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
end;
|
||||
|
||||
function InitializeSetup(): Boolean;
|
||||
begin
|
||||
// Possible future improvements:
|
||||
// if version less or same => just launch app
|
||||
// if upgrade => check if same app is running and wait for it to exit
|
||||
// Add pack200/unpack200 support?
|
||||
Result := True;
|
||||
end;
|
||||
87
main/installer-win/pom.xml
Normal file
@@ -0,0 +1,87 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-win</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator Windows installer</name>
|
||||
|
||||
<properties>
|
||||
<javafx.application.name>Cryptomator</javafx.application.name>
|
||||
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
|
||||
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.7</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>create-deployment-bundle</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
|
||||
|
||||
<!-- Define application to build -->
|
||||
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
|
||||
<!-- Create main application jar -->
|
||||
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
|
||||
<fx:application refid="fxApp" />
|
||||
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
|
||||
</fx:resources>
|
||||
<fx:manifest>
|
||||
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
|
||||
<fx:attribute name="Implementation-Version" value="${project.version}" />
|
||||
</fx:manifest>
|
||||
</fx:jar>
|
||||
|
||||
<!-- Create native package -->
|
||||
<fx:deploy nativeBundles="exe" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
|
||||
<fx:application refid="fxApp"/>
|
||||
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||
<fx:platform javafx="2.2+" j2se="8.0" >
|
||||
<fx:property name="logPath" value="%appdata%/Cryptomator/cryptomator.log" />
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="true" />
|
||||
</fx:deploy>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
108
main/pom.xml
@@ -1,10 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. Contributors: Sebastian Stenzel - initial API and implementation -->
|
||||
<!--
|
||||
Copyright (c) 2014 Sebastian Stenzel
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - initial API and implementation
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.10.0</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator</name>
|
||||
|
||||
@@ -32,8 +39,10 @@
|
||||
<commons-collections.version>4.0</commons-collections.version>
|
||||
<commons-lang3.version>3.3.2</commons-lang3.version>
|
||||
<commons-codec.version>1.10</commons-codec.version>
|
||||
<jackson-databind.version>2.4.4</jackson-databind.version>
|
||||
</properties>
|
||||
<commons-httpclient.version>3.1</commons-httpclient.version>
|
||||
<jackson-databind.version>2.4.4</jackson-databind.version>
|
||||
<mockito.version>1.10.19</mockito.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
@@ -102,6 +111,32 @@
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>${commons-codec.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- org.apache.httpcomponents:httpclient is newer, but jackrabbit uses this version. We don't have a reason to upgrade -->
|
||||
<groupId>commons-httpclient</groupId>
|
||||
<artifactId>commons-httpclient</artifactId>
|
||||
<version>${commons-httpclient.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>18.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- DI -->
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger</artifactId>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger-compiler</artifactId>
|
||||
<version>2.0.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
@@ -110,13 +145,19 @@
|
||||
<version>${jackson-databind.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JUnit -->
|
||||
<!-- JUnit / Mockito -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -137,6 +178,10 @@
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<modules>
|
||||
@@ -146,7 +191,60 @@
|
||||
<module>ui</module>
|
||||
</modules>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>debian</id>
|
||||
<modules>
|
||||
<module>installer-debian</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>osx</id>
|
||||
<modules>
|
||||
<module>installer-osx</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>win</id>
|
||||
<modules>
|
||||
<module>installer-win</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>win-portable</id>
|
||||
<modules>
|
||||
<module>installer-win-portable</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>uber-jar</id>
|
||||
<modules>
|
||||
<module>uber-jar</module>
|
||||
</modules>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/libs</outputDirectory>
|
||||
<includeScope>runtime</includeScope>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
||||
57
main/uber-jar/pom.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) 2014 Sebastian Stenzel
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - initial API and implementation
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>uber-jar</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Single über jar with all dependencies</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<finalName>Cryptomator-${project.parent.version}</finalName>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Main-Class>org.cryptomator.ui.Cryptomator</Main-Class>
|
||||
<Implementation-Version>${project.version}</Implementation-Version>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 361 KiB |
@@ -12,18 +12,11 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>ui</artifactId>
|
||||
<name>Cryptomator GUI</name>
|
||||
|
||||
<properties>
|
||||
<javafx.application.name>Cryptomator</javafx.application.name>
|
||||
<exec.mainClass>org.cryptomator.ui.MainApplication</exec.mainClass>
|
||||
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
|
||||
<controlsfx.version>8.20.8</controlsfx.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
@@ -39,6 +32,12 @@
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- apache commons -->
|
||||
<dependency>
|
||||
@@ -49,72 +48,20 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- UI -->
|
||||
<dependency>
|
||||
<groupId>org.controlsfx</groupId>
|
||||
<artifactId>controlsfx</artifactId>
|
||||
<version>${controlsfx.version}</version>
|
||||
<groupId>commons-httpclient</groupId>
|
||||
<artifactId>commons-httpclient</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DI -->
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger-compiler</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<finalName>${javafx.application.name}</finalName>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Main-Class>${exec.mainClass}</Main-Class>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.7</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>create-deployment-bundle</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
|
||||
|
||||
<fx:deploy nativeBundles="all" outdir="${project.build.directory}/dist" outfile="${project.build.finalName}" verbose="false">
|
||||
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||
<fx:platform javafx="2.2+" j2se="8.0" />
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" includes="${javafx.application.name}.jar" />
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="true" />
|
||||
</fx:deploy>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
149
main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java
Normal file
@@ -0,0 +1,149 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 cryptomator.org
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Tillmann Gaida - initial implementation
|
||||
* Sebastian Stenzel - refactoring
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.application.Application;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.util.SingleInstanceManager;
|
||||
import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
|
||||
import org.eclipse.jetty.util.ConcurrentHashSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Cryptomator {
|
||||
public static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
|
||||
|
||||
public static final CompletableFuture<Consumer<File>> OPEN_FILE_HANDLER = new CompletableFuture<>();
|
||||
|
||||
private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
|
||||
private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
/*
|
||||
* On OSX we're in an awkward position. We need to register a handler in the main thread of this application. However, we can't
|
||||
* even pass objects to the application, so we're forced to use a static CompletableFuture for the handler, which actually opens
|
||||
* the file in the application.
|
||||
*
|
||||
* Code taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
|
||||
*/
|
||||
try {
|
||||
final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
|
||||
final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
|
||||
final Method getApplication = applicationClass.getMethod("getApplication");
|
||||
final Object application = getApplication.invoke(null);
|
||||
final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
|
||||
|
||||
final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
|
||||
final OpenFilesHandlerClassHandler openFilesHandlerHandler = new OpenFilesHandlerClassHandler();
|
||||
final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandlerHandler);
|
||||
|
||||
setOpenFileHandler.invoke(application, openFilesHandlerObject);
|
||||
} catch (ReflectiveOperationException | RuntimeException e) {
|
||||
// Since we're trying to call OS-specific code, we'll just have
|
||||
// to hope for the best.
|
||||
LOG.error("exception adding OSX file open handler", e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Perform certain things on VM termination.
|
||||
*/
|
||||
Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER);
|
||||
|
||||
/*
|
||||
* Before starting the application, we check if there is already an instance running on this computer. If so, we send our command
|
||||
* line arguments to that instance and quit.
|
||||
*/
|
||||
final Optional<RemoteInstance> remoteInstance = SingleInstanceManager.getRemoteInstance(MainApplication.APPLICATION_KEY);
|
||||
|
||||
if (remoteInstance.isPresent()) {
|
||||
try (RemoteInstance instance = remoteInstance.get()) {
|
||||
LOG.info("An instance of Cryptomator is already running at {}.", instance.getRemotePort());
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
remoteInstance.get().sendMessage(args[i], 100);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error forwarding arguments to remote instance", e);
|
||||
}
|
||||
} else {
|
||||
Application.launch(MainApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void addShutdownTask(Runnable r) {
|
||||
SHUTDOWN_TASKS.add(r);
|
||||
}
|
||||
|
||||
public static void removeShutdownTask(Runnable r) {
|
||||
SHUTDOWN_TASKS.remove(r);
|
||||
}
|
||||
|
||||
private static class CleanShutdownPerformer extends Thread {
|
||||
@Override
|
||||
public void run() {
|
||||
LOG.debug("Shutting down");
|
||||
SHUTDOWN_TASKS.forEach(r -> {
|
||||
try {
|
||||
r.run();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("exception while shutting down", e);
|
||||
}
|
||||
});
|
||||
SHUTDOWN_TASKS.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleOpenFileRequest(File file) {
|
||||
try {
|
||||
OPEN_FILE_HANDLER.get().accept(file);
|
||||
} catch (Exception e) {
|
||||
LOG.error("exception handling file open event for file " + file.getAbsolutePath(), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler class taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
|
||||
*/
|
||||
private static class OpenFilesHandlerClassHandler implements InvocationHandler {
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
if (method.getName().equals("openFiles")) {
|
||||
final Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
|
||||
final Method getFiles = openFilesEventClass.getMethod("getFiles");
|
||||
Object e = args[0];
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
final List<File> ff = (List<File>) getFiles.invoke(e);
|
||||
for (File f : ff) {
|
||||
handleOpenFileRequest(f);
|
||||
}
|
||||
} catch (RuntimeException ee) {
|
||||
throw ee;
|
||||
} catch (Exception ee) {
|
||||
throw new RuntimeException(ee);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.ui.controllers.MainController;
|
||||
import org.cryptomator.ui.util.DeferredCloser;
|
||||
|
||||
import dagger.Component;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = CryptomatorModule.class)
|
||||
interface CryptomatorComponent {
|
||||
ExecutorService executorService();
|
||||
|
||||
DeferredCloser deferredCloser();
|
||||
|
||||
MainController mainController();
|
||||
}
|
||||
101
main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java
Normal file
@@ -0,0 +1,101 @@
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.SamplingCryptorDecorator;
|
||||
import org.cryptomator.crypto.aes256.Aes256Cryptor;
|
||||
import org.cryptomator.ui.model.VaultObjectMapperProvider;
|
||||
import org.cryptomator.ui.settings.Settings;
|
||||
import org.cryptomator.ui.settings.SettingsProvider;
|
||||
import org.cryptomator.ui.util.DeferredCloser;
|
||||
import org.cryptomator.ui.util.DeferredCloser.Closer;
|
||||
import org.cryptomator.ui.util.SemVerComparator;
|
||||
import org.cryptomator.ui.util.mount.WebDavMounter;
|
||||
import org.cryptomator.ui.util.mount.WebDavMounterProvider;
|
||||
import org.cryptomator.webdav.WebDavServer;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import javafx.application.Application;
|
||||
|
||||
@Module
|
||||
class CryptomatorModule {
|
||||
|
||||
private final Application application;
|
||||
private final DeferredCloser deferredCloser;
|
||||
|
||||
public CryptomatorModule(Application application) {
|
||||
this.application = application;
|
||||
this.deferredCloser = new DeferredCloser();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
Application provideApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
DeferredCloser provideDeferredCloser() {
|
||||
return deferredCloser;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("SemVer")
|
||||
Comparator<String> provideSemVerComparator() {
|
||||
return new SemVerComparator();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("VaultJsonMapper")
|
||||
ObjectMapper provideVaultObjectMapper(VaultObjectMapperProvider vaultObjectMapperProvider) {
|
||||
return vaultObjectMapperProvider.get();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
Settings provideSettings(SettingsProvider settingsProvider) {
|
||||
return settingsProvider.get();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
ExecutorService provideExecutorService() {
|
||||
return closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
WebDavMounter provideWebDavMounter(WebDavMounterProvider webDavMounterProvider) {
|
||||
return webDavMounterProvider.get();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
WebDavServer provideWebDavServer() {
|
||||
final WebDavServer webDavServer = new WebDavServer();
|
||||
webDavServer.start();
|
||||
return closeLater(webDavServer, WebDavServer::stop);
|
||||
}
|
||||
|
||||
@Provides
|
||||
Cryptor provideCryptor() {
|
||||
return SamplingCryptorDecorator.decorate(new Aes256Cryptor());
|
||||
}
|
||||
|
||||
private <T> T closeLater(T object, Closer<T> closer) {
|
||||
return deferredCloser.closeLater(object, closer).get().get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.FileVisitor;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.CharUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.aes256.Aes256Cryptor;
|
||||
import org.cryptomator.files.EncryptingFileVisitor;
|
||||
import org.cryptomator.ui.controls.ClearOnDisableListener;
|
||||
import org.cryptomator.ui.controls.SecPasswordField;
|
||||
import org.cryptomator.ui.model.Directory;
|
||||
import org.cryptomator.ui.util.FXThreads;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class InitializeController implements Initializable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
|
||||
private static final int MAX_USERNAME_LENGTH = 250;
|
||||
|
||||
private ResourceBundle localization;
|
||||
private Directory directory;
|
||||
private InitializationListener listener;
|
||||
|
||||
@FXML
|
||||
private TextField usernameField;
|
||||
|
||||
@FXML
|
||||
private SecPasswordField passwordField;
|
||||
|
||||
@FXML
|
||||
private SecPasswordField retypePasswordField;
|
||||
|
||||
@FXML
|
||||
private Button okButton;
|
||||
|
||||
@FXML
|
||||
private ProgressIndicator progressIndicator;
|
||||
|
||||
@FXML
|
||||
private Label messageLabel;
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.localization = rb;
|
||||
usernameField.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
|
||||
usernameField.textProperty().addListener(this::usernameFieldDidChange);
|
||||
passwordField.textProperty().addListener(this::passwordFieldDidChange);
|
||||
retypePasswordField.textProperty().addListener(this::retypePasswordFieldDidChange);
|
||||
retypePasswordField.disableProperty().addListener(new ClearOnDisableListener(retypePasswordField));
|
||||
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Username field
|
||||
// ****************************************
|
||||
|
||||
public void filterAlphanumericKeyEvents(KeyEvent t) {
|
||||
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
|
||||
return;
|
||||
}
|
||||
char c = t.getCharacter().charAt(0);
|
||||
if (!CharUtils.isAsciiAlphanumeric(c)) {
|
||||
t.consume();
|
||||
}
|
||||
}
|
||||
|
||||
public void usernameFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
if (StringUtils.length(newValue) > MAX_USERNAME_LENGTH) {
|
||||
usernameField.setText(newValue.substring(0, MAX_USERNAME_LENGTH));
|
||||
}
|
||||
passwordField.setDisable(StringUtils.isEmpty(newValue));
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Password field
|
||||
// ****************************************
|
||||
|
||||
private void passwordFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
retypePasswordField.setDisable(StringUtils.isEmpty(newValue));
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Retype password field
|
||||
// ****************************************
|
||||
|
||||
private void retypePasswordFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText());
|
||||
okButton.setDisable(!passwordsAreEqual);
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// OK button
|
||||
// ****************************************
|
||||
|
||||
@FXML
|
||||
protected void initializeVault(ActionEvent event) {
|
||||
setControlsDisabled(true);
|
||||
if (!isDirectoryEmpty() && !shouldEncryptExistingFiles()) {
|
||||
return;
|
||||
}
|
||||
final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT;
|
||||
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
|
||||
final CharSequence password = passwordField.getCharacters();
|
||||
OutputStream masterKeyOutputStream = null;
|
||||
try {
|
||||
progressIndicator.setVisible(true);
|
||||
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
|
||||
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
|
||||
final Future<?> futureDone = FXThreads.runOnBackgroundThread(this::encryptExistingContents);
|
||||
FXThreads.runOnMainThreadWhenFinished(futureDone, (result) -> {
|
||||
progressIndicator.setVisible(false);
|
||||
progressIndicator.setVisible(false);
|
||||
directory.getCryptor().swipeSensitiveData();
|
||||
if (listener != null) {
|
||||
listener.didInitialize(this);
|
||||
}
|
||||
});
|
||||
} catch (FileAlreadyExistsException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
|
||||
} catch (InvalidPathException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
|
||||
} catch (IOException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
LOG.error("I/O Exception", ex);
|
||||
} finally {
|
||||
usernameField.setText(null);
|
||||
passwordField.swipe();
|
||||
retypePasswordField.swipe();
|
||||
IOUtils.closeQuietly(masterKeyOutputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private void setControlsDisabled(boolean disable) {
|
||||
usernameField.setDisable(disable);
|
||||
passwordField.setDisable(disable);
|
||||
retypePasswordField.setDisable(disable);
|
||||
okButton.setDisable(disable);
|
||||
}
|
||||
|
||||
private boolean isDirectoryEmpty() {
|
||||
try {
|
||||
final DirectoryStream<Path> dirContents = Files.newDirectoryStream(directory.getPath());
|
||||
return !dirContents.iterator().hasNext();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to analyze directory.", e);
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldEncryptExistingFiles() {
|
||||
final Alert alert = new Alert(AlertType.CONFIRMATION);
|
||||
alert.setTitle(localization.getString("initialize.alert.directoryIsNotEmpty.title"));
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(localization.getString("initialize.alert.directoryIsNotEmpty.content"));
|
||||
|
||||
final Optional<ButtonType> result = alert.showAndWait();
|
||||
return ButtonType.OK.equals(result.get());
|
||||
}
|
||||
|
||||
private void encryptExistingContents() {
|
||||
try {
|
||||
final FileVisitor<Path> visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile);
|
||||
Files.walkFileTree(directory.getPath(), visitor);
|
||||
} catch (IOException ex) {
|
||||
LOG.error("I/O Exception", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldEncryptExistingFile(Path path) {
|
||||
final String name = path.getFileName().toString();
|
||||
return !directory.getPath().equals(path) && !name.endsWith(Aes256Cryptor.BASIC_FILE_EXT) && !name.endsWith(Aes256Cryptor.METADATA_FILE_EXT) && !name.endsWith(Aes256Cryptor.MASTERKEY_FILE_EXT);
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public Directory getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public void setDirectory(Directory directory) {
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
public InitializationListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
public void setListener(InitializationListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/* callback */
|
||||
|
||||
interface InitializationListener {
|
||||
void didInitialize(InitializeController ctrl);
|
||||
}
|
||||
|
||||
}
|
||||