Merge branch 'release/1.6.0'

This commit is contained in:
Julian Raufelder 2021-10-15 11:38:59 +02:00
commit 7e5d64fe6a
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
567 changed files with 25419 additions and 18876 deletions

View File

@ -2,4 +2,7 @@ escape_special_characters: 0
commit_message: '[ci skip]'
files:
- source: /presentation/src/main/res/values/strings.xml
translation: /presentation/src/main/res/values-%two_letters_code%/strings.xml
translation: /presentation/src/main/res/values-%android_code%/%original_file_name%
skip_untranslated_strings: true
skip_untranslated_files: false
export_only_approved: false

View File

@ -1,51 +0,0 @@
---
name: "Bug Report"
about: "Create a report to help us improve"
labels: type:bug
---
<!--
Please make sure to:
- Comply with our code of conduct: https://github.com/cryptomator/android/blob/develop/.github/CODE_OF_CONDUCT.md
- Search for existing similar issues first: https://github.com/cryptomator/android/issues?q=
⚠️ IMPORTANT: If you don't stick to this template, the issue will get closed.
-->
### Description
[Summarize your problem.]
### System Setup
* Android version: [Shown in the settings of Android]
* Cryptomator version: [Shown in the settings of Cryptomator]
* Cloud type: [Dropbox/Google Drive/OneDrive/pCloud/WebDAV/S3/Local storage]
### Steps to Reproduce
1. [First step]
2. [Second step]
3. [and so on…]
#### Expected Behavior
[What you expect to happen.]
#### Actual Behavior
[What actually happens.]
#### Reproducibility
[Always/Intermittent/Only once]
### Additional Information
[Any additional information, log files, screenshots or a movie while reproducing the problem, configuration, or data that might be necessary to reproduce the issue.]
<!--
If you want to add the log file enable debug mode, reproduce the problem and send it to us: https://community.cryptomator.org/t/how-do-i-enable-debug-mode-on-android/66
-->

95
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@ -0,0 +1,95 @@
name: Bug Report
description: Create a report to help us improve
labels: ["type:bug"]
body:
- type: checkboxes
id: terms
attributes:
label: Please agree to the following
options:
- label: I have searched [existing issues](https://github.com/cryptomator/android/issues?q=) for duplicates
required: true
- label: I agree to follow this project's [Code of Conduct](https://github.com/cryptomator/android/blob/develop/.github/CODE_OF_CONDUCT.md)
required: true
- type: input
id: summary
attributes:
label: Summary
placeholder: Please summarize your problem.
validations:
required: true
- type: textarea
id: software-versions
attributes:
label: System Setup
description: |
What software is involved? Please provide version numbers as well.
value: |
- Android: [Version shown in the settings of Android"]
- Cryptomator: [Version shown in the settings of Cryptomator]
-
render: markdown
validations:
required: true
- type: dropdown
id: cloud-type
attributes:
label: Cloud Type
description: Where is your vault located?
multiple: true
options:
- Dropbox
- Google Drive
- OneDrive
- WebDAV
- pCloud
- S3
- Local storage
validations:
required: false
- type: textarea
id: reproduction-steps
attributes:
label: Steps to Reproduce
value: |
1. [First Step]
2. [Second Step]
3. …
validations:
required: true
- type: textarea
id: expected-behaviour
attributes:
label: Expected Behavior
placeholder: What you expect to happen.
validations:
required: true
- type: textarea
id: actual-behaviour
attributes:
label: Actual Behavior
placeholder: What actually happens.
validations:
required: true
- type: dropdown
id: reproducibility
attributes:
label: Reproducibility
description: How often does the described behaviour occur?
options:
- Always
- Intermittent
- Only once
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Log Output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: textarea
id: further-info
attributes:
label: Anything else?
description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue?

View File

@ -1,28 +0,0 @@
---
name: "Feature Request"
about: "Suggest an idea for this project"
labels: type:feature-request
---
<!--
Please make sure to:
- Comply with our code of conduct: https://github.com/cryptomator/android/blob/develop/.github/CODE_OF_CONDUCT.md
- Search for existing similar issues first: https://github.com/cryptomator/android/issues?q=
-->
### Summary
[One paragraph explanation of the feature.]
### Motivation
[Why are we doing this? What use cases does it support? What is the expected outcome?]
### Considered Alternatives
[A clear and concise description of the alternative solutions you've considered.]
### Additional Context
[Add any other context or screenshots about the feature request here.]

37
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: Feature Request
description: Suggest an idea for this project
labels: ["type:feature-request"]
body:
- type: checkboxes
id: terms
attributes:
label: Please agree to the following
options:
- label: I have searched [existing issues](https://github.com/cryptomator/android/issues?q=) for duplicates
required: true
- label: I agree to follow this project's [Code of Conduct](https://github.com/cryptomator/android/blob/develop/.github/CODE_OF_CONDUCT.md)
required: true
- type: input
id: summary
attributes:
label: Summary
placeholder: Please summarize your feature request.
validations:
required: true
- type: textarea
id: motivation
attributes:
label: Motivation
description: Who requires this feature? What problem does the user face? How would this feature solve the problem?
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Considered Alternatives
description: What current alternatives or workarounds have you considered? Is there a different way to solve the same problem?
- type: textarea
id: context
attributes:
label: Anything else?
description: Any context, suggestions, screenshots, or concepts you want to share?

View File

@ -15,6 +15,6 @@ jobs:
fetch-depth: 0
- uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 11
- name: Build and Test
run: bash ./gradlew clean test --stacktrace

View File

@ -1,38 +0,0 @@
name: Bug Report Triage
on:
issues:
types: [opened]
jobs:
closeTemplateViolation:
name: Validate bug report against issue template
runs-on: ubuntu-latest
if: contains(github.event.issue.labels.*.name, 'type:bug')
steps:
- name: Check "Description"
if: |
!contains(github.event.issue.body, env.MUST_CONTAIN)
|| contains(toJson(github.event.issue.body), env.MUST_NOT_CONTAIN)
run: exit 1
env:
MUST_CONTAIN: '### Description'
MUST_NOT_CONTAIN: '### Description\r\n\r\n[Summarize your problem.]\r\n\r\n### System Setup'
- name: Check "Steps to Reproduce"
if: |
!contains(github.event.issue.body, env.MUST_CONTAIN)
|| contains(toJson(github.event.issue.body), env.MUST_NOT_CONTAIN)
run: exit 1
env:
MUST_CONTAIN: '### Steps to Reproduce'
MUST_NOT_CONTAIN: '### Steps to Reproduce\r\n\r\n1. [First step]\r\n2. [Second step]\r\n3. [and so on…]\r\n\r\n#### Expected Behavior'
- name: Close issue if one of the checks failed
if: ${{ failure() }}
uses: peter-evans/close-issue@v1
with:
comment: |
This bug report did ignore our issue template. 😞
Auto-closing this issue, since it is most likely not useful.
_This decision was made by a bot. If you think the bot is wrong, let us know and we'll reopen this issue._

3
.gitignore vendored
View File

@ -49,3 +49,6 @@ local.properties
# fdroid
**/fastlane/repo/**
**/fastlane/tmp/**
**/fastlane/izzyscript/iod-scan-apk.php
**/fastlane/izzyscript/current_iod-scan-apk.php
**/fastlane/izzyscript/current_result_*.json

8
.gitmodules vendored
View File

@ -1,9 +1,9 @@
[submodule "msa-auth-for-android"]
path = msa-auth-for-android
path = lib/msa-auth-for-android
url = https://github.com/SailReal/msa-auth-for-android.git
[submodule "subsampling-scale-image-view"]
path = subsampling-scale-image-view
path = lib/subsampling-scale-image-view
url = https://github.com/SailReal/subsampling-scale-image-view.git
[submodule "pcloud-sdk-java"]
path = pcloud-sdk-java
url = https://github.com/SailReal/pcloud-sdk-java
path = lib/pcloud-sdk-java
url = https://github.com/SailReal/pcloud-sdk-java.git

View File

@ -31,6 +31,7 @@
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="999" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="999" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<Properties>
<option name="KEEP_BLANK_LINES" value="true" />
@ -178,10 +179,11 @@
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
</component>

2
.idea/misc.xml generated
View File

@ -45,7 +45,7 @@
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -3,6 +3,7 @@
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />

8
.idea/vcs.xml generated
View File

@ -2,8 +2,8 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/msa-auth-for-android" vcs="Git" />
<mapping directory="$PROJECT_DIR$/pcloud-sdk-java" vcs="Git" />
<mapping directory="$PROJECT_DIR$/subsampling-scale-image-view" vcs="Git" />
<mapping directory="$PROJECT_DIR$/lib/msa-auth-for-android" vcs="Git" />
<mapping directory="$PROJECT_DIR$/lib/pcloud-sdk-java" vcs="Git" />
<mapping directory="$PROJECT_DIR$/lib/subsampling-scale-image-view" vcs="Git" />
</component>
</project>
</project>

View File

@ -1,65 +1,80 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.3)
addressable (2.7.0)
CFPropertyList (3.0.4)
rexml
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
apktools (0.7.4)
rubyzip (~> 2.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.1.1)
aws-partitions (1.444.0)
aws-sdk-core (3.114.0)
aws-eventstream (1.2.0)
aws-partitions (1.509.0)
aws-sdk-core (3.121.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.43.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (1.48.0)
aws-sdk-core (~> 3, >= 3.120.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.93.1)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-s3 (1.103.0)
aws-sdk-core (~> 3, >= 3.120.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.3)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
bcrypt_pbkdf (1.0.1)
bcrypt_pbkdf (1.1.0)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.3)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
ed25519 (1.2.4)
emoji_regex (3.2.2)
excon (0.80.0)
faraday (1.3.0)
emoji_regex (3.2.3)
excon (0.86.0)
faraday (1.8.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0.1)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-net_http (1.0.1)
faraday_middleware (1.0.0)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday_middleware (1.1.0)
faraday (~> 1.0)
fastimage (2.2.3)
fastlane (2.180.1)
fastimage (2.2.5)
fastlane (2.195.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
@ -68,19 +83,20 @@ GEM
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.37.0, < 0.39.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
@ -89,79 +105,75 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-aws_s3 (2.0.2)
fastlane-plugin-aws_s3 (2.0.3)
apktools (~> 0.7)
aws-sdk-s3 (~> 1)
mime-types (~> 3.3)
fastlane-plugin-get_version_name (0.2.2)
gh_inspector (1.1.3)
google-api-client (0.38.0)
google-apis-androidpublisher_v3 (0.11.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-apis-core (0.3.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.14)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
retriable (>= 2.0, < 4.a)
rexml
signet (~> 0.14)
webrick
google-apis-iamcredentials_v1 (0.3.0)
google-apis-core (~> 0.1)
google-apis-storage_v1 (0.3.0)
google-apis-core (~> 0.1)
google-apis-iamcredentials_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.5.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.8.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.1.0)
google-cloud-storage (1.31.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.34.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (0.16.1)
googleauth (1.0.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.14)
highline (1.7.10)
http-cookie (1.0.3)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
json (2.5.1)
jwt (2.2.2)
jwt (2.2.3)
memoist (0.16.2)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0225)
mime-types-data (3.2021.0704)
mini_magick (4.11.0)
mini_mime (1.1.0)
mini_mime (1.1.1)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
net-sftp (2.1.2)
net-ssh (>= 2.6.5)
net-ssh (5.2.0)
net-sftp (3.0.0)
net-ssh (>= 5.0.0, < 7.0.0)
net-ssh (6.1.0)
optparse (0.1.1)
os (1.1.1)
plist (3.6.0)
public_suffix (4.0.6)
rake (13.0.3)
rake (13.0.6)
representable (3.1.1)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
@ -169,18 +181,17 @@ GEM
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
ruby2_keywords (0.0.4)
rubyzip (2.3.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.15.0)
addressable (~> 2.3)
signet (0.16.0)
addressable (~> 2.8)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
@ -192,16 +203,17 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
unf_ext (0.0.8)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.19.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)

View File

@ -3,7 +3,7 @@
[![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator)
[![Community](https://img.shields.io/badge/help-Community-orange.svg)](https://community.cryptomator.org)
[![Documentation](https://img.shields.io/badge/help-Docs-orange.svg)](https://docs.cryptomator.org)
[![Crowdin](https://badges.crowdin.net/cryptomator-android/localized.svg)](https://crowdin.com/project/cryptomator-android)
[![Crowdin](https://badges.crowdin.net/cryptomator/localized.svg)](https://translate.cryptomator.org/)
Cryptomator offers multi-platform transparent client-side encryption of your files in the cloud.
@ -19,7 +19,7 @@ Cryptomator for Android is currently available in the following distribution ch
### Dependencies
* Git
* JDK 8
* JDK 11
* Gradle
### Run Git and Gradle
@ -45,43 +45,6 @@ Please make sure before creating a PR, to apply the code style by executing refo
Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](.github/CODE_OF_CONDUCT.md).
## Deployment
Follow these steps to deploy a release:
1. Check `TODO`/`FIXME` comments
- Create issue for or delete
- Regexp for "Find in Path": `\W(TODO|FIXME)(?! #[0-9]{1,4}:)`
1. Merge translations
1. Check latest dependencies
1. Create release branch
1. Test database migration
1. Smoke-Test changed or added functionality
1. Update version
1. Create and commit release notes
1. Merge in `main`
1. Create tag and execute deploy app using Fastlane
1. Close GitHub-issues or move them to next milestone
1. Close milestone
1. Update version on website ([cryptomator.org/android](https://cryptomator.org/android/))
### Release Notes
Before tagging the release, create and commit the release notes. For Playstore create [fastlane/metadata/android/de-DE/changelogs/default.txt](https://github.com/cryptomator/android/blob/develop/fastlane/metadata/android/de-DE/changelogs/default.txt), [fastlane/metadata/android/en-US/changelogs/default.txt](https://github.com/cryptomator/android/blob/develop/fastlane/metadata/android/en-US/changelogs/default.txt) and for the website create [fastlane/release-notes.html](https://github.com/cryptomator/android/blob/develop/fastlane/release-notes.html).
### Deploy app using Fastlane
Deploy production version to Google Play, Website/GitHub-Releases and F-Droid using `fastlane android deploy` or `bundle exec fastlane deploy`
There are further targets and options like `beta`, see [fastlane/README.md](https://github.com/cryptomator/android/blob/develop/fastlane/README.md)
### Initial setup Fastlane
1. Make sure you copied `.default.env` to `.env` in the `fastlane` folder and filled out those variables.
1. Install Ruby (depends on OS, Ubuntu): `sudo apt install ruby-dev`
1. Install fastlane (depends on OS, Ubuntu): `gem install fastlane -N`
1. Install `fdroidserver` using `apt`, `pacman`, ..., see https://f-droid.org/docs/Installing_the_Server_and_Repo_Tools/
## License
This project is dual-licensed under the GPLv3 for FOSS projects as well as a commercial license for independent software vendors and resellers. If you want to modify this application under different conditions, feel free to contact our support team.

View File

@ -1,18 +1,15 @@
apply from: 'buildsystem/ci.gradle'
apply from: 'buildsystem/dependencies.gradle'
apply plugin: "com.vanniktech.android.junit.jacoco"
buildscript {
ext.kotlin_version = '1.4.32'
ext.kotlin_version = '1.5.31'
repositories {
jcenter()
mavenCentral()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0'
classpath 'com.fernandocejas.frodo:frodo-plugin:0.8.3'
classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.7.1.1"
@ -42,7 +39,7 @@ allprojects {
ext {
androidApplicationId = 'org.cryptomator'
androidVersionCode = getVersionCode()
androidVersionName = '1.5.18'
androidVersionName = '1.6.0'
}
repositories {
mavenCentral()

View File

@ -1,13 +0,0 @@
def ciServer = 'TRAVIS'
def executingOnCI = "true" == System.getenv(ciServer)
// Since for CI we always do full clean builds, we don't want to pre-dex
// See http://tools.android.com/tech-docs/new-build-system/tips
subprojects {
project.plugins.whenPluginAdded { plugin ->
if ('com.android.build.gradle.AppPlugin' == plugin.class.name ||
'com.android.build.gradle.LibraryPlugin' == plugin.class.name) {
project.android.dexOptions.preDexLibraries = !executingOnCI
}
}
}

View File

@ -1,12 +1,13 @@
allprojects {
repositories {
jcenter()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
ext {
androidBuildToolsVersion = "29.0.2"
androidMinSdkVersion = 23
androidBuildToolsVersion = "30.0.2"
androidMinSdkVersion = 24
androidTargetSdkVersion = 29
androidCompileSdkVersion = 29
@ -17,8 +18,10 @@ ext {
// support lib
androidSupportAnnotationsVersion = '1.2.0'
androidSupportAppcompatVersion = '1.2.0'
androidSupportDesignVersion = '1.3.0'
androidSupportAppcompatVersion = '1.3.1'
androidSupportDesignVersion = '1.4.0'
coreDesugaringVersion = '1.1.5'
// app frameworks and utilities
@ -26,70 +29,73 @@ ext {
rxAndroidVersion = '2.1.1'
rxBindingVersion = '2.2.0'
daggerVersion = '2.35'
daggerVersion = '2.39'
gsonVersion = '2.8.6'
gsonVersion = '2.8.8'
okHttpVersion = '4.9.1'
okHttpVersion = '4.9.2'
okHttpDigestVersion = '2.5'
velocityVersion = '1.7'
velocityVersion = '2.3'
timberVersion = '4.7.1'
timberVersion = '5.0.1'
zxcvbnVersion = '1.5.0'
zxcvbnVersion = '1.5.2'
scaleImageViewVersion = '3.10.0'
lruFileCacheVersion = '1.0'
lruFileCacheVersion = '1.2'
// KEEP IN SYNC WITH GENERATOR VERSION IN root build.gradle
greenDaoVersion = '3.3.0'
// cloud provider libs
cryptolibVersion = '2.0.2'
// do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x
cryptolibVersion = '1.3.0'
dropboxVersion = '4.0.1'
awsAndroidSdkS3 = '2.23.0'
dropboxVersion = '4.0.0'
googleApiServicesVersion = 'v3-rev197-1.25.0'
googlePlayServicesVersion = '19.0.0'
googleClientVersion = '1.31.4'
googleApiServicesVersion = 'v3-rev20210919-1.32.1'
googlePlayServicesVersion = '19.2.0'
googleClientVersion = '1.32.1' // keep in sync with https://github.com/SailReal/google-http-java-client
/*
update using https://github.com/SailReal/google-http-java-client with `mvn clean install`,
copying `google-http-client-*.jar` and `google-http-client-android-*.jar` into the lib folder of this project
*/
trackingFreeGoogleCLientVersion = '1.40.1'
msgraphVersion = '2.10.0'
minIoVersion = '8.3.0'
staxVersion = '1.2.0' // needed for minIO
commonsCodecVersion = '1.15'
recyclerViewFastScrollVersion = '2.0.1'
// testing dependencies
jUnitVersion = '5.7.1'
jUnit4Version = '4.13.1'
jUnitVersion = '5.8.1'
assertJVersion = '1.7.1'
mockitoVersion = '3.9.0'
mockitoInlineVersion = '3.9.0'
mockitoVersion = '3.12.4'
mockitoKotlinVersion = '3.2.0'
hamcrestVersion = '1.3'
dexmakerVersion = '1.0'
espressoVersion = '3.3.0'
espressoVersion = '3.4.0'
testingSupportLibVersion = '0.1'
runnerVersion = '1.3.0'
rulesVersion = '1.3.0'
contributionVersion = '3.3.0'
runnerVersion = '1.4.0'
rulesVersion = '1.4.0'
contributionVersion = '3.4.0'
uiautomatorVersion = '2.2.0'
androidxCoreVersion = '1.3.2'
androidxFragmentVersion = '1.3.2'
androidxCoreVersion = '1.6.0'
androidxFragmentVersion = '1.3.6'
androidxViewpagerVersion = '1.0.0'
androidxSwiperefreshVersion = '1.1.0'
androidxPreferenceVersion = '1.0.0' // 1.1.0 and 1.1.2 does have a bug with the text size
androidxRecyclerViewVersion = '1.2.0'
androidxPreferenceVersion = '1.1.1'
androidxRecyclerViewVersion = '1.2.1'
androidxDocumentfileVersion = '1.0.1'
androidxBiometricVersion = '1.1.0'
androidxTestCoreVersion = '1.3.0'
androidxTestCoreVersion = '1.4.0'
jsonWebTokenApiVersion = '0.11.2'
@ -103,7 +109,6 @@ ext {
androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}",
androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}",
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
awsAndroidS3 : "com.amazonaws:aws-android-sdk-s3:${awsAndroidSdkS3}",
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",
@ -112,11 +117,14 @@ ext {
dagger : "com.google.dagger:dagger:${daggerVersion}",
daggerCompiler : "com.google.dagger:dagger-compiler:${daggerVersion}",
design : "com.google.android.material:material:${androidSupportDesignVersion}",
coreDesugaring : "com.android.tools:desugar_jdk_libs:${coreDesugaringVersion}",
dropbox : "com.dropbox.core:dropbox-core-sdk:${dropboxVersion}",
espresso : "androidx.test.espresso:espresso-core:${espressoVersion}",
googleApiClientAndroid: "com.google.api-client:google-api-client-android:${googleClientVersion}",
googleApiServicesDrive: "com.google.apis:google-api-services-drive:${googleApiServicesVersion}",
googlePlayServicesAuth: "com.google.android.gms:play-services-auth:${googlePlayServicesVersion}",
trackingFreeGoogleCLient : files("lib/google-http-client-${trackingFreeGoogleCLientVersion}.jar"),
trackingFreeGoogleAndroidCLient: files("lib/google-http-client-android-${trackingFreeGoogleCLientVersion}.jar"),
greenDao : "org.greenrobot:greendao:${greenDaoVersion}",
gson : "com.google.code.gson:gson:${gsonVersion}",
hamcrest : "org.hamcrest:hamcrest-all:${hamcrestVersion}",
@ -125,28 +133,30 @@ ext {
junitApi : "org.junit.jupiter:junit-jupiter-api:${jUnitVersion}",
junitEngine : "org.junit.jupiter:junit-jupiter-engine:${jUnitVersion}",
junitParams : "org.junit.jupiter:junit-jupiter-params:${jUnitVersion}",
junit4 : "org.junit.jupiter:junit-jupiter:${jUnit4Version}",
junit4Engine : "org.junit.vintage:junit-vintage-engine:${jUnitVersion}",
msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}",
minIo : "io.minio:minio:${minIoVersion}",
mockito : "org.mockito:mockito-core:${mockitoVersion}",
mockitoInline : "org.mockito:mockito-inline:${mockitoInlineVersion}",
mockitoInline : "org.mockito:mockito-inline:${mockitoVersion}",
mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:${mockitoKotlinVersion}",
msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}",
multidex : "androidx.multidex:multidex:${multidexVersion}",
okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}",
okHttpDigest : "com.burgstaller:okhttp-digest:${okHttpDigestVersion}",
okHttpDigest : "io.github.rburgst:okhttp-digest:${okHttpDigestVersion}",
recyclerViewFastScroll: "com.simplecityapps:recyclerview-fastscroll:${recyclerViewFastScrollVersion}",
rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}",
rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}",
rxBinding : "com.jakewharton.rxbinding2:rxbinding:${rxBindingVersion}",
stax : "stax:stax:${staxVersion}",
testingSupportLib : "com.android.support.test:testing-support-lib:${testingSupportLibVersion}",
timber : "com.jakewharton.timber:timber:${timberVersion}",
velocity : "org.apache.velocity:velocity:${velocityVersion}",
velocity : "org.apache.velocity:velocity-engine-core:${velocityVersion}",
runner : "androidx.test:runner:${runnerVersion}",
rules : "androidx.test:rules:${rulesVersion}",
contribution : "androidx.test.espresso:espresso-contrib:${contributionVersion}",
uiAutomator : "androidx.test.uiautomator:uiautomator:${uiautomatorVersion}",
zxcvbn : "com.nulab-inc:zxcvbn:${zxcvbnVersion}",
scaleImageView : "com.davemorrissey.labs:subsampling-scale-image-view:${scaleImageViewVersion}",
lruFileCache : "com.tomclaw.cache:cache:${lruFileCacheVersion}",
lruFileCache : "com.github.solkin:disk-lru-cache:${lruFileCacheVersion}",
jsonWebTokenApi : "io.jsonwebtoken:jjwt-api:${jsonWebTokenApiVersion}",
jsonWebTokenImpl : "io.jsonwebtoken:jjwt-impl:${jsonWebTokenApiVersion}",
jsonWebTokenJson : "io.jsonwebtoken:jjwt-orgjson:${jsonWebTokenApiVersion}"

View File

@ -17,11 +17,15 @@ android {
buildConfigField 'int', 'VERSION_CODE', "${globalConfiguration["androidVersionCode"]}"
buildConfigField "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\""
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
}
lintOptions {
@ -71,10 +75,14 @@ android {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/']
}
}
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
}
}
greendao {
schemaVersion 6
schemaVersion 9
}
configurations.all {
@ -90,6 +98,8 @@ dependencies {
implementation project(':msa-auth-for-android')
implementation project(':pcloud-sdk-java')
coreLibraryDesugaring dependencies.coreDesugaring
// cryptomator
implementation dependencies.cryptolib
@ -98,32 +108,66 @@ dependencies {
// dagger
annotationProcessor dependencies.daggerCompiler
implementation dependencies.dagger
api dependencies.jsonWebTokenApi
implementation dependencies.jsonWebTokenImpl
implementation dependencies.jsonWebTokenJson
// cloud
implementation dependencies.awsAndroidS3
implementation dependencies.dropbox
implementation dependencies.msgraph
playstoreImplementation dependencies.googlePlayServicesAuth
apkstoreImplementation dependencies.googlePlayServicesAuth
implementation dependencies.stax
api dependencies.minIo
playstoreImplementation(dependencies.googlePlayServicesAuth) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: 'googlehttpclient'
exclude group: "com.google.http-client", module: "google-http-client"
}
apkstoreImplementation(dependencies.googlePlayServicesAuth) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: "google-http-client"
exclude group: "com.google.http-client", module: "google-http-client"
}
playstoreImplementation(dependencies.googleApiServicesDrive) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: 'googlehttpclient'
exclude group: "com.google.http-client", module: "google-http-client"
}
apkstoreImplementation(dependencies.googleApiServicesDrive) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: "google-http-client"
exclude group: "com.google.http-client", module: "google-http-client"
}
playstoreImplementation(dependencies.googleApiClientAndroid) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: "google-http-client"
exclude module: "jetified-google-http-client"
exclude group: "com.google.http-client", module: "google-http-client"
exclude group: "com.google.http-client", module: "jetified-google-http-client"
}
apkstoreImplementation(dependencies.googleApiClientAndroid) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: "google-http-client"
exclude module: "jetified-google-http-client"
exclude group: "com.google.http-client", module: "google-http-client"
exclude group: "com.google.http-client", module: "jetified-google-http-client"
}
playstoreImplementation dependencies.trackingFreeGoogleCLient
apkstoreImplementation dependencies.trackingFreeGoogleCLient
playstoreImplementation dependencies.trackingFreeGoogleAndroidCLient
apkstoreImplementation dependencies.trackingFreeGoogleAndroidCLient
// rest
implementation dependencies.rxJava
implementation dependencies.rxAndroid
@ -146,12 +190,16 @@ dependencies {
testImplementation dependencies.junitApi
testRuntimeOnly dependencies.junitEngine
testImplementation dependencies.junitParams
testImplementation dependencies.junit4
testRuntimeOnly dependencies.junit4Engine
testImplementation dependencies.mockito
testImplementation dependencies.mockitoKotlin
testImplementation dependencies.mockitoInline
testImplementation dependencies.hamcrest
androidTestImplementation(dependencies.runner) {
exclude group: 'com.android.support', module: 'support-annotations'
}
}
configurations {
@ -161,3 +209,16 @@ configurations {
static def getApiKey(key) {
return System.getenv().getOrDefault(key, "")
}
tasks.withType(Test) {
testLogging {
events "failed"
showExceptions true
exceptionFormat "full"
showCauses true
showStackTraces true
showStandardStreams = false
}
}

View File

@ -0,0 +1,410 @@
package org.cryptomator.data.db
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import androidx.test.InstrumentationRegistry
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
import org.cryptomator.data.db.entities.CloudEntityDao
import org.cryptomator.data.db.entities.UpdateCheckEntityDao
import org.cryptomator.data.db.entities.VaultEntityDao
import org.cryptomator.domain.CloudType
import org.cryptomator.util.SharedPreferencesHandler
import org.cryptomator.util.crypto.CredentialCryptor
import org.greenrobot.greendao.database.Database
import org.greenrobot.greendao.database.StandardDatabase
import org.greenrobot.greendao.internal.DaoConfig
import org.hamcrest.CoreMatchers
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@SmallTest
class UpgradeDatabaseTest {
private val context = InstrumentationRegistry.getTargetContext()
private val sharedPreferencesHandler = SharedPreferencesHandler(context)
private lateinit var db: Database
@Before
fun setup() {
db = StandardDatabase(SQLiteDatabase.create(null))
}
@After
fun tearDown() {
db.close()
}
@Test
fun upgradeAll() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
Upgrade3To4().applyTo(db, 3)
Upgrade4To5().applyTo(db, 4)
Upgrade5To6().applyTo(db, 5)
Upgrade6To7().applyTo(db, 6)
Upgrade7To8().applyTo(db, 7)
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll()
VaultEntityDao(DaoConfig(db, VaultEntityDao::class.java)).loadAll()
UpdateCheckEntityDao(DaoConfig(db, UpdateCheckEntityDao::class.java)).loadAll()
}
@Test
fun upgrade2To3() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
val url = "url"
val username = "username"
val webdavCertificate = "webdavCertificate"
val accessToken = "accessToken"
Sql.update("CLOUD_ENTITY")
.where("TYPE", Sql.eq("DROPBOX"))
.set("ACCESS_TOKEN", Sql.toString(accessToken))
.set("WEBDAV_URL", Sql.toString(url))
.set("USERNAME", Sql.toString(username))
.set("WEBDAV_CERTIFICATE", Sql.toString(webdavCertificate))
.executeOn(db)
Sql.update("CLOUD_ENTITY")
.where("TYPE", Sql.eq("ONEDRIVE"))
.set("ACCESS_TOKEN", Sql.toString("NOT USED"))
.set("WEBDAV_URL", Sql.toString(url))
.set("USERNAME", Sql.toString(username))
.set("WEBDAV_CERTIFICATE", Sql.toString(webdavCertificate))
.executeOn(db)
context.getSharedPreferences("com.microsoft.live", Context.MODE_PRIVATE).edit().putString("refresh_token", accessToken).commit()
Upgrade2To3(context).applyTo(db, 2)
checkUpgrade2to3ResultForCloud("DROPBOX", accessToken, url, username, webdavCertificate)
checkUpgrade2to3ResultForCloud("ONEDRIVE", accessToken, url, username, webdavCertificate)
Assert.assertThat(context.getSharedPreferences("com.microsoft.live", Context.MODE_PRIVATE).getString("refresh_token", null), CoreMatchers.nullValue())
}
private fun checkUpgrade2to3ResultForCloud(cloudName: String, accessToken: String, url: String, username: String, webdavCertificate: String) {
Sql.query("CLOUD_ENTITY").where("TYPE", Sql.eq(cloudName)).executeOn(db).use {
it.moveToFirst()
Assert.assertThat(CredentialCryptor.getInstance(context).decrypt(it.getString(it.getColumnIndex("ACCESS_TOKEN"))), CoreMatchers.`is`(accessToken))
Assert.assertThat(it.getString(it.getColumnIndex("WEBDAV_URL")), CoreMatchers.`is`(url))
Assert.assertThat(it.getString(it.getColumnIndex("USERNAME")), CoreMatchers.`is`(username))
Assert.assertThat(it.getString(it.getColumnIndex("WEBDAV_CERTIFICATE")), CoreMatchers.`is`(webdavCertificate))
}
}
@Test
fun upgrade3To4() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
val ids = arrayOf("10", "20", "31", "32", "51")
ids.forEach {
Sql.insertInto("VAULT_ENTITY") //
.integer("_id", Integer.parseInt(it)) //
.integer("FOLDER_CLOUD_ID", 1) //
.text("FOLDER_PATH", "path${it}") //
.text("FOLDER_NAME", "name${it}") //
.text("CLOUD_TYPE", CloudType.DROPBOX.name) //
.text("PASSWORD", "password${it}") //
.executeOn(db)
}
Upgrade3To4().applyTo(db, 3)
Sql.query("VAULT_ENTITY").where("CLOUD_TYPE", Sql.eq(CloudType.DROPBOX.name)).executeOn(db).use {
Assert.assertThat(it.count, CoreMatchers.`is`(ids.size))
while (it.moveToNext()) {
Assert.assertThat(it.getString(it.getColumnIndex("_id")), CoreMatchers.`is`(ids[it.position]))
Assert.assertThat(it.getInt(it.getColumnIndex("POSITION")), CoreMatchers.`is`(it.position))
Assert.assertThat(it.getInt(it.getColumnIndex("FOLDER_CLOUD_ID")), CoreMatchers.`is`(1))
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_PATH")), CoreMatchers.`is`("path${ids[it.position]}"))
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_NAME")), CoreMatchers.`is`("name${ids[it.position]}"))
Assert.assertThat(it.getString(it.getColumnIndex("CLOUD_TYPE")), CoreMatchers.`is`(CloudType.DROPBOX.name))
Assert.assertThat(it.getString(it.getColumnIndex("PASSWORD")), CoreMatchers.`is`("password${ids[it.position]}"))
}
}
}
@Test
fun upgrade4To5() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
Upgrade3To4().applyTo(db, 3)
val cloudId = 15
val cloudUrl = "url"
val username = "username"
val webdavCertificate = "webdavCertificate"
val accessToken = "accessToken"
val vaultId = 25
val folderPath = "path"
val folderName = "name"
val password = "password"
val position = 10
Sql.insertInto("CLOUD_ENTITY") //
.integer("_id", cloudId) //
.text("TYPE", CloudType.WEBDAV.name) //
.text("ACCESS_TOKEN", accessToken) //
.text("WEBDAV_URL", cloudUrl) //
.text("USERNAME", username) //
.text("WEBDAV_CERTIFICATE", webdavCertificate) //
.executeOn(db)
Sql.insertInto("VAULT_ENTITY") //
.integer("_id", vaultId) //
.integer("FOLDER_CLOUD_ID", cloudId) //
.text("FOLDER_PATH", folderPath) //
.text("FOLDER_NAME", folderName) //
.text("CLOUD_TYPE", CloudType.WEBDAV.name) //
.text("PASSWORD", password) //
.integer("POSITION", position) //
.executeOn(db)
Upgrade4To5().applyTo(db, 4)
Sql.query("CLOUD_ENTITY").where("TYPE", Sql.eq(CloudType.WEBDAV.name)).executeOn(db).use {
it.moveToFirst()
Assert.assertThat(it.getInt(it.getColumnIndex("_id")), CoreMatchers.`is`(cloudId))
Assert.assertThat(it.getString(it.getColumnIndex("ACCESS_TOKEN")), CoreMatchers.`is`(accessToken))
Assert.assertThat(it.getString(it.getColumnIndex("URL")), CoreMatchers.`is`(cloudUrl))
Assert.assertThat(it.getString(it.getColumnIndex("USERNAME")), CoreMatchers.`is`(username))
Assert.assertThat(it.getString(it.getColumnIndex("WEBDAV_CERTIFICATE")), CoreMatchers.`is`(webdavCertificate))
}
Sql.query("VAULT_ENTITY").where("CLOUD_TYPE", Sql.eq(CloudType.WEBDAV.name)).executeOn(db).use {
it.moveToFirst()
Assert.assertThat(it.getInt(it.getColumnIndex("_id")), CoreMatchers.`is`(vaultId))
Assert.assertThat(it.getInt(it.getColumnIndex("FOLDER_CLOUD_ID")), CoreMatchers.`is`(cloudId))
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_PATH")), CoreMatchers.`is`(folderPath))
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_NAME")), CoreMatchers.`is`(folderName))
Assert.assertThat(it.getString(it.getColumnIndex("CLOUD_TYPE")), CoreMatchers.`is`(CloudType.WEBDAV.name))
Assert.assertThat(it.getString(it.getColumnIndex("PASSWORD")), CoreMatchers.`is`(password))
Assert.assertThat(it.getInt(it.getColumnIndex("POSITION")), CoreMatchers.`is`(position))
}
}
@Test
fun upgrade5To6() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
Upgrade3To4().applyTo(db, 3)
Upgrade4To5().applyTo(db, 4)
val cloudId = 15
val cloudUrl = "url"
val username = "username"
val webdavCertificate = "webdavCertificate"
val accessToken = "accessToken"
val vaultId = 25
val folderPath = "path"
val folderName = "name"
val password = "password"
val position = 10
Sql.insertInto("CLOUD_ENTITY") //
.integer("_id", cloudId) //
.text("TYPE", CloudType.WEBDAV.name) //
.text("ACCESS_TOKEN", accessToken) //
.text("URL", cloudUrl) //
.text("USERNAME", username) //
.text("WEBDAV_CERTIFICATE", webdavCertificate) //
.executeOn(db)
Sql.insertInto("VAULT_ENTITY") //
.integer("_id", vaultId) //
.integer("FOLDER_CLOUD_ID", cloudId) //
.text("FOLDER_PATH", folderPath) //
.text("FOLDER_NAME", folderName) //
.text("CLOUD_TYPE", CloudType.WEBDAV.name) //
.text("PASSWORD", password) //
.integer("POSITION", position) //
.executeOn(db)
Upgrade5To6().applyTo(db, 5)
Sql.query("CLOUD_ENTITY").where("TYPE", Sql.eq(CloudType.WEBDAV.name)).executeOn(db).use {
it.moveToFirst()
Assert.assertThat(it.getInt(it.getColumnIndex("_id")), CoreMatchers.`is`(cloudId))
Assert.assertThat(it.getString(it.getColumnIndex("ACCESS_TOKEN")), CoreMatchers.`is`(accessToken))
Assert.assertThat(it.getString(it.getColumnIndex("URL")), CoreMatchers.`is`(cloudUrl))
Assert.assertThat(it.getString(it.getColumnIndex("USERNAME")), CoreMatchers.`is`(username))
Assert.assertThat(it.getString(it.getColumnIndex("WEBDAV_CERTIFICATE")), CoreMatchers.`is`(webdavCertificate))
}
Sql.query("VAULT_ENTITY").where("CLOUD_TYPE", Sql.eq(CloudType.WEBDAV.name)).executeOn(db).use {
it.moveToFirst()
Assert.assertThat(it.getInt(it.getColumnIndex("_id")), CoreMatchers.`is`(vaultId))
Assert.assertThat(it.getInt(it.getColumnIndex("FOLDER_CLOUD_ID")), CoreMatchers.`is`(cloudId))
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_PATH")), CoreMatchers.`is`(folderPath))
Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_NAME")), CoreMatchers.`is`(folderName))
Assert.assertThat(it.getString(it.getColumnIndex("CLOUD_TYPE")), CoreMatchers.`is`(CloudType.WEBDAV.name))
Assert.assertThat(it.getString(it.getColumnIndex("PASSWORD")), CoreMatchers.`is`(password))
Assert.assertThat(it.getInt(it.getColumnIndex("POSITION")), CoreMatchers.`is`(position))
}
}
@Test
fun upgrade6To7() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
Upgrade3To4().applyTo(db, 3)
Upgrade4To5().applyTo(db, 4)
Upgrade5To6().applyTo(db, 5)
val licenseToken = "licenseToken"
val releaseNote = "releaseNote"
val version = "version"
val urlApk = "urlApk"
val urlReleaseNote = "urlReleaseNote"
Sql.update("UPDATE_CHECK_ENTITY")
.set("LICENSE_TOKEN", Sql.toString(licenseToken))
.set("RELEASE_NOTE", Sql.toString(releaseNote))
.set("VERSION", Sql.toString(version))
.set("URL_TO_APK", Sql.toString(urlApk))
.set("URL_TO_RELEASE_NOTE", Sql.toString(urlReleaseNote))
.executeOn(db)
Upgrade6To7().applyTo(db, 6)
Sql.query("UPDATE_CHECK_ENTITY").executeOn(db).use {
it.moveToFirst()
Assert.assertThat(it.getString(it.getColumnIndex("LICENSE_TOKEN")), CoreMatchers.`is`(licenseToken))
Assert.assertThat(it.getString(it.getColumnIndex("RELEASE_NOTE")), CoreMatchers.`is`(releaseNote))
Assert.assertThat(it.getString(it.getColumnIndex("VERSION")), CoreMatchers.`is`(version))
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_APK")), CoreMatchers.`is`(urlApk))
Assert.assertThat(it.getString(it.getColumnIndex("APK_SHA256")), CoreMatchers.nullValue())
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_RELEASE_NOTE")), CoreMatchers.`is`(urlReleaseNote))
}
}
@Test
fun recoverUpgrade6to7DueToSQLiteExceptionThrown() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
Upgrade3To4().applyTo(db, 3)
Upgrade4To5().applyTo(db, 4)
Upgrade5To6().applyTo(db, 5)
val licenseToken = "licenseToken"
Sql.update("UPDATE_CHECK_ENTITY")
.set("LICENSE_TOKEN", Sql.toString(licenseToken))
.set("RELEASE_NOTE", Sql.toString("releaseNote"))
.set("VERSION", Sql.toString("version"))
.set("URL_TO_APK", Sql.toString("urlApk"))
.set("URL_TO_RELEASE_NOTE", Sql.toString("urlReleaseNote"))
.executeOn(db)
Sql.alterTable("UPDATE_CHECK_ENTITY").renameTo("UPDATE_CHECK_ENTITY_OLD").executeOn(db)
Sql.createTable("UPDATE_CHECK_ENTITY") //
.id() //
.optionalText("LICENSE_TOKEN") //
.optionalText("RELEASE_NOTE") //
.optionalText("VERSION") //
.optionalText("URL_TO_APK") //
.optionalText("APK_SHA256") //
.optionalText("URL_TO_RELEASE_NOTE") //
.executeOn(db)
Upgrade6To7().tryToRecoverFromSQLiteException(db)
Sql.query("UPDATE_CHECK_ENTITY").executeOn(db).use {
it.moveToFirst()
Assert.assertThat(it.getString(it.getColumnIndex("LICENSE_TOKEN")), CoreMatchers.`is`(licenseToken))
Assert.assertThat(it.getString(it.getColumnIndex("RELEASE_NOTE")), CoreMatchers.nullValue())
Assert.assertThat(it.getString(it.getColumnIndex("VERSION")), CoreMatchers.nullValue())
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_APK")), CoreMatchers.nullValue())
Assert.assertThat(it.getString(it.getColumnIndex("APK_SHA256")), CoreMatchers.nullValue())
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_RELEASE_NOTE")), CoreMatchers.nullValue())
}
}
@Test
fun upgrade7To8() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
Upgrade3To4().applyTo(db, 3)
Upgrade4To5().applyTo(db, 4)
Upgrade5To6().applyTo(db, 5)
Upgrade6To7().applyTo(db, 6)
Sql.insertInto("CLOUD_ENTITY") //
.integer("_id", 15) //
.text("TYPE", CloudType.S3.name) //
.text("URL", "url") //
.text("USERNAME", "username") //
.text("WEBDAV_CERTIFICATE", "certificate") //
.text("ACCESS_TOKEN", "accessToken")
.text("S3_BUCKET", "s3Bucket") //
.text("S3_REGION", "s3Region") //
.text("S3_SECRET_KEY", "s3SecretKey") //
.executeOn(db)
Sql.insertInto("VAULT_ENTITY") //
.integer("_id", 25) //
.integer("FOLDER_CLOUD_ID", 15) //
.text("FOLDER_PATH", "path") //
.text("FOLDER_NAME", "name") //
.text("CLOUD_TYPE", CloudType.S3.name) //
.text("PASSWORD", "password") //
.integer("POSITION", 10) //
.executeOn(db)
Sql.query("CLOUD_ENTITY").executeOn(db).use {
Assert.assertThat(it.count, CoreMatchers.`is`(5))
}
Upgrade7To8().applyTo(db, 7)
Sql.query("CLOUD_ENTITY").executeOn(db).use {
Assert.assertThat(it.count, CoreMatchers.`is`(4))
}
Sql.query("VAULT_ENTITY").executeOn(db).use {
Assert.assertThat(it.moveToFirst(), CoreMatchers.`is`(false))
}
}
@Test
fun upgrade8To9() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
Upgrade3To4().applyTo(db, 3)
Upgrade4To5().applyTo(db, 4)
Upgrade5To6().applyTo(db, 5)
Upgrade6To7().applyTo(db, 6)
Upgrade7To8().applyTo(db, 7)
sharedPreferencesHandler.setBetaScreenDialogAlreadyShown(true)
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
Assert.assertThat(sharedPreferencesHandler.isBetaModeAlreadyShown(), CoreMatchers.`is`(false))
}
}

View File

@ -1,223 +0,0 @@
package org.cryptomator.data.cloud;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import java.io.File;
import java.io.OutputStream;
import java.util.List;
public abstract class InterceptingCloudContentRepository<CloudType extends Cloud, NodeType extends CloudNode, DirType extends CloudFolder, FileType extends CloudFile> implements CloudContentRepository<CloudType, NodeType, DirType, FileType> {
private final CloudContentRepository<CloudType, NodeType, DirType, FileType> delegate;
protected InterceptingCloudContentRepository(CloudContentRepository<CloudType, NodeType, DirType, FileType> delegate) {
this.delegate = delegate;
}
protected abstract void throwWrappedIfRequired(Exception e) throws BackendException;
@Override
public DirType root(CloudType cloud) throws BackendException {
try {
return delegate.root(cloud);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public DirType resolve(CloudType cloud, String path) throws BackendException {
try {
return delegate.resolve(cloud, path);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public FileType file(DirType parent, String name) throws BackendException {
try {
return delegate.file(parent, name);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public FileType file(DirType parent, String name, Optional<Long> size) throws BackendException {
try {
return delegate.file(parent, name, size);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public DirType folder(DirType parent, String name) throws BackendException {
try {
return delegate.folder(parent, name);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public boolean exists(NodeType node) throws BackendException {
try {
return delegate.exists(node);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public List<? extends CloudNode> list(DirType folder) throws BackendException {
try {
return delegate.list(folder);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public DirType create(DirType folder) throws BackendException {
try {
return delegate.create(folder);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public DirType move(DirType source, DirType target) throws BackendException {
try {
return delegate.move(source, target);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public FileType move(FileType source, FileType target) throws BackendException {
try {
return delegate.move(source, target);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public FileType write(FileType file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
try {
return delegate.write(file, data, progressAware, replace, size);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public void read(FileType file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
try {
delegate.read(file, encryptedTmpFile, data, progressAware);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public void delete(NodeType node) throws BackendException {
try {
delegate.delete(node);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public String checkAuthenticationAndRetrieveCurrentAccount(CloudType cloud) throws BackendException {
try {
return delegate.checkAuthenticationAndRetrieveCurrentAccount(cloud);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
@Override
public void logout(CloudType cloud) throws BackendException {
try {
delegate.logout(cloud);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
}
}

View File

@ -0,0 +1,216 @@
package org.cryptomator.data.cloud
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.UploadState
import java.io.File
import java.io.OutputStream
abstract class InterceptingCloudContentRepository<CloudType : Cloud, NodeType : CloudNode, DirType : CloudFolder, FileType : CloudFile> protected constructor(private val delegate: CloudContentRepository<CloudType, NodeType, DirType, FileType>) :
CloudContentRepository<CloudType, NodeType, DirType, FileType> {
@Throws(BackendException::class)
protected abstract fun throwWrappedIfRequired(e: Exception)
@Throws(BackendException::class)
override fun root(cloud: CloudType): DirType {
return try {
delegate.root(cloud)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun resolve(cloud: CloudType, path: String): DirType {
return try {
delegate.resolve(cloud, path)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun file(parent: DirType, name: String): FileType {
return try {
delegate.file(parent, name)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun file(parent: DirType, name: String, size: Long?): FileType {
return try {
delegate.file(parent, name, size)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun folder(parent: DirType, name: String): DirType {
return try {
delegate.folder(parent, name)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun exists(node: NodeType): Boolean {
return try {
delegate.exists(node)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun list(folder: DirType): List<NodeType> {
return try {
delegate.list(folder)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun create(folder: DirType): DirType {
return try {
delegate.create(folder)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun move(source: DirType, target: DirType): DirType {
return try {
delegate.move(source, target)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun move(source: FileType, target: FileType): FileType {
return try {
delegate.move(source, target)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun write(file: FileType, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): FileType {
return try {
delegate.write(file, data, progressAware, replace, size)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun read(file: FileType, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
try {
delegate.read(file, encryptedTmpFile, data, progressAware)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun delete(node: NodeType) {
try {
delegate.delete(node)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: CloudType): String {
return try {
delegate.checkAuthenticationAndRetrieveCurrentAccount(cloud)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
@Throws(BackendException::class)
override fun logout(cloud: CloudType) {
try {
delegate.logout(cloud)
} catch (e: BackendException) {
throwWrappedIfRequired(e)
throw e
} catch (e: RuntimeException) {
throwWrappedIfRequired(e)
throw e
}
}
}

View File

@ -1,28 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import com.google.common.io.BaseEncoding;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Utility class for generating a suffix for the backup file to make it unique to its original master key file.
*/
class BackupFileIdSuffixGenerator {
/**
* Computes the SHA-256 digest of the given byte array and returns a file suffix containing the first 4 bytes in hex string format.
*
* @param fileBytes the input byte for which the digest is computed
* @return "." + first 4 bytes of SHA-256 digest in hex string format
*/
static String generate(byte[] fileBytes) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(fileBytes);
return "." + BaseEncoding.base16().encode(digest, 0, 4);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Every Java Platform must support the Message Digest algorithm SHA-256", e);
}
}
}

View File

@ -0,0 +1,28 @@
package org.cryptomator.data.cloud.crypto
import com.google.common.io.BaseEncoding
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
/**
* Utility class for generating a suffix for the backup file to make it unique to its original master key file.
*/
internal object BackupFileIdSuffixGenerator {
/**
* Computes the SHA-256 digest of the given byte array and returns a file suffix containing the first 4 bytes in hex string format.
*
* @param fileBytes the input byte for which the digest is computed
* @return "." + first 4 bytes of SHA-256 digest in hex string format
*/
@JvmStatic
fun generate(fileBytes: ByteArray): String {
return try {
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(fileBytes)
"." + BaseEncoding.base16().encode(digest, 0, 4)
} catch (e: NoSuchAlgorithmException) {
throw IllegalStateException("Every Java Platform must support the Message Digest algorithm SHA-256", e)
}
}
}

View File

@ -1,135 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import android.content.Context;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import org.cryptomator.util.Supplier;
import java.io.File;
import java.io.OutputStream;
import java.util.List;
import static java.lang.String.format;
class CryptoCloudContentRepository implements CloudContentRepository<CryptoCloud, CryptoNode, CryptoFolder, CryptoFile> {
private final CryptoImplDecorator cryptoImpl;
CryptoCloudContentRepository(Context context, CloudContentRepository cloudContentRepository, CryptoCloud cloud, Supplier<Cryptor> cryptor) {
CloudFolder vaultLocation;
try {
vaultLocation = cloudContentRepository.resolve(cloud.getVault().getCloud(), cloud.getVault().getPath());
} catch (BackendException e) {
throw new FatalBackendException(e);
}
switch (cloud.getVault().getVersion()) {
case 7:
this.cryptoImpl = new CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7());
break;
case 6:
case 5:
this.cryptoImpl = new CryptoImplVaultFormatPre7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormatPre7());
break;
default:
throw new IllegalStateException(format("No CryptoImpl for vault version %d.", cloud.getVault().getVersion()));
}
}
@Override
public synchronized CryptoFolder root(CryptoCloud cloud) throws BackendException {
return cryptoImpl.root(cloud);
}
@Override
public CryptoFolder resolve(CryptoCloud cloud, String path) throws BackendException {
return cryptoImpl.resolve(cloud, path);
}
@Override
public CryptoFile file(CryptoFolder parent, String name) throws BackendException {
return cryptoImpl.file(parent, name);
}
@Override
public CryptoFile file(CryptoFolder parent, String name, Optional<Long> size) throws BackendException {
return cryptoImpl.file(parent, name, size);
}
@Override
public CryptoFolder folder(CryptoFolder parent, String name) throws BackendException {
return cryptoImpl.folder(parent, name);
}
@Override
public boolean exists(CryptoNode node) throws BackendException {
return cryptoImpl.exists(node);
}
@Override
public List<CryptoNode> list(CryptoFolder folder) throws BackendException {
return cryptoImpl.list(folder);
}
@Override
public CryptoFolder create(CryptoFolder folder) throws BackendException {
try {
return cryptoImpl.create(folder);
} catch (CloudNodeAlreadyExistsException e) {
throw new CloudNodeAlreadyExistsException(folder.getName());
}
}
@Override
public CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException {
try {
return cryptoImpl.move(source, target);
} catch (CloudNodeAlreadyExistsException e) {
throw new CloudNodeAlreadyExistsException(target.getName());
}
}
@Override
public CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException {
try {
return cryptoImpl.move(source, target);
} catch (CloudNodeAlreadyExistsException e) {
throw new CloudNodeAlreadyExistsException(target.getName());
}
}
@Override
public CryptoFile write(CryptoFile file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException {
return cryptoImpl.write(file, data, progressAware, replace, length);
}
@Override
public void read(CryptoFile file, Optional<File> tmpEncryptedFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
cryptoImpl.read(file, data, progressAware);
}
@Override
public void delete(CryptoNode node) throws BackendException {
cryptoImpl.delete(node);
}
@Override
public String checkAuthenticationAndRetrieveCurrentAccount(CryptoCloud cloud) throws BackendException {
return cryptoImpl.currentAccount(cloud);
}
@Override
public void logout(CryptoCloud cloud) throws BackendException {
// empty
}
}

View File

@ -0,0 +1,128 @@
package org.cryptomator.data.cloud.crypto
import android.content.Context
import org.cryptomator.cryptolib.api.Cryptor
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.UploadState
import java.io.File
import java.io.OutputStream
import java.util.function.Supplier
internal class CryptoCloudContentRepository(context: Context, cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>, cloud: CryptoCloud, cryptor: Supplier<Cryptor>) :
CloudContentRepository<CryptoCloud, CryptoNode, CryptoFolder, CryptoFile> {
private var cryptoImpl: CryptoImplDecorator
@Synchronized
@Throws(BackendException::class)
override fun root(cloud: CryptoCloud): CryptoFolder {
return cryptoImpl.root(cloud)
}
override fun resolve(cloud: CryptoCloud, path: String): CryptoFolder {
return cryptoImpl.resolve(cloud, path)
}
@Throws(BackendException::class)
override fun file(parent: CryptoFolder, name: String): CryptoFile {
return cryptoImpl.file(parent, name)
}
@Throws(BackendException::class)
override fun file(parent: CryptoFolder, name: String, size: Long?): CryptoFile {
return cryptoImpl.file(parent, name, size)
}
@Throws(BackendException::class)
override fun folder(parent: CryptoFolder, name: String): CryptoFolder {
return cryptoImpl.folder(parent, name)
}
@Throws(BackendException::class)
override fun exists(node: CryptoNode): Boolean {
return cryptoImpl.exists(node)
}
@Throws(BackendException::class)
override fun list(folder: CryptoFolder): List<CryptoNode> {
return cryptoImpl.list(folder)
}
@Throws(BackendException::class)
override fun create(folder: CryptoFolder): CryptoFolder {
return try {
cryptoImpl.create(folder)
} catch (e: CloudNodeAlreadyExistsException) {
throw CloudNodeAlreadyExistsException(folder.name)
}
}
@Throws(BackendException::class)
override fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder {
return try {
cryptoImpl.move(source, target)
} catch (e: CloudNodeAlreadyExistsException) {
throw CloudNodeAlreadyExistsException(target.name)
}
}
@Throws(BackendException::class)
override fun move(source: CryptoFile, target: CryptoFile): CryptoFile {
return try {
cryptoImpl.move(source, target)
} catch (e: CloudNodeAlreadyExistsException) {
throw CloudNodeAlreadyExistsException(target.name)
}
}
@Throws(BackendException::class)
override fun write(file: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): CryptoFile {
return cryptoImpl.write(file, data, progressAware, replace, size)
}
@Throws(BackendException::class)
override fun read(file: CryptoFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
cryptoImpl.read(file, data, progressAware)
}
@Throws(BackendException::class)
override fun delete(node: CryptoNode) {
cryptoImpl.delete(node)
}
@Throws(BackendException::class)
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: CryptoCloud): String {
return cryptoImpl.currentAccount(cloud)
}
@Throws(BackendException::class)
override fun logout(cloud: CryptoCloud) {
// empty
}
init {
val vaultLocation: CloudFolder = try {
cloudContentRepository.resolve(cloud.vault.cloud, cloud.vault.path)
} catch (e: BackendException) {
throw FatalBackendException(e)
}
cryptoImpl = when (cloud.vault.format) {
7 -> CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormat7())
8 -> CryptoImplVaultFormat8(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormat7(), cloud.vault.shorteningThreshold)
6, 5 -> CryptoImplVaultFormatPre7(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormatPre7())
else -> throw IllegalStateException(String.format("No CryptoImpl for vault format %d.", cloud.vault.format))
}
}
}

View File

@ -2,13 +2,14 @@ package org.cryptomator.data.cloud.crypto;
import android.content.Context;
import com.google.common.base.Optional;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.MissingCryptorException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -38,7 +39,7 @@ public class CryptoCloudContentRepositoryFactory implements CloudContentReposito
}
@Override
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
public CloudContentRepository<CryptoCloud, CryptoNode, CryptoFolder, CryptoFile> cloudContentRepositoryFor(Cloud cloud) {
CryptoCloud cryptoCloud = (CryptoCloud) cloud;
Vault vault = cryptoCloud.getVault();
return new CryptoCloudContentRepository(context, cloudContentRepository.get(), cryptoCloud, cryptors.get(vault));
@ -50,7 +51,7 @@ public class CryptoCloudContentRepositoryFactory implements CloudContentReposito
public void deregisterCryptor(Vault vault, boolean assertPresent) {
Optional<Cryptor> cryptor = cryptors.remove(vault);
if (cryptor.isAbsent()) {
if (!cryptor.isPresent()) {
if (assertPresent) {
throw new IllegalStateException(format("No cryptor registered for vault %s", vault));
}

View File

@ -1,223 +1,97 @@
package org.cryptomator.data.cloud.crypto;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.KeyFile;
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
import com.google.common.base.Optional;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.CancellationException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.Flag;
import org.cryptomator.domain.usecases.vault.UnlockToken;
import org.cryptomator.util.Optional;
import java.io.ByteArrayOutputStream;
import java.text.Normalizer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import javax.inject.Inject;
import javax.inject.Singleton;
import static android.R.attr.version;
import static java.text.Normalizer.normalize;
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME;
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_BACKUP_FILE_EXT;
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_FILE_NAME;
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION;
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MIN_VAULT_VERSION;
import static org.cryptomator.data.cloud.crypto.CryptoConstants.ROOT_DIR_ID;
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VERSION_WITH_NORMALIZED_PASSWORDS;
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME;
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME;
import static org.cryptomator.domain.Vault.aCopyOf;
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
@Singleton
public class CryptoCloudFactory {
private final CryptorProvider cryptorProvider;
private final CloudContentRepository cloudContentRepository;
private final CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile> cloudContentRepository;
private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
private final SecureRandom secureRandom = new SecureRandom();
@Inject
public CryptoCloudFactory( //
CloudContentRepository cloudContentRepository, //
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory, //
CryptorProvider cryptorProvider) {
this.cryptorProvider = cryptorProvider;
public CryptoCloudFactory(CloudContentRepository/*<Cloud, CloudNode, CloudFolder, CloudFile>*/ cloudContentRepository, //
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) {
this.cloudContentRepository = cloudContentRepository;
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
}
public void create(CloudFolder location, CharSequence password) throws BackendException {
Cryptor cryptor = cryptorProvider.createNew();
try {
KeyFile keyFile = cryptor.writeKeysToMasterkeyFile(normalizePassword(password, version), MAX_VAULT_VERSION);
writeKeyFile(location, keyFile);
createRootFolder(location, cryptor);
} finally {
cryptor.destroy();
}
}
private void createRootFolder(CloudFolder location, Cryptor cryptor) throws BackendException {
CloudFolder dFolder = cloudContentRepository.folder(location, DATA_DIR_NAME);
dFolder = cloudContentRepository.create(dFolder);
String rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(ROOT_DIR_ID);
CloudFolder lvl1Folder = cloudContentRepository.folder(dFolder, rootDirHash.substring(0, 2));
lvl1Folder = cloudContentRepository.create(lvl1Folder);
CloudFolder lvl2Folder = cloudContentRepository.folder(lvl1Folder, rootDirHash.substring(2));
cloudContentRepository.create(lvl2Folder);
cryptoCloudProvider(Optional.absent()).create(location, password);
}
public Cloud decryptedViewOf(Vault vault) throws BackendException {
return new CryptoCloud(aCopyOf(vault).build());
}
public Vault unlock(Vault vault, CharSequence password, Flag cancelledFlag) throws BackendException {
return unlock(createUnlockToken(vault), password, cancelledFlag);
}
public Vault unlock(UnlockToken token, CharSequence password, Flag cancelledFlag) throws BackendException {
UnlockTokenImpl impl = (UnlockTokenImpl) token;
Cryptor cryptor = cryptorFor(impl.getKeyFile(), password);
if (cancelledFlag.get()) {
throw new CancellationException();
}
cryptoCloudContentRepositoryFactory.registerCryptor(impl.getVault(), cryptor);
return aCopyOf(token.getVault()) //
.withVersion(impl.getKeyFile().getVersion()) //
.build();
}
public UnlockTokenImpl createUnlockToken(Vault vault) throws BackendException {
public Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException {
CloudFolder vaultLocation = cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
return createUnlockToken(vault, vaultLocation);
String jwt = new String(readConfigFileData(vaultLocation), StandardCharsets.UTF_8);
return Optional.of(VaultConfig.decode(jwt));
}
private UnlockTokenImpl createUnlockToken(Vault vault, CloudFolder location) throws BackendException {
byte[] keyFileData = readKeyFileData(location);
UnlockTokenImpl unlockToken = new UnlockTokenImpl(vault, keyFileData);
assertVaultVersionIsSupported(unlockToken.getKeyFile().getVersion());
return unlockToken;
private byte[] readConfigFileData(CloudFolder location) throws BackendException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME);
cloudContentRepository.read(vaultFile, null, data, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD);
return data.toByteArray();
}
private Cryptor cryptorFor(KeyFile keyFile, CharSequence password) {
return cryptorProvider.createFromKeyFile(keyFile, normalizePassword(password, keyFile.getVersion()), keyFile.getVersion());
public Vault unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
return cryptoCloudProvider(unverifiedVaultConfig).unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag);
}
private CloudFolder vaultLocation(Vault vault) throws BackendException {
return cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
public Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
return cryptoCloudProvider(unverifiedVaultConfig).unlock(token, unverifiedVaultConfig, password, cancelledFlag);
}
public boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException {
try {
// create a cryptor, which checks the password, then destroy it immediately
cryptorFor(createUnlockToken(vault).getKeyFile(), password).destroy();
return true;
} catch (InvalidPassphraseException e) {
return false;
}
public UnlockToken createUnlockToken(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException {
return cryptoCloudProvider(unverifiedVaultConfig).createUnlockToken(vault, unverifiedVaultConfig);
}
public boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException {
return cryptoCloudProvider(unverifiedVaultConfig).isVaultPasswordValid(vault, unverifiedVaultConfig, password);
}
public void lock(Vault vault) {
cryptoCloudContentRepositoryFactory.deregisterCryptor(vault);
}
private void assertVaultVersionIsSupported(int version) {
if (version < MIN_VAULT_VERSION) {
throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION);
} else if (version > MAX_VAULT_VERSION) {
throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION);
}
public void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException {
cryptoCloudProvider(unverifiedVaultConfig).changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword);
}
private void writeKeyFile(CloudFolder location, KeyFile keyFile) throws BackendException {
byte[] data = keyFile.serialize();
cloudContentRepository.write(masterkeyFile(location), ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, false, data.length);
}
private byte[] readKeyFileData(CloudFolder location) throws BackendException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
cloudContentRepository.read(masterkeyFile(location), Optional.empty(), data, NO_OP_PROGRESS_AWARE);
return data.toByteArray();
}
private CloudFile masterkeyFile(CloudFolder location) throws BackendException {
return cloudContentRepository.file(location, MASTERKEY_FILE_NAME);
}
private CloudFile masterkeyBackupFile(CloudFolder location, byte[] data) throws BackendException {
String fileName = MASTERKEY_FILE_NAME + BackupFileIdSuffixGenerator.generate(data) + MASTERKEY_BACKUP_FILE_EXT;
return cloudContentRepository.file(location, fileName);
}
public void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException {
CloudFolder vaultLocation = vaultLocation(vault);
ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream();
cloudContentRepository.read(masterkeyFile(vaultLocation), Optional.empty(), dataOutputStream, NO_OP_PROGRESS_AWARE);
byte[] data = dataOutputStream.toByteArray();
int vaultVersion = KeyFile.parse(data).getVersion();
createBackupMasterKeyFile(data, vaultLocation);
createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, vaultLocation);
}
private void createBackupMasterKeyFile(byte[] data, CloudFolder vaultLocation) throws BackendException {
cloudContentRepository.write( //
masterkeyBackupFile(vaultLocation, data), //
ByteArrayDataSource.from(data), //
NO_OP_PROGRESS_AWARE, //
true, //
data.length);
}
private void createNewMasterKeyFile(byte[] data, int vaultVersion, String oldPassword, String newPassword, CloudFolder vaultLocation) throws BackendException {
byte[] newMasterKeyFile = Cryptors.changePassphrase(cryptorProvider, //
data, //
normalizePassword(oldPassword, vaultVersion), //
normalizePassword(newPassword, vaultVersion));
cloudContentRepository.write(masterkeyFile(vaultLocation), //
ByteArrayDataSource.from(newMasterKeyFile), //
NO_OP_PROGRESS_AWARE, //
true, //
newMasterKeyFile.length);
}
private CharSequence normalizePassword(CharSequence password, int vaultVersion) {
if (vaultVersion >= VERSION_WITH_NORMALIZED_PASSWORDS) {
return normalize(password, Normalizer.Form.NFC);
private CryptoCloudProvider cryptoCloudProvider(Optional<UnverifiedVaultConfig> unverifiedVaultConfigOptional) {
if (unverifiedVaultConfigOptional.isPresent()) {
if (MASTERKEY_SCHEME.equals(unverifiedVaultConfigOptional.get().getKeyId().getScheme())) {
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
}
throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme()));
} else {
return password;
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
}
}
private static class UnlockTokenImpl implements UnlockToken {
private final Vault vault;
private final byte[] keyFileData;
private UnlockTokenImpl(Vault vault, byte[] keyFileData) {
this.vault = vault;
this.keyFileData = keyFileData;
}
@Override
public Vault getVault() {
return vault;
}
public KeyFile getKeyFile() {
return KeyFile.parse(keyFileData);
}
}
}

View File

@ -0,0 +1,28 @@
package org.cryptomator.data.cloud.crypto;
import com.google.common.base.Optional;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.usecases.cloud.Flag;
import org.cryptomator.domain.usecases.vault.UnlockToken;
public interface CryptoCloudProvider {
void create(CloudFolder location, CharSequence password) throws BackendException;
Vault unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException;
UnlockToken createUnlockToken(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException;
Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException;
boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException;
void lock(Vault vault);
void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException;
}

View File

@ -1,14 +0,0 @@
package org.cryptomator.data.cloud.crypto;
class CryptoConstants {
static final String ROOT_DIR_ID = "";
static final String DATA_DIR_NAME = "d";
static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator";
static final String MASTERKEY_BACKUP_FILE_EXT = ".bkup";
static final int MAX_VAULT_VERSION = 7;
static final int VERSION_WITH_NORMALIZED_PASSWORDS = 6;
static final int MIN_VAULT_VERSION = 5;
}

View File

@ -0,0 +1,22 @@
package org.cryptomator.data.cloud.crypto
import org.cryptomator.cryptolib.api.CryptorProvider
object CryptoConstants {
const val MASTERKEY_SCHEME = "masterkeyfile"
const val MASTERKEY_FILE_NAME = "masterkey.cryptomator"
const val ROOT_DIR_ID = ""
const val DATA_DIR_NAME = "d"
const val VAULT_FILE_NAME = "vault.cryptomator"
const val MASTERKEY_BACKUP_FILE_EXT = ".bkup"
const val DEFAULT_MASTERKEY_FILE_VERSION = 999
const val MAX_VAULT_VERSION = 8
const val MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG = 7
const val VERSION_WITH_NORMALIZED_PASSWORDS = 6
const val MIN_VAULT_VERSION = 5
const val DEFAULT_MAX_FILE_NAME = 220
val PEPPER = ByteArray(0)
val DEFAULT_CIPHER_COMBO = CryptorProvider.Scheme.SIV_CTRMAC
}

View File

@ -1,82 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.util.Optional;
import java.util.Date;
class CryptoFile implements CloudFile, CryptoNode {
private final String name;
private final String path;
private final Optional<Long> size;
private final CloudFile cloudFile;
private final CryptoFolder parent;
public CryptoFile(CryptoFolder parent, String name, String path, Optional<Long> size, CloudFile cloudFile) {
this.parent = parent;
this.name = name;
this.path = path;
this.size = size;
this.cloudFile = cloudFile;
}
@Override
public Cloud getCloud() {
return parent.getCloud();
}
@Override
public String getName() {
return name;
}
@Override
public String getPath() {
return path;
}
@Override
public CryptoFolder getParent() {
return parent;
}
@Override
public Optional<Long> getSize() {
return size;
}
@Override
public Optional<Date> getModified() {
return cloudFile.getModified();
}
/**
* @return The actual file in the underlying, i.e. decorated, CloudContentRepository
*/
CloudFile getCloudFile() {
return cloudFile;
}
@Override
public boolean equals(Object obj) {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
if (obj == this) {
return true;
}
return internalEquals((CryptoFile) obj);
}
private boolean internalEquals(CryptoFile obj) {
return path != null && path.equals(obj.path);
}
@Override
public int hashCode() {
return path == null ? 0 : path.hashCode();
}
}

View File

@ -0,0 +1,37 @@
package org.cryptomator.data.cloud.crypto
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import java.util.Date
class CryptoFile(
override val parent: CryptoFolder, override val name: String, override val path: String, override val size: Long?,
/**
* @return The actual file in the underlying, i.e. decorated, CloudContentRepository
*/
val cloudFile: CloudFile
) : CloudFile, CryptoNode {
override val cloud: Cloud?
get() = parent.cloud
override val modified: Date?
get() = cloudFile.modified
override fun equals(other: Any?): Boolean {
if (other == null || javaClass != other.javaClass) {
return false
}
return if (other === this) {
true
} else internalEquals(other as CryptoFile)
}
private fun internalEquals(obj: CryptoFile): Boolean {
return path == obj.path
}
override fun hashCode(): Int {
return path.hashCode()
}
}

View File

@ -1,72 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
class CryptoFolder implements CloudFolder, CryptoNode {
private final String name;
private final String path;
private final CryptoFolder parent;
private final CloudFile dirFile;
CryptoFolder(CryptoFolder parent, String name, String path, CloudFile dirFile) {
this.parent = parent;
this.name = name;
this.path = path;
this.dirFile = dirFile;
}
@Override
public Cloud getCloud() {
return parent.getCloud();
}
@Override
public String getName() {
return name;
}
@Override
public String getPath() {
return path;
}
@Override
public CryptoFolder getParent() {
return parent;
}
/**
* @return the file containing the directory id, in the underlying, i.e. decorated, CloudContentRepository
*/
CloudFile getDirFile() {
return dirFile;
}
@Override
public boolean equals(Object obj) {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
if (obj == this) {
return true;
}
return internalEquals((CryptoFolder) obj);
}
private boolean internalEquals(CryptoFolder obj) {
return path != null && path.equals(obj.path);
}
@Override
public int hashCode() {
return path == null ? 0 : path.hashCode();
}
@Override
public CryptoFolder withCloud(Cloud cloud) {
return new CryptoFolder(parent.withCloud(cloud), name, path, dirFile);
}
}

View File

@ -0,0 +1,38 @@
package org.cryptomator.data.cloud.crypto
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder
open class CryptoFolder(
override val parent: CryptoFolder?, override val name: String, override val path: String,
/**
* @return the file containing the directory id, in the underlying, i.e. decorated, CloudContentRepository
*/
val dirFile: CloudFile?
) : CloudFolder, CryptoNode {
override val cloud: Cloud?
get() = parent?.cloud
override fun equals(other: Any?): Boolean {
if (other == null || javaClass != other.javaClass) {
return false
}
return if (other === this) {
true
} else internalEquals(other as CryptoFolder)
}
private fun internalEquals(obj: CryptoFolder): Boolean {
return path == obj.path
}
override fun hashCode(): Int {
return path.hashCode()
}
override fun withCloud(cloud: Cloud?): CryptoFolder? {
return CryptoFolder(parent?.withCloud(cloud), name, path, dirFile)
}
}

View File

@ -1,423 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import android.content.Context;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.DecryptingReadableByteChannel;
import org.cryptomator.cryptolib.EncryptingWritableByteChannel;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
import org.cryptomator.domain.exception.EmptyDirFileException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.NoDirFileException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.DownloadFileReplacingProgressAware;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.FileBasedDataSource;
import org.cryptomator.domain.usecases.cloud.Progress;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import org.cryptomator.util.Supplier;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.UUID;
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME;
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
abstract class CryptoImplDecorator {
final CloudContentRepository cloudContentRepository;
final Context context;
final DirIdCache dirIdCache;
private final Supplier<Cryptor> cryptor;
private final CloudFolder storageLocation;
private RootCryptoFolder root;
CryptoImplDecorator(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
this.context = context;
this.cryptor = cryptor;
this.cloudContentRepository = cloudContentRepository;
this.storageLocation = storageLocation;
this.dirIdCache = dirIdCache;
}
abstract CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException;
abstract String decryptName(String dirId, String encryptedName);
abstract String encryptName(CryptoFolder cryptoParent, String name) throws BackendException;
abstract Optional<String> extractEncryptedName(String ciphertextName);
abstract List<CryptoNode> list(CryptoFolder cryptoFolder) throws BackendException;
abstract String encryptFolderName(CryptoFolder cryptoFolder, String name) throws BackendException;
abstract CryptoSymlink symlink(CryptoFolder cryptoParent, String cleartextName, String target) throws BackendException;
abstract CryptoFolder create(CryptoFolder folder) throws BackendException;
abstract CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException;
abstract CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException;
abstract void delete(CloudNode node) throws BackendException;
abstract CryptoFile write(CryptoFile cryptoFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException;
abstract String loadDirId(CryptoFolder folder) throws BackendException, EmptyDirFileException;
abstract DirIdInfo createDirIdInfo(CryptoFolder folder) throws BackendException;
private String dirHash(String directoryId) {
return cryptor().fileNameCryptor().hashDirectoryId(directoryId);
}
private CloudFolder dataFolder() throws BackendException {
return cloudContentRepository.folder(storageLocation, DATA_DIR_NAME);
}
String path(CloudFolder base, String name) {
return base.getPath() + "/" + name;
}
File getInternalCache() {
return context.getCacheDir();
}
List<CryptoFolder> deepCollectSubfolders(CryptoFolder source) throws BackendException {
Queue<CryptoFolder> queue = new LinkedList<>();
queue.add(source);
List<CryptoFolder> result = new LinkedList<>();
while (!queue.isEmpty()) {
CryptoFolder folder = queue.remove();
List<CryptoFolder> subfolders = shallowCollectSubfolders(folder);
queue.addAll(subfolders);
result.addAll(subfolders);
}
Collections.reverse(result);
return result;
}
private List<CryptoFolder> shallowCollectSubfolders(CryptoFolder source) throws BackendException {
List<CryptoFolder> result = new LinkedList<>();
try {
List<CryptoNode> list = list(source);
for (CloudNode node : list) {
if (node instanceof CryptoFolder) {
result.add((CryptoFolder) node);
}
}
} catch (NoDirFileException e) {
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
}
return result;
}
public RootCryptoFolder root(CryptoCloud cryptoCloud) throws BackendException {
if (root == null) {
root = new RootCryptoFolder(cryptoCloud);
}
return root;
}
public CryptoFolder resolve(CryptoCloud cloud, String path) throws BackendException {
if (path.startsWith("/")) {
path = path.substring(1);
}
String[] names = path.split("/");
CryptoFolder folder = root(cloud);
for (String name : names) {
folder = folder(folder, name);
}
return folder;
}
public CryptoFile file(CryptoFolder cryptoParent, String cleartextName) throws BackendException {
return file(cryptoParent, cleartextName, Optional.empty());
}
public CryptoFile file(CryptoFolder cryptoParent, String cleartextName, Optional<Long> cleartextSize) throws BackendException {
String ciphertextName = encryptFileName(cryptoParent, cleartextName);
return file(cryptoParent, cleartextName, ciphertextName, cleartextSize);
}
private CryptoFile file(CryptoFolder cryptoParent, String cleartextName, String ciphertextName, Optional<Long> cleartextSize) throws BackendException {
Optional<Long> ciphertextSize;
if (cleartextSize.isPresent()) {
ciphertextSize = Optional.of(Cryptors.ciphertextSize(cleartextSize.get(), cryptor()) + cryptor().fileHeaderCryptor().headerSize());
} else {
ciphertextSize = Optional.empty();
}
CloudFile cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).getCloudFolder(), ciphertextName, ciphertextSize);
return file(cryptoParent, cleartextName, cloudFile, cleartextSize);
}
CryptoFile file(CryptoFile cryptoFile, CloudFile cloudFile, Optional<Long> cleartextSize) throws BackendException {
return file(cryptoFile.getParent(), cryptoFile.getName(), cloudFile, cleartextSize);
}
CryptoFile file(CryptoFolder cryptoParent, String cleartextName, CloudFile cloudFile, Optional<Long> cleartextSize) throws BackendException {
return new CryptoFile(cryptoParent, cleartextName, path(cryptoParent, cleartextName), cleartextSize, cloudFile);
}
private String encryptFileName(CryptoFolder cryptoParent, String name) throws BackendException {
return encryptName(cryptoParent, name);
}
CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName, CloudFile dirFile) throws BackendException {
return new CryptoFolder(cryptoParent, cleartextName, path(cryptoParent, cleartextName), dirFile);
}
CryptoFolder folder(CryptoFolder cryptoFolder, CloudFile dirFile) throws BackendException {
return new CryptoFolder(cryptoFolder.getParent(), cryptoFolder.getName(), cryptoFolder.getPath(), dirFile);
}
boolean exists(CloudNode node) throws BackendException {
if (node instanceof CryptoFolder) {
return exists((CryptoFolder) node);
} else if (node instanceof CryptoFile) {
return exists((CryptoFile) node);
} else if (node instanceof CryptoSymlink) {
return exists((CryptoSymlink) node);
} else {
throw new IllegalArgumentException("Unexpected CloudNode type: " + node.getClass());
}
}
private boolean exists(CryptoFolder folder) throws BackendException {
return cloudContentRepository.exists(folder.getDirFile()) && cloudContentRepository.exists(dirIdInfo(folder).getCloudFolder());
}
private boolean exists(CryptoFile file) throws BackendException {
return cloudContentRepository.exists(file.getCloudFile());
}
private boolean exists(CryptoSymlink symlink) throws BackendException {
return cloudContentRepository.exists(symlink.getCloudFile());
}
void assertCryptoFolderAlreadyExists(CryptoFolder cryptoFolder) throws BackendException {
if (cloudContentRepository.exists(cryptoFolder.getDirFile()) //
|| cloudContentRepository.exists(file(cryptoFolder.getParent(), cryptoFolder.getName()))) {
throw new CloudNodeAlreadyExistsException(cryptoFolder.getName());
}
}
void assertCryptoFileAlreadyExists(CryptoFile cryptoFile) throws BackendException {
if (cloudContentRepository.exists(cryptoFile.getCloudFile()) //
|| cloudContentRepository.exists(folder(cryptoFile.getParent(), cryptoFile.getName()).getDirFile())) {
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
}
}
private CryptoFile writeFromTmpFile(DataSource originalDataSource, final CryptoFile cryptoFile, File encryptedFile, final ProgressAware<UploadState> progressAware, boolean replace) throws BackendException, IOException {
CryptoFile targetFile = targetFile(cryptoFile, replace);
return file(targetFile, //
cloudContentRepository.write( //
targetFile.getCloudFile(), //
originalDataSource.decorate(FileBasedDataSource.from(encryptedFile)), //
new UploadFileReplacingProgressAware(cryptoFile, progressAware), //
replace, //
encryptedFile.length()), //
cryptoFile.getSize());
}
private CryptoFile targetFile(CryptoFile cryptoFile, boolean replace) throws BackendException {
if (replace || !cloudContentRepository.exists(cryptoFile)) {
return cryptoFile;
}
return firstNonExistingAutoRenamedFile(cryptoFile);
}
private CryptoFile firstNonExistingAutoRenamedFile(CryptoFile original) throws BackendException {
String name = original.getName();
String nameWithoutExtension = nameWithoutExtension(name);
String extension = extension(name);
int counter = 1;
CryptoFile result;
do {
String newFileName = nameWithoutExtension + " (" + counter + ")" + extension;
result = file(original.getParent(), newFileName, original.getSize());
counter++;
} while (cloudContentRepository.exists(result));
return result;
}
String nameWithoutExtension(String name) {
int lastDot = name.lastIndexOf(".");
if (lastDot == -1) {
return name;
}
return name.substring(0, lastDot);
}
String extension(String name) {
int lastDot = name.lastIndexOf(".");
if (lastDot == -1) {
return "";
}
return name.substring(lastDot + 1);
}
public void read(CryptoFile cryptoFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
CloudFile ciphertextFile = cryptoFile.getCloudFile();
try {
File encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware);
progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile)));
try (ReadableByteChannel readableByteChannel = Channels.newChannel(new FileInputStream(encryptedTmpFile)); //
ReadableByteChannel decryptingReadableByteChannel = new DecryptingReadableByteChannel(readableByteChannel, cryptor(), true)) {
ByteBuffer buff = ByteBuffer.allocate(cryptor().fileContentCryptor().ciphertextChunkSize());
long cleartextSize = cryptoFile.getSize().orElse(Long.MAX_VALUE);
long decrypted = 0;
int read;
while ((read = decryptingReadableByteChannel.read(buff)) > 0) {
buff.flip();
data.write(buff.array(), 0, buff.remaining());
decrypted += read;
progressAware.onProgress(progress(DownloadState.decryption(cryptoFile)).between(0).and(cleartextSize).withValue(decrypted));
}
} finally {
encryptedTmpFile.delete();
progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile)));
}
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
private File readToTmpFile(CryptoFile cryptoFile, CloudFile file, ProgressAware progressAware) throws BackendException, IOException {
File encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", getInternalCache());
try (OutputStream encryptedData = new FileOutputStream(encryptedTmpFile)) {
cloudContentRepository.read(file, Optional.of(encryptedTmpFile), encryptedData, new DownloadFileReplacingProgressAware(cryptoFile, progressAware));
return encryptedTmpFile;
}
}
public String currentAccount(Cloud cloud) throws BackendException {
return cloudContentRepository.checkAuthenticationAndRetrieveCurrentAccount(cloud);
}
DirIdInfo dirIdInfo(CryptoFolder folder) throws BackendException {
DirIdInfo dirIdInfo = dirIdCache.get(folder);
if (dirIdInfo == null) {
return createDirIdInfo(folder);
}
return dirIdInfo;
}
DirIdInfo createDirIdInfoFor(String dirId) throws BackendException {
String dirHash = dirHash(dirId);
CloudFolder lvl2Dir = lvl2Dir(dirHash);
return new DirIdInfo(dirId, lvl2Dir);
}
byte[] loadContentsOfDirFile(CryptoFolder folder) throws BackendException, EmptyDirFileException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
cloudContentRepository.read(folder.getDirFile(), Optional.empty(), out, NO_OP_PROGRESS_AWARE);
if (dirfileIsEmpty(out)) {
throw new EmptyDirFileException(folder.getName(), folder.getDirFile().getPath());
}
return out.toByteArray();
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
String newDirId() {
return UUID.randomUUID().toString();
}
boolean dirfileIsEmpty(ByteArrayOutputStream out) {
return out.size() == 0;
}
private CloudFolder lvl2Dir(String dirHash) throws BackendException {
return cloudContentRepository.folder(lvl1Dir(dirHash), dirHash.substring(2));
}
private CloudFolder lvl1Dir(String dirHash) throws BackendException {
return cloudContentRepository.folder(dataFolder(), dirHash.substring(0, 2));
}
Cryptor cryptor() {
return cryptor.get();
}
CloudFolder storageLocation() {
return storageLocation;
}
void addFolderToCache(CryptoFolder result, DirIdCache.DirIdInfo dirInfo) {
dirIdCache.put(result, dirInfo);
}
void evictFromCache(CryptoFolder cryptoFolder) {
dirIdCache.evict(cryptoFolder);
}
CryptoFile writeShortNameFile(CryptoFile cryptoFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException {
if (!replace) {
assertCryptoFileAlreadyExists(cryptoFile);
}
try (InputStream stream = data.open(context)) {
File encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", getInternalCache());
try (WritableByteChannel writableByteChannel = Channels.newChannel(new FileOutputStream(encryptedTmpFile)); //
WritableByteChannel encryptingWritableByteChannel = new EncryptingWritableByteChannel(writableByteChannel, cryptor())) {
progressAware.onProgress(Progress.started(UploadState.encryption(cryptoFile)));
ByteBuffer buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize());
long ciphertextSize = Cryptors.ciphertextSize(cryptoFile.getSize().get(), cryptor()) + cryptor().fileHeaderCryptor().headerSize();
int read;
long encrypted = 0;
while ((read = stream.read(buff.array())) > 0) {
buff.limit(read);
int written = encryptingWritableByteChannel.write(buff);
buff.flip();
encrypted += written;
progressAware.onProgress(progress(UploadState.encryption(cryptoFile)).between(0).and(ciphertextSize).withValue(encrypted));
}
encryptingWritableByteChannel.close();
progressAware.onProgress(Progress.completed(UploadState.encryption(cryptoFile)));
return writeFromTmpFile(data, cryptoFile, encryptedTmpFile, progressAware, replace);
} catch (Throwable e) {
throw e;
} finally {
encryptedTmpFile.delete();
}
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
}

View File

@ -0,0 +1,454 @@
package org.cryptomator.data.cloud.crypto
import android.content.Context
import org.cryptomator.cryptolib.api.Cryptor
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.EmptyDirFileException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NoDirFileException
import org.cryptomator.domain.exception.ParentFolderIsNullException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.DownloadFileReplacingProgressAware
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from
import org.cryptomator.domain.usecases.cloud.Progress
import org.cryptomator.domain.usecases.cloud.UploadState
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.channels.Channels
import java.util.LinkedList
import java.util.Queue
import java.util.UUID
import java.util.function.Supplier
abstract class CryptoImplDecorator(
val context: Context,
private val cryptor: Supplier<Cryptor>,
val cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>,
private val storageLocation: CloudFolder,
val dirIdCache: DirIdCache,
val shorteningThreshold: Int
) {
@Volatile
private var root: RootCryptoFolder? = null
@Throws(BackendException::class)
abstract fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder
abstract fun decryptName(dirId: String, encryptedName: String): String?
@Throws(BackendException::class)
abstract fun encryptName(cryptoParent: CryptoFolder, name: String): String
abstract fun extractEncryptedName(ciphertextName: String): String?
@Throws(BackendException::class)
abstract fun list(cryptoFolder: CryptoFolder): List<CryptoNode>
@Throws(BackendException::class)
abstract fun encryptFolderName(cryptoFolder: CryptoFolder, name: String): String
@Throws(BackendException::class)
abstract fun symlink(cryptoParent: CryptoFolder, cleartextName: String, target: String): CryptoSymlink
@Throws(BackendException::class)
abstract fun create(folder: CryptoFolder): CryptoFolder
@Throws(BackendException::class)
abstract fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder
@Throws(BackendException::class)
abstract fun move(source: CryptoFile, target: CryptoFile): CryptoFile
@Throws(BackendException::class)
abstract fun delete(node: CloudNode)
@Throws(BackendException::class)
abstract fun write(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile
@Throws(BackendException::class, EmptyDirFileException::class)
abstract fun loadDirId(folder: CryptoFolder): String
@Throws(BackendException::class)
abstract fun createDirIdInfo(folder: CryptoFolder): DirIdInfo
private fun dirHash(directoryId: String): String {
return cryptor().fileNameCryptor().hashDirectoryId(directoryId)
}
@Throws(BackendException::class)
private fun dataFolder(): CloudFolder {
return cloudContentRepository.folder(storageLocation, CryptoConstants.DATA_DIR_NAME)
}
fun path(base: CloudFolder, name: String): String {
return base.path + "/" + name
}
val internalCache: File
get() = context.cacheDir
@Throws(BackendException::class)
fun deepCollectSubfolders(source: CryptoFolder): List<CryptoFolder> {
val queue: Queue<CryptoFolder> = LinkedList()
queue.add(source)
val result: MutableList<CryptoFolder> = LinkedList()
while (!queue.isEmpty()) {
val folder = queue.remove()
val subfolders = shallowCollectSubfolders(folder)
queue.addAll(subfolders)
result.addAll(subfolders)
}
result.reverse()
return result
}
@Throws(BackendException::class)
private fun shallowCollectSubfolders(source: CryptoFolder): List<CryptoFolder> {
return try {
list(source).filterIsInstance<CryptoFolder>()
} catch (e: NoDirFileException) {
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
emptyList()
}
}
@Throws(BackendException::class)
@Synchronized
fun root(cryptoCloud: CryptoCloud): RootCryptoFolder = root ?: RootCryptoFolder(cryptoCloud).also { root = it }
@Throws(BackendException::class)
fun resolve(cloud: CryptoCloud, path: String): CryptoFolder {
val names = path.removePrefix("/").split("/").toTypedArray()
var folder: CryptoFolder = root(cloud)
for (name in names) {
folder = folder(folder, name)
}
return folder
}
@Throws(BackendException::class)
fun file(cryptoParent: CryptoFolder, cleartextName: String): CryptoFile {
return file(cryptoParent, cleartextName, null)
}
@Throws(BackendException::class)
fun file(cryptoParent: CryptoFolder, cleartextName: String, cleartextSize: Long?): CryptoFile {
val ciphertextName = encryptFileName(cryptoParent, cleartextName)
return file(cryptoParent, cleartextName, ciphertextName, cleartextSize)
}
@Throws(BackendException::class)
private fun file(cryptoParent: CryptoFolder, cleartextName: String, ciphertextName: String, cleartextSize: Long?): CryptoFile {
val ciphertextSize = cleartextSize?.let { cryptor().fileContentCryptor().ciphertextSize(it) + cryptor().fileHeaderCryptor().headerSize() }
val cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, ciphertextName, ciphertextSize)
return file(cryptoParent, cleartextName, cloudFile, cleartextSize)
}
@Throws(BackendException::class)
fun file(cryptoFile: CryptoFile, cloudFile: CloudFile, cleartextSize: Long?): CryptoFile {
return file(cryptoFile.parent, cryptoFile.name, cloudFile, cleartextSize)
}
@Throws(BackendException::class)
fun file(cryptoParent: CryptoFolder, cleartextName: String, cloudFile: CloudFile, cleartextSize: Long?): CryptoFile {
return CryptoFile(cryptoParent, cleartextName, path(cryptoParent, cleartextName), cleartextSize, cloudFile)
}
@Throws(BackendException::class)
private fun encryptFileName(cryptoParent: CryptoFolder, name: String): String {
return encryptName(cryptoParent, name)
}
@Throws(BackendException::class)
fun folder(cryptoParent: CryptoFolder, cleartextName: String, dirFile: CloudFile): CryptoFolder {
return CryptoFolder(cryptoParent, cleartextName, path(cryptoParent, cleartextName), dirFile)
}
@Throws(BackendException::class)
fun folder(cryptoFolder: CryptoFolder, dirFile: CloudFile): CryptoFolder {
return CryptoFolder(cryptoFolder.parent, cryptoFolder.name, cryptoFolder.path, dirFile)
}
@Throws(BackendException::class)
fun exists(node: CloudNode): Boolean {
return when (node) {
is CryptoFolder -> {
exists(node)
}
is CryptoFile -> {
exists(node)
}
is CryptoSymlink -> {
exists(node)
}
else -> {
throw IllegalArgumentException("Unexpected CloudNode type: " + node.javaClass)
}
}
}
@Throws(BackendException::class)
private fun exists(folder: CryptoFolder): Boolean {
requireNotNull(folder.dirFile)
return cloudContentRepository.exists(folder.dirFile) && cloudContentRepository.exists(dirIdInfo(folder).cloudFolder)
}
@Throws(BackendException::class)
private fun exists(file: CryptoFile): Boolean {
return cloudContentRepository.exists(file.cloudFile)
}
@Throws(BackendException::class)
private fun exists(symlink: CryptoSymlink): Boolean {
return cloudContentRepository.exists(symlink.cloudFile)
}
@Throws(BackendException::class)
fun assertCryptoFolderAlreadyExists(cryptoFolder: CryptoFolder) {
requireNotNull(cryptoFolder.dirFile)
requireNotNull(cryptoFolder.parent)
cryptoFolder.parent?.let { cryptosParent ->
if (cloudContentRepository.exists(cryptoFolder.dirFile)
|| cloudContentRepository.exists(file(cryptosParent, cryptoFolder.name))
) {
throw CloudNodeAlreadyExistsException(cryptoFolder.name)
}
} ?: throw ParentFolderIsNullException(cryptoFolder.name)
}
@Throws(BackendException::class)
fun assertCryptoFileAlreadyExists(cryptoFile: CryptoFile) {
val dirFile = folder(cryptoFile.parent, cryptoFile.name).dirFile
requireNotNull(dirFile)
if (cloudContentRepository.exists(cryptoFile.cloudFile) //
|| cloudContentRepository.exists(dirFile)
) {
throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false")
}
}
@Throws(BackendException::class, IOException::class)
private fun writeFromTmpFile(originalDataSource: DataSource, cryptoFile: CryptoFile, encryptedFile: File, progressAware: ProgressAware<UploadState>, replace: Boolean): CryptoFile {
val targetFile = targetFile(cryptoFile, replace)
return file(
targetFile, //
cloudContentRepository.write( //
targetFile.cloudFile, //
originalDataSource.decorate(from(encryptedFile)), //
UploadFileReplacingProgressAware(cryptoFile, progressAware), //
replace, //
encryptedFile.length()
), //
cryptoFile.size
)
}
@Throws(BackendException::class)
private fun targetFile(cryptoFile: CryptoFile, replace: Boolean): CryptoFile {
return if (replace || !cloudContentRepository.exists(cryptoFile)) {
cryptoFile
} else firstNonExistingAutoRenamedFile(cryptoFile)
}
@Throws(BackendException::class)
private fun firstNonExistingAutoRenamedFile(original: CryptoFile): CryptoFile {
val name = original.name
val nameWithoutExtension = nameWithoutExtension(name)
val extension = extension(name)
var counter = 1
var result: CryptoFile
do {
val newFileName = "$nameWithoutExtension ($counter)$extension"
result = file(original.parent, newFileName, original.size)
counter++
} while (cloudContentRepository.exists(result))
return result
}
fun nameWithoutExtension(name: String): String {
val lastDot = name.lastIndexOf(".")
return if (lastDot == -1) {
name
} else name.substring(0, lastDot)
}
fun extension(name: String): String {
val lastDot = name.lastIndexOf(".")
return if (lastDot == -1) {
""
} else name.substring(lastDot + 1)
}
@Throws(BackendException::class)
fun read(cryptoFile: CryptoFile, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
val ciphertextFile = cryptoFile.cloudFile
try {
val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware)
progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile)))
try {
Channels.newChannel(FileInputStream(encryptedTmpFile)).use { readableByteChannel ->
DecryptingReadableByteChannel(readableByteChannel, cryptor(), true).use { decryptingReadableByteChannel ->
val buff = ByteBuffer.allocate(cryptor().fileContentCryptor().ciphertextChunkSize())
val cleartextSize = cryptoFile.size ?: Long.MAX_VALUE
var decrypted: Long = 0
var read: Int
while (decryptingReadableByteChannel.read(buff).also { read = it } > 0) {
buff.flip()
data.write(buff.array(), 0, buff.remaining())
decrypted += read.toLong()
progressAware
.onProgress(
Progress.progress(DownloadState.decryption(cryptoFile)) //
.between(0) //
.and(cleartextSize) //
.withValue(decrypted)
)
}
}
}
} finally {
encryptedTmpFile.delete()
progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile)))
}
} catch (e: IOException) {
throw FatalBackendException(e)
}
}
@Throws(BackendException::class, IOException::class)
private fun readToTmpFile(cryptoFile: CryptoFile, file: CloudFile, progressAware: ProgressAware<DownloadState>): File {
val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache)
FileOutputStream(encryptedTmpFile).use { encryptedData ->
cloudContentRepository.read(file, encryptedTmpFile, encryptedData, DownloadFileReplacingProgressAware(cryptoFile, progressAware))
return encryptedTmpFile
}
}
@Throws(BackendException::class)
fun currentAccount(cloud: Cloud): String {
return cloudContentRepository.checkAuthenticationAndRetrieveCurrentAccount(cloud)
}
@Throws(BackendException::class)
fun dirIdInfo(folder: CryptoFolder): DirIdInfo {
return dirIdCache[folder] ?: return createDirIdInfo(folder)
}
@Throws(BackendException::class)
fun createDirIdInfoFor(dirId: String): DirIdInfo {
val dirHash = dirHash(dirId)
val lvl2Dir = lvl2Dir(dirHash)
return DirIdInfo(dirId, lvl2Dir)
}
@Throws(BackendException::class, EmptyDirFileException::class)
fun loadContentsOfDirFile(folder: CryptoFolder): ByteArray {
folder.dirFile?.let {
try {
ByteArrayOutputStream().use { out ->
cloudContentRepository.read(it, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
if (dirfileIsEmpty(out)) {
throw EmptyDirFileException(folder.name, folder.dirFile.path)
}
return out.toByteArray()
}
} catch (e: IOException) {
throw FatalBackendException(e)
}
} ?: throw FatalBackendException("Dir file is null")
}
fun newDirId(): String {
return UUID.randomUUID().toString()
}
fun dirfileIsEmpty(out: ByteArrayOutputStream): Boolean {
return out.size() == 0
}
@Throws(BackendException::class)
private fun lvl2Dir(dirHash: String): CloudFolder {
return cloudContentRepository.folder(lvl1Dir(dirHash), dirHash.substring(2))
}
@Throws(BackendException::class)
private fun lvl1Dir(dirHash: String): CloudFolder {
return cloudContentRepository.folder(dataFolder(), dirHash.substring(0, 2))
}
fun cryptor(): Cryptor {
return cryptor.get()
}
fun storageLocation(): CloudFolder {
return storageLocation
}
fun addFolderToCache(result: CryptoFolder, dirInfo: DirIdInfo) {
dirIdCache.put(result, dirInfo)
}
fun evictFromCache(cryptoFolder: CryptoFolder) {
dirIdCache.evict(cryptoFolder)
}
@Throws(BackendException::class)
fun writeShortNameFile(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile {
if (!replace) {
assertCryptoFileAlreadyExists(cryptoFile)
}
try {
data.open(context)?.use { stream ->
requireNotNull(cryptoFile.size)
val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache)
try {
Channels.newChannel(FileOutputStream(encryptedTmpFile)).use { writableByteChannel ->
EncryptingWritableByteChannel(writableByteChannel, cryptor()).use { encryptingWritableByteChannel ->
progressAware.onProgress(Progress.started(UploadState.encryption(cryptoFile)))
val buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize())
val ciphertextSize = cryptor().fileContentCryptor().ciphertextSize(cryptoFile.size) + cryptor().fileHeaderCryptor().headerSize()
var read: Int
var encrypted: Long = 0
while (stream.read(buff.array()).also { read = it } > 0) {
buff.limit(read)
val written = encryptingWritableByteChannel.write(buff)
buff.flip()
encrypted += written.toLong()
progressAware.onProgress(Progress.progress(UploadState.encryption(cryptoFile)).between(0).and(ciphertextSize).withValue(encrypted))
}
encryptingWritableByteChannel.close()
progressAware.onProgress(Progress.completed(UploadState.encryption(cryptoFile)))
return writeFromTmpFile(data, cryptoFile, encryptedTmpFile, progressAware, replace)
}
}
} finally {
encryptedTmpFile.delete()
}
} ?: throw IllegalStateException("InputStream shouldn't be null")
} catch (e: IOException) {
throw FatalBackendException(e)
}
}
}

View File

@ -1,546 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import android.content.Context;
import com.google.common.io.BaseEncoding;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.EncryptingWritableByteChannel;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
import org.cryptomator.domain.exception.EmptyDirFileException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.NoDirFileException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
import org.cryptomator.domain.exception.SymLinkException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware;
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.FileBasedDataSource;
import org.cryptomator.domain.usecases.cloud.Progress;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import org.cryptomator.util.Supplier;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import timber.log.Timber;
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
import static org.cryptomator.util.Encodings.UTF_8;
final class CryptoImplVaultFormat7 extends CryptoImplDecorator {
private static final int SHORT_NAMES_MAX_LENGTH = 220;
private static final String CLOUD_NODE_EXT = ".c9r";
private static final String LONG_NODE_FILE_EXT = ".c9s";
private static final String CLOUD_FOLDER_DIR_FILE_PRE = "dir";
private static final String LONG_NODE_FILE_CONTENT_CONTENTS = "contents";
private static final String LONG_NODE_FILE_CONTENT_NAME = "name";
private static final String CLOUD_NODE_SYMLINK_PRE = "symlink";
private static final Pattern BASE64_ENCRYPTED_NAME_PATTERN = Pattern.compile("^([A-Za-z0-9+/\\-_]{4})*([A-Za-z0-9+/\\-]{4}|[A-Za-z0-9+/\\-_]{3}=|[A-Za-z0-9+/\\-_]{2}==)?$");
private static final BaseEncoding BASE64 = BaseEncoding.base64Url();
CryptoImplVaultFormat7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache);
}
@Override
CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException {
String dirFileName = encryptFolderName(cryptoParent, cleartextName);
CloudFolder dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).getCloudFolder(), dirFileName);
CloudFile dirFile = cloudContentRepository.file(dirFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT);
return folder(cryptoParent, cleartextName, dirFile);
}
@Override
String encryptName(CryptoFolder cryptoFolder, String name) throws BackendException {
String ciphertextName = cryptor() //
.fileNameCryptor() //
.encryptFilename(BASE64, name, dirIdInfo(cryptoFolder).getId().getBytes(UTF_8)) + CLOUD_NODE_EXT;
if (ciphertextName.length() > SHORT_NAMES_MAX_LENGTH) {
ciphertextName = deflate(cryptoFolder, ciphertextName);
}
return ciphertextName;
}
private String deflate(CryptoFolder cryptoParent, String longFileName) throws BackendException {
byte[] longFilenameBytes = longFileName.getBytes(UTF_8);
byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes);
String shortFileName = BASE64.encode(hash) + LONG_NODE_FILE_EXT;
CloudFolder dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).getCloudFolder(), shortFileName);
// if folder already exists in case of renaming
if (!cloudContentRepository.exists(dirFolder)) {
dirFolder = cloudContentRepository.create(dirFolder);
}
byte[] data = longFileName.getBytes(UTF_8);
CloudFile cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT, Optional.of((long) data.length));
cloudContentRepository.write(cloudFile, ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, true, data.length);
return shortFileName;
}
private CloudFile metadataFile(CloudNode cloudNode) throws BackendException {
CloudFolder cloudFolder;
if (cloudNode instanceof CloudFile) {
cloudFolder = cloudNode.getParent();
} else if (cloudNode instanceof CloudFolder) {
cloudFolder = (CloudFolder) cloudNode;
} else {
throw new IllegalStateException("Should be file or folder");
}
return cloudContentRepository.file(cloudFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT);
}
private String inflate(CloudNode cloudNode) throws BackendException {
CloudFile metadataFile = metadataFile(cloudNode);
ByteArrayOutputStream out = new ByteArrayOutputStream();
cloudContentRepository.read(metadataFile, Optional.empty(), out, NO_OP_PROGRESS_AWARE);
return new String(out.toByteArray(), UTF_8);
}
@Override
String decryptName(String dirId, String encryptedName) {
Optional<String> ciphertextName = extractEncryptedName(encryptedName);
if (ciphertextName.isPresent()) {
return cryptor().fileNameCryptor().decryptFilename(BASE64, ciphertextName.get(), dirId.getBytes(UTF_8));
} else {
return null;
}
}
@Override
List<CryptoNode> list(CryptoFolder cryptoFolder) throws BackendException {
dirIdCache.evictSubFoldersOf(cryptoFolder);
DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(cryptoFolder);
String dirId = dirIdInfo(cryptoFolder).getId();
CloudFolder lvl2Dir = dirIdInfo.getCloudFolder();
List<CloudNode> ciphertextNodes;
try {
ciphertextNodes = cloudContentRepository.list(lvl2Dir);
} catch (NoSuchCloudFileException e) {
if (cryptoFolder instanceof RootCryptoFolder) {
Timber.tag("CryptoFs").e("No lvl2Dir exists for root folder in %s", lvl2Dir.getPath());
throw new FatalBackendException(String.format("No lvl2Dir exists for root folder in %s", lvl2Dir.getPath()), e);
} else if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder.getDirFile().getParent(), CLOUD_NODE_SYMLINK_PRE + CLOUD_NODE_EXT))) {
throw new SymLinkException();
} else if (!cloudContentRepository.exists(cryptoFolder.getDirFile())) {
Timber.tag("CryptoFs").e("No dir file exists in %s", cryptoFolder.getDirFile().getPath());
throw new NoDirFileException(cryptoFolder.getName(), cryptoFolder.getDirFile().getPath());
}
return Collections.emptyList();
}
List<CryptoNode> result = new ArrayList<>();
for (CloudNode node : ciphertextNodes) {
ciphertextToCleartextNode(cryptoFolder, dirId, node).ifPresent(result::add);
}
return result;
}
private Optional<CryptoNode> ciphertextToCleartextNode(CryptoFolder cryptoFolder, String dirId, CloudNode cloudNode) throws BackendException {
String ciphertextName = cloudNode.getName();
Optional<CloudFile> longNameFolderDirFile = Optional.empty();
Optional<CloudFile> longNameFile = Optional.empty();
if (ciphertextName.endsWith(CLOUD_NODE_EXT)) {
ciphertextName = nameWithoutExtension(ciphertextName);
} else if (ciphertextName.endsWith(LONG_NODE_FILE_EXT)) {
Optional<String> ciphertextNameOption = longNodeCiphertextName(cloudNode);
if (ciphertextNameOption.isPresent()) {
ciphertextName = ciphertextNameOption.get();
} else {
return Optional.empty();
}
List<CloudNode> subfiles = cloudContentRepository.list((CloudFolder) cloudNode);
for (CloudNode cloudNode1 : subfiles) {
switch (cloudNode1.getName()) {
case LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT:
longNameFile = Optional.of((CloudFile) cloudNode1);
break;
case CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT:
longNameFolderDirFile = Optional.of((CloudFile) cloudNode1);
break;
case CLOUD_NODE_SYMLINK_PRE + CLOUD_NODE_EXT:
return Optional.empty();
}
}
}
try {
String cleartextName = decryptName(dirId, ciphertextName);
if (cleartextName == null) {
Timber.tag("CryptoFs").w("Failed to parse cipher text name of: %s", cloudNode.getPath());
return Optional.empty();
}
return cloudNodeFromName(cloudNode, cryptoFolder, cleartextName, longNameFile, longNameFolderDirFile);
} catch (AuthenticationFailedException e) {
Timber.tag("CryptoFs").w(e, "File/Folder name authentication failed: %s", cloudNode.getPath());
return Optional.empty();
} catch (IllegalArgumentException e) {
Timber.tag("CryptoFs").w(e, "Illegal ciphertext filename/folder: %s", cloudNode.getPath());
return Optional.empty();
}
}
private Optional<CryptoNode> cloudNodeFromName(CloudNode cloudNode, CryptoFolder cryptoFolder, String cleartextName, Optional<CloudFile> longNameFile, Optional<CloudFile> dirFile) throws BackendException {
if (cloudNode instanceof CloudFile) {
CloudFile cloudFile = (CloudFile) cloudNode;
Optional<Long> cleartextSize = Optional.empty();
if (cloudFile.getSize().isPresent()) {
long ciphertextSizeWithoutHeader = cloudFile.getSize().get() - cryptor().fileHeaderCryptor().headerSize();
if (ciphertextSizeWithoutHeader >= 0) {
cleartextSize = Optional.of(Cryptors.cleartextSize(ciphertextSizeWithoutHeader, cryptor()));
}
}
return Optional.of(file(cryptoFolder, cleartextName, cloudFile, cleartextSize));
} else if (cloudNode instanceof CloudFolder) {
if (longNameFile.isPresent()) {
// long file
Optional<Long> cleartextSize = Optional.empty();
if (longNameFile.get().getSize().isPresent()) {
long ciphertextSizeWithoutHeader = longNameFile.get().getSize().get() - cryptor().fileHeaderCryptor().headerSize();
if (ciphertextSizeWithoutHeader >= 0) {
cleartextSize = Optional.of(Cryptors.cleartextSize(ciphertextSizeWithoutHeader, cryptor()));
}
}
return Optional.of(file(cryptoFolder, cleartextName, longNameFile.get(), cleartextSize));
} else {
// folder
if (dirFile.isPresent()) {
return Optional.of(folder(cryptoFolder, cleartextName, dirFile.get()));
} else {
CloudFile constructedDirFile = cloudContentRepository.file((CloudFolder) cloudNode, "dir" + CLOUD_NODE_EXT);
return Optional.of(folder(cryptoFolder, cleartextName, constructedDirFile));
}
}
}
return Optional.empty();
}
private Optional<String> longNodeCiphertextName(CloudNode cloudNode) {
try {
String ciphertextName = inflate(cloudNode);
ciphertextName = nameWithoutExtension(ciphertextName);
return Optional.of(ciphertextName);
} catch (NoSuchCloudFileException e) {
Timber.tag("CryptoFs").e("Missing %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.getPath());
return Optional.empty();
} catch (BackendException e) {
Timber.tag("CryptoFs").e(e, "Failed to read %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.getPath());
return Optional.empty();
}
}
@Override
DirIdCache.DirIdInfo createDirIdInfo(CryptoFolder folder) throws BackendException {
String dirId = loadDirId(folder);
return dirIdCache.put(folder, createDirIdInfoFor(dirId));
}
@Override
String encryptFolderName(CryptoFolder cryptoFolder, String name) throws BackendException {
return encryptName(cryptoFolder, name);
}
@Override
CryptoSymlink symlink(CryptoFolder cryptoParent, String cleartextName, String target) throws BackendException {
return null;
}
@Override
String loadDirId(CryptoFolder folder) throws BackendException, EmptyDirFileException {
CloudFile dirFile = null;
if (folder.getDirFile() != null) {
dirFile = folder.getDirFile();
}
if (RootCryptoFolder.isRoot(folder)) {
return CryptoConstants.ROOT_DIR_ID;
} else if (dirFile != null && cloudContentRepository.exists(dirFile)) {
return new String(loadContentsOfDirFile(dirFile), UTF_8);
} else {
return newDirId();
}
}
private byte[] loadContentsOfDirFile(CloudFile file) throws BackendException, EmptyDirFileException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
cloudContentRepository.read(file, Optional.empty(), out, NO_OP_PROGRESS_AWARE);
if (dirfileIsEmpty(out)) {
throw new EmptyDirFileException(file.getName(), file.getPath());
}
return out.toByteArray();
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
CryptoFolder create(CryptoFolder folder) throws BackendException {
boolean shortName = false;
if (folder.getDirFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) {
assertCryptoLongDirFileAlreadyExists(folder);
} else {
assertCryptoFolderAlreadyExists(folder);
shortName = true;
}
DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(folder);
CloudFolder createdCloudFolder = cloudContentRepository.create(dirIdInfo.getCloudFolder());
CloudFolder dirFolder = folder.getDirFile().getParent();
CloudFile dirFile = folder.getDirFile();
if (shortName) {
dirFolder = cloudContentRepository.create(dirFolder);
dirFile = cloudContentRepository.file(dirFolder, folder.getDirFile().getName());
}
byte[] dirId = dirIdInfo.getId().getBytes(UTF_8);
CloudFile createdDirFile = cloudContentRepository.write(dirFile, ByteArrayDataSource.from(dirId), NO_OP_PROGRESS_AWARE, false, dirId.length);
CryptoFolder result = folder(folder, createdDirFile);
addFolderToCache(result, dirIdInfo.withCloudFolder(createdCloudFolder));
return result;
}
@Override
Optional<String> extractEncryptedName(String ciphertextName) {
final Matcher matcher = BASE64_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName);
if (matcher.find(0)) {
return Optional.of(matcher.group());
} else {
return Optional.empty();
}
}
@Override
CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException {
boolean shortName = false;
if (target.getDirFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) {
assertCryptoLongDirFileAlreadyExists(target);
} else {
assertCryptoFolderAlreadyExists(target);
shortName = true;
}
CloudFile targetDirFile = target.getDirFile();
if (shortName) {
CloudFolder targetDirFolder = cloudContentRepository.create(target.getDirFile().getParent());
targetDirFile = cloudContentRepository.file(targetDirFolder, target.getDirFile().getName());
}
CryptoFolder result = folder(target.getParent(), target.getName(), cloudContentRepository.move(source.getDirFile(), targetDirFile));
cloudContentRepository.delete(source.getDirFile().getParent());
evictFromCache(source);
evictFromCache(target);
return result;
}
@Override
CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException {
if (source.getCloudFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) {
CloudFolder targetDirFolder = cloudContentRepository.folder(target.getCloudFile().getParent(), target.getCloudFile().getName());
CryptoFile cryptoFile;
if (target.getCloudFile().getName().endsWith(LONG_NODE_FILE_EXT)) {
assertCryptoLongDirFileAlreadyExists(targetDirFolder);
cryptoFile = moveLongFileToLongFile(source, target, targetDirFolder);
} else {
assertCryptoFileAlreadyExists(target);
cryptoFile = moveLongFileToShortFile(source, target);
}
CloudFolder sourceDirFolder = cloudContentRepository.folder(source.getCloudFile().getParent().getParent(), source.getCloudFile().getParent().getName());
cloudContentRepository.delete(sourceDirFolder);
return cryptoFile;
} else {
CloudFolder targetDirFolder = cloudContentRepository.folder(target.getCloudFile().getParent(), target.getCloudFile().getName());
if (target.getCloudFile().getName().endsWith(LONG_NODE_FILE_EXT)) {
assertCryptoLongDirFileAlreadyExists(targetDirFolder);
return moveShortFileToLongFile(source, target, targetDirFolder);
} else {
assertCryptoFileAlreadyExists(target);
return file(target, cloudContentRepository.move(source.getCloudFile(), target.getCloudFile()), source.getSize());
}
}
}
private CryptoFile moveLongFileToLongFile(CryptoFile source, CryptoFile target, CloudFolder targetDirFolder) throws BackendException {
CloudFile sourceFile = cloudContentRepository.file(source.getCloudFile().getParent(), LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT);
CloudFile movedFile = cloudContentRepository.move(sourceFile, cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT));
return file(target, movedFile, movedFile.getSize());
}
private CryptoFile moveLongFileToShortFile(CryptoFile source, CryptoFile target) throws BackendException {
CloudFile sourceFile = cloudContentRepository.file(source.getCloudFile().getParent(), LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT);
CloudFile movedFile = cloudContentRepository.move(sourceFile, target.getCloudFile());
return file(target, movedFile, movedFile.getSize());
}
private CryptoFile moveShortFileToLongFile(CryptoFile source, CryptoFile target, CloudFolder targetDirFolder) throws BackendException {
CloudFile movedFile = cloudContentRepository.move(source.getCloudFile(), cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT));
return file(target, movedFile, movedFile.getSize());
}
@Override
void delete(CloudNode node) throws BackendException {
if (node instanceof CryptoFolder) {
CryptoFolder cryptoFolder = (CryptoFolder) node;
List<CryptoFolder> cryptoSubfolders = deepCollectSubfolders(cryptoFolder);
for (CryptoFolder cryptoSubfolder : cryptoSubfolders) {
try {
cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).getCloudFolder());
} catch (NoSuchCloudFileException e) {
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
}
}
try {
cloudContentRepository.delete(dirIdInfo(cryptoFolder).getCloudFolder());
} catch (NoSuchCloudFileException e) {
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
}
cloudContentRepository.delete(cryptoFolder.getDirFile().getParent());
evictFromCache(cryptoFolder);
} else if (node instanceof CryptoFile) {
CryptoFile cryptoFile = (CryptoFile) node;
if (cryptoFile.getCloudFile().getParent().getName().endsWith(LONG_NODE_FILE_EXT)) {
cloudContentRepository.delete(cryptoFile.getCloudFile().getParent());
} else {
cloudContentRepository.delete(cryptoFile.getCloudFile());
}
}
}
@Override
public CryptoFile write(CryptoFile cryptoFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException {
if (cryptoFile.getCloudFile().getName().endsWith(LONG_NODE_FILE_EXT)) {
return writeLongFile(cryptoFile, data, progressAware, replace, length);
} else {
return writeShortNameFile(cryptoFile, data, progressAware, replace, length);
}
}
private CryptoFile writeLongFile(CryptoFile cryptoFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException {
CloudFolder dirFolder = cloudContentRepository.folder(cryptoFile.getCloudFile().getParent(), cryptoFile.getCloudFile().getName());
CloudFile cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, data.size(context));
assertCryptoLongDirFileAlreadyExists(dirFolder);
try (InputStream stream = data.open(context)) {
File encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", getInternalCache());
try (WritableByteChannel writableByteChannel = Channels.newChannel(new FileOutputStream(encryptedTmpFile)); //
WritableByteChannel encryptingWritableByteChannel = new EncryptingWritableByteChannel(writableByteChannel, cryptor())) {
progressAware.onProgress(Progress.started(UploadState.encryption(cloudFile)));
ByteBuffer buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize());
long ciphertextSize = Cryptors.ciphertextSize(cloudFile.getSize().get(), cryptor()) + cryptor().fileHeaderCryptor().headerSize();
int read;
long encrypted = 0;
while ((read = stream.read(buff.array())) > 0) {
buff.limit(read);
int written = encryptingWritableByteChannel.write(buff);
buff.flip();
encrypted += written;
progressAware.onProgress(progress(UploadState.encryption(cloudFile)).between(0).and(ciphertextSize).withValue(encrypted));
}
encryptingWritableByteChannel.close();
progressAware.onProgress(Progress.completed(UploadState.encryption(cloudFile)));
CloudFile targetFile = targetFile(cryptoFile, cloudFile, replace);
return file(cryptoFile, //
cloudContentRepository.write( //
targetFile, //
data.decorate(FileBasedDataSource.from(encryptedTmpFile)), //
new UploadFileReplacingProgressAware(cryptoFile, progressAware), //
replace, //
encryptedTmpFile.length()), //
cryptoFile.getSize());
} catch (Throwable e) {
throw e;
} finally {
encryptedTmpFile.delete();
}
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
private CloudFile targetFile(CryptoFile cryptoFile, CloudFile cloudFile, boolean replace) throws BackendException {
if (replace || !cloudContentRepository.exists(cloudFile)) {
return cloudFile;
}
return firstNonExistingAutoRenamedFile(cryptoFile);
}
private CloudFile firstNonExistingAutoRenamedFile(CryptoFile original) throws BackendException {
String name = original.getName();
String nameWithoutExtension = nameWithoutExtension(name);
String extension = extension(name);
if (!extension.isEmpty()) {
extension = "." + extension;
}
int counter = 1;
CryptoFile result;
CloudFile cloudFile;
do {
String newFileName = nameWithoutExtension + " (" + counter + ")" + extension;
result = file(original.getParent(), newFileName, original.getSize());
counter++;
CloudFolder dirFolder = cloudContentRepository.folder(result.getCloudFile().getParent(), result.getCloudFile().getName());
cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, result.getSize());
} while (cloudContentRepository.exists(cloudFile));
return cloudFile;
}
private void assertCryptoLongDirFileAlreadyExists(CloudFolder cryptoFolder) throws BackendException {
if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT))) {
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
}
}
}

View File

@ -0,0 +1,538 @@
package org.cryptomator.data.cloud.crypto
import android.content.Context
import com.google.common.io.BaseEncoding
import org.cryptomator.cryptolib.api.AuthenticationFailedException
import org.cryptomator.cryptolib.api.Cryptor
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel
import org.cryptomator.cryptolib.common.MessageDigestSupplier
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.EmptyDirFileException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NoDirFileException
import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.exception.ParentFolderIsNullException
import org.cryptomator.domain.exception.SymLinkException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from
import org.cryptomator.domain.usecases.cloud.Progress
import org.cryptomator.domain.usecases.cloud.UploadState
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.channels.Channels
import java.nio.charset.StandardCharsets
import java.util.UUID
import java.util.function.Supplier
import java.util.regex.Pattern
import kotlin.streams.toList
import timber.log.Timber
open class CryptoImplVaultFormat7 : CryptoImplDecorator {
constructor(
context: Context,
cryptor: Supplier<Cryptor>,
cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>,
storageLocation: CloudFolder,
dirIdCache: DirIdCache
) : super(
context, cryptor, cloudContentRepository, storageLocation, dirIdCache, CryptoConstants.DEFAULT_MAX_FILE_NAME
)
constructor(
context: Context,
cryptor: Supplier<Cryptor>,
cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>,
storageLocation: CloudFolder,
dirIdCache: DirIdCache,
shorteningThreshold: Int
) : super(
context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold
)
@Throws(BackendException::class)
override fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder {
val dirFileName = encryptFolderName(cryptoParent, cleartextName)
val dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).cloudFolder, dirFileName)
val dirFile = cloudContentRepository.file(dirFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT)
return folder(cryptoParent, cleartextName, dirFile)
}
@Throws(BackendException::class)
override fun encryptName(cryptoParent: CryptoFolder, name: String): String {
var ciphertextName: String = cryptor() //
.fileNameCryptor() //
.encryptFilename(BaseEncoding.base64Url(), name, dirIdInfo(cryptoParent).id.toByteArray(StandardCharsets.UTF_8)) + CLOUD_NODE_EXT
if (ciphertextName.length > shorteningThreshold) {
ciphertextName = deflate(cryptoParent, ciphertextName)
}
return ciphertextName
}
@Throws(BackendException::class)
private fun deflate(cryptoParent: CryptoFolder, longFileName: String): String {
val longFilenameBytes = longFileName.toByteArray(StandardCharsets.UTF_8)
val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes)
val shortFileName = BaseEncoding.base64Url().encode(hash) + LONG_NODE_FILE_EXT
var dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).cloudFolder, shortFileName)
// if folder already exists in case of renaming
if (!cloudContentRepository.exists(dirFolder)) {
dirFolder = cloudContentRepository.create(dirFolder)
}
val data = longFileName.toByteArray(StandardCharsets.UTF_8)
val cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT, data.size.toLong())
cloudContentRepository.write(cloudFile, from(data), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, true, data.size.toLong())
return shortFileName
}
@Throws(BackendException::class)
private fun metadataFile(cloudNode: CloudNode): CloudFile {
val cloudFolder = when (cloudNode) {
is CloudFile -> {
cloudNode.parent
}
is CloudFolder -> {
cloudNode
}
else -> {
throw IllegalStateException("Should be file or folder")
}
}
return cloudContentRepository.file(cloudFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT)
}
@Throws(BackendException::class)
private fun inflate(cloudNode: CloudNode): String {
val metadataFile = metadataFile(cloudNode)
val out = ByteArrayOutputStream()
cloudContentRepository.read(metadataFile, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
return String(out.toByteArray(), StandardCharsets.UTF_8)
}
override fun decryptName(dirId: String, encryptedName: String): String? {
return extractEncryptedName(encryptedName)?.let {
return cryptor().fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), it, dirId.toByteArray(StandardCharsets.UTF_8))
}
}
@Throws(BackendException::class)
override fun list(cryptoFolder: CryptoFolder): List<CryptoNode> {
dirIdCache.evictSubFoldersOf(cryptoFolder)
val dirIdInfo = dirIdInfo(cryptoFolder)
val dirId = dirIdInfo(cryptoFolder).id
val lvl2Dir = dirIdInfo.cloudFolder
val ciphertextNodes: List<CloudNode> = try {
cloudContentRepository.list(lvl2Dir)
} catch (e: NoSuchCloudFileException) {
if (cryptoFolder is RootCryptoFolder) {
Timber.tag("CryptoFs").e("No lvl2Dir exists for root folder in %s", lvl2Dir.path)
throw FatalBackendException(String.format("No lvl2Dir exists for root folder in %s", lvl2Dir.path), e)
} else if (cryptoFolder.dirFile == null) {
Timber.tag("CryptoFs").e(String.format("Dir-file of folder is null %s", lvl2Dir.path))
throw FatalBackendException(String.format("Dir-file of folder is null %s", lvl2Dir.path))
} else if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder.dirFile.parent, CLOUD_NODE_SYMLINK_PRE + CLOUD_NODE_EXT))) {
throw SymLinkException()
} else if (!cloudContentRepository.exists(cryptoFolder.dirFile)) {
Timber.tag("CryptoFs").e("No dir file exists in %s", cryptoFolder.dirFile.path)
throw NoDirFileException(cryptoFolder.name, cryptoFolder.dirFile.path)
}
return emptyList()
}
return ciphertextNodes
.parallelStream()
.map { node ->
ciphertextToCleartextNode(cryptoFolder, dirId, node)
}
.toList()
.filterNotNull()
}
@Throws(BackendException::class)
private fun ciphertextToCleartextNode(cryptoFolder: CryptoFolder, dirId: String, cloudNode: CloudNode): CryptoNode? {
var ciphertextName = cloudNode.name
var longNameFolderDirFile: CloudFile? = null
var longNameFile: CloudFile? = null
if (ciphertextName.endsWith(CLOUD_NODE_EXT)) {
ciphertextName = nameWithoutExtension(ciphertextName)
} else if (ciphertextName.endsWith(LONG_NODE_FILE_EXT)) {
ciphertextName = (longNodeCiphertextName(cloudNode) ?: return null)
for (node in cloudContentRepository.list((cloudNode as CloudFolder))) {
when (node.name) {
LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT -> longNameFile = node as CloudFile
CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT -> longNameFolderDirFile = node as CloudFile
}
}
}
return try {
val cleartextName = decryptName(dirId, ciphertextName)
if (cleartextName == null) {
Timber.tag("CryptoFs").w("Failed to parse cipher text name of: %s", cloudNode.path)
return null
}
cloudNodeFromName(cloudNode, cryptoFolder, cleartextName, longNameFile, longNameFolderDirFile)
} catch (e: AuthenticationFailedException) {
Timber.tag("CryptoFs").w(e, "File/Folder name authentication failed: %s", cloudNode.path)
null
} catch (e: IllegalArgumentException) {
Timber.tag("CryptoFs").w(e, "Illegal ciphertext filename/folder: %s", cloudNode.path)
null
}
}
@Throws(BackendException::class)
private fun cloudNodeFromName(cloudNode: CloudNode, cryptoFolder: CryptoFolder, cleartextName: String, longNameFile: CloudFile?, dirFile: CloudFile?): CryptoNode? {
if (cloudNode is CloudFile) {
val cleartextSize = cloudNode.size?.let {
val ciphertextSizeWithoutHeader = it - cryptor().fileHeaderCryptor().headerSize()
if (ciphertextSizeWithoutHeader >= 0) {
cryptor().fileContentCryptor().cleartextSize(ciphertextSizeWithoutHeader)
} else {
null
}
}
return file(cryptoFolder, cleartextName, cloudNode, cleartextSize)
} else if (cloudNode is CloudFolder) {
return if (longNameFile != null) {
// long file
val cleartextSize = longNameFile.size?.let {
val ciphertextSizeWithoutHeader: Long = it - cryptor().fileHeaderCryptor().headerSize()
if (ciphertextSizeWithoutHeader >= 0) {
cryptor().fileContentCryptor().cleartextSize(ciphertextSizeWithoutHeader)
} else {
null
}
}
file(cryptoFolder, cleartextName, longNameFile, cleartextSize)
} else {
// folder
if (dirFile != null) {
folder(cryptoFolder, cleartextName, dirFile)
} else {
val constructedDirFile = cloudContentRepository.file(cloudNode, "dir$CLOUD_NODE_EXT")
folder(cryptoFolder, cleartextName, constructedDirFile)
}
}
}
return null
}
private fun longNodeCiphertextName(cloudNode: CloudNode): String? {
return try {
val ciphertextName = inflate(cloudNode)
nameWithoutExtension(ciphertextName)
} catch (e: NoSuchCloudFileException) {
Timber.tag("CryptoFs").e("Missing %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.path)
null
} catch (e: BackendException) {
Timber.tag("CryptoFs").e(e, "Failed to read %s%s for cloud node: %s", LONG_NODE_FILE_CONTENT_NAME, LONG_NODE_FILE_EXT, cloudNode.path)
null
}
}
@Throws(BackendException::class)
override fun createDirIdInfo(folder: CryptoFolder): DirIdInfo {
val dirId = loadDirId(folder)
return dirIdCache.put(folder, createDirIdInfoFor(dirId))
}
@Throws(BackendException::class)
override fun encryptFolderName(cryptoFolder: CryptoFolder, name: String): String {
return encryptName(cryptoFolder, name)
}
@Throws(BackendException::class)
override fun symlink(cryptoParent: CryptoFolder, cleartextName: String, target: String): CryptoSymlink {
throw FatalBackendException("FOOOO") // FIXME
}
@Throws(BackendException::class, EmptyDirFileException::class)
override fun loadDirId(folder: CryptoFolder): String {
var dirFile: CloudFile? = null
if (folder.dirFile != null) {
dirFile = folder.dirFile
}
return if (RootCryptoFolder.isRoot(folder)) {
CryptoConstants.ROOT_DIR_ID
} else if (dirFile != null && cloudContentRepository.exists(dirFile)) {
String(loadContentsOfDirFile(dirFile), StandardCharsets.UTF_8)
} else {
newDirId()
}
}
@Throws(BackendException::class, EmptyDirFileException::class)
private fun loadContentsOfDirFile(file: CloudFile): ByteArray {
try {
ByteArrayOutputStream().use { out ->
cloudContentRepository.read(file, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
if (dirfileIsEmpty(out)) {
throw EmptyDirFileException(file.parent.name, file.path)
}
return out.toByteArray()
}
} catch (e: IOException) {
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun create(folder: CryptoFolder): CryptoFolder {
requireNotNull(folder.dirFile)
var shortName = false
if (folder.dirFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) {
assertCryptoLongDirFileAlreadyExists(folder)
} else {
assertCryptoFolderAlreadyExists(folder)
shortName = true
}
val dirIdInfo = dirIdInfo(folder)
val createdCloudFolder = cloudContentRepository.create(dirIdInfo.cloudFolder)
var dirFolder = folder.dirFile.parent
var dirFile = folder.dirFile
if (shortName) {
dirFolder = cloudContentRepository.create(dirFolder)
dirFile = cloudContentRepository.file(dirFolder, folder.dirFile.name)
}
val dirId = dirIdInfo.id.toByteArray(StandardCharsets.UTF_8)
val createdDirFile = cloudContentRepository.write(dirFile, from(dirId), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, dirId.size.toLong())
val result = folder(folder, createdDirFile)
addFolderToCache(result, dirIdInfo.withCloudFolder(createdCloudFolder))
return result
}
override fun extractEncryptedName(ciphertextName: String): String? {
val matcher = BASE64_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName)
return if (matcher.find(0)) {
matcher.group()
} else {
null
}
}
@Throws(BackendException::class)
override fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder {
requireNotNull(source.dirFile)
requireNotNull(target.dirFile)
target.parent?.let { targetsParent ->
var shortName = false
if (target.dirFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) {
assertCryptoLongDirFileAlreadyExists(target)
} else {
assertCryptoFolderAlreadyExists(target)
shortName = true
}
var targetDirFile = target.dirFile
if (shortName) {
val targetDirFolder = cloudContentRepository.create(target.dirFile.parent)
targetDirFile = cloudContentRepository.file(targetDirFolder, target.dirFile.name)
}
val result = folder(targetsParent, target.name, cloudContentRepository.move(source.dirFile, targetDirFile))
cloudContentRepository.delete(source.dirFile.parent)
evictFromCache(source)
evictFromCache(target)
return result
} ?: throw ParentFolderIsNullException(target.name)
}
@Throws(BackendException::class)
override fun move(source: CryptoFile, target: CryptoFile): CryptoFile {
return if (source.cloudFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) {
val targetDirFolder = cloudContentRepository.folder(target.cloudFile.parent, target.cloudFile.name)
val cryptoFile: CryptoFile = if (target.cloudFile.name.endsWith(LONG_NODE_FILE_EXT)) {
assertCryptoLongDirFileAlreadyExists(targetDirFolder)
moveLongFileToLongFile(source, target, targetDirFolder)
} else {
assertCryptoFileAlreadyExists(target)
moveLongFileToShortFile(source, target)
}
source.cloudFile.parent.parent?.let {
val sourceDirFolder = cloudContentRepository.folder(it, source.cloudFile.parent.name)
cloudContentRepository.delete(sourceDirFolder)
}
cryptoFile
} else {
if (target.cloudFile.name.endsWith(LONG_NODE_FILE_EXT)) {
val targetDirFolder = cloudContentRepository.folder(target.cloudFile.parent, target.cloudFile.name)
assertCryptoLongDirFileAlreadyExists(targetDirFolder)
moveShortFileToLongFile(source, target, targetDirFolder)
} else {
assertCryptoFileAlreadyExists(target)
file(target, cloudContentRepository.move(source.cloudFile, target.cloudFile), source.size)
}
}
}
@Throws(BackendException::class)
private fun moveLongFileToLongFile(source: CryptoFile, target: CryptoFile, targetDirFolder: CloudFolder): CryptoFile {
requireNotNull(source.cloudFile.parent)
val sourceFile = cloudContentRepository.file(source.cloudFile.parent, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT)
val movedFile = cloudContentRepository.move(sourceFile, cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT))
return file(target, movedFile, movedFile.size)
}
@Throws(BackendException::class)
private fun moveLongFileToShortFile(source: CryptoFile, target: CryptoFile): CryptoFile {
requireNotNull(source.cloudFile.parent)
val sourceFile = cloudContentRepository.file(source.cloudFile.parent, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT)
val movedFile = cloudContentRepository.move(sourceFile, target.cloudFile)
return file(target, movedFile, movedFile.size)
}
@Throws(BackendException::class)
private fun moveShortFileToLongFile(source: CryptoFile, target: CryptoFile, targetDirFolder: CloudFolder): CryptoFile {
val movedFile = cloudContentRepository.move(source.cloudFile, cloudContentRepository.file(targetDirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT))
return file(target, movedFile, movedFile.size)
}
@Throws(BackendException::class)
override fun delete(node: CloudNode) {
if (node is CryptoFolder) {
requireNotNull(node.dirFile)
val cryptoSubfolders = deepCollectSubfolders(node)
for (cryptoSubfolder in cryptoSubfolders) {
try {
cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).cloudFolder)
} catch (e: NoSuchCloudFileException) {
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
}
}
try {
cloudContentRepository.delete(dirIdInfo(node).cloudFolder)
} catch (e: NoSuchCloudFileException) {
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
}
cloudContentRepository.delete(node.dirFile.parent)
evictFromCache(node)
} else if (node is CryptoFile) {
if (node.cloudFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) {
cloudContentRepository.delete(node.cloudFile.parent)
} else {
cloudContentRepository.delete(node.cloudFile)
}
}
}
@Throws(BackendException::class)
override fun write(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile {
return if (cryptoFile.cloudFile.name.endsWith(LONG_NODE_FILE_EXT)) {
writeLongFile(cryptoFile, data, progressAware, replace, length)
} else {
writeShortNameFile(cryptoFile, data, progressAware, replace, length)
}
}
@Throws(BackendException::class)
private fun writeLongFile(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile {
val dirFolder = cloudContentRepository.folder(cryptoFile.cloudFile.parent, cryptoFile.cloudFile.name)
val cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, data.size(context))
assertCryptoLongDirFileAlreadyExists(dirFolder)
try {
data.open(context)?.use { stream ->
val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache)
try {
Channels.newChannel(FileOutputStream(encryptedTmpFile)).use { writableByteChannel ->
EncryptingWritableByteChannel(writableByteChannel, cryptor()).use { encryptingWritableByteChannel ->
cloudFile.size?.let { size ->
progressAware.onProgress(Progress.started(UploadState.encryption(cloudFile)))
val buff = ByteBuffer.allocate(cryptor().fileContentCryptor().cleartextChunkSize())
val ciphertextSize = cryptor().fileContentCryptor().ciphertextSize(size) + cryptor().fileHeaderCryptor().headerSize()
var read: Int
var encrypted: Long = 0
while (stream.read(buff.array()).also { read = it } > 0) {
buff.limit(read)
val written = encryptingWritableByteChannel.write(buff)
buff.flip()
encrypted += written.toLong()
progressAware.onProgress(Progress.progress(UploadState.encryption(cloudFile)).between(0).and(ciphertextSize).withValue(encrypted))
}
encryptingWritableByteChannel.close()
progressAware.onProgress(Progress.completed(UploadState.encryption(cloudFile)))
val targetFile = targetFile(cryptoFile, cloudFile, replace)
return file(
cryptoFile, //
cloudContentRepository.write( //
targetFile, //
data.decorate(from(encryptedTmpFile)),
UploadFileReplacingProgressAware(cryptoFile, progressAware), //
replace, //
encryptedTmpFile.length()
), //
cryptoFile.size
)
} ?: throw FatalBackendException("CloudFile size shouldn't be null")
}
}
} finally {
encryptedTmpFile.delete()
}
} ?: throw FatalBackendException("InputStream shouldn't be null")
} catch (e: IOException) {
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
private fun targetFile(cryptoFile: CryptoFile, cloudFile: CloudFile, replace: Boolean): CloudFile {
return if (replace || !cloudContentRepository.exists(cloudFile)) {
cloudFile
} else firstNonExistingAutoRenamedFile(cryptoFile)
}
@Throws(BackendException::class)
private fun firstNonExistingAutoRenamedFile(original: CryptoFile): CloudFile {
val name = original.name
val nameWithoutExtension = nameWithoutExtension(name)
var extension = extension(name)
if (extension.isNotEmpty()) {
extension = ".$extension"
}
var counter = 1
var result: CryptoFile
var cloudFile: CloudFile
do {
val newFileName = "$nameWithoutExtension ($counter)$extension"
result = file(original.parent, newFileName, original.size)
counter++
val dirFolder = cloudContentRepository.folder(result.cloudFile.parent, result.cloudFile.name)
cloudFile = cloudContentRepository.file(dirFolder, LONG_NODE_FILE_CONTENT_CONTENTS + CLOUD_NODE_EXT, result.size)
} while (cloudContentRepository.exists(cloudFile))
return cloudFile
}
@Throws(BackendException::class)
private fun assertCryptoLongDirFileAlreadyExists(cryptoFolder: CloudFolder) {
if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT))) {
throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false")
}
}
companion object {
private const val CLOUD_NODE_EXT = ".c9r"
private const val LONG_NODE_FILE_EXT = ".c9s"
private const val CLOUD_FOLDER_DIR_FILE_PRE = "dir"
private const val LONG_NODE_FILE_CONTENT_CONTENTS = "contents"
private const val LONG_NODE_FILE_CONTENT_NAME = "name"
private const val CLOUD_NODE_SYMLINK_PRE = "symlink"
private val BASE64_ENCRYPTED_NAME_PATTERN = Pattern.compile("^([A-Za-z0-9+/\\-_]{4})*([A-Za-z0-9+/\\-]{4}|[A-Za-z0-9+/\\-_]{3}=|[A-Za-z0-9+/\\-_]{2}==)?$")
}
}

View File

@ -0,0 +1,21 @@
package org.cryptomator.data.cloud.crypto
import android.content.Context
import org.cryptomator.cryptolib.api.Cryptor
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.repository.CloudContentRepository
import java.util.function.Supplier
class CryptoImplVaultFormat8 internal constructor(
context: Context,
cryptor: Supplier<Cryptor>,
cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>,
storageLocation: CloudFolder,
dirIdCache: DirIdCache,
shorteningThreshold: Int
) : CryptoImplVaultFormat7(
context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold
)

View File

@ -1,270 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import android.content.Context;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.exception.AlreadyExistException;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.EmptyDirFileException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import org.cryptomator.util.Supplier;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import timber.log.Timber;
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
import static org.cryptomator.util.Encodings.UTF_8;
final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator {
private static final int SHORT_NAMES_MAX_LENGTH = 129;
private static final String DIR_PREFIX = "0";
private static final String SYMLINK_PREFIX = "1S";
private static final String LONG_NAME_FILE_EXT = ".lng";
private static final String METADATA_DIR_NAME = "m";
private static final BaseNCodec BASE32 = new Base32();
private static final Pattern BASE32_ENCRYPTED_NAME_PATTERN = Pattern.compile("^(0|1S)?(([A-Z2-7]{8})*[A-Z2-7=]{8})$");
CryptoImplVaultFormatPre7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache);
}
@Override
CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException {
String dirFileName = encryptFolderName(cryptoParent, cleartextName);
CloudFile dirFile = cloudContentRepository.file(dirIdInfo(cryptoParent).getCloudFolder(), dirFileName);
return folder(cryptoParent, cleartextName, dirFile);
}
@Override
CryptoFolder create(CryptoFolder folder) throws BackendException {
assertCryptoFolderAlreadyExists(folder);
DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(folder);
CloudFolder createdCloudFolder = cloudContentRepository.create(dirIdInfo.getCloudFolder());
byte[] dirId = dirIdInfo.getId().getBytes(UTF_8);
CloudFile createdDirFile = cloudContentRepository.write(folder.getDirFile(), ByteArrayDataSource.from(dirId), NO_OP_PROGRESS_AWARE, false, dirId.length);
CryptoFolder result = folder(folder, createdDirFile);
addFolderToCache(result, dirIdInfo.withCloudFolder(createdCloudFolder));
return result;
}
@Override
String encryptName(CryptoFolder cryptoParent, String name) throws BackendException {
return encryptName(cryptoParent, name, "");
}
private String encryptName(CryptoFolder cryptoParent, String name, String prefix) throws BackendException {
String ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(name, dirIdInfo(cryptoParent).getId().getBytes(UTF_8));
if (ciphertextName.length() > SHORT_NAMES_MAX_LENGTH) {
ciphertextName = deflate(ciphertextName);
}
return ciphertextName;
}
private String deflate(String longFileName) throws BackendException {
byte[] longFilenameBytes = longFileName.getBytes(UTF_8);
byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes);
String shortFileName = BASE32.encodeAsString(hash) + LONG_NAME_FILE_EXT;
CloudFile metadataFile = metadataFile(shortFileName);
byte[] data = longFileName.getBytes(UTF_8);
try {
cloudContentRepository.create(metadataFile.getParent());
} catch (AlreadyExistException e) {
}
cloudContentRepository.write(metadataFile, ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, true, data.length);
return shortFileName;
}
private String inflate(String shortFileName) throws BackendException {
CloudFile metadataFile = metadataFile(shortFileName);
ByteArrayOutputStream out = new ByteArrayOutputStream();
cloudContentRepository.read(metadataFile, Optional.empty(), out, NO_OP_PROGRESS_AWARE);
return new String(out.toByteArray(), UTF_8);
}
private CloudFile inflatePermanently(CloudFile cloudFile, String longFileName) throws BackendException {
Timber.tag("CryptoFs").i("inflatePermanently: %s -> %s", cloudFile.getName(), longFileName);
CloudFile newCiphertextFile = cloudContentRepository.file(cloudFile.getParent(), longFileName);
cloudContentRepository.move(cloudFile, newCiphertextFile);
return newCiphertextFile;
}
private CloudFile metadataFile(String shortFilename) throws BackendException {
CloudFolder firstLevelFolder = cloudContentRepository.folder(metadataFolder(), shortFilename.substring(0, 2));
CloudFolder secondLevelFolder = cloudContentRepository.folder(firstLevelFolder, shortFilename.substring(2, 4));
return cloudContentRepository.file(secondLevelFolder, shortFilename);
}
private CloudFolder metadataFolder() throws BackendException {
return cloudContentRepository.folder(storageLocation(), METADATA_DIR_NAME);
}
@Override
List<CryptoNode> list(CryptoFolder cryptoFolder) throws BackendException {
DirIdCache.DirIdInfo dirIdInfo = dirIdInfo(cryptoFolder);
String dirId = dirIdInfo(cryptoFolder).getId();
CloudFolder lvl2Dir = dirIdInfo.getCloudFolder();
List<CloudNode> ciphertextNodes = cloudContentRepository.list(lvl2Dir);
List<CryptoNode> result = new ArrayList<>();
for (CloudNode node : ciphertextNodes) {
if (node instanceof CloudFile) {
ciphertextToCleartextNode(cryptoFolder, dirId, node).ifPresent(result::add);
}
}
return result;
}
private Optional<CryptoNode> ciphertextToCleartextNode(CryptoFolder cryptoFolder, String dirId, CloudNode cloudNode) throws BackendException {
CloudFile cloudFile = (CloudFile) cloudNode;
String ciphertextName = cloudFile.getName();
if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) {
try {
ciphertextName = inflate(ciphertextName);
if (ciphertextName.length() <= SHORT_NAMES_MAX_LENGTH) {
cloudFile = inflatePermanently(cloudFile, ciphertextName);
}
} catch (NoSuchCloudFileException e) {
Timber.tag("CryptoFs").e("Missing mFile: %s", ciphertextName);
return Optional.empty();
} catch (BackendException e) {
Timber.tag("CryptoFs").e(e, "Failed to read mFile: %s", ciphertextName);
return Optional.empty();
}
}
String cleartextName;
try {
cleartextName = decryptName(dirId, ciphertextName.toUpperCase());
} catch (AuthenticationFailedException e) {
Timber.tag("CryptoFs").w("File name authentication failed: %s", cloudFile.getPath());
return Optional.empty();
} catch (IllegalArgumentException e) {
Timber.tag("CryptoFs").d("Illegal ciphertext filename: %s", cloudFile.getPath());
return Optional.empty();
}
if (cleartextName == null || ciphertextName.startsWith(SYMLINK_PREFIX)) {
return Optional.empty();
} else if (ciphertextName.startsWith(DIR_PREFIX)) {
return Optional.of(folder(cryptoFolder, cleartextName, cloudFile));
} else {
Optional<Long> cleartextSize = Optional.empty();
if (cloudFile.getSize().isPresent()) {
long ciphertextSizeWithoutHeader = cloudFile.getSize().get() - cryptor().fileHeaderCryptor().headerSize();
if (ciphertextSizeWithoutHeader >= 0) {
cleartextSize = Optional.of(Cryptors.cleartextSize(ciphertextSizeWithoutHeader, cryptor()));
}
}
return Optional.of(file(cryptoFolder, cleartextName, cloudFile, cleartextSize));
}
}
@Override
String decryptName(String dirId, String encryptedName) {
Optional<String> ciphertextName = extractEncryptedName(encryptedName);
if (ciphertextName.isPresent()) {
return cryptor().fileNameCryptor().decryptFilename(ciphertextName.get(), dirId.getBytes(UTF_8));
} else {
return null;
}
}
@Override
Optional<String> extractEncryptedName(String ciphertextName) {
Matcher matcher = BASE32_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName);
if (matcher.find(0)) {
return Optional.of(matcher.group(2));
} else {
return Optional.empty();
}
}
@Override
CryptoSymlink symlink(CryptoFolder cryptoParent, String cleartextName, String target) throws BackendException {
String ciphertextName = encryptSymlinkName(cryptoParent, cleartextName);
CloudFile cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).getCloudFolder(), ciphertextName);
return new CryptoSymlink(cryptoParent, cleartextName, path(cryptoParent, cleartextName), target, cloudFile);
}
private String encryptSymlinkName(CryptoFolder cryptoFolder, String name) throws BackendException {
return encryptName(cryptoFolder, name, SYMLINK_PREFIX);
}
@Override
String encryptFolderName(CryptoFolder cryptoFolder, String name) throws BackendException {
return encryptName(cryptoFolder, name, DIR_PREFIX);
}
@Override
CryptoFolder move(CryptoFolder source, CryptoFolder target) throws BackendException {
assertCryptoFolderAlreadyExists(target);
CryptoFolder result = folder(target.getParent(), target.getName(), cloudContentRepository.move(source.getDirFile(), target.getDirFile()));
evictFromCache(source);
evictFromCache(target);
return result;
}
@Override
CryptoFile move(CryptoFile source, CryptoFile target) throws BackendException {
assertCryptoFileAlreadyExists(target);
return file(target, cloudContentRepository.move(source.getCloudFile(), target.getCloudFile()), source.getSize());
}
@Override
void delete(CloudNode node) throws BackendException {
if (node instanceof CryptoFolder) {
CryptoFolder cryptoFolder = (CryptoFolder) node;
List<CryptoFolder> cryptoSubfolders = deepCollectSubfolders(cryptoFolder);
for (CryptoFolder cryptoSubfolder : cryptoSubfolders) {
cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).getCloudFolder());
}
cloudContentRepository.delete(dirIdInfo(cryptoFolder).getCloudFolder());
cloudContentRepository.delete(cryptoFolder.getDirFile());
evictFromCache(cryptoFolder);
} else if (node instanceof CryptoFile) {
CryptoFile cryptoFile = (CryptoFile) node;
cloudContentRepository.delete(cryptoFile.getCloudFile());
}
}
@Override
String loadDirId(CryptoFolder folder) throws BackendException, EmptyDirFileException {
if (RootCryptoFolder.isRoot(folder)) {
return CryptoConstants.ROOT_DIR_ID;
} else if (cloudContentRepository.exists(folder.getDirFile())) {
return new String(loadContentsOfDirFile(folder), UTF_8);
} else {
return newDirId();
}
}
@Override
DirIdCache.DirIdInfo createDirIdInfo(CryptoFolder folder) throws BackendException {
String dirId = loadDirId(folder);
return dirIdCache.put(folder, createDirIdInfoFor(dirId));
}
@Override
public CryptoFile write(CryptoFile cryptoFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long length) throws BackendException {
return writeShortNameFile(cryptoFile, data, progressAware, replace, length);
}
}

View File

@ -0,0 +1,283 @@
package org.cryptomator.data.cloud.crypto
import android.content.Context
import com.google.common.io.BaseEncoding
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.binary.BaseNCodec
import org.cryptomator.cryptolib.api.AuthenticationFailedException
import org.cryptomator.cryptolib.api.Cryptor
import org.cryptomator.cryptolib.common.MessageDigestSupplier
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo
import org.cryptomator.data.cloud.crypto.RootCryptoFolder.Companion.isRoot
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.exception.AlreadyExistException
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.EmptyDirFileException
import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.exception.ParentFolderIsNullException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.UploadState
import java.io.ByteArrayOutputStream
import java.nio.charset.StandardCharsets
import java.util.function.Supplier
import java.util.regex.Pattern
import kotlin.streams.toList
import timber.log.Timber
internal class CryptoImplVaultFormatPre7(
context: Context,
cryptor: Supplier<Cryptor>,
cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>,
storageLocation: CloudFolder,
dirIdCache: DirIdCache
) :
CryptoImplDecorator(
context, cryptor, cloudContentRepository, storageLocation, dirIdCache, SHORTENING_THRESHOLD
) {
@Throws(BackendException::class)
override fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder {
val dirFileName = encryptFolderName(cryptoParent, cleartextName)
val dirFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, dirFileName)
return folder(cryptoParent, cleartextName, dirFile)
}
@Throws(BackendException::class)
override fun create(folder: CryptoFolder): CryptoFolder {
requireNotNull(folder.dirFile)
assertCryptoFolderAlreadyExists(folder)
val dirIdInfo = dirIdInfo(folder)
val createdCloudFolder = cloudContentRepository.create(dirIdInfo.cloudFolder)
val dirId = dirIdInfo.id.toByteArray(StandardCharsets.UTF_8)
val createdDirFile = cloudContentRepository.write(folder.dirFile, from(dirId), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, dirId.size.toLong())
return folder(folder, createdDirFile).also {
addFolderToCache(it, dirIdInfo.withCloudFolder(createdCloudFolder))
}
}
@Throws(BackendException::class)
override fun encryptName(cryptoParent: CryptoFolder, name: String): String {
return encryptName(cryptoParent, name, "")
}
@Throws(BackendException::class)
private fun encryptName(cryptoParent: CryptoFolder, name: String, prefix: String): String {
var ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(BaseEncoding.base32(), name, dirIdInfo(cryptoParent).id.toByteArray(StandardCharsets.UTF_8))
if (ciphertextName.length > shorteningThreshold) {
ciphertextName = deflate(ciphertextName)
}
return ciphertextName
}
@Throws(BackendException::class)
private fun deflate(longFileName: String): String {
val longFilenameBytes = longFileName.toByteArray(StandardCharsets.UTF_8)
val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes)
val shortFileName = BASE32.encodeAsString(hash) + LONG_NAME_FILE_EXT
val metadataFile = metadataFile(shortFileName)
val data = longFileName.toByteArray(StandardCharsets.UTF_8)
try {
cloudContentRepository.create(metadataFile.parent)
} catch (e: AlreadyExistException) {
}
cloudContentRepository.write(metadataFile, from(data), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, true, data.size.toLong())
return shortFileName
}
@Throws(BackendException::class)
private fun inflate(shortFileName: String): String {
val metadataFile = metadataFile(shortFileName)
val out = ByteArrayOutputStream()
cloudContentRepository.read(metadataFile, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
return String(out.toByteArray(), StandardCharsets.UTF_8)
}
@Throws(BackendException::class)
private fun inflatePermanently(cloudFile: CloudFile, longFileName: String): CloudFile {
Timber.tag("CryptoFs").i("inflatePermanently: %s -> %s", cloudFile.name, longFileName)
val newCiphertextFile = cloudContentRepository.file(cloudFile.parent, longFileName)
cloudContentRepository.move(cloudFile, newCiphertextFile)
return newCiphertextFile
}
@Throws(BackendException::class)
private fun metadataFile(shortFilename: String): CloudFile {
val firstLevelFolder = cloudContentRepository.folder(metadataFolder(), shortFilename.substring(0, 2))
val secondLevelFolder = cloudContentRepository.folder(firstLevelFolder, shortFilename.substring(2, 4))
return cloudContentRepository.file(secondLevelFolder, shortFilename)
}
@Throws(BackendException::class)
private fun metadataFolder(): CloudFolder {
return cloudContentRepository.folder(storageLocation(), METADATA_DIR_NAME)
}
@Throws(BackendException::class)
override fun list(cryptoFolder: CryptoFolder): List<CryptoNode> {
val dirIdInfo = dirIdInfo(cryptoFolder)
val dirId = dirIdInfo(cryptoFolder).id
val lvl2Dir = dirIdInfo.cloudFolder
return cloudContentRepository
.list(lvl2Dir)
.filterIsInstance<CloudFile>()
.parallelStream()
.map { node ->
ciphertextToCleartextNode(cryptoFolder, dirId, node)
}
.toList()
.filterNotNull()
}
@Throws(BackendException::class)
private fun ciphertextToCleartextNode(cryptoFolder: CryptoFolder, dirId: String, cloudNode: CloudFile): CryptoNode? {
var cloudFile = cloudNode
var ciphertextName = cloudFile.name
if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) {
try {
ciphertextName = inflate(ciphertextName)
if (ciphertextName.length <= shorteningThreshold) {
cloudFile = inflatePermanently(cloudFile, ciphertextName)
}
} catch (e: NoSuchCloudFileException) {
Timber.tag("CryptoFs").e("Missing mFile: %s", ciphertextName)
return null
} catch (e: BackendException) {
Timber.tag("CryptoFs").e(e, "Failed to read mFile: %s", ciphertextName)
return null
}
}
val cleartextName: String? = try {
decryptName(dirId, ciphertextName.uppercase())
} catch (e: AuthenticationFailedException) {
Timber.tag("CryptoFs").w("File name authentication failed: %s", cloudFile.path)
return null
} catch (e: IllegalArgumentException) {
Timber.tag("CryptoFs").d("Illegal ciphertext filename: %s", cloudFile.path)
return null
}
return if (cleartextName == null || ciphertextName.startsWith(SYMLINK_PREFIX)) {
null
} else if (ciphertextName.startsWith(DIR_PREFIX)) {
folder(cryptoFolder, cleartextName, cloudFile)
} else {
val cleartextSize = cloudFile.size?.let {
val ciphertextSizeWithoutHeader: Long = it - cryptor().fileHeaderCryptor().headerSize()
if (ciphertextSizeWithoutHeader >= 0) {
cryptor().fileContentCryptor().cleartextSize(ciphertextSizeWithoutHeader)
} else {
null
}
}
file(cryptoFolder, cleartextName, cloudFile, cleartextSize)
}
}
override fun decryptName(dirId: String, encryptedName: String): String? {
val ciphertextName = extractEncryptedName(encryptedName)
return if (ciphertextName != null) {
cryptor().fileNameCryptor().decryptFilename(BaseEncoding.base32(), ciphertextName, dirId.toByteArray(StandardCharsets.UTF_8))
} else {
null
}
}
override fun extractEncryptedName(ciphertextName: String): String? {
val matcher = BASE32_ENCRYPTED_NAME_PATTERN.matcher(ciphertextName)
return if (matcher.find(0)) {
matcher.group(2)
} else {
null
}
}
@Throws(BackendException::class)
override fun symlink(cryptoParent: CryptoFolder, cleartextName: String, target: String): CryptoSymlink {
val ciphertextName = encryptSymlinkName(cryptoParent, cleartextName)
val cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, ciphertextName)
return CryptoSymlink(cryptoParent, cleartextName, path(cryptoParent, cleartextName), target, cloudFile)
}
@Throws(BackendException::class)
private fun encryptSymlinkName(cryptoFolder: CryptoFolder, name: String): String {
return encryptName(cryptoFolder, name, SYMLINK_PREFIX)
}
@Throws(BackendException::class)
override fun encryptFolderName(cryptoFolder: CryptoFolder, name: String): String {
return encryptName(cryptoFolder, name, DIR_PREFIX)
}
@Throws(BackendException::class)
override fun move(source: CryptoFolder, target: CryptoFolder): CryptoFolder {
requireNotNull(source.dirFile)
requireNotNull(target.dirFile)
target.parent?.let {
assertCryptoFolderAlreadyExists(target)
return folder(it, target.name, cloudContentRepository.move(source.dirFile, target.dirFile)).also {
evictFromCache(source)
evictFromCache(target)
}
} ?: throw ParentFolderIsNullException(target.name)
}
@Throws(BackendException::class)
override fun move(source: CryptoFile, target: CryptoFile): CryptoFile {
assertCryptoFileAlreadyExists(target)
return file(target, cloudContentRepository.move(source.cloudFile, target.cloudFile), source.size)
}
@Throws(BackendException::class)
override fun delete(node: CloudNode) {
if (node is CryptoFolder) {
requireNotNull(node.dirFile)
val cryptoSubfolders = deepCollectSubfolders(node)
for (cryptoSubfolder in cryptoSubfolders) {
cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).cloudFolder)
}
cloudContentRepository.delete(dirIdInfo(node).cloudFolder)
cloudContentRepository.delete(node.dirFile)
evictFromCache(node)
} else if (node is CryptoFile) {
cloudContentRepository.delete(node.cloudFile)
}
}
@Throws(BackendException::class, EmptyDirFileException::class)
override fun loadDirId(folder: CryptoFolder): String {
return if (isRoot(folder)) {
CryptoConstants.ROOT_DIR_ID
} else if (folder.dirFile != null && cloudContentRepository.exists(folder.dirFile)) {
String(loadContentsOfDirFile(folder), StandardCharsets.UTF_8)
} else {
newDirId()
}
}
@Throws(BackendException::class)
override fun createDirIdInfo(folder: CryptoFolder): DirIdInfo {
val dirId = loadDirId(folder)
return dirIdCache.put(folder, createDirIdInfoFor(dirId))
}
@Throws(BackendException::class)
override fun write(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile {
return writeShortNameFile(cryptoFile, data, progressAware, replace, length)
}
companion object {
const val SHORTENING_THRESHOLD = 129
private const val DIR_PREFIX = "0"
private const val SYMLINK_PREFIX = "1S"
private const val LONG_NAME_FILE_EXT = ".lng"
private const val METADATA_DIR_NAME = "m"
private val BASE32: BaseNCodec = Base32()
private val BASE32_ENCRYPTED_NAME_PATTERN = Pattern.compile("^(0|1S)?(([A-Z2-7]{8})*[A-Z2-7=]{8})$")
}
}

View File

@ -1,7 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import org.cryptomator.domain.CloudNode;
interface CryptoNode extends CloudNode {
}

View File

@ -0,0 +1,5 @@
package org.cryptomator.data.cloud.crypto
import org.cryptomator.domain.CloudNode
interface CryptoNode : CloudNode

View File

@ -1,82 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.util.Optional;
import java.util.Date;
class CryptoSymlink implements CloudFile, CryptoNode {
private final String name;
private final String path;
private final String target;
private final CloudFile cloudFile;
private final CryptoFolder parent;
public CryptoSymlink(CryptoFolder parent, String name, String path, String target, CloudFile cloudFile) {
this.parent = parent;
this.name = name;
this.path = path;
this.target = target;
this.cloudFile = cloudFile;
}
@Override
public Cloud getCloud() {
return parent.getCloud();
}
@Override
public String getName() {
return name;
}
@Override
public String getPath() {
return path;
}
@Override
public CryptoFolder getParent() {
return parent;
}
@Override
public Optional<Long> getSize() {
return Optional.of((long) target.length());
}
@Override
public Optional<Date> getModified() {
return cloudFile.getModified();
}
/**
* @return The actual file in the underlying, i.e. decorated, CloudContentRepository
*/
CloudFile getCloudFile() {
return cloudFile;
}
@Override
public boolean equals(Object obj) {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
if (obj == this) {
return true;
}
return internalEquals((CryptoSymlink) obj);
}
private boolean internalEquals(CryptoSymlink obj) {
return path != null && path.equals(obj.path);
}
@Override
public int hashCode() {
return path == null ? 0 : path.hashCode();
}
}

View File

@ -0,0 +1,40 @@
package org.cryptomator.data.cloud.crypto
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import java.util.Date
class CryptoSymlink(
override val parent: CryptoFolder, override val name: String, override val path: String, private val target: String,
/**
* @return The actual file in the underlying, i.e. decorated, CloudContentRepository
*/
val cloudFile: CloudFile
) : CloudFile, CryptoNode {
override val cloud: Cloud?
get() = parent.cloud
override val size: Long
get() = target.length.toLong()
override val modified: Date?
get() = cloudFile.modified
override fun equals(other: Any?): Boolean {
if (other == null || javaClass != other.javaClass) {
return false
}
return if (other === this) {
true
} else internalEquals(other as CryptoSymlink)
}
private fun internalEquals(obj: CryptoSymlink): Boolean {
return path == obj.path
}
override fun hashCode(): Int {
return path.hashCode()
}
}

View File

@ -1,144 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.MissingCryptorException;
import org.cryptomator.util.Optional;
import org.cryptomator.util.Supplier;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public abstract class Cryptors {
Cryptors() {
}
public abstract boolean isEmpty();
public abstract int size();
public abstract Supplier<Cryptor> get(Vault vault);
public abstract Optional<Cryptor> remove(Vault vault);
public abstract boolean putIfAbsent(Vault vault, Cryptor cryptor);
public static class Delegating extends Cryptors {
private final Cryptors.Default fallback = new Cryptors.Default();
private volatile Cryptors.Default delegate;
public synchronized void setDelegate(Cryptors.Default delegate) {
delegate.putAll(fallback.cryptors);
this.delegate = delegate;
}
public synchronized void removeDelegate() {
fallback.putAll(delegate.cryptors);
this.delegate = null;
}
@Override
public synchronized boolean isEmpty() {
return delegate().isEmpty();
}
@Override
public synchronized int size() {
return delegate().size();
}
@Override
public synchronized Supplier<Cryptor> get(Vault vault) {
return delegate().get(vault);
}
@Override
public synchronized Optional<Cryptor> remove(Vault vault) {
return delegate().remove(vault);
}
@Override
public synchronized boolean putIfAbsent(Vault vault, Cryptor cryptor) {
return delegate().putIfAbsent(vault, cryptor);
}
private synchronized Cryptors delegate() {
if (delegate == null) {
return fallback;
} else {
return delegate;
}
}
}
public static class Default extends Cryptors {
private final ConcurrentMap<Vault, Cryptor> cryptors = new ConcurrentHashMap<>();
private Runnable onChangeListener = () -> {
};
public boolean isEmpty() {
return cryptors.isEmpty();
}
public int size() {
return cryptors.size();
}
public Supplier<Cryptor> get(final Vault vault) {
return () -> {
Cryptor cryptor = cryptors.get(vault);
if (cryptor == null) {
throw new MissingCryptorException();
} else {
return cryptor;
}
};
}
public Optional<Cryptor> remove(Vault vault) {
Optional<Cryptor> result = Optional.ofNullable(cryptors.remove(vault));
if (result.isPresent()) {
onChangeListener.run();
}
return result;
}
public boolean putIfAbsent(Vault vault, Cryptor cryptor) {
if (cryptors.putIfAbsent(vault, cryptor) == null) {
onChangeListener.run();
return true;
} else {
return false;
}
}
public void setOnChangeListener(Runnable onChangeListener) {
this.onChangeListener = onChangeListener;
}
public void putAll(Map<Vault, Cryptor> cryptors) {
this.cryptors.putAll(cryptors);
onChangeListener.run();
}
public void destroyAll() {
while (!isEmpty()) {
Iterator<Cryptor> cryptorIterator = cryptors.values().iterator();
while (cryptorIterator.hasNext()) {
cryptorIterator.next().destroy();
cryptorIterator.remove();
}
}
onChangeListener.run();
}
}
}

View File

@ -0,0 +1,131 @@
package org.cryptomator.data.cloud.crypto
import com.google.common.base.Optional
import org.cryptomator.cryptolib.api.Cryptor
import org.cryptomator.domain.Vault
import org.cryptomator.domain.exception.MissingCryptorException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import java.util.function.Supplier
abstract class Cryptors internal constructor() {
abstract fun isEmpty(): Boolean
abstract fun size(): Int
abstract operator fun get(vault: Vault): Supplier<Cryptor>
abstract fun remove(vault: Vault): Optional<Cryptor>
abstract fun putIfAbsent(vault: Vault, cryptor: Cryptor): Boolean
class Delegating : Cryptors() {
private val fallback = Default()
@Volatile
private var delegate: Default? = null
@Synchronized
fun setDelegate(delegate: Default) {
delegate.putAll(fallback.cryptors)
this.delegate = delegate
}
@Synchronized
fun removeDelegate() {
delegate?.let {
fallback.putAll(it.cryptors)
}.also { delegate = null }
}
@Synchronized
override fun isEmpty(): Boolean {
return delegate().isEmpty()
}
@Synchronized
override fun size(): Int {
return delegate().size()
}
@Synchronized
override fun get(vault: Vault): Supplier<Cryptor> {
return delegate()[vault]
}
@Synchronized
override fun remove(vault: Vault): Optional<Cryptor> {
return delegate().remove(vault)
}
@Synchronized
override fun putIfAbsent(vault: Vault, cryptor: Cryptor): Boolean {
return delegate().putIfAbsent(vault, cryptor)
}
@Synchronized
private fun delegate(): Cryptors {
return delegate ?: fallback
}
}
class Default : Cryptors() {
val cryptors: ConcurrentMap<Vault, Cryptor> = ConcurrentHashMap()
private var onChangeListener = Runnable {}
override fun isEmpty(): Boolean {
return cryptors.isEmpty()
}
override fun size(): Int {
return cryptors.size
}
override fun get(vault: Vault): Supplier<Cryptor> {
return Supplier {
cryptors[vault] ?: throw MissingCryptorException()
}
}
override fun remove(vault: Vault): Optional<Cryptor> {
val result = Optional.fromNullable(cryptors.remove(vault))
if (result.isPresent) {
onChangeListener.run()
}
return result
}
override fun putIfAbsent(vault: Vault, cryptor: Cryptor): Boolean {
return if (cryptors.putIfAbsent(vault, cryptor) == null) {
onChangeListener.run()
true
} else {
false
}
}
fun setOnChangeListener(onChangeListener: Runnable) {
this.onChangeListener = onChangeListener
}
fun putAll(cryptors: Map<Vault, Cryptor>) {
this.cryptors.putAll(cryptors)
onChangeListener.run()
}
fun destroyAll() {
while (!isEmpty()) {
val cryptorIterator = cryptors.values.iterator()
while (cryptorIterator.hasNext()) {
cryptorIterator.next().destroy()
cryptorIterator.remove()
}
}
onChangeListener.run()
}
}
}

View File

@ -1,38 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import org.cryptomator.domain.CloudFolder;
interface DirIdCache {
DirIdInfo get(CryptoFolder folder);
DirIdInfo put(CryptoFolder folder, DirIdInfo dirIdInfo);
void evict(CryptoFolder folder);
void evictSubFoldersOf(CryptoFolder cryptoFolder);
class DirIdInfo {
private final String id;
private final CloudFolder cloudFolder;
DirIdInfo(String id, CloudFolder cloudFolder) {
this.id = id;
this.cloudFolder = cloudFolder;
}
public String getId() {
return id;
}
public CloudFolder getCloudFolder() {
return cloudFolder;
}
DirIdInfo withCloudFolder(CloudFolder cloudFolder) {
return new DirIdInfo(id, cloudFolder);
}
}
}

View File

@ -0,0 +1,21 @@
package org.cryptomator.data.cloud.crypto
import org.cryptomator.domain.CloudFolder
interface DirIdCache {
operator fun get(folder: CryptoFolder): DirIdInfo?
fun put(folder: CryptoFolder, dirIdInfo: DirIdInfo): DirIdInfo
fun evict(folder: CryptoFolder)
fun evictSubFoldersOf(cryptoFolder: CryptoFolder)
class DirIdInfo internal constructor(val id: String, val cloudFolder: CloudFolder) {
fun withCloudFolder(cloudFolder: CloudFolder): DirIdInfo {
return DirIdInfo(id, cloudFolder)
}
}
}

View File

@ -1,80 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import android.util.LruCache;
import java.util.Map;
class DirIdCacheFormat7 implements DirIdCache {
private static final int MAX_SIZE = 1024;
private final LruCache<DirIdCacheKey, DirIdInfo> cache = new LruCache<>(MAX_SIZE);
DirIdCacheFormat7() {
}
@Override
public DirIdInfo get(CryptoFolder folder) {
return cache.get(DirIdCacheKey.toKey(folder));
}
@Override
public DirIdInfo put(CryptoFolder folder, DirIdInfo dirIdInfo) {
DirIdCacheKey key = DirIdCacheKey.toKey(folder);
cache.put(key, dirIdInfo);
return dirIdInfo;
}
@Override
public void evict(CryptoFolder folder) {
DirIdCacheKey key = DirIdCacheKey.toKey(folder);
cache.remove(key);
}
@Override
public void evictSubFoldersOf(CryptoFolder folder) {
Map<DirIdCacheKey, DirIdInfo> cacheSnapshot = cache.snapshot();
for (Map.Entry<DirIdCacheKey, DirIdInfo> cacheEntry : cacheSnapshot.entrySet()) {
if (cacheEntry.getKey().path.startsWith(folder.getPath() + "/")) {
cache.remove(cacheEntry.getKey());
}
}
}
private static class DirIdCacheKey {
private final String path;
private DirIdCacheKey(String path) {
this.path = path;
}
static DirIdCacheKey toKey(CryptoFolder folder) {
return new DirIdCacheKey(folder.getPath());
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return internalEquals((DirIdCacheKey) obj);
}
private boolean internalEquals(DirIdCacheKey o) {
return (path == null ? o.path == null : path.equals(o.path));
}
@Override
public int hashCode() {
final int prime = 31;
int hash = 1940604225;
hash = hash * prime + (path == null ? 0 : path.hashCode());
return hash;
}
}
}

View File

@ -0,0 +1,74 @@
package org.cryptomator.data.cloud.crypto
import android.util.LruCache
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo
internal class DirIdCacheFormat7 : DirIdCache {
private val cache = LruCache<DirIdCacheKey, DirIdInfo>(MAX_SIZE)
override fun get(folder: CryptoFolder): DirIdInfo? {
return cache[DirIdCacheKey.toKey(folder)]
}
override fun put(folder: CryptoFolder, dirIdInfo: DirIdInfo): DirIdInfo {
val key = DirIdCacheKey.toKey(folder)
cache.put(key, dirIdInfo)
return dirIdInfo
}
override fun evict(folder: CryptoFolder) {
val key = DirIdCacheKey.toKey(folder)
cache.remove(key)
}
override fun evictSubFoldersOf(cryptoFolder: CryptoFolder) {
val cacheSnapshot = cache.snapshot()
cacheSnapshot.forEach { (key) ->
if (key.path?.startsWith(cryptoFolder.path + "/") == true) {
cache.remove(key)
}
}
}
private class DirIdCacheKey private constructor(path: String) {
val path: String?
override fun equals(other: Any?): Boolean {
if (other === this) {
return true
}
return if (other == null || javaClass != other.javaClass) {
false
} else internalEquals(other as DirIdCacheKey)
}
private fun internalEquals(o: DirIdCacheKey): Boolean {
return if (path == null) o.path == null else path == o.path
}
override fun hashCode(): Int {
val prime = 31
var hash = 1940604225
hash = hash * prime + (path?.hashCode() ?: 0)
return hash
}
companion object {
fun toKey(folder: CryptoFolder): DirIdCacheKey {
return DirIdCacheKey(folder.path)
}
}
init {
this.path = path
}
}
companion object {
private const val MAX_SIZE = 1024
}
}

View File

@ -1,88 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import android.util.LruCache;
import org.cryptomator.domain.CloudFile;
import java.util.Date;
class DirIdCacheFormatPre7 implements DirIdCache {
private static final int MAX_SIZE = 1024;
private final LruCache<DirIdCacheKey, DirIdInfo> cache = new LruCache<>(MAX_SIZE);
DirIdCacheFormatPre7() {
}
public DirIdInfo get(CryptoFolder folder) {
return cache.get(DirIdCacheKey.toKey(folder));
}
public DirIdInfo put(CryptoFolder folder, DirIdInfo dirIdInfo) {
DirIdCacheKey key = DirIdCacheKey.toKey(folder);
cache.put(key, dirIdInfo);
cache.remove(key.withoutModified());
return dirIdInfo;
}
public void evict(CryptoFolder folder) {
DirIdCacheKey key = DirIdCacheKey.toKey(folder);
cache.remove(key);
cache.remove(key.withoutModified());
}
@Override
public void evictSubFoldersOf(CryptoFolder cryptoFolder) {
// no implementation needed
}
private static class DirIdCacheKey {
private final String path;
private final Date modified;
private DirIdCacheKey(CloudFile dirFile) {
this.path = dirFile == null ? null : dirFile.getPath();
this.modified = dirFile == null ? null : dirFile.getModified().orElse(null);
}
private DirIdCacheKey(String path) {
this.path = path;
this.modified = null;
}
static DirIdCacheKey toKey(CryptoFolder folder) {
return new DirIdCacheKey(folder.getDirFile());
}
DirIdCacheKey withoutModified() {
return new DirIdCacheKey(path);
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return internalEquals((DirIdCacheKey) obj);
}
private boolean internalEquals(DirIdCacheKey o) {
return (path == null ? o.path == null : path.equals(o.path)) //
&& (modified == null ? o.modified == null : modified.equals(o.modified));
}
@Override
public int hashCode() {
final int prime = 31;
int hash = 1940604225;
hash = hash * prime + (path == null ? 0 : path.hashCode());
hash = hash * prime + (modified == null ? 0 : modified.hashCode());
return hash;
}
}
}

View File

@ -0,0 +1,86 @@
package org.cryptomator.data.cloud.crypto
import android.util.LruCache
import org.cryptomator.data.cloud.crypto.DirIdCache.DirIdInfo
import org.cryptomator.domain.CloudFile
import java.util.Date
internal class DirIdCacheFormatPre7 : DirIdCache {
private val cache = LruCache<DirIdCacheKey, DirIdInfo>(MAX_SIZE)
override fun get(folder: CryptoFolder): DirIdInfo? {
return cache[DirIdCacheKey.toKey(folder)]
}
override fun put(folder: CryptoFolder, dirIdInfo: DirIdInfo): DirIdInfo {
val key = DirIdCacheKey.toKey(folder)
cache.put(key, dirIdInfo)
cache.remove(key.withoutModified())
return dirIdInfo
}
override fun evict(folder: CryptoFolder) {
val key = DirIdCacheKey.toKey(folder)
cache.remove(key)
cache.remove(key.withoutModified())
}
override fun evictSubFoldersOf(cryptoFolder: CryptoFolder) {
// no implementation needed
}
private class DirIdCacheKey {
private val path: String?
private val modified: Date?
private constructor(dirFile: CloudFile?) {
path = dirFile?.path
modified = dirFile?.modified
}
private constructor(path: String?) {
this.path = path
modified = null
}
fun withoutModified(): DirIdCacheKey {
return DirIdCacheKey(path)
}
override fun equals(other: Any?): Boolean {
if (other === this) {
return true
}
return if (other == null || javaClass != other.javaClass) {
false
} else internalEquals(other as DirIdCacheKey)
}
private fun internalEquals(o: DirIdCacheKey): Boolean {
return ((if (path == null) o.path == null else path == o.path) //
&& if (modified == null) o.modified == null else modified == o.modified)
}
override fun hashCode(): Int {
val prime = 31
var hash = 1940604225
hash = hash * prime + (path?.hashCode() ?: 0)
hash = hash * prime + (modified?.hashCode() ?: 0)
return hash
}
companion object {
fun toKey(folder: CryptoFolder): DirIdCacheKey {
return DirIdCacheKey(folder.dirFile)
}
}
}
companion object {
private const val MAX_SIZE = 1024
}
}

View File

@ -0,0 +1,293 @@
package org.cryptomator.data.cloud.crypto
import com.google.common.base.Optional
import org.cryptomator.cryptolib.api.Cryptor
import org.cryptomator.cryptolib.api.CryptorProvider
import org.cryptomator.cryptolib.api.InvalidPassphraseException
import org.cryptomator.cryptolib.api.Masterkey
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException
import org.cryptomator.cryptolib.common.MasterkeyFileAccess
import org.cryptomator.data.cloud.crypto.VaultConfig.Companion.createVaultConfig
import org.cryptomator.data.cloud.crypto.VaultConfig.Companion.verify
import org.cryptomator.data.cloud.crypto.VaultConfig.VaultConfigBuilder
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.UnverifiedVaultConfig
import org.cryptomator.domain.Vault
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.CancellationException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.vaultconfig.UnsupportedMasterkeyLocationException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource.Companion.from
import org.cryptomator.domain.usecases.cloud.Flag
import org.cryptomator.domain.usecases.vault.UnlockToken
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.net.URI
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
import java.text.Normalizer
class MasterkeyCryptoCloudProvider(
private val cloudContentRepository: CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile>, //
private val cryptoCloudContentRepositoryFactory: CryptoCloudContentRepositoryFactory, //
private val secureRandom: SecureRandom
) : CryptoCloudProvider {
@Throws(BackendException::class)
override fun create(location: CloudFolder, password: CharSequence) {
// Just for testing (id in VaultConfig is auto generated which makes sense while creating a vault but not for testing)
create(location, password, createVaultConfig())
}
// Visible for testing
@Throws(BackendException::class)
fun create(location: CloudFolder, password: CharSequence?, vaultConfigBuilder: VaultConfigBuilder) {
// 1. write masterkey:
val masterkey = Masterkey.generate(secureRandom)
try {
ByteArrayOutputStream().use { data ->
MasterkeyFileAccess(CryptoConstants.PEPPER, secureRandom).persist(masterkey, data, password, CryptoConstants.DEFAULT_MASTERKEY_FILE_VERSION)
cloudContentRepository.write(legacyMasterkeyFile(location), from(data.toByteArray()), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, data.size().toLong())
}
} catch (e: IOException) {
throw FatalBackendException("Failed to write masterkey", e)
}
// 2. initialize vault:
val vaultConfig = vaultConfigBuilder //
.vaultFormat(CryptoConstants.MAX_VAULT_VERSION) //
.cipherCombo(CryptoConstants.DEFAULT_CIPHER_COMBO) //
.keyId(URI.create(String.format("%s:%s", CryptoConstants.MASTERKEY_SCHEME, CryptoConstants.MASTERKEY_FILE_NAME))) //
.shorteningThreshold(CryptoConstants.DEFAULT_MAX_FILE_NAME) //
.build()
val encodedVaultConfig = vaultConfig.toToken(masterkey.encoded).toByteArray(StandardCharsets.UTF_8)
val vaultFile = cloudContentRepository.file(location, CryptoConstants.VAULT_FILE_NAME)
cloudContentRepository.write(vaultFile, from(encodedVaultConfig), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, encodedVaultConfig.size.toLong())
// 3. create root folder:
createRootFolder(location, cryptorFor(masterkey, vaultConfig.cipherCombo))
}
@Throws(BackendException::class)
private fun createRootFolder(location: CloudFolder, cryptor: Cryptor) {
var dFolder = cloudContentRepository.folder(location, CryptoConstants.DATA_DIR_NAME)
dFolder = cloudContentRepository.create(dFolder)
val rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(CryptoConstants.ROOT_DIR_ID)
var lvl1Folder = cloudContentRepository.folder(dFolder, rootDirHash.substring(0, 2))
lvl1Folder = cloudContentRepository.create(lvl1Folder)
val lvl2Folder = cloudContentRepository.folder(lvl1Folder, rootDirHash.substring(2))
cloudContentRepository.create(lvl2Folder)
}
@Throws(BackendException::class)
override fun unlock(vault: Vault, unverifiedVaultConfig: Optional<UnverifiedVaultConfig>, password: CharSequence, cancelledFlag: Flag): Vault {
return unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag)
}
@Throws(BackendException::class)
override fun unlock(token: UnlockToken, unverifiedVaultConfig: Optional<UnverifiedVaultConfig>, password: CharSequence, cancelledFlag: Flag): Vault {
val impl = token as UnlockTokenImpl
return try {
val masterkey = impl.getKeyFile(password)
val vaultFormat: Int
val shorteningThreshold: Int
val cryptor: Cryptor
if (unverifiedVaultConfig.isPresent) {
val vaultConfig = verify(masterkey.encoded, unverifiedVaultConfig.get())
vaultFormat = vaultConfig.vaultFormat
assertVaultVersionIsSupported(vaultConfig.vaultFormat)
shorteningThreshold = vaultConfig.shorteningThreshold
cryptor = cryptorFor(masterkey, vaultConfig.cipherCombo)
} else {
vaultFormat = MasterkeyFileAccess.readAllegedVaultVersion(impl.keyFileData)
assertLegacyVaultVersionIsSupported(vaultFormat)
shorteningThreshold = if (vaultFormat > 6) CryptoConstants.DEFAULT_MAX_FILE_NAME else CryptoImplVaultFormatPre7.SHORTENING_THRESHOLD
cryptor = cryptorFor(masterkey, CryptorProvider.Scheme.SIV_CTRMAC)
}
if (cancelledFlag.get()) {
throw CancellationException()
}
val vault = Vault.aCopyOf(token.vault) //
.withUnlocked(true) //
.withFormat(vaultFormat) //
.withShorteningThreshold(shorteningThreshold) //
.build()
cryptoCloudContentRepositoryFactory.registerCryptor(vault, cryptor)
vault
} catch (e: IOException) {
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun createUnlockToken(vault: Vault, unverifiedVaultConfig: Optional<UnverifiedVaultConfig>): UnlockTokenImpl {
val vaultLocation = vaultLocation(vault)
return if (unverifiedVaultConfig.isPresent) {
createUnlockToken(vault, masterkeyFile(vaultLocation, unverifiedVaultConfig.get()))
} else {
createUnlockToken(vault, legacyMasterkeyFile(vaultLocation))
}
}
@Throws(BackendException::class)
private fun masterkeyFile(vaultLocation: CloudFolder, unverifiedVaultConfig: UnverifiedVaultConfig): CloudFile {
val path = unverifiedVaultConfig.keyId.schemeSpecificPart
if (path != CryptoConstants.MASTERKEY_FILE_NAME) {
throw UnsupportedMasterkeyLocationException(unverifiedVaultConfig)
}
return cloudContentRepository.file(vaultLocation, path)
}
@Throws(BackendException::class)
private fun legacyMasterkeyFile(location: CloudFolder): CloudFile {
return cloudContentRepository.file(location, CryptoConstants.MASTERKEY_FILE_NAME)
}
@Throws(BackendException::class)
private fun createUnlockToken(vault: Vault, location: CloudFile): UnlockTokenImpl {
val keyFileData = readKeyFileData(location)
return UnlockTokenImpl(vault, keyFileData)
}
@Throws(BackendException::class)
private fun readKeyFileData(masterkeyFile: CloudFile): ByteArray {
val data = ByteArrayOutputStream()
cloudContentRepository.read(masterkeyFile, null, data, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
return data.toByteArray()
}
// Visible for testing
fun cryptorFor(keyFile: Masterkey?, vaultCipherCombo: CryptorProvider.Scheme): Cryptor {
return CryptorProvider.forScheme(vaultCipherCombo).provide(keyFile, secureRandom)
}
@Throws(BackendException::class)
override fun isVaultPasswordValid(vault: Vault, unverifiedVaultConfig: Optional<UnverifiedVaultConfig>, password: CharSequence): Boolean {
return try {
// create a cryptor, which checks the password, then destroy it immediately
val unlockToken = createUnlockToken(vault, unverifiedVaultConfig)
val masterkey = unlockToken.getKeyFile(password)
val vaultCipherCombo = if (unverifiedVaultConfig.isPresent) {
val vaultConfig = verify(masterkey.encoded, unverifiedVaultConfig.get())
assertVaultVersionIsSupported(vaultConfig.vaultFormat)
vaultConfig.cipherCombo
} else {
val vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(unlockToken.keyFileData)
assertLegacyVaultVersionIsSupported(vaultVersion)
CryptorProvider.Scheme.SIV_CTRMAC
}
cryptorFor(masterkey, vaultCipherCombo).destroy()
true
} catch (e: InvalidPassphraseException) {
false
} catch (e: IOException) {
throw FatalBackendException(e)
}
}
override fun lock(vault: Vault) {
cryptoCloudContentRepositoryFactory.deregisterCryptor(vault)
}
private fun assertVaultVersionIsSupported(version: Int) {
if (version < CryptoConstants.MIN_VAULT_VERSION) {
throw UnsupportedVaultFormatException(version, CryptoConstants.MIN_VAULT_VERSION)
} else if (version > CryptoConstants.MAX_VAULT_VERSION) {
throw UnsupportedVaultFormatException(version, CryptoConstants.MAX_VAULT_VERSION)
}
}
private fun assertLegacyVaultVersionIsSupported(version: Int) {
if (version < CryptoConstants.MIN_VAULT_VERSION) {
throw UnsupportedVaultFormatException(version, CryptoConstants.MIN_VAULT_VERSION)
} else if (version > CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG) {
throw UnsupportedVaultFormatException(version, CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG)
}
}
@Throws(BackendException::class)
override fun changePassword(vault: Vault, unverifiedVaultConfig: Optional<UnverifiedVaultConfig>, oldPassword: String, newPassword: String) {
val vaultLocation = vaultLocation(vault)
val masterkeyFile = if (unverifiedVaultConfig.isPresent) {
masterkeyFile(vaultLocation, unverifiedVaultConfig.get())
} else {
legacyMasterkeyFile(vaultLocation)
}
val dataOutputStream = ByteArrayOutputStream()
cloudContentRepository.read(masterkeyFile, null, dataOutputStream, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
val data = dataOutputStream.toByteArray()
val vaultVersion: Int
if (unverifiedVaultConfig.isPresent) {
vaultVersion = unverifiedVaultConfig.get().vaultFormat
assertVaultVersionIsSupported(vaultVersion)
} else {
try {
vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(data)
assertLegacyVaultVersionIsSupported(vaultVersion)
} catch (e: IOException) {
throw FatalBackendException("Failed to read legacy vault version", e)
}
}
createBackupMasterKeyFile(data, masterkeyFile)
createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, masterkeyFile)
}
@Throws(BackendException::class)
private fun vaultLocation(vault: Vault): CloudFolder {
return cloudContentRepository.resolve(vault.cloud, vault.path)
}
@Throws(BackendException::class)
private fun createBackupMasterKeyFile(data: ByteArray, masterkeyFile: CloudFile) {
cloudContentRepository.write(masterkeyBackupFile(masterkeyFile, data), from(data), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, true, data.size.toLong())
}
@Throws(BackendException::class)
private fun masterkeyBackupFile(masterkeyFile: CloudFile, data: ByteArray): CloudFile {
val fileName = masterkeyFile.name + BackupFileIdSuffixGenerator.generate(data) + CryptoConstants.MASTERKEY_BACKUP_FILE_EXT
return cloudContentRepository.file(masterkeyFile.parent, fileName)
}
@Throws(BackendException::class)
private fun createNewMasterKeyFile(data: ByteArray, vaultVersion: Int, oldPassword: String, newPassword: String, masterkeyFile: CloudFile) {
try {
val newMasterKeyFile = MasterkeyFileAccess(CryptoConstants.PEPPER, secureRandom) //
.changePassphrase(data, normalizePassword(oldPassword, vaultVersion), normalizePassword(newPassword, vaultVersion))
cloudContentRepository.write(
masterkeyFile, //
from(newMasterKeyFile), //
ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, //
true, //
newMasterKeyFile.size.toLong()
)
} catch (e: IOException) {
throw FatalBackendException("Failed to read legacy vault version", e)
}
}
private fun normalizePassword(password: CharSequence, vaultVersion: Int): CharSequence {
return if (vaultVersion >= CryptoConstants.VERSION_WITH_NORMALIZED_PASSWORDS) {
Normalizer.normalize(password, Normalizer.Form.NFC)
} else {
password
}
}
class UnlockTokenImpl(private val vault: Vault, val keyFileData: ByteArray) : UnlockToken {
override fun getVault(): Vault {
return vault
}
@Throws(IOException::class)
fun getKeyFile(password: CharSequence?): Masterkey {
return MasterkeyFileAccess(CryptoConstants.PEPPER, SecureRandom()).load(ByteArrayInputStream(keyFileData), password)
}
}
}

View File

@ -1,27 +0,0 @@
package org.cryptomator.data.cloud.crypto;
import org.cryptomator.domain.Cloud;
class RootCryptoFolder extends CryptoFolder {
private final CryptoCloud cloud;
public RootCryptoFolder(CryptoCloud cloud) {
super(null, "", "", null);
this.cloud = cloud;
}
public static boolean isRoot(CryptoFolder folder) {
return folder instanceof RootCryptoFolder;
}
@Override
public Cloud getCloud() {
return cloud;
}
@Override
public CryptoFolder withCloud(Cloud cloud) {
return new RootCryptoFolder((CryptoCloud) cloud);
}
}

View File

@ -0,0 +1,18 @@
package org.cryptomator.data.cloud.crypto
import org.cryptomator.domain.Cloud
class RootCryptoFolder(override val cloud: CryptoCloud) : CryptoFolder(null, "", "", null) {
override fun withCloud(cloud: Cloud?): CryptoFolder {
return RootCryptoFolder(cloud as CryptoCloud)
}
companion object {
@JvmStatic
fun isRoot(folder: CryptoFolder): Boolean {
return folder is RootCryptoFolder
}
}
}

View File

@ -0,0 +1,153 @@
package org.cryptomator.data.cloud.crypto
import org.cryptomator.cryptolib.api.CryptorProvider
import org.cryptomator.domain.UnverifiedVaultConfig
import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException
import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException
import org.cryptomator.domain.exception.vaultconfig.VaultVersionMismatchException
import java.net.URI
import java.security.Key
import java.util.UUID
import io.jsonwebtoken.Claims
import io.jsonwebtoken.IncorrectClaimException
import io.jsonwebtoken.JwsHeader
import io.jsonwebtoken.JwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.MissingClaimException
import io.jsonwebtoken.SigningKeyResolverAdapter
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.SignatureException
import kotlin.properties.Delegates
class VaultConfig private constructor(builder: VaultConfigBuilder) {
val keyId: URI
val id: String
val vaultFormat: Int
val cipherCombo: CryptorProvider.Scheme
val shorteningThreshold: Int
fun toToken(rawKey: ByteArray): String {
return Jwts.builder()
.setHeaderParam(JSON_KEY_ID, keyId.toASCIIString()) //
.setId(id) //
.claim(JSON_KEY_VAULTFORMAT, vaultFormat) //
.claim(JSON_KEY_CIPHERCONFIG, cipherCombo.name) //
.claim(JSON_KEY_SHORTENING_THRESHOLD, shorteningThreshold) //
.signWith(Keys.hmacShaKeyFor(rawKey)) //
.compact()
}
class VaultConfigBuilder {
internal var id: String = UUID.randomUUID().toString()
internal var vaultFormat = CryptoConstants.MAX_VAULT_VERSION;
internal var cipherCombo = CryptoConstants.DEFAULT_CIPHER_COMBO
internal var shorteningThreshold = CryptoConstants.DEFAULT_MAX_FILE_NAME;
lateinit var keyId: URI
fun keyId(keyId: URI): VaultConfigBuilder {
this.keyId = keyId
return this
}
fun cipherCombo(cipherCombo: CryptorProvider.Scheme): VaultConfigBuilder {
this.cipherCombo = cipherCombo
return this
}
fun shorteningThreshold(shorteningThreshold: Int): VaultConfigBuilder {
this.shorteningThreshold = shorteningThreshold
return this
}
fun id(id: String): VaultConfigBuilder {
this.id = id
return this
}
fun vaultFormat(vaultFormat: Int): VaultConfigBuilder {
this.vaultFormat = vaultFormat
return this
}
fun build(): VaultConfig {
return VaultConfig(this)
}
}
companion object {
private const val JSON_KEY_VAULTFORMAT = "format"
private const val JSON_KEY_CIPHERCONFIG = "cipherCombo"
private const val JSON_KEY_SHORTENING_THRESHOLD = "shorteningThreshold"
private const val JSON_KEY_ID = "kid"
@JvmStatic
@Throws(VaultConfigLoadException::class)
fun decode(token: String): UnverifiedVaultConfig {
val unverifiedSigningKeyResolver = UnverifiedSigningKeyResolver()
// At this point we can't verify the signature because we don't have the masterkey yet.
try {
Jwts.parserBuilder().setSigningKeyResolver(unverifiedSigningKeyResolver).build().parse(token)
} catch (e: IllegalArgumentException) {
return UnverifiedVaultConfig(token, unverifiedSigningKeyResolver.keyId, unverifiedSigningKeyResolver.vaultFormat)
}
throw VaultConfigLoadException("Failed to load vaultconfig")
}
@JvmStatic
@Throws(VaultKeyInvalidException::class, VaultVersionMismatchException::class, VaultConfigLoadException::class)
fun verify(rawKey: ByteArray, unverifiedVaultConfig: UnverifiedVaultConfig): VaultConfig {
return try {
val parser = Jwts //
.parserBuilder() //
.setSigningKey(rawKey) //
.require(JSON_KEY_VAULTFORMAT, unverifiedVaultConfig.vaultFormat) //
.build() //
.parseClaimsJws(unverifiedVaultConfig.jwt)
val vaultConfigBuilder = createVaultConfig() //
.keyId(unverifiedVaultConfig.keyId)
.id(parser.header[JSON_KEY_ID] as String) //
.cipherCombo(CryptorProvider.Scheme.valueOf(parser.body.get(JSON_KEY_CIPHERCONFIG, String::class.java))) //
.vaultFormat(unverifiedVaultConfig.vaultFormat) //
.shorteningThreshold(parser.body[JSON_KEY_SHORTENING_THRESHOLD] as Int)
VaultConfig(vaultConfigBuilder)
} catch (e: JwtException) {
when (e) {
is MissingClaimException, is IncorrectClaimException -> throw VaultVersionMismatchException("Vault config not for version " + unverifiedVaultConfig.vaultFormat)
is SignatureException -> throw VaultKeyInvalidException()
else -> throw VaultConfigLoadException(e)
}
}
}
@JvmStatic
fun createVaultConfig(): VaultConfigBuilder {
return VaultConfigBuilder()
}
}
private class UnverifiedSigningKeyResolver : SigningKeyResolverAdapter() {
lateinit var keyId: URI
var vaultFormat: Int by Delegates.notNull()
override fun resolveSigningKey(jwsHeader: JwsHeader<*>, claims: Claims): Key? {
keyId = URI.create(jwsHeader.keyId)
vaultFormat = claims[JSON_KEY_VAULTFORMAT] as Int
return null
}
}
init {
id = builder.id
keyId = builder.keyId
vaultFormat = builder.vaultFormat
cipherCombo = builder.cipherCombo
shorteningThreshold = builder.shorteningThreshold
}
}

View File

@ -1,56 +0,0 @@
package org.cryptomator.data.cloud.dropbox;
import android.content.Context;
import com.dropbox.core.DbxRequestConfig;
import com.dropbox.core.http.OkHttp3Requestor;
import com.dropbox.core.v2.DbxClientV2;
import org.cryptomator.data.BuildConfig;
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor;
import java.util.Locale;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import timber.log.Timber;
import static org.cryptomator.data.util.NetworkTimeout.CONNECTION;
import static org.cryptomator.data.util.NetworkTimeout.READ;
import static org.cryptomator.data.util.NetworkTimeout.WRITE;
class DropboxClientFactory {
private DbxClientV2 sDbxClient;
private static Interceptor httpLoggingInterceptor(Context context) {
return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context);
}
public DbxClientV2 getClient(String accessToken, Context context) {
if (sDbxClient == null) {
sDbxClient = createDropboxClient(accessToken, context);
}
return sDbxClient;
}
private DbxClientV2 createDropboxClient(String accessToken, Context context) {
String userLocale = Locale.getDefault().toString();
OkHttpClient okHttpClient = new OkHttpClient() //
.newBuilder() //
.connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) //
.readTimeout(READ.getTimeout(), READ.getUnit()) //
.writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) //
.addInterceptor(httpLoggingInterceptor(context)) //
.build();
DbxRequestConfig requestConfig = DbxRequestConfig //
.newBuilder("Cryptomator-Android/" + BuildConfig.VERSION_NAME) //
.withUserLocale(userLocale) //
.withHttpRequestor(new OkHttp3Requestor(okHttpClient)) //
.build();
return new DbxClientV2(requestConfig, accessToken);
}
}

View File

@ -0,0 +1,60 @@
package org.cryptomator.data.cloud.dropbox
import android.content.Context
import com.dropbox.core.DbxRequestConfig
import com.dropbox.core.http.OkHttp3Requestor
import com.dropbox.core.v2.DbxClientV2
import org.cryptomator.data.BuildConfig
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor
import org.cryptomator.data.util.NetworkTimeout
import org.cryptomator.util.crypto.CredentialCryptor
import java.util.Locale
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import timber.log.Timber
class DropboxClientFactory {
companion object {
@Volatile
private var instance: DbxClientV2? = null
@Synchronized
fun getInstance(accessToken: String, context: Context): DbxClientV2 = instance ?: createDropboxClient(decrypt(accessToken, context), context).also { instance = it }
private fun decrypt(password: String, context: Context): String {
return CredentialCryptor.getInstance(context).decrypt(password)
}
private fun createDropboxClient(accessToken: String, context: Context): DbxClientV2 {
val userLocale = Locale.getDefault().toString()
val okHttpClient = OkHttpClient() //
.newBuilder() //
.connectTimeout(NetworkTimeout.CONNECTION.timeout, NetworkTimeout.CONNECTION.unit) //
.readTimeout(NetworkTimeout.READ.timeout, NetworkTimeout.READ.unit) //
.writeTimeout(NetworkTimeout.WRITE.timeout, NetworkTimeout.WRITE.unit) //
.addInterceptor(httpLoggingInterceptor(context)) //
.build()
val requestConfig = DbxRequestConfig //
.newBuilder("Cryptomator-Android/" + BuildConfig.VERSION_NAME) //
.withUserLocale(userLocale) //
.withHttpRequestor(OkHttp3Requestor(okHttpClient)) //
.build()
return DbxClientV2(requestConfig, accessToken)
}
private fun httpLoggingInterceptor(context: Context): Interceptor {
val logger = object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Timber.tag("OkHttp").d(message)
}
}
return HttpLoggingInterceptor(logger, context)
}
}
}

View File

@ -1,213 +0,0 @@
package org.cryptomator.data.cloud.dropbox;
import android.content.Context;
import com.dropbox.core.DbxException;
import com.dropbox.core.InvalidAccessTokenException;
import com.dropbox.core.NetworkIOException;
import com.dropbox.core.v2.files.CreateFolderErrorException;
import com.dropbox.core.v2.files.DeleteErrorException;
import com.dropbox.core.v2.files.DownloadErrorException;
import com.dropbox.core.v2.files.ListFolderErrorException;
import com.dropbox.core.v2.files.RelocationErrorException;
import org.cryptomator.data.cloud.InterceptingCloudContentRepository;
import org.cryptomator.domain.DropboxCloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.NetworkConnectionException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import static org.cryptomator.util.ExceptionUtil.contains;
import static org.cryptomator.util.ExceptionUtil.extract;
class DropboxCloudContentRepository extends InterceptingCloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile> {
private final DropboxCloud cloud;
public DropboxCloudContentRepository(DropboxCloud cloud, Context context) {
super(new Intercepted(cloud, context));
this.cloud = cloud;
}
@Override
protected void throwWrappedIfRequired(Exception e) throws BackendException {
throwConnectionErrorIfRequired(e);
throwWrongCredentialsExceptionIfRequired(e);
}
private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException {
if (contains(e, NetworkIOException.class)) {
throw new NetworkConnectionException(e);
}
}
private void throwWrongCredentialsExceptionIfRequired(Exception e) {
if (contains(e, InvalidAccessTokenException.class)) {
throw new WrongCredentialsException(cloud);
}
}
private static class Intercepted implements CloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile> {
private final DropboxImpl cloud;
public Intercepted(DropboxCloud cloud, Context context) {
this.cloud = new DropboxImpl(cloud, context);
}
public DropboxFolder root(DropboxCloud cloud) {
return this.cloud.root();
}
@Override
public DropboxFolder resolve(DropboxCloud cloud, String path) {
return this.cloud.resolve(path);
}
@Override
public DropboxFile file(DropboxFolder parent, String name) {
return cloud.file(parent, name);
}
@Override
public DropboxFile file(DropboxFolder parent, String name, Optional<Long> size) throws BackendException {
return cloud.file(parent, name, size);
}
@Override
public DropboxFolder folder(DropboxFolder parent, String name) {
return cloud.folder(parent, name);
}
@Override
public boolean exists(DropboxNode node) throws BackendException {
try {
return cloud.exists(node);
} catch (DbxException e) {
throw new FatalBackendException(e);
}
}
@Override
public List<DropboxNode> list(DropboxFolder folder) throws BackendException {
try {
return cloud.list(folder);
} catch (DbxException e) {
if (e instanceof ListFolderErrorException) {
if (((ListFolderErrorException) e).errorValue.getPathValue().isNotFound()) {
throw new NoSuchCloudFileException();
}
}
throw new FatalBackendException(e);
}
}
@Override
public DropboxFolder create(DropboxFolder folder) throws BackendException {
try {
return cloud.create(folder);
} catch (DbxException e) {
if (e instanceof CreateFolderErrorException) {
throw new CloudNodeAlreadyExistsException(folder.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public DropboxFolder move(DropboxFolder source, DropboxFolder target) throws BackendException {
try {
return (DropboxFolder) cloud.move(source, target);
} catch (DbxException e) {
if (e instanceof RelocationErrorException) {
if (extract(e, RelocationErrorException.class).get().errorValue.isFromLookup()) {
throw new NoSuchCloudFileException(source.getName());
}
throw new CloudNodeAlreadyExistsException(target.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public DropboxFile move(DropboxFile source, DropboxFile target) throws BackendException {
try {
return (DropboxFile) cloud.move(source, target);
} catch (DbxException e) {
if (e instanceof RelocationErrorException) {
throw new CloudNodeAlreadyExistsException(target.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public DropboxFile write(DropboxFile uploadFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
try {
return cloud.write(uploadFile, data, progressAware, replace, size);
} catch (IOException | DbxException e) {
if (contains(e, NoSuchCloudFileException.class)) {
throw new NoSuchCloudFileException(uploadFile.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public void read(DropboxFile file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
try {
cloud.read(file, encryptedTmpFile, data, progressAware);
} catch (IOException | DbxException e) {
if (contains(e, DownloadErrorException.class)) {
if (extract(e, DownloadErrorException.class).get().errorValue.getPathValue().isNotFound()) {
throw new NoSuchCloudFileException(file.getName());
}
}
throw new FatalBackendException(e);
}
}
@Override
public void delete(DropboxNode node) throws BackendException {
try {
cloud.delete(node);
} catch (DbxException e) {
if (contains(e, DeleteErrorException.class)) {
if (extract(e, DeleteErrorException.class).get().errorValue.getPathLookupValue().isNotFound()) {
throw new NoSuchCloudFileException(node.getName());
}
}
throw new FatalBackendException(e);
}
}
@Override
public String checkAuthenticationAndRetrieveCurrentAccount(DropboxCloud cloud) throws BackendException {
try {
return this.cloud.currentAccount();
} catch (DbxException e) {
throw new FatalBackendException(e);
}
}
@Override
public void logout(DropboxCloud cloud) throws BackendException {
// empty
}
}
}

View File

@ -0,0 +1,210 @@
package org.cryptomator.data.cloud.dropbox
import android.content.Context
import com.dropbox.core.DbxException
import com.dropbox.core.InvalidAccessTokenException
import com.dropbox.core.NetworkIOException
import com.dropbox.core.v2.files.CreateFolderErrorException
import com.dropbox.core.v2.files.DeleteErrorException
import com.dropbox.core.v2.files.DownloadErrorException
import com.dropbox.core.v2.files.GetMetadataErrorException
import com.dropbox.core.v2.files.ListFolderErrorException
import com.dropbox.core.v2.files.RelocationErrorException
import org.cryptomator.data.cloud.InterceptingCloudContentRepository
import org.cryptomator.domain.DropboxCloud
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NetworkConnectionException
import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.exception.authentication.WrongCredentialsException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.UploadState
import org.cryptomator.util.ExceptionUtil
import java.io.File
import java.io.IOException
import java.io.OutputStream
internal class DropboxCloudContentRepository(private val cloud: DropboxCloud, context: Context) : InterceptingCloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile>(Intercepted(cloud, context)){
@Throws(BackendException::class)
override fun throwWrappedIfRequired(e: Exception) {
throwConnectionErrorIfRequired(e)
throwWrongCredentialsExceptionIfRequired(e)
}
@Throws(NetworkConnectionException::class)
private fun throwConnectionErrorIfRequired(e: Exception) {
if (ExceptionUtil.contains(e, NetworkIOException::class.java)) {
throw NetworkConnectionException(e)
}
}
private fun throwWrongCredentialsExceptionIfRequired(e: Exception) {
if (ExceptionUtil.contains(e, InvalidAccessTokenException::class.java)) {
throw WrongCredentialsException(cloud)
}
}
private class Intercepted(cloud: DropboxCloud, context: Context) : CloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile> {
private val cloud: DropboxImpl = DropboxImpl(cloud, context)
override fun root(cloud: DropboxCloud): DropboxFolder {
return this.cloud.root()
}
override fun resolve(cloud: DropboxCloud, path: String): DropboxFolder {
return this.cloud.resolve(path)
}
override fun file(parent: DropboxFolder, name: String): DropboxFile {
return cloud.file(parent, name, null)
}
@Throws(BackendException::class)
override fun file(parent: DropboxFolder, name: String, size: Long?): DropboxFile {
return cloud.file(parent, name, size)
}
override fun folder(parent: DropboxFolder, name: String): DropboxFolder {
return cloud.folder(parent, name)
}
@Throws(BackendException::class)
override fun exists(node: DropboxNode): Boolean {
return try {
cloud.exists(node)
} catch (e: DbxException) {
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun list(folder: DropboxFolder): List<DropboxNode> {
return try {
cloud.list(folder)
} catch (e: DbxException) {
if (e is ListFolderErrorException) {
if (e.errorValue.pathValue.isNotFound) {
throw NoSuchCloudFileException()
}
}
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun create(folder: DropboxFolder): DropboxFolder {
return try {
cloud.create(folder)
} catch (e: DbxException) {
if (e is CreateFolderErrorException) {
throw CloudNodeAlreadyExistsException(folder.name)
}
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun move(source: DropboxFolder, target: DropboxFolder): DropboxFolder {
return try {
cloud.move(source, target) as DropboxFolder
} catch (e: DbxException) {
if (e is RelocationErrorException) {
if (ExceptionUtil.extract(e, RelocationErrorException::class.java).get().errorValue.isFromLookup) {
throw NoSuchCloudFileException(source.name)
}
throw CloudNodeAlreadyExistsException(target.name)
}
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun move(source: DropboxFile, target: DropboxFile): DropboxFile {
return try {
cloud.move(source, target) as DropboxFile
} catch (e: DbxException) {
if (e is RelocationErrorException) {
throw CloudNodeAlreadyExistsException(target.name)
}
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun write(file: DropboxFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): DropboxFile {
return try {
cloud.write(file, data, progressAware, replace, size)
} catch (e: IOException) {
if (ExceptionUtil.contains(e, NoSuchCloudFileException::class.java)) {
throw NoSuchCloudFileException(file.name)
}
throw FatalBackendException(e)
} catch (e: DbxException) {
if (ExceptionUtil.contains(e, NoSuchCloudFileException::class.java)) {
throw NoSuchCloudFileException(file.name)
}
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun read(file: DropboxFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
try {
cloud.read(file, encryptedTmpFile, data, progressAware)
} catch (e: IOException) {
mapToNoSuchCloudFileExceptionIfMatches(e, file)?.let { throw it } ?: throw FatalBackendException(e)
} catch (e: DbxException) {
mapToNoSuchCloudFileExceptionIfMatches(e, file)?.let { throw it } ?: throw FatalBackendException(e)
}
}
private fun mapToNoSuchCloudFileExceptionIfMatches(e: Exception, file: DropboxFile) : NoSuchCloudFileException? {
if (ExceptionUtil.contains(e, GetMetadataErrorException::class.java)) {
if (ExceptionUtil.extract(e, GetMetadataErrorException::class.java).get().errorValue.pathValue.isNotFound) {
return NoSuchCloudFileException(file.name)
}
}
else if (ExceptionUtil.contains(e, DownloadErrorException::class.java)) {
if (ExceptionUtil.extract(e, DownloadErrorException::class.java).get().errorValue.pathValue.isNotFound) {
return NoSuchCloudFileException(file.name)
}
}
return null
}
@Throws(BackendException::class)
override fun delete(node: DropboxNode) {
try {
cloud.delete(node)
} catch (e: DbxException) {
if (ExceptionUtil.contains(e, DeleteErrorException::class.java)) {
if (ExceptionUtil.extract(e, DeleteErrorException::class.java).get().errorValue.pathLookupValue.isNotFound) {
throw NoSuchCloudFileException(node.name)
}
}
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: DropboxCloud): String {
return try {
this.cloud.currentAccount()
} catch (e: DbxException) {
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun logout(cloud: DropboxCloud) {
// empty
}
}
}

View File

@ -28,7 +28,7 @@ public class DropboxCloudContentRepositoryFactory implements CloudContentReposit
}
@Override
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
public CloudContentRepository<DropboxCloud, DropboxNode, DropboxFolder, DropboxFile> cloudContentRepositoryFor(Cloud cloud) {
return new DropboxCloudContentRepository((DropboxCloud) cloud, context);
}

View File

@ -1,39 +0,0 @@
package org.cryptomator.data.cloud.dropbox;
import com.dropbox.core.v2.files.FileMetadata;
import com.dropbox.core.v2.files.FolderMetadata;
import com.dropbox.core.v2.files.Metadata;
import org.cryptomator.util.Optional;
class DropboxCloudNodeFactory {
public static DropboxFile from(DropboxFolder parent, FileMetadata metadata) {
return new DropboxFile(parent, metadata.getName(), metadata.getPathDisplay(), Optional.ofNullable(metadata.getSize()), Optional.ofNullable(metadata.getServerModified()));
}
public static DropboxFile file(DropboxFolder parent, String name, Optional<Long> size, String path) {
return new DropboxFile(parent, name, path, size, Optional.empty());
}
public static DropboxFolder from(DropboxFolder parent, FolderMetadata metadata) {
return new DropboxFolder(parent, metadata.getName(), getNodePath(parent, metadata.getName()));
}
private static String getNodePath(DropboxFolder parent, String name) {
return parent.getPath() + "/" + name;
}
public static DropboxFolder folder(DropboxFolder parent, String name, String path) {
return new DropboxFolder(parent, name, path);
}
public static DropboxNode from(DropboxFolder parent, Metadata metadata) {
if (metadata instanceof FileMetadata) {
return from(parent, (FileMetadata) metadata);
} else {
return from(parent, (FolderMetadata) metadata);
}
}
}

View File

@ -0,0 +1,39 @@
package org.cryptomator.data.cloud.dropbox
import com.dropbox.core.v2.files.FileMetadata
import com.dropbox.core.v2.files.FolderMetadata
import com.dropbox.core.v2.files.Metadata
internal object DropboxCloudNodeFactory {
fun from(parent: DropboxFolder, metadata: FileMetadata): DropboxFile {
return DropboxFile(parent, metadata.name, metadata.pathDisplay, metadata.size, metadata.serverModified)
}
@JvmStatic
fun file(parent: DropboxFolder, name: String, size: Long?, path: String): DropboxFile {
return DropboxFile(parent, name, path, size, null)
}
fun from(parent: DropboxFolder, metadata: FolderMetadata): DropboxFolder {
return DropboxFolder(parent, metadata.name, getNodePath(parent, metadata.name))
}
private fun getNodePath(parent: DropboxFolder, name: String): String {
return parent.path + "/" + name
}
@JvmStatic
fun folder(parent: DropboxFolder?, name: String, path: String): DropboxFolder {
return DropboxFolder(parent, name, path)
}
@JvmStatic
fun from(parent: DropboxFolder, metadata: Metadata): DropboxNode {
return if (metadata is FileMetadata) {
from(parent, metadata)
} else {
from(parent, metadata as FolderMetadata)
}
}
}

View File

@ -1,55 +0,0 @@
package org.cryptomator.data.cloud.dropbox;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.util.Optional;
import java.util.Date;
class DropboxFile implements CloudFile, DropboxNode {
private final DropboxFolder parent;
private final String name;
private final String path;
private final Optional<Long> size;
private final Optional<Date> modified;
public DropboxFile(DropboxFolder parent, String name, String path, Optional<Long> size, Optional<Date> modified) {
this.parent = parent;
this.name = name;
this.path = path;
this.size = size;
this.modified = modified;
}
@Override
public Cloud getCloud() {
return parent.getCloud();
}
@Override
public String getName() {
return name;
}
@Override
public String getPath() {
return path;
}
@Override
public DropboxFolder getParent() {
return parent;
}
@Override
public Optional<Long> getSize() {
return size;
}
@Override
public Optional<Date> getModified() {
return modified;
}
}

View File

@ -0,0 +1,11 @@
package org.cryptomator.data.cloud.dropbox
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import java.util.Date
internal class DropboxFile(override val parent: DropboxFolder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, DropboxNode {
override val cloud: Cloud?
get() = parent.cloud
}

View File

@ -1,42 +0,0 @@
package org.cryptomator.data.cloud.dropbox;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFolder;
class DropboxFolder implements CloudFolder, DropboxNode {
private final DropboxFolder parent;
private final String name;
private final String path;
public DropboxFolder(DropboxFolder parent, String name, String path) {
this.parent = parent;
this.name = name;
this.path = path;
}
@Override
public Cloud getCloud() {
return parent.getCloud();
}
@Override
public String getName() {
return name;
}
@Override
public String getPath() {
return path;
}
@Override
public DropboxFolder getParent() {
return parent;
}
@Override
public DropboxFolder withCloud(Cloud cloud) {
return new DropboxFolder(parent.withCloud(cloud), name, path);
}
}

View File

@ -0,0 +1,14 @@
package org.cryptomator.data.cloud.dropbox
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFolder
open class DropboxFolder(override val parent: DropboxFolder?, override val name: String, override val path: String) : CloudFolder, DropboxNode {
override val cloud: Cloud?
get() = parent?.cloud
override fun withCloud(cloud: Cloud?): DropboxFolder? {
return DropboxFolder(parent?.withCloud(cloud), name, path)
}
}

View File

@ -1,466 +0,0 @@
package org.cryptomator.data.cloud.dropbox;
import android.content.Context;
import com.dropbox.core.DbxException;
import com.dropbox.core.NetworkIOException;
import com.dropbox.core.RetryException;
import com.dropbox.core.v2.DbxClientV2;
import com.dropbox.core.v2.files.CommitInfo;
import com.dropbox.core.v2.files.CreateFolderResult;
import com.dropbox.core.v2.files.FileMetadata;
import com.dropbox.core.v2.files.FolderMetadata;
import com.dropbox.core.v2.files.GetMetadataErrorException;
import com.dropbox.core.v2.files.ListFolderResult;
import com.dropbox.core.v2.files.Metadata;
import com.dropbox.core.v2.files.RelocationResult;
import com.dropbox.core.v2.files.UploadSessionCursor;
import com.dropbox.core.v2.files.UploadSessionFinishErrorException;
import com.dropbox.core.v2.files.UploadSessionLookupErrorException;
import com.dropbox.core.v2.files.WriteMode;
import com.dropbox.core.v2.users.FullAccount;
import com.tomclaw.cache.DiskLruCache;
import org.cryptomator.data.util.TransferredBytesAwareInputStream;
import org.cryptomator.data.util.TransferredBytesAwareOutputStream;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.DropboxCloud;
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.authentication.AuthenticationException;
import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.Progress;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import org.cryptomator.util.SharedPreferencesHandler;
import org.cryptomator.util.crypto.CredentialCryptor;
import org.cryptomator.util.file.LruFileCacheUtil;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import timber.log.Timber;
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
import static org.cryptomator.util.file.LruFileCacheUtil.Cache.DROPBOX;
import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache;
import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache;
class DropboxImpl {
private static final long CHUNKED_UPLOAD_CHUNK_SIZE = 8L << 20;
private static final int CHUNKED_UPLOAD_MAX_ATTEMPTS = 5;
private final DropboxClientFactory clientFactory = new DropboxClientFactory();
private final DropboxCloud cloud;
private final RootDropboxFolder root;
private final Context context;
private final SharedPreferencesHandler sharedPreferencesHandler;
private DiskLruCache diskLruCache;
DropboxImpl(DropboxCloud cloud, Context context) {
if (cloud.accessToken() == null) {
throw new NoAuthenticationProvidedException(cloud);
}
this.cloud = cloud;
this.root = new RootDropboxFolder(cloud);
this.context = context;
sharedPreferencesHandler = new SharedPreferencesHandler(context);
}
private static void sleepQuietly(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ex) {
throw new FatalBackendException("Error uploading to Dropbox: interrupted during backoff.");
}
}
private DbxClientV2 client() throws AuthenticationException {
return clientFactory.getClient(decrypt(cloud.accessToken()), context);
}
private String decrypt(String password) {
return CredentialCryptor //
.getInstance(context) //
.decrypt(password);
}
public DropboxFolder root() {
return root;
}
public DropboxFolder resolve(String path) {
if (path.startsWith("/")) {
path = path.substring(1);
}
String[] names = path.split("/");
DropboxFolder folder = root;
for (String name : names) {
folder = folder(folder, name);
}
return folder;
}
public DropboxFile file(CloudFolder folder, String name) {
return file(folder, name, Optional.empty());
}
public DropboxFile file(CloudFolder folder, String name, Optional<Long> size) {
return DropboxCloudNodeFactory.file( //
(DropboxFolder) folder, //
name, //
size, //
folder.getPath() + '/' + name);
}
public DropboxFolder folder(CloudFolder folder, String name) {
return DropboxCloudNodeFactory.folder( //
(DropboxFolder) folder, //
name, //
folder.getPath() + '/' + name);
}
public boolean exists(CloudNode node) throws AuthenticationException, DbxException {
try {
Metadata metadata = client() //
.files() //
.getMetadata(node.getPath());
if (node instanceof CloudFolder) {
return metadata instanceof FolderMetadata;
} else {
return metadata instanceof FileMetadata;
}
} catch (GetMetadataErrorException e) {
if (e.errorValue.isPath()) {
return false;
}
throw e;
}
}
public List<DropboxNode> list(CloudFolder folder) throws AuthenticationException, DbxException {
List<DropboxNode> result = new ArrayList<>();
ListFolderResult listFolderResult = null;
do {
if (listFolderResult == null) {
listFolderResult = client() //
.files() //
.listFolder(folder.getPath());
} else {
String cursor = listFolderResult.getCursor();
listFolderResult = client() //
.files() //
.listFolderContinue(cursor);
}
List<Metadata> entryMetadata = listFolderResult.getEntries();
for (Metadata metadata : entryMetadata) {
result.add(DropboxCloudNodeFactory.from( //
(DropboxFolder) folder, //
metadata));
}
} while (listFolderResult.getHasMore());
return result;
}
public DropboxFolder create(CloudFolder folder) throws AuthenticationException, DbxException {
CreateFolderResult createFolderResult = client() //
.files() //
.createFolderV2(folder.getPath());
return DropboxCloudNodeFactory.from( //
(DropboxFolder) folder.getParent(), //
createFolderResult.getMetadata());
}
public CloudNode move(CloudNode source, CloudNode target) throws AuthenticationException, DbxException {
RelocationResult relocationResult = client() //
.files() //
.moveV2(source.getPath(), target.getPath());
return DropboxCloudNodeFactory.from( //
(DropboxFolder) target.getParent(), //
relocationResult.getMetadata());
}
public DropboxFile write(DropboxFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, long size) throws AuthenticationException, DbxException, IOException, CloudNodeAlreadyExistsException {
if (exists(file) && !replace) {
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
}
progressAware.onProgress(Progress.started(UploadState.upload(file)));
WriteMode writeMode = WriteMode.ADD;
if (replace) {
writeMode = WriteMode.OVERWRITE;
}
// "Upload the file with simple upload API if it is small enough, otherwise use chunked
// upload API for better performance. Arbitrarily chose 2 times our chunk size as the
// deciding factor. This should really depend on your network."
// Source: https://github.com/dropbox/dropbox-sdk-java/blob/master/examples/upload-file/src/main/java/com/dropbox/core/examples/upload_file/Main.java
if (size <= (2 * CHUNKED_UPLOAD_CHUNK_SIZE)) {
uploadFile(file, data, progressAware, writeMode, size);
} else {
chunkedUploadFile(file, data, progressAware, writeMode, size);
}
FileMetadata metadata = (FileMetadata) client() //
.files() //
.getMetadata(file.getPath());
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
return DropboxCloudNodeFactory.from( //
file.getParent(), //
metadata);
}
private void uploadFile(final DropboxFile file, DataSource data, final ProgressAware<UploadState> progressAware, WriteMode writeMode, final long size) //
throws AuthenticationException, DbxException, IOException {
try (TransferredBytesAwareInputStream in = new TransferredBytesAwareInputStream(data.open(context)) {
@Override
public void bytesTransferred(long transferred) {
progressAware.onProgress( //
progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(transferred));
}
}) {
client() //
.files() //
.uploadBuilder(file.getPath()) //
.withMode(writeMode) //
.uploadAndFinish(in);
}
}
private void chunkedUploadFile(final DropboxFile file, DataSource data, final ProgressAware<UploadState> progressAware, WriteMode writeMode, final long size) throws AuthenticationException, DbxException, IOException {
// Assert our file is at least the chunk upload size. We make this assumption in the code
// below to simplify the logic.
if (size < CHUNKED_UPLOAD_CHUNK_SIZE) {
throw new FatalBackendException("File too small, use uploadFile() instead.");
}
long uploaded = 0L;
DbxException thrown = null;
try (InputStream stream = data.open(context)) {
// Chunked uploads have 3 phases, each of which can accept uploaded bytes:
//
// (1) Start: initiate the upload and get an upload session ID
// (2) Append: upload chunks of the file to append to our session
// (3) Finish: commit the upload and close the session
//
// We track how many bytes we uploaded to determine which phase we should be in.
String sessionId = null;
for (int i = 0; i < CHUNKED_UPLOAD_MAX_ATTEMPTS; i++) {
if (i > 0) {
Timber.v("Retrying chunked upload (" + (i + 1) + " / " + CHUNKED_UPLOAD_MAX_ATTEMPTS + " attempts)");
}
try {
// if this is a retry, make sure seek to the correct offset
stream.skip(uploaded);
// (1) Start
if (sessionId == null) {
sessionId = client() //
.files() //
.uploadSessionStart() //
.uploadAndFinish(new TransferredBytesAwareInputStream(stream) {
@Override
public void bytesTransferred(long transferred) {
progressAware.onProgress( //
progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(transferred));
}
}, CHUNKED_UPLOAD_CHUNK_SIZE).getSessionId();
uploaded += CHUNKED_UPLOAD_CHUNK_SIZE;
progressAware.onProgress( //
progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(uploaded));
}
UploadSessionCursor cursor = new UploadSessionCursor(sessionId, uploaded);
// (2) Append
while ((size - uploaded) > CHUNKED_UPLOAD_CHUNK_SIZE) {
final long fullyUploaded = uploaded;
client() //
.files() //
.uploadSessionAppendV2(cursor) //
.uploadAndFinish(new TransferredBytesAwareInputStream(stream) {
@Override
public void bytesTransferred(long transferred) {
progressAware.onProgress( //
progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(fullyUploaded + transferred));
}
}, CHUNKED_UPLOAD_CHUNK_SIZE);
uploaded += CHUNKED_UPLOAD_CHUNK_SIZE;
progressAware.onProgress( //
progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(uploaded));
cursor = new UploadSessionCursor(sessionId, uploaded);
}
// (3) Finish
long remaining = size - uploaded;
CommitInfo commitInfo = CommitInfo //
.newBuilder(file.getPath()) //
.withMode(writeMode) //
.build();
client() //
.files() //
.uploadSessionFinish(cursor, commitInfo) //
.uploadAndFinish(stream, remaining);
return;
} catch (RetryException ex) {
thrown = ex;
// RetryExceptions are never automatically retried by the client for uploads. Must
// catch this exception even if DbxRequestConfig.getMaxRetries() > 0.
sleepQuietly(ex.getBackoffMillis());
} catch (NetworkIOException ex) {
thrown = ex;
// Network issue with Dropbox (maybe a timeout?), try again.
} catch (UploadSessionLookupErrorException ex) {
if (ex.errorValue.isIncorrectOffset()) {
thrown = ex;
// Server offset into the stream doesn't match our offset (uploaded). Seek to
// the expected offset according to the server and try again.
uploaded = ex. //
errorValue. //
getIncorrectOffsetValue(). //
getCorrectOffset();
} else {
throw new FatalBackendException(ex);
}
} catch (UploadSessionFinishErrorException ex) {
if (ex.errorValue.isLookupFailed() && ex.errorValue.getLookupFailedValue().isIncorrectOffset()) {
thrown = ex;
// Server offset into the stream doesn't match our offset (uploaded). Seek to
// the expected offset according to the server and try again.
uploaded = ex. //
errorValue. //
getLookupFailedValue(). //
getIncorrectOffsetValue(). //
getCorrectOffset();
} else {
throw new FatalBackendException(ex);
}
}
}
}
throw new FatalBackendException("Maxed out upload attempts to Dropbox.", thrown);
}
public void read(CloudFile file, Optional<File> encryptedTmpFile, OutputStream data, final ProgressAware<DownloadState> progressAware) throws DbxException, IOException {
progressAware.onProgress(Progress.started(DownloadState.download(file)));
Optional<String> cacheKey = Optional.empty();
Optional<File> cacheFile = Optional.empty();
if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) {
final FileMetadata fileMetadata = (FileMetadata) client() //
.files() //
.getMetadata(file.getPath());
cacheKey = Optional.of(fileMetadata.getId() + fileMetadata.getRev());
java.io.File cachedFile = diskLruCache.get(cacheKey.get());
cacheFile = cachedFile != null ? Optional.of(cachedFile) : Optional.empty();
}
if (sharedPreferencesHandler.useLruCache() && cacheFile.isPresent()) {
try {
retrieveFromLruCache(cacheFile.get(), data);
} catch (IOException e) {
Timber.tag("DropboxImpl").w(e, "Error while retrieving content from Cache, get from web request");
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware);
}
} else {
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware);
}
progressAware.onProgress(Progress.completed(DownloadState.download(file)));
}
private void writeToData(final CloudFile file, //
final OutputStream data, //
final Optional<File> encryptedTmpFile, //
final Optional<String> cacheKey, //
final ProgressAware<DownloadState> progressAware) throws DbxException, IOException {
try (TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) {
@Override
public void bytesTransferred(long transferred) {
progressAware.onProgress( //
progress(DownloadState.download(file)) //
.between(0) //
.and(file.getSize().orElse(Long.MAX_VALUE)) //
.withValue(transferred));
}
}) {
client() //
.files() //
.download(file.getPath()) //
.download(out);
}
if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) {
try {
storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get());
} catch (IOException e) {
Timber.tag("DropboxImpl").e(e, "Failed to write downloaded file in LRU cache");
}
}
}
private boolean createLruCache(int cacheSize) {
if (diskLruCache == null) {
try {
diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(DROPBOX), cacheSize);
} catch (IOException e) {
Timber.tag("DropboxImpl").e(e, "Failed to setup LRU cache");
return false;
}
}
return true;
}
public void delete(CloudNode node) throws AuthenticationException, DbxException {
client() //
.files() //
.deleteV2(node.getPath());
}
public String currentAccount() throws AuthenticationException, DbxException {
FullAccount currentAccount = client() //
.users() //
.getCurrentAccount();
return currentAccount.getName().getDisplayName();
}
}

View File

@ -0,0 +1,397 @@
package org.cryptomator.data.cloud.dropbox
import android.content.Context
import com.dropbox.core.DbxException
import com.dropbox.core.NetworkIOException
import com.dropbox.core.RetryException
import com.dropbox.core.v2.DbxClientV2
import com.dropbox.core.v2.files.CommitInfo
import com.dropbox.core.v2.files.FileMetadata
import com.dropbox.core.v2.files.FolderMetadata
import com.dropbox.core.v2.files.GetMetadataErrorException
import com.dropbox.core.v2.files.ListFolderResult
import com.dropbox.core.v2.files.UploadSessionCursor
import com.dropbox.core.v2.files.UploadSessionFinishErrorException
import com.dropbox.core.v2.files.UploadSessionLookupErrorException
import com.dropbox.core.v2.files.WriteMode
import com.tomclaw.cache.DiskLruCache
import org.cryptomator.data.cloud.dropbox.DropboxCloudNodeFactory.file
import org.cryptomator.data.cloud.dropbox.DropboxCloudNodeFactory.folder
import org.cryptomator.data.cloud.dropbox.DropboxCloudNodeFactory.from
import org.cryptomator.data.util.TransferredBytesAwareInputStream
import org.cryptomator.data.util.TransferredBytesAwareOutputStream
import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.DropboxCloud
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.ParentFolderIsNullException
import org.cryptomator.domain.exception.authentication.AuthenticationException
import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.Progress
import org.cryptomator.domain.usecases.cloud.UploadState
import org.cryptomator.util.SharedPreferencesHandler
import org.cryptomator.util.file.LruFileCacheUtil
import org.cryptomator.util.file.LruFileCacheUtil.Companion.retrieveFromLruCache
import java.io.File
import java.io.IOException
import java.io.OutputStream
import java.util.ArrayList
import timber.log.Timber
internal class DropboxImpl(cloud: DropboxCloud, context: Context) {
private val cloud: DropboxCloud
private val root: RootDropboxFolder
private val context: Context
private val sharedPreferencesHandler: SharedPreferencesHandler
private var diskLruCache: DiskLruCache? = null
@Throws(AuthenticationException::class)
private fun client(): DbxClientV2 {
return DropboxClientFactory.getInstance(cloud.accessToken(), context)
}
fun root(): DropboxFolder {
return root
}
fun resolve(path: String): DropboxFolder {
val names = path.removePrefix("/").split("/").toTypedArray()
var folder: DropboxFolder = root
for (name in names) {
folder = folder(folder, name)
}
return folder
}
fun file(folder: DropboxFolder, name: String, size: Long?): DropboxFile {
return file(folder, name, size, folder.path + '/' + name)
}
fun folder(folder: DropboxFolder, name: String): DropboxFolder {
return folder(folder, name, folder.path + '/' + name)
}
@Throws(AuthenticationException::class, DbxException::class)
fun exists(node: CloudNode): Boolean {
return try {
val metadata = client() //
.files() //
.getMetadata(node.path)
if (node is CloudFolder) {
metadata is FolderMetadata
} else {
metadata is FileMetadata
}
} catch (e: GetMetadataErrorException) {
if (e.errorValue.isPath) {
return false
}
throw e
}
}
@Throws(AuthenticationException::class, DbxException::class)
fun list(folder: DropboxFolder): List<DropboxNode> {
val result: MutableList<DropboxNode> = ArrayList()
var listFolderResult: ListFolderResult? = null
do {
listFolderResult = if (listFolderResult == null) {
client().files().listFolder(folder.path)
} else {
client().files().listFolderContinue(listFolderResult.cursor)
}
listFolderResult.entries.parallelStream().forEach {
result.add(from(folder, it))
}
} while (listFolderResult?.hasMore == true)
return result
}
@Throws(AuthenticationException::class, DbxException::class)
fun create(folder: DropboxFolder): DropboxFolder {
folder.parent?.let {
val createFolderResult = client().files().createFolderV2(folder.path)
return from(it, createFolderResult.metadata)
} ?: throw ParentFolderIsNullException(folder.name)
}
@Throws(AuthenticationException::class, DbxException::class)
fun move(source: DropboxNode, target: DropboxNode): DropboxNode {
target.parent?.let { targetsParent ->
val relocationResult = client().files().moveV2(source.path, target.path)
return from(targetsParent, relocationResult.metadata)
} ?: throw ParentFolderIsNullException(target.name)
}
@Throws(AuthenticationException::class, DbxException::class, IOException::class, CloudNodeAlreadyExistsException::class)
fun write(file: DropboxFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): DropboxFile {
if (!replace && exists(file)) {
throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false")
}
progressAware.onProgress(Progress.started(UploadState.upload(file)))
var writeMode = WriteMode.ADD
if (replace) {
writeMode = WriteMode.OVERWRITE
}
// "Upload the file with simple upload API if it is small enough, otherwise use chunked
// upload API for better performance. Arbitrarily chose 2 times our chunk size as the
// deciding factor. This should really depend on your network."
// Source: https://github.com/dropbox/dropbox-sdk-java/blob/master/examples/upload-file/src/main/java/com/dropbox/core/examples/upload_file/Main.java
if (size <= 2 * CHUNKED_UPLOAD_CHUNK_SIZE) {
uploadFile(file, data, progressAware, writeMode, size)
} else {
chunkedUploadFile(file, data, progressAware, writeMode, size)
}
val metadata = client().files().getMetadata(file.path)
progressAware.onProgress(Progress.completed(UploadState.upload(file)))
return from(file.parent, metadata) as DropboxFile
}
@Throws(AuthenticationException::class, DbxException::class, IOException::class)
private fun uploadFile(file: DropboxFile, data: DataSource, progressAware: ProgressAware<UploadState>, writeMode: WriteMode, size: Long) {
data.open(context)?.use { inputStream ->
object : TransferredBytesAwareInputStream(inputStream) {
override fun bytesTransferred(transferred: Long) {
progressAware.onProgress( //
Progress.progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(transferred)
)
}
}.use {
client() //
.files() //
.uploadBuilder(file.path) //
.withMode(writeMode) //
.uploadAndFinish(it)
}
} ?: Timber.tag("").e("InputStream shouldn't be null")
}
@Throws(AuthenticationException::class, DbxException::class, IOException::class)
private fun chunkedUploadFile(file: DropboxFile, data: DataSource, progressAware: ProgressAware<UploadState>, writeMode: WriteMode, size: Long) {
// Assert our file is at least the chunk upload size. We make this assumption in the code
// below to simplify the logic.
if (size < CHUNKED_UPLOAD_CHUNK_SIZE) {
throw FatalBackendException("File too small, use uploadFile() instead.")
}
var uploaded = 0L
var thrown: DbxException? = null
data.open(context)?.use {
// Chunked uploads have 3 phases, each of which can accept uploaded bytes:
//
// (1) Start: initiate the upload and get an upload session ID
// (2) Append: upload chunks of the file to append to our session
// (3) Finish: commit the upload and close the session
//
// We track how many bytes we uploaded to determine which phase we should be in.
var sessionId: String? = null
for (i in 0 until CHUNKED_UPLOAD_MAX_ATTEMPTS) {
if (i > 0) {
Timber.v("Retrying chunked upload (" + (i + 1) + " / " + CHUNKED_UPLOAD_MAX_ATTEMPTS + " attempts)")
}
try {
// if this is a retry, make sure seek to the correct offset
it.skip(uploaded)
// (1) Start
if (sessionId == null) {
sessionId = client() //
.files() //
.uploadSessionStart() //
.uploadAndFinish(object : TransferredBytesAwareInputStream(it) {
override fun bytesTransferred(transferred: Long) {
progressAware.onProgress( //
Progress.progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(transferred)
)
}
}, CHUNKED_UPLOAD_CHUNK_SIZE).sessionId
uploaded += CHUNKED_UPLOAD_CHUNK_SIZE
progressAware.onProgress( //
Progress.progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(uploaded)
)
}
var cursor = UploadSessionCursor(sessionId, uploaded)
// (2) Append
while (size - uploaded > CHUNKED_UPLOAD_CHUNK_SIZE) {
val fullyUploaded = uploaded
client() //
.files() //
.uploadSessionAppendV2(cursor) //
.uploadAndFinish(object : TransferredBytesAwareInputStream(it) {
override fun bytesTransferred(transferred: Long) {
progressAware.onProgress( //
Progress.progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(fullyUploaded + transferred)
)
}
}, CHUNKED_UPLOAD_CHUNK_SIZE)
uploaded += CHUNKED_UPLOAD_CHUNK_SIZE
progressAware.onProgress( //
Progress.progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(uploaded)
)
cursor = UploadSessionCursor(sessionId, uploaded)
}
// (3) Finish
val remaining = size - uploaded
val commitInfo = CommitInfo //
.newBuilder(file.path) //
.withMode(writeMode) //
.build()
client() //
.files() //
.uploadSessionFinish(cursor, commitInfo) //
.uploadAndFinish(it, remaining)
return
} catch (ex: RetryException) {
thrown = ex
// RetryExceptions are never automatically retried by the client for uploads. Must
// catch this exception even if DbxRequestConfig.getMaxRetries() > 0.
sleepQuietly(ex.backoffMillis)
} catch (ex: NetworkIOException) {
thrown = ex
// Network issue with Dropbox (maybe a timeout?), try again.
} catch (ex: UploadSessionLookupErrorException) {
if (ex.errorValue.isIncorrectOffset) {
thrown = ex
// Server offset into the stream doesn't match our offset (uploaded). Seek to
// the expected offset according to the server and try again.
uploaded = ex.errorValue.incorrectOffsetValue.correctOffset
} else {
throw FatalBackendException(ex)
}
} catch (ex: UploadSessionFinishErrorException) {
if (ex.errorValue.isLookupFailed && ex.errorValue.lookupFailedValue.isIncorrectOffset) {
thrown = ex
// Server offset into the stream doesn't match our offset (uploaded). Seek to
// the expected offset according to the server and try again.
uploaded = ex.errorValue.lookupFailedValue.incorrectOffsetValue.correctOffset
} else {
throw FatalBackendException(ex)
}
}
}
} ?: throw FatalBackendException("InputStream is null")
throw FatalBackendException("Maxed out upload attempts to Dropbox.", thrown)
}
@Throws(DbxException::class, IOException::class)
fun read(file: CloudFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
progressAware.onProgress(Progress.started(DownloadState.download(file)))
var cacheKey: String? = null
var cacheFile: File? = null
if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) {
val fileMetadata = client() //
.files() //
.getMetadata(file.path) as FileMetadata
cacheKey = fileMetadata.id + fileMetadata.rev
cacheFile = diskLruCache?.let { it[cacheKey] }
}
if (sharedPreferencesHandler.useLruCache() && cacheFile != null) {
try {
retrieveFromLruCache(cacheFile, data)
} catch (e: IOException) {
Timber.tag("DropboxImpl").w(e, "Error while retrieving content from Cache, get from web request")
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware)
}
} else {
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware)
}
progressAware.onProgress(Progress.completed(DownloadState.download(file)))
}
@Throws(DbxException::class, IOException::class)
private fun writeToData(file: CloudFile, data: OutputStream, encryptedTmpFile: File?, cacheKey: String?, progressAware: ProgressAware<DownloadState>) {
object : TransferredBytesAwareOutputStream(data) {
override fun bytesTransferred(transferred: Long) {
progressAware.onProgress( //
Progress.progress(DownloadState.download(file)) //
.between(0) //
.and(file.size ?: Long.MAX_VALUE) //
.withValue(transferred)
)
}
}.use {
client() //
.files() //
.download(file.path) //
.download(it)
}
if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile != null && cacheKey != null) {
try {
diskLruCache?.let {
LruFileCacheUtil.storeToLruCache(it, cacheKey, encryptedTmpFile)
} ?: Timber.tag("DropboxImpl").e("Failed to store item in LRU cache")
} catch (e: IOException) {
Timber.tag("DropboxImpl").e(e, "Failed to write downloaded file in LRU cache")
}
}
}
private fun createLruCache(cacheSize: Int): Boolean {
if (diskLruCache == null) {
diskLruCache = try {
DiskLruCache.create(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.DROPBOX), cacheSize.toLong())
} catch (e: IOException) {
Timber.tag("DropboxImpl").e(e, "Failed to setup LRU cache")
return false
}
}
return true
}
@Throws(AuthenticationException::class, DbxException::class)
fun delete(node: CloudNode) {
client().files().deleteV2(node.path)
}
@Throws(AuthenticationException::class, DbxException::class)
fun currentAccount(): String {
val currentAccount = client().users().currentAccount
return currentAccount.name.displayName
}
companion object {
private const val CHUNKED_UPLOAD_CHUNK_SIZE = 8L shl 20
private const val CHUNKED_UPLOAD_MAX_ATTEMPTS = 5
private fun sleepQuietly(millis: Long) {
try {
Thread.sleep(millis)
} catch (ex: InterruptedException) {
throw FatalBackendException("Error uploading to Dropbox: interrupted during backoff.")
}
}
}
init {
if (cloud.accessToken() == null) {
throw NoAuthenticationProvidedException(cloud)
}
this.cloud = cloud
this.root = RootDropboxFolder(cloud)
this.context = context
sharedPreferencesHandler = SharedPreferencesHandler(context)
}
}

View File

@ -1,7 +0,0 @@
package org.cryptomator.data.cloud.dropbox;
import org.cryptomator.domain.CloudNode;
interface DropboxNode extends CloudNode {
}

View File

@ -0,0 +1,9 @@
package org.cryptomator.data.cloud.dropbox
import org.cryptomator.domain.CloudNode
interface DropboxNode : CloudNode {
override val parent: DropboxFolder?
}

View File

@ -1,24 +0,0 @@
package org.cryptomator.data.cloud.dropbox;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.DropboxCloud;
class RootDropboxFolder extends DropboxFolder {
private final DropboxCloud cloud;
public RootDropboxFolder(DropboxCloud cloud) {
super(null, "", "");
this.cloud = cloud;
}
@Override
public DropboxCloud getCloud() {
return cloud;
}
@Override
public DropboxFolder withCloud(Cloud cloud) {
return new RootDropboxFolder((DropboxCloud) cloud);
}
}

View File

@ -0,0 +1,11 @@
package org.cryptomator.data.cloud.dropbox
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.DropboxCloud
internal class RootDropboxFolder(override val cloud: DropboxCloud) : DropboxFolder(null, "", "") {
override fun withCloud(cloud: Cloud?): DropboxFolder {
return RootDropboxFolder(cloud as DropboxCloud)
}
}

View File

@ -1,54 +0,0 @@
package org.cryptomator.data.cloud.local.file;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.util.Optional;
import java.util.Date;
class LocalFile implements CloudFile, LocalNode {
private final LocalFolder parent;
private final String name;
private final String path;
private final Optional<Long> size;
private final Optional<Date> modified;
LocalFile(LocalFolder parent, String name, String path, Optional<Long> size, Optional<Date> modified) {
this.parent = parent;
this.name = name;
this.path = path;
this.size = size;
this.modified = modified;
}
@Override
public Cloud getCloud() {
return parent.getCloud();
}
@Override
public String getName() {
return name;
}
@Override
public String getPath() {
return path;
}
@Override
public LocalFolder getParent() {
return parent;
}
@Override
public Optional<Long> getSize() {
return size;
}
@Override
public Optional<Date> getModified() {
return modified;
}
}

View File

@ -0,0 +1,11 @@
package org.cryptomator.data.cloud.local.file
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import java.util.Date
class LocalFile(override val parent: LocalFolder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, LocalNode {
override val cloud: Cloud?
get() = parent.cloud
}

View File

@ -1,42 +0,0 @@
package org.cryptomator.data.cloud.local.file;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFolder;
class LocalFolder implements CloudFolder, LocalNode {
private final LocalFolder parent;
private final String name;
private final String path;
LocalFolder(LocalFolder parent, String name, String path) {
this.parent = parent;
this.name = name;
this.path = path;
}
@Override
public Cloud getCloud() {
return parent.getCloud();
}
@Override
public String getName() {
return name;
}
@Override
public String getPath() {
return path;
}
@Override
public LocalFolder getParent() {
return parent;
}
@Override
public LocalFolder withCloud(Cloud cloud) {
return new LocalFolder(parent.withCloud(cloud), name, path);
}
}

View File

@ -0,0 +1,14 @@
package org.cryptomator.data.cloud.local.file
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFolder
open class LocalFolder(override val parent: LocalFolder?, override val name: String, override val path: String) : CloudFolder, LocalNode {
override val cloud: Cloud?
get() = parent?.cloud
override fun withCloud(cloud: Cloud?): LocalFolder? {
return LocalFolder(parent?.withCloud(cloud), name, path)
}
}

View File

@ -1,7 +0,0 @@
package org.cryptomator.data.cloud.local.file;
import org.cryptomator.domain.CloudNode;
interface LocalNode extends CloudNode {
}

View File

@ -0,0 +1,9 @@
package org.cryptomator.data.cloud.local.file
import org.cryptomator.domain.CloudNode
interface LocalNode : CloudNode {
override val parent: LocalFolder?
}

View File

@ -1,121 +0,0 @@
package org.cryptomator.data.cloud.local.file;
import android.content.Context;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.LocalStorageCloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import static org.cryptomator.util.ExceptionUtil.contains;
public class LocalStorageContentRepository implements CloudContentRepository<LocalStorageCloud, LocalNode, LocalFolder, LocalFile> {
private final LocalStorageImpl localStorageImpl;
public LocalStorageContentRepository(Context context, LocalStorageCloud localStorageCloud) {
this.localStorageImpl = new LocalStorageImpl(context, localStorageCloud);
}
@Override
public LocalFolder root(LocalStorageCloud cloud) throws BackendException {
return localStorageImpl.root();
}
@Override
public LocalFolder resolve(LocalStorageCloud cloud, String path) throws BackendException {
return localStorageImpl.resolve(path);
}
@Override
public LocalFile file(LocalFolder parent, String name) throws BackendException {
return localStorageImpl.file(parent, name);
}
@Override
public LocalFile file(LocalFolder parent, String name, Optional<Long> size) throws BackendException {
return localStorageImpl.file(parent, name, size);
}
@Override
public LocalFolder folder(LocalFolder parent, String name) throws BackendException {
return localStorageImpl.folder(parent, name);
}
@Override
public boolean exists(LocalNode node) throws BackendException {
return localStorageImpl.exists(node);
}
@Override
public List<CloudNode> list(LocalFolder folder) throws BackendException {
return localStorageImpl.list(folder);
}
@Override
public LocalFolder create(LocalFolder folder) throws BackendException {
return localStorageImpl.create(folder);
}
@Override
public LocalFolder move(LocalFolder source, LocalFolder target) throws BackendException {
return (LocalFolder) localStorageImpl.move(source, target);
}
@Override
public LocalFile move(LocalFile source, LocalFile target) throws BackendException {
return (LocalFile) localStorageImpl.move(source, target);
}
@Override
public LocalFile write(LocalFile file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
try {
return localStorageImpl.write(file, data, progressAware, replace, size);
} catch (IOException e) {
if (contains(e, FileNotFoundException.class)) {
throw new NoSuchCloudFileException(file.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public void read(LocalFile file, Optional<File> tmpEncryptedFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
try {
localStorageImpl.read(file, data, progressAware);
} catch (IOException e) {
if (contains(e, FileNotFoundException.class)) {
throw new NoSuchCloudFileException(file.getName());
}
throw new FatalBackendException(e);
}
}
@Override
public void delete(LocalNode node) throws BackendException {
localStorageImpl.delete(node);
}
@Override
public String checkAuthenticationAndRetrieveCurrentAccount(LocalStorageCloud cloud) throws BackendException {
return null;
}
@Override
public void logout(LocalStorageCloud cloud) throws BackendException {
// empty
}
}

View File

@ -0,0 +1,111 @@
package org.cryptomator.data.cloud.local.file
import android.content.Context
import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.UploadState
import org.cryptomator.util.ExceptionUtil
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.OutputStream
class LocalStorageContentRepository(context: Context, localStorageCloud: LocalStorageCloud) : CloudContentRepository<LocalStorageCloud, LocalNode, LocalFolder, LocalFile> {
private val localStorageImpl: LocalStorageImpl = LocalStorageImpl(context, localStorageCloud)
@Throws(BackendException::class)
override fun root(cloud: LocalStorageCloud): LocalFolder {
return localStorageImpl.root()
}
override fun resolve(cloud: LocalStorageCloud, path: String): LocalFolder {
return localStorageImpl.resolve(path)
}
@Throws(BackendException::class)
override fun file(parent: LocalFolder, name: String): LocalFile {
return localStorageImpl.file(parent, name, null)
}
@Throws(BackendException::class)
override fun file(parent: LocalFolder, name: String, size: Long?): LocalFile {
return localStorageImpl.file(parent, name, size)
}
@Throws(BackendException::class)
override fun folder(parent: LocalFolder, name: String): LocalFolder {
return localStorageImpl.folder(parent, name)
}
@Throws(BackendException::class)
override fun exists(node: LocalNode): Boolean {
return localStorageImpl.exists(node)
}
@Throws(BackendException::class)
override fun list(folder: LocalFolder): List<LocalNode> {
return localStorageImpl.list(folder)
}
@Throws(BackendException::class)
override fun create(folder: LocalFolder): LocalFolder {
return localStorageImpl.create(folder)
}
@Throws(BackendException::class)
override fun move(source: LocalFolder, target: LocalFolder): LocalFolder {
return localStorageImpl.move(source, target) as LocalFolder
}
@Throws(BackendException::class)
override fun move(source: LocalFile, target: LocalFile): LocalFile {
return localStorageImpl.move(source, target) as LocalFile
}
@Throws(BackendException::class)
override fun write(file: LocalFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): LocalFile {
return try {
localStorageImpl.write(file, data, progressAware, replace, size)
} catch (e: IOException) {
if (ExceptionUtil.contains(e, FileNotFoundException::class.java)) {
throw NoSuchCloudFileException(file.name)
}
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun read(file: LocalFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
try {
localStorageImpl.read(file, data, progressAware)
} catch (e: IOException) {
if (ExceptionUtil.contains(e, FileNotFoundException::class.java)) {
throw NoSuchCloudFileException(file.name)
}
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun delete(node: LocalNode) {
localStorageImpl.delete(node)
}
@Throws(BackendException::class)
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: LocalStorageCloud): String {
return ""
}
@Throws(BackendException::class)
override fun logout(cloud: LocalStorageCloud) {
// empty
}
}

View File

@ -1,193 +0,0 @@
package org.cryptomator.data.cloud.local.file;
import android.content.Context;
import org.cryptomator.data.util.TransferredBytesAwareInputStream;
import org.cryptomator.data.util.TransferredBytesAwareOutputStream;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.LocalStorageCloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.Progress;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import static org.cryptomator.data.util.CopyStream.copyStreamToStream;
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
class LocalStorageImpl {
private final Context context;
private final RootLocalFolder root;
LocalStorageImpl(Context context, LocalStorageCloud localStorageCloud) {
this.context = context;
this.root = new RootLocalFolder(localStorageCloud);
}
public LocalFolder root() {
return root;
}
public LocalFolder resolve(String path) {
if (path.startsWith(root.getPath())) {
path = path.substring(root.getPath().length() + 1);
}
String[] names = path.split("/");
LocalFolder folder = root;
for (String name : names) {
folder = folder(folder, name);
}
return folder;
}
public LocalFile file(CloudFolder folder, String name) {
return file(folder, name, Optional.empty());
}
public LocalFile file(CloudFolder folder, String name, Optional<Long> size) {
return LocalStorageNodeFactory.file( //
(LocalFolder) folder, //
name, //
folder.getPath() + '/' + name, //
size, //
Optional.empty());
}
public LocalFolder folder(CloudFolder folder, String name) {
return LocalStorageNodeFactory.folder( //
(LocalFolder) folder, //
name, //
folder.getPath() + '/' + name);
}
public boolean exists(CloudNode node) {
return new File(node.getPath()).exists();
}
public List<CloudNode> list(LocalFolder folder) throws BackendException {
List<CloudNode> result = new ArrayList<>();
File localDirectory = new File(folder.getPath());
if (!exists(folder)) {
throw new NoSuchCloudFileException();
}
for (File file : localDirectory.listFiles()) {
result.add(LocalStorageNodeFactory.from(folder, file));
}
return result;
}
public LocalFolder create(LocalFolder folder) throws BackendException {
File createFolder = new File(folder.getPath());
if (createFolder.exists()) {
throw new CloudNodeAlreadyExistsException(folder.getName());
}
if (!createFolder.mkdirs()) {
throw new FatalBackendException("Couldn't create a local folder at " + folder.getPath());
}
return LocalStorageNodeFactory.folder( //
folder.getParent(), //
createFolder);
}
public LocalNode move(CloudNode source, CloudNode target) throws BackendException {
File sourceFile = new File(source.getPath());
File targetFile = new File(target.getPath());
if (targetFile.exists()) {
throw new CloudNodeAlreadyExistsException(target.getName());
}
if (!sourceFile.exists()) {
throw new NoSuchCloudFileException(source.getName());
}
if (!sourceFile.renameTo(targetFile)) {
throw new FatalBackendException("Couldn't move " + source.getPath() + " to " + target.getPath());
}
return LocalStorageNodeFactory.from((LocalFolder) target.getParent(), targetFile);
}
public void delete(CloudNode node) {
File fileOrDirectory = new File(node.getPath());
if (!deleteRecursive(fileOrDirectory)) {
throw new FatalBackendException("Couldn't delete local CloudNode " + fileOrDirectory);
}
}
private boolean deleteRecursive(File fileOrDirectory) {
if (fileOrDirectory.isDirectory()) {
for (File child : fileOrDirectory.listFiles()) {
deleteRecursive(child);
}
}
return fileOrDirectory.delete();
}
public LocalFile write(final CloudFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, final long size) throws IOException, BackendException {
if (exists(file) && !replace) {
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
}
progressAware.onProgress(Progress.started(UploadState.upload(file)));
File localFile = new File(file.getPath());
try (OutputStream out = new FileOutputStream(localFile); TransferredBytesAwareInputStream in = new TransferredBytesAwareInputStream(data.open(context)) {
@Override
public void bytesTransferred(long transferred) {
progressAware.onProgress( //
progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(transferred));
}
}) {
copyStreamToStream(in, out);
}
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
return LocalStorageNodeFactory.file( //
(LocalFolder) file.getParent(), //
file.getName(), //
localFile.getPath(), //
Optional.of(localFile.length()), //
Optional.of(new Date(localFile.lastModified())));
}
public void read(final LocalFile file, OutputStream data, final ProgressAware<DownloadState> progressAware) throws IOException {
progressAware.onProgress(Progress.started(DownloadState.download(file)));
File localFile = new File(file.getPath());
try (InputStream in = new FileInputStream(localFile); TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) {
@Override
public void bytesTransferred(long transferred) {
progressAware //
.onProgress(progress(DownloadState.download(file)) //
.between(0) //
.and(localFile.length()) //
.withValue(transferred));
}
}) {
copyStreamToStream(in, out);
}
progressAware.onProgress(Progress.completed(DownloadState.download(file)));
}
}

View File

@ -0,0 +1,165 @@
package org.cryptomator.data.cloud.local.file
import android.content.Context
import org.cryptomator.data.util.CopyStream
import org.cryptomator.data.util.TransferredBytesAwareInputStream
import org.cryptomator.data.util.TransferredBytesAwareOutputStream
import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.exception.ParentFolderIsNullException
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.Progress
import org.cryptomator.domain.usecases.cloud.UploadState
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.Date
internal class LocalStorageImpl(private val context: Context, localStorageCloud: LocalStorageCloud) {
private val root: RootLocalFolder = RootLocalFolder(localStorageCloud)
fun root(): LocalFolder {
return root
}
fun resolve(path: String): LocalFolder {
val names = path.substring(root.path.length + 1).split("/").toTypedArray()
var folder: LocalFolder = root
for (name in names) {
folder = folder(folder, name)
}
return folder
}
fun file(folder: LocalFolder, name: String, size: Long?): LocalFile {
return LocalStorageNodeFactory.file(folder, name, folder.path + '/' + name, size, null)
}
fun folder(folder: LocalFolder, name: String): LocalFolder {
return LocalStorageNodeFactory.folder(folder, name, folder.path + '/' + name)
}
fun exists(node: CloudNode): Boolean {
return File(node.path).exists()
}
@Throws(BackendException::class)
fun list(folder: LocalFolder): List<LocalNode> {
val localDirectory = File(folder.path)
if (!exists(folder)) {
throw NoSuchCloudFileException()
}
return localDirectory.listFiles()?.map { file -> LocalStorageNodeFactory.from(folder, file) }
?: throw FatalBackendException("listFiles() shouldn't return null")
}
@Throws(BackendException::class)
fun create(folder: LocalFolder): LocalFolder {
folder.parent?.let { parentFolder ->
val createFolder = File(folder.path)
if (createFolder.exists()) {
throw CloudNodeAlreadyExistsException(folder.name)
}
if (!createFolder.mkdirs()) {
throw FatalBackendException("Couldn't create a local folder at " + folder.path)
}
return LocalStorageNodeFactory.folder(parentFolder, createFolder)
} ?: throw ParentFolderIsNullException(folder.name)
}
@Throws(BackendException::class)
fun move(source: LocalNode, target: LocalNode): LocalNode {
target.parent?.let {
val sourceFile = File(source.path)
val targetFile = File(target.path)
if (targetFile.exists()) {
throw CloudNodeAlreadyExistsException(target.name)
}
if (!sourceFile.exists()) {
throw NoSuchCloudFileException(source.name)
}
if (!sourceFile.renameTo(targetFile)) {
throw FatalBackendException("Couldn't move " + source.path + " to " + target.path)
}
return LocalStorageNodeFactory.from(it, targetFile)
} ?: throw ParentFolderIsNullException(target.name)
}
fun delete(node: CloudNode) {
val fileOrDirectory = File(node.path)
if (!deleteRecursive(fileOrDirectory)) {
throw FatalBackendException("Couldn't delete local CloudNode $fileOrDirectory")
}
}
private fun deleteRecursive(fileOrDirectory: File): Boolean {
if (fileOrDirectory.isDirectory) {
fileOrDirectory.listFiles()?.forEach {
deleteRecursive(it)
}
}
return fileOrDirectory.delete()
}
@Throws(IOException::class, BackendException::class)
fun write(file: LocalFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): LocalFile {
if (!replace && exists(file)) {
throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false")
}
progressAware.onProgress(Progress.started(UploadState.upload(file)))
val localFile = File(file.path)
FileOutputStream(localFile).use { out ->
data.open(context)?.use { inputStream ->
object : TransferredBytesAwareInputStream(inputStream) {
override fun bytesTransferred(transferred: Long) {
progressAware.onProgress( //
Progress.progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(transferred)
)
}
}.use { CopyStream.copyStreamToStream(it, out) }
} ?: throw FatalBackendException("InputStream shouldn't be null")
}
progressAware.onProgress(Progress.completed(UploadState.upload(file)))
return LocalStorageNodeFactory.file( //
file.parent, //
file.name, //
localFile.path, //
localFile.length(), //
Date(localFile.lastModified())
)
}
@Throws(IOException::class)
fun read(file: LocalFile, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
progressAware.onProgress(Progress.started(DownloadState.download(file)))
val localFile = File(file.path)
FileInputStream(localFile).use { inputStream ->
object : TransferredBytesAwareOutputStream(data) {
override fun bytesTransferred(transferred: Long) {
progressAware //
.onProgress(
Progress.progress(DownloadState.download(file)) //
.between(0) //
.and(localFile.length()) //
.withValue(transferred)
)
}
}.use { out -> CopyStream.copyStreamToStream(inputStream, out) }
}
progressAware.onProgress(Progress.completed(DownloadState.download(file)))
}
}

View File

@ -1,34 +0,0 @@
package org.cryptomator.data.cloud.local.file;
import org.cryptomator.util.Optional;
import java.io.File;
import java.util.Date;
class LocalStorageNodeFactory {
public static LocalNode from(LocalFolder parent, File file) {
if (file.isDirectory()) {
return folder(parent, file);
} else {
return file( //
parent, //
file.getName(), //
file.getPath(), //
Optional.of(file.length()), //
Optional.of(new Date(file.lastModified())));
}
}
public static LocalFolder folder(LocalFolder parent, File file) {
return folder(parent, file.getName(), file.getPath());
}
public static LocalFolder folder(LocalFolder parent, String name, String path) {
return new LocalFolder(parent, name, path);
}
public static LocalFile file(LocalFolder folder, String name, String path, Optional<Long> size, Optional<Date> modified) {
return new LocalFile(folder, name, path, size, modified);
}
}

View File

@ -0,0 +1,36 @@
package org.cryptomator.data.cloud.local.file
import java.io.File
import java.util.Date
internal object LocalStorageNodeFactory {
@JvmStatic
fun from(parent: LocalFolder, file: File): LocalNode {
return if (file.isDirectory) {
folder(parent, file)
} else {
file( //
parent, //
file.name, //
file.path, //
file.length(), //
Date(file.lastModified())
)
}
}
fun folder(parent: LocalFolder, file: File): LocalFolder {
return folder(parent, file.name, file.path)
}
@JvmStatic
fun folder(parent: LocalFolder, name: String, path: String): LocalFolder {
return LocalFolder(parent, name, path)
}
@JvmStatic
fun file(folder: LocalFolder, name: String, path: String, size: Long?, modified: Date?): LocalFile {
return LocalFile(folder, name, path, size, modified)
}
}

View File

@ -1,26 +0,0 @@
package org.cryptomator.data.cloud.local.file;
import android.os.Environment;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.LocalStorageCloud;
public class RootLocalFolder extends LocalFolder {
private final LocalStorageCloud localStorageCloud;
public RootLocalFolder(LocalStorageCloud localStorageCloud) {
super(null, "", Environment.getExternalStorageDirectory().getPath());
this.localStorageCloud = localStorageCloud;
}
@Override
public Cloud getCloud() {
return localStorageCloud;
}
@Override
public RootLocalFolder withCloud(Cloud cloud) {
return new RootLocalFolder((LocalStorageCloud) cloud);
}
}

View File

@ -0,0 +1,15 @@
package org.cryptomator.data.cloud.local.file
import android.os.Environment
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.LocalStorageCloud
class RootLocalFolder(private val localStorageCloud: LocalStorageCloud) : LocalFolder(null, "", Environment.getExternalStorageDirectory().path) {
override val cloud: Cloud
get() = localStorageCloud
override fun withCloud(cloud: Cloud?): RootLocalFolder {
return RootLocalFolder(cloud as LocalStorageCloud)
}
}

View File

@ -1,74 +0,0 @@
package org.cryptomator.data.cloud.local.storageaccessframework;
import android.util.LruCache;
import org.cryptomator.domain.CloudFolder;
class DocumentIdCache {
private final LruCache<String, NodeInfo> cache;
DocumentIdCache() {
cache = new LruCache<>(1000);
}
public NodeInfo get(String path) {
return cache.get(path);
}
<T extends LocalStorageAccessNode> T cache(T value) {
add(value);
return value;
}
public void add(LocalStorageAccessNode node) {
add(node.getPath(), new NodeInfo(node));
}
private void add(String path, NodeInfo info) {
cache.put(path, info);
}
public void remove(LocalStorageAccessNode node) {
remove(node.getPath());
}
private void remove(String path) {
removeChildren(path);
cache.remove(path);
}
private void removeChildren(String path) {
String prefix = path + '/';
for (String key : cache.snapshot().keySet()) {
if (key.startsWith(prefix)) {
cache.remove(key);
}
}
}
static class NodeInfo {
private final String id;
private final boolean isFolder;
private NodeInfo(LocalStorageAccessNode node) {
this(node.getDocumentId(), node instanceof CloudFolder);
}
NodeInfo(String id, boolean isFolder) {
this.id = id;
this.isFolder = isFolder;
}
public String getId() {
return id;
}
public boolean isFolder() {
return isFolder;
}
}
}

View File

@ -0,0 +1,51 @@
package org.cryptomator.data.cloud.local.storageaccessframework
import android.util.LruCache
import org.cryptomator.domain.CloudFolder
internal class DocumentIdCache {
private val cache: LruCache<String, NodeInfo> = LruCache(1000)
operator fun get(path: String): NodeInfo? {
return cache[path]
}
fun <T : LocalStorageAccessNode> cache(value: T): T {
add(value)
return value
}
fun add(node: LocalStorageAccessNode) {
add(node.path, NodeInfo(node))
}
private fun add(path: String, info: NodeInfo) {
cache.put(path, info)
}
fun remove(node: LocalStorageAccessNode) {
remove(node.path)
}
private fun remove(path: String) {
removeChildren(path)
cache.remove(path)
}
private fun removeChildren(path: String) {
val prefix = "$path/"
for (key in cache.snapshot().keys) {
if (key.startsWith(prefix)) {
cache.remove(key)
}
}
}
internal class NodeInfo(val id: String?, val isFolder: Boolean) {
constructor(node: LocalStorageAccessNode) : this(node.documentId, node is CloudFolder)
}
}

View File

@ -1,96 +0,0 @@
package org.cryptomator.data.cloud.local.storageaccessframework;
import android.net.Uri;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.util.Optional;
import java.util.Date;
import static android.net.Uri.parse;
class LocalStorageAccessFile implements CloudFile, LocalStorageAccessNode {
private final LocalStorageAccessFolder parent;
private final String name;
private final String path;
private final Optional<Long> size;
private final Optional<Date> modified;
private final String documentId;
private final String documentUri;
LocalStorageAccessFile(LocalStorageAccessFolder parent, String name, String path, Optional<Long> size, Optional<Date> modified, String documentId, String documentUri) {
this.parent = parent;
this.name = name;
this.path = path;
this.size = size;
this.modified = modified;
this.documentId = documentId;
this.documentUri = documentUri;
}
@Override
public Cloud getCloud() {
return parent.getCloud();
}
@Override
public String getName() {
return name;
}
@Override
public String getPath() {
return path;
}
@Override
public Uri getUri() {
return parse(documentUri);
}
@Override
public LocalStorageAccessFolder getParent() {
return parent;
}
@Override
public String getDocumentId() {
return documentId;
}
@Override
public Optional<Long> getSize() {
return size;
}
@Override
public Optional<Date> getModified() {
return modified;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return internalEquals((LocalStorageAccessFile) obj);
}
private boolean internalEquals(LocalStorageAccessFile o) {
return path.equals(o.path);
}
@Override
public int hashCode() {
final int prime = 31;
int hash = 56127034;
hash = hash * prime + path.hashCode();
return hash;
}
}

View File

@ -0,0 +1,42 @@
package org.cryptomator.data.cloud.local.storageaccessframework
import android.net.Uri
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import java.util.Date
class LocalStorageAccessFile(
override val parent: LocalStorageAccessFolder,
override val name: String,
override val path: String,
override val size: Long?,
override val modified: Date?,
override val documentId: String?,
private val documentUri: String?
) : CloudFile, LocalStorageAccessNode {
override val cloud: Cloud?
get() = parent.cloud
override val uri: Uri
get() = Uri.parse(documentUri)
override fun equals(other: Any?): Boolean {
if (other === this) {
return true
}
return if (other == null || javaClass != other.javaClass) {
false
} else internalEquals(other as LocalStorageAccessFile)
}
private fun internalEquals(o: LocalStorageAccessFile): Boolean {
return path == o.path
}
override fun hashCode(): Int {
val prime = 31
var hash = 56127034
hash = hash * prime + path.hashCode()
return hash
}
}

View File

@ -1,87 +0,0 @@
package org.cryptomator.data.cloud.local.storageaccessframework;
import android.net.Uri;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFolder;
import static android.net.Uri.parse;
class LocalStorageAccessFolder implements CloudFolder, LocalStorageAccessNode {
private final LocalStorageAccessFolder parent;
private final String name;
private final String path;
private final String documentId;
private final String documentUri;
LocalStorageAccessFolder(LocalStorageAccessFolder parent, String name, String path, String documentId, String documentUri) {
this.parent = parent;
this.name = name;
this.path = path;
this.documentId = documentId;
this.documentUri = documentUri;
}
@Override
public Cloud getCloud() {
return parent.getCloud();
}
@Override
public String getName() {
return name;
}
@Override
public String getPath() {
return path;
}
@Override
public Uri getUri() {
if (documentUri == null) {
return null;
}
return parse(documentUri);
}
@Override
public LocalStorageAccessFolder getParent() {
return parent;
}
@Override
public String getDocumentId() {
return documentId;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return internalEquals((LocalStorageAccessFolder) obj);
}
private boolean internalEquals(LocalStorageAccessFolder o) {
return path.equals(o.path);
}
@Override
public int hashCode() {
final int prime = 31;
int hash = 341797327;
hash = hash * prime + path.hashCode();
return hash;
}
@Override
public LocalStorageAccessFolder withCloud(Cloud cloud) {
return new LocalStorageAccessFolder(parent.withCloud(cloud), name, path, documentId, documentUri);
}
}

View File

@ -0,0 +1,40 @@
package org.cryptomator.data.cloud.local.storageaccessframework
import android.net.Uri
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFolder
open class LocalStorageAccessFolder(override val parent: LocalStorageAccessFolder?, override val name: String, override val path: String, override val documentId: String?, private val documentUri: String?) :
CloudFolder, LocalStorageAccessNode {
override val cloud: Cloud?
get() = parent?.cloud
override val uri: Uri?
get() = if (documentUri == null) {
null
} else Uri.parse(documentUri)
override fun equals(other: Any?): Boolean {
if (other === this) {
return true
}
return if (other == null || javaClass != other.javaClass) {
false
} else internalEquals(other as LocalStorageAccessFolder)
}
private fun internalEquals(o: LocalStorageAccessFolder): Boolean {
return path == o.path
}
override fun hashCode(): Int {
val prime = 31
var hash = 341797327
hash = hash * prime + path.hashCode()
return hash
}
override fun withCloud(cloud: Cloud?): LocalStorageAccessFolder? {
return LocalStorageAccessFolder(parent?.withCloud(cloud), name, path, documentId, documentUri)
}
}

View File

@ -1,123 +0,0 @@
package org.cryptomator.data.cloud.local.storageaccessframework;
import android.content.Context;
import android.os.Build;
import androidx.annotation.RequiresApi;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.LocalStorageCloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import org.cryptomator.util.file.MimeTypes;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class LocalStorageAccessFrameworkContentRepository implements CloudContentRepository<LocalStorageCloud, LocalStorageAccessNode, LocalStorageAccessFolder, LocalStorageAccessFile> {
private final LocalStorageAccessFrameworkImpl localStorageAccessFramework;
public LocalStorageAccessFrameworkContentRepository(Context context, MimeTypes mimeTypes, LocalStorageCloud localStorageCloud) {
this.localStorageAccessFramework = new LocalStorageAccessFrameworkImpl(context, mimeTypes, localStorageCloud, new DocumentIdCache());
}
@Override
public LocalStorageAccessFolder root(LocalStorageCloud cloud) throws BackendException {
return localStorageAccessFramework.root();
}
@Override
public LocalStorageAccessFolder resolve(LocalStorageCloud cloud, String path) throws BackendException {
return localStorageAccessFramework.resolve(path);
}
@Override
public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name) throws BackendException {
return localStorageAccessFramework.file(parent, name);
}
@Override
public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, Optional<Long> size) throws BackendException {
return localStorageAccessFramework.file(parent, name, size);
}
@Override
public LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name) throws BackendException {
return localStorageAccessFramework.folder(parent, name);
}
@Override
public boolean exists(LocalStorageAccessNode node) throws BackendException {
return localStorageAccessFramework.exists(node);
}
@Override
public List<CloudNode> list(LocalStorageAccessFolder folder) throws BackendException {
return localStorageAccessFramework.list(folder);
}
@Override
public LocalStorageAccessFolder create(LocalStorageAccessFolder folder) throws BackendException {
return localStorageAccessFramework.create(folder);
}
@Override
public LocalStorageAccessFolder move(LocalStorageAccessFolder source, LocalStorageAccessFolder target) throws BackendException {
if (source.getDocumentId() == null) {
throw new NoSuchCloudFileException(source.getName());
}
return (LocalStorageAccessFolder) localStorageAccessFramework.move(source, target);
}
@Override
public LocalStorageAccessFile move(LocalStorageAccessFile source, LocalStorageAccessFile target) throws BackendException {
return (LocalStorageAccessFile) localStorageAccessFramework.move(source, target);
}
@Override
public LocalStorageAccessFile write(LocalStorageAccessFile file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
try {
return localStorageAccessFramework.write(file, data, progressAware, replace, size);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public void read(LocalStorageAccessFile file, Optional<File> tmpEnctypted, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
try {
if (file.getDocumentId() == null) {
throw new NoSuchCloudFileException(file.getName());
}
localStorageAccessFramework.read(file, data, progressAware);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public void delete(LocalStorageAccessNode node) throws BackendException {
localStorageAccessFramework.delete(node);
}
@Override
public String checkAuthenticationAndRetrieveCurrentAccount(LocalStorageCloud cloud) throws BackendException {
return null;
}
@Override
public void logout(LocalStorageCloud cloud) throws BackendException {
// empty
}
}

View File

@ -0,0 +1,111 @@
package org.cryptomator.data.cloud.local.storageaccessframework
import android.content.Context
import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.UploadState
import org.cryptomator.util.file.MimeTypes
import java.io.File
import java.io.IOException
import java.io.OutputStream
class LocalStorageAccessFrameworkContentRepository(context: Context, mimeTypes: MimeTypes, localStorageCloud: LocalStorageCloud) :
CloudContentRepository<LocalStorageCloud, LocalStorageAccessNode, LocalStorageAccessFolder, LocalStorageAccessFile> {
private val localStorageAccessFramework: LocalStorageAccessFrameworkImpl = LocalStorageAccessFrameworkImpl(context, mimeTypes, localStorageCloud, DocumentIdCache())
@Throws(BackendException::class)
override fun root(cloud: LocalStorageCloud): LocalStorageAccessFolder {
return localStorageAccessFramework.root()
}
override fun resolve(cloud: LocalStorageCloud, path: String): LocalStorageAccessFolder {
return localStorageAccessFramework.resolve(path)
}
@Throws(BackendException::class)
override fun file(parent: LocalStorageAccessFolder, name: String): LocalStorageAccessFile {
return localStorageAccessFramework.file(parent, name, null)
}
@Throws(BackendException::class)
override fun file(parent: LocalStorageAccessFolder, name: String, size: Long?): LocalStorageAccessFile {
return localStorageAccessFramework.file(parent, name, size)
}
@Throws(BackendException::class)
override fun folder(parent: LocalStorageAccessFolder, name: String): LocalStorageAccessFolder {
return localStorageAccessFramework.folder(parent, name)
}
@Throws(BackendException::class)
override fun exists(node: LocalStorageAccessNode): Boolean {
return localStorageAccessFramework.exists(node)
}
@Throws(BackendException::class)
override fun list(folder: LocalStorageAccessFolder): List<LocalStorageAccessNode> {
return localStorageAccessFramework.list(folder)
}
@Throws(BackendException::class)
override fun create(folder: LocalStorageAccessFolder): LocalStorageAccessFolder {
return localStorageAccessFramework.create(folder)
}
@Throws(BackendException::class)
override fun move(source: LocalStorageAccessFolder, target: LocalStorageAccessFolder): LocalStorageAccessFolder {
if (source.documentId == null) {
throw NoSuchCloudFileException(source.name)
}
return localStorageAccessFramework.move(source, target) as LocalStorageAccessFolder
}
@Throws(BackendException::class)
override fun move(source: LocalStorageAccessFile, target: LocalStorageAccessFile): LocalStorageAccessFile {
return localStorageAccessFramework.move(source, target) as LocalStorageAccessFile
}
@Throws(BackendException::class)
override fun write(file: LocalStorageAccessFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): LocalStorageAccessFile {
return try {
localStorageAccessFramework.write(file, data, progressAware, replace, size)
} catch (e: IOException) {
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun read(file: LocalStorageAccessFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
try {
if (file.documentId == null) {
throw NoSuchCloudFileException(file.name)
}
localStorageAccessFramework.read(file, data, progressAware)
} catch (e: IOException) {
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun delete(node: LocalStorageAccessNode) {
localStorageAccessFramework.delete(node)
}
@Throws(BackendException::class)
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: LocalStorageCloud): String {
return ""
}
@Throws(BackendException::class)
override fun logout(cloud: LocalStorageCloud) {
// empty
}
}

View File

@ -1,536 +0,0 @@
package org.cryptomator.data.cloud.local.storageaccessframework;
import android.content.ContentResolver;
import android.content.Context;
import android.content.UriPermission;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import androidx.annotation.RequiresApi;
import androidx.documentfile.provider.DocumentFile;
import org.cryptomator.data.util.TransferredBytesAwareInputStream;
import org.cryptomator.data.util.TransferredBytesAwareOutputStream;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.LocalStorageCloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
import org.cryptomator.domain.exception.NotFoundException;
import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.Progress;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import org.cryptomator.util.Supplier;
import org.cryptomator.util.file.MimeType;
import org.cryptomator.util.file.MimeTypes;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import timber.log.Timber;
import static org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.from;
import static org.cryptomator.data.util.CopyStream.closeQuietly;
import static org.cryptomator.data.util.CopyStream.copyStreamToStream;
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class LocalStorageAccessFrameworkImpl {
private final Context context;
private final RootLocalStorageAccessFolder root;
private final DocumentIdCache idCache;
private final MimeTypes mimeTypes;
LocalStorageAccessFrameworkImpl(Context context, MimeTypes mimeTypes, LocalStorageCloud cloud, DocumentIdCache documentIdCache) {
this.mimeTypes = mimeTypes;
if (!hasUriPermissions(context, cloud.rootUri())) {
throw new NoAuthenticationProvidedException(cloud);
}
this.context = context;
this.root = new RootLocalStorageAccessFolder(cloud);
this.idCache = documentIdCache;
}
private boolean hasUriPermissions(Context context, String uri) {
Optional<UriPermission> uriPermission = uriPermissionFor(context, uri);
return uriPermission.isPresent() //
&& uriPermission.get().isReadPermission() //
&& uriPermission.get().isWritePermission();
}
private Optional<UriPermission> uriPermissionFor(Context context, String uri) {
for (UriPermission uriPermission : context.getContentResolver().getPersistedUriPermissions()) {
if (uri.equals(uriPermission.getUri().toString())) {
return Optional.of(uriPermission);
}
}
return Optional.empty();
}
public LocalStorageAccessFolder root() {
return root;
}
public LocalStorageAccessFolder resolve(String path) throws BackendException {
if (path.startsWith("/")) {
path = path.substring(1);
}
String[] names = path.split("/");
LocalStorageAccessFolder folder = root;
for (String name : names) {
folder = folder(folder, name);
}
return folder;
}
public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name) throws BackendException {
return file( //
parent, //
name, //
Optional.empty());
}
public LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, Optional<Long> size) throws BackendException {
if (parent.getDocumentId() == null) {
return LocalStorageAccessFrameworkNodeFactory.file( //
parent, //
name, //
size);
}
String path = LocalStorageAccessFrameworkNodeFactory.getNodePath(parent, name);
DocumentIdCache.NodeInfo nodeInfo = idCache.get(path);
if (nodeInfo != null && !nodeInfo.isFolder()) {
return LocalStorageAccessFrameworkNodeFactory.file( //
parent, //
name, //
path, //
size, //
nodeInfo.getId());
}
List<LocalStorageAccessNode> cloudNodes = listFilesWithNameFilter(parent, name);
if (cloudNodes.size() > 0) {
LocalStorageAccessNode cloudNode = cloudNodes.get(0);
if (cloudNode instanceof LocalStorageAccessFile) {
return idCache.cache((LocalStorageAccessFile) cloudNode);
}
}
return LocalStorageAccessFrameworkNodeFactory.file( //
parent, //
name, //
size);
}
public LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name) throws BackendException {
if (parent.getDocumentId() == null) {
return LocalStorageAccessFrameworkNodeFactory.folder( //
parent, //
name);
}
String path = LocalStorageAccessFrameworkNodeFactory.getNodePath(parent, name);
DocumentIdCache.NodeInfo nodeInfo = idCache.get(path);
if (nodeInfo != null && nodeInfo.isFolder()) {
return LocalStorageAccessFrameworkNodeFactory.folder( //
parent, //
name, //
nodeInfo.getId());
}
List<LocalStorageAccessNode> cloudNodes = listFilesWithNameFilter(parent, name);
if (cloudNodes.size() > 0) {
LocalStorageAccessNode cloudNode = cloudNodes.get(0);
if (cloudNode instanceof LocalStorageAccessFolder) {
return idCache.cache((LocalStorageAccessFolder) cloudNode);
}
}
return LocalStorageAccessFrameworkNodeFactory.folder( //
parent, //
name);
}
private List<LocalStorageAccessNode> listFilesWithNameFilter(LocalStorageAccessFolder parent, String name) throws BackendException {
if (parent.getUri() == null) {
List<LocalStorageAccessNode> parents = listFilesWithNameFilter(parent.getParent(), parent.getName());
if (parents.isEmpty() || !(parents.get(0) instanceof LocalStorageAccessFolder)) {
throw new NoSuchCloudFileException(name);
}
parent = (LocalStorageAccessFolder) parents.get(0);
}
Cursor childCursor = null;
try {
childCursor = contentResolver() //
.query( //
DocumentsContract.buildChildDocumentsUriUsingTree( //
parent.getUri(), //
parent.getDocumentId()), //
new String[] {Document.COLUMN_DISPLAY_NAME, // cursor position 0
Document.COLUMN_MIME_TYPE, // cursor position 1
Document.COLUMN_SIZE, // cursor position 2
Document.COLUMN_LAST_MODIFIED, // cursor position 3
Document.COLUMN_DOCUMENT_ID // cursor position 4
}, //
null, //
null, //
null);
List<LocalStorageAccessNode> result = new ArrayList<>();
while (childCursor != null && childCursor.moveToNext()) {
if (childCursor.getString(0).equals(name)) {
result.add(idCache.cache(from(parent, childCursor)));
}
}
return result;
} catch (IllegalArgumentException e) {
if (e.getMessage().contains(FileNotFoundException.class.getCanonicalName())) {
throw new NoSuchCloudFileException(name);
}
throw new FatalBackendException(e);
} finally {
closeQuietly(childCursor);
}
}
public boolean exists(LocalStorageAccessNode node) throws BackendException {
try {
List<LocalStorageAccessNode> cloudNodes = listFilesWithNameFilter( //
node.getParent(), //
node.getName());
boolean documentExists = cloudNodes.size() > 0;
if (documentExists) {
idCache.add(cloudNodes.get(0));
}
return documentExists;
} catch (NoSuchCloudFileException e) {
return false;
}
}
public List<CloudNode> list(LocalStorageAccessFolder folder) throws BackendException {
Cursor childCursor = contentResolver() //
.query( //
DocumentsContract.buildChildDocumentsUriUsingTree( //
folder.getUri(), //
folder.getDocumentId()), //
new String[] { //
Document.COLUMN_DISPLAY_NAME, // cursor position 0
Document.COLUMN_MIME_TYPE, // cursor position 1
Document.COLUMN_SIZE, // cursor position 2
Document.COLUMN_LAST_MODIFIED, // cursor position 3
Document.COLUMN_DOCUMENT_ID // cursor position 4
}, null, null, null);
try {
List<CloudNode> result = new ArrayList<>();
while (childCursor != null && childCursor.moveToNext()) {
result.add(idCache.cache(from(folder, childCursor)));
}
return result;
} finally {
closeQuietly(childCursor);
}
}
public LocalStorageAccessFolder create(LocalStorageAccessFolder folder) throws BackendException {
if (folder //
.getParent() //
.getDocumentId() == null) {
folder = new LocalStorageAccessFolder( //
create(folder.getParent()), //
folder.getName(), //
folder.getPath(), //
null, //
null);
}
Uri createdDocument;
try {
createdDocument = DocumentsContract.createDocument( //
contentResolver(), //
folder.getParent().getUri(), //
Document.MIME_TYPE_DIR, //
folder.getName());
} catch (FileNotFoundException e) {
throw new NoSuchCloudFileException(folder.getName());
}
return idCache.cache( //
LocalStorageAccessFrameworkNodeFactory.folder( //
folder.getParent(), //
buildDocumentFile(createdDocument)));
}
public LocalStorageAccessNode move(LocalStorageAccessNode source, LocalStorageAccessNode target) throws BackendException {
if (exists(target)) {
throw new CloudNodeAlreadyExistsException(target.getName());
}
idCache.remove(source);
idCache.remove(target);
boolean isRename = !source //
.getName() //
.equals(target.getName());
boolean isMove = !source //
.getParent() //
.equals(target.getParent());
LocalStorageAccessNode renamedSource = source;
if (isRename) {
renamedSource = rename(source, target.getName());
}
if (isMove) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return idCache.cache( //
moveForApiStartingFrom24(renamedSource, target));
} else {
return idCache.cache( //
moveForApiBelow24(renamedSource, target));
}
}
return renamedSource;
}
private LocalStorageAccessNode rename(LocalStorageAccessNode source, String name) throws NoSuchCloudFileException {
Uri newUri = null;
try {
newUri = DocumentsContract.renameDocument( //
contentResolver(), //
source.getUri(), //
name);
} catch (FileNotFoundException e) {
// Bug in Android 9 see #460
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.P) {
throw new NoSuchCloudFileException(source.getName());
}
}
// Bug in Android 9 see #460
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
try {
List<LocalStorageAccessNode> cloudNodes = listFilesWithNameFilter( //
source.getParent(), //
name);
newUri = cloudNodes.get(0).getUri();
} catch (BackendException e) {
Timber.tag("LocalStgeAccessFrkImpl").e(e);
}
}
return LocalStorageAccessFrameworkNodeFactory.from( //
source.getParent(), //
buildDocumentFile(newUri));
}
@RequiresApi(api = Build.VERSION_CODES.N)
private LocalStorageAccessNode moveForApiStartingFrom24(LocalStorageAccessNode source, LocalStorageAccessNode target) throws NoSuchCloudFileException {
Uri movedTargetUri;
try {
movedTargetUri = DocumentsContract.moveDocument( //
contentResolver(), //
source.getUri(), //
source.getParent().getUri(), //
target.getParent().getUri());
} catch (FileNotFoundException e) {
throw new NoSuchCloudFileException(source.getName());
}
return from( //
target.getParent(), //
buildDocumentFile(movedTargetUri));
}
private LocalStorageAccessNode moveForApiBelow24(LocalStorageAccessNode source, LocalStorageAccessNode target) throws BackendException {
try {
LocalStorageAccessNode result;
if (source instanceof CloudFolder) {
result = moveForApiBelow24( //
(LocalStorageAccessFolder) source, //
(LocalStorageAccessFolder) target);
} else {
result = moveForApiBelow24( //
(LocalStorageAccessFile) source, //
(LocalStorageAccessFile) target);
}
delete(source);
return result;
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
private LocalStorageAccessFolder moveForApiBelow24(LocalStorageAccessFolder source, LocalStorageAccessFolder target) throws IOException, BackendException {
if (!exists(target.getParent())) {
throw new NoSuchCloudFileException(target.getParent().getPath());
}
LocalStorageAccessFolder createdFolder = create(target);
for (CloudNode child : list(source)) {
if (child instanceof CloudFolder) {
moveForApiBelow24( //
(LocalStorageAccessFolder) child, //
folder(target, child.getName()));
} else {
moveForApiBelow24( //
(LocalStorageAccessFile) child, //
file(target, child.getName()));
}
}
return createdFolder;
}
private LocalStorageAccessFile moveForApiBelow24(final LocalStorageAccessFile source, LocalStorageAccessFile target) throws IOException, BackendException {
DataSource dataSource = new DataSource() {
@Override
public void close() throws IOException {
// do nothing
}
@Override
public Optional<Long> size(Context context) {
return source.getSize();
}
@Override
public InputStream open(Context context) throws IOException {
return contentResolver().openInputStream(source.getUri());
}
@Override
public DataSource decorate(DataSource delegate) {
return delegate;
}
};
return write(target, dataSource, NO_OP_PROGRESS_AWARE, true, source.getSize().get());
}
public LocalStorageAccessFile write( //
LocalStorageAccessFile file, //
final DataSource data, //
final ProgressAware<UploadState> progressAware, //
final boolean replace, //
final long size) throws IOException, BackendException {
progressAware.onProgress(Progress.started(UploadState.upload(file)));
Optional<Uri> fileUri = existingFileUri(file);
if (fileUri.isPresent() && !replace) {
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
}
if (file.getParent().getUri() == null) {
LocalStorageAccessFolder parent = (LocalStorageAccessFolder) listFilesWithNameFilter(file.getParent().getParent(), file.getParent().getName()).get(0);
String tmpFileUri = fileUri.isPresent() ? fileUri.get().toString() : "";
file = new LocalStorageAccessFile(parent, file.getName(), file.getPath(), file.getSize(), file.getModified(), file.getDocumentId(), tmpFileUri);
}
final LocalStorageAccessFile tmpFile = file;
Uri uploadUri = fileUri.orElseGet(createNewDocumentSupplier(tmpFile));
if (uploadUri == null) {
throw new NotFoundException(tmpFile.getName());
}
try (OutputStream out = contentResolver().openOutputStream(uploadUri); //
TransferredBytesAwareInputStream in = new TransferredBytesAwareInputStream(data.open(context)) {
@Override
public void bytesTransferred(long transferred) {
progressAware //
.onProgress(progress(UploadState.upload(tmpFile)) //
.between(0) //
.and(size) //
.withValue(transferred));
}
}) {
if (out instanceof FileOutputStream) {
((FileOutputStream) out).getChannel().truncate(0);
}
copyStreamToStream(in, out);
}
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
return LocalStorageAccessFrameworkNodeFactory.file( //
file.getParent(), //
buildDocumentFile(uploadUri));
}
private Supplier<Uri> createNewDocumentSupplier(final LocalStorageAccessFile file) {
return () -> {
MimeType mimeType = mimeTypes.fromFilename(file.getName()) //
.orElse(MimeType.APPLICATION_OCTET_STREAM);
try {
return DocumentsContract.createDocument( //
contentResolver(), //
file.getParent().getUri(), //
mimeType.toString(), //
file.getName());
} catch (FileNotFoundException e) {
return null;
}
};
}
private Optional<Uri> existingFileUri(LocalStorageAccessFile file) throws BackendException {
List<LocalStorageAccessNode> nodes = listFilesWithNameFilter( //
file.getParent(), //
file.getName());
if (nodes.size() > 0) {
return Optional.of(nodes.get(0).getUri());
} else {
return Optional.empty();
}
}
public void read(final LocalStorageAccessFile file, final OutputStream data, final ProgressAware<DownloadState> progressAware) throws IOException {
progressAware.onProgress(Progress.started(DownloadState.download(file)));
try (InputStream in = contentResolver().openInputStream(file.getUri()); //
TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) {
@Override
public void bytesTransferred(long transferred) {
progressAware.onProgress(progress(DownloadState.download(file)) //
.between(0) //
.and(file.getSize().orElse(Long.MAX_VALUE)) //
.withValue(transferred));
}
}) {
copyStreamToStream(in, out);
}
progressAware.onProgress(Progress.completed(DownloadState.download(file)));
}
public void delete(LocalStorageAccessNode node) throws NoSuchCloudFileException {
try {
DocumentsContract.deleteDocument( //
contentResolver(), //
node.getUri());
} catch (FileNotFoundException e) {
throw new NoSuchCloudFileException(node.getName());
}
idCache.remove(node);
}
private DocumentFile buildDocumentFile(Uri fileUri) {
return DocumentFile.fromSingleUri(context, fileUri);
}
private ContentResolver contentResolver() {
return context.getContentResolver();
}
}

View File

@ -0,0 +1,399 @@
package org.cryptomator.data.cloud.local.storageaccessframework
import android.content.ContentResolver
import android.content.Context
import android.content.UriPermission
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.file
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.folder
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.from
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.getNodePath
import org.cryptomator.data.util.CopyStream
import org.cryptomator.data.util.TransferredBytesAwareInputStream
import org.cryptomator.data.util.TransferredBytesAwareOutputStream
import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.exception.NotFoundException
import org.cryptomator.domain.exception.ParentFolderIsNullException
import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.Progress
import org.cryptomator.domain.usecases.cloud.UploadState
import org.cryptomator.util.file.MimeType
import org.cryptomator.util.file.MimeTypes
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.ArrayList
import java.util.function.Supplier
internal class LocalStorageAccessFrameworkImpl(context: Context, private val mimeTypes: MimeTypes, cloud: LocalStorageCloud, documentIdCache: DocumentIdCache) {
private val context: Context
private val root: RootLocalStorageAccessFolder
private val idCache: DocumentIdCache
private fun hasUriPermissions(context: Context, uri: String): Boolean {
val uriPermission = uriPermissionFor(context, uri)
return uriPermission != null && uriPermission.isReadPermission && uriPermission.isWritePermission
}
private fun uriPermissionFor(context: Context, uri: String): UriPermission? {
return context
.contentResolver
.persistedUriPermissions
.find { uriPermission -> uriPermission.uri.toString() == uri }
}
fun root(): LocalStorageAccessFolder {
return root
}
@Throws(BackendException::class)
fun resolve(path: String): LocalStorageAccessFolder {
val names = path.removePrefix("/").split("/").toTypedArray()
var folder: LocalStorageAccessFolder = root
for (name in names) {
folder = folder(folder, name)
}
return folder
}
@Throws(BackendException::class)
fun file(parent: LocalStorageAccessFolder, name: String, size: Long?): LocalStorageAccessFile {
if (parent.documentId == null) {
return LocalStorageAccessFrameworkNodeFactory.file(parent, name, size)
}
val path = getNodePath(parent, name)
val nodeInfo = idCache[path]
if (nodeInfo != null && !nodeInfo.isFolder && nodeInfo.id != null) {
return file(parent, name, path, size, nodeInfo.id)
}
listFilesWithNameFilter(parent, name).getOrNull(0)?.let {
if(it is LocalStorageAccessFile) {
return idCache.cache(it)
}
}
return LocalStorageAccessFrameworkNodeFactory.file(parent, name, size)
}
@Throws(BackendException::class)
fun folder(parent: LocalStorageAccessFolder, name: String): LocalStorageAccessFolder {
if (parent.documentId == null) {
return LocalStorageAccessFrameworkNodeFactory.folder(parent, name)
}
val path = getNodePath(parent, name)
val nodeInfo = idCache[path]
if (nodeInfo != null && nodeInfo.isFolder && nodeInfo.id != null) {
return folder(parent, name, nodeInfo.id)
}
listFilesWithNameFilter(parent, name).getOrNull(0)?.let {
if(it is LocalStorageAccessFolder) {
return idCache.cache(it)
}
}
return LocalStorageAccessFrameworkNodeFactory.folder(parent, name)
}
@Throws(BackendException::class)
private fun listFilesWithNameFilter(parent: LocalStorageAccessFolder, name: String): List<LocalStorageAccessNode> {
var parent = parent
if (parent.uri == null) {
parent.parent?.let {
val parents = listFilesWithNameFilter(it, parent.name)
if (parents.isEmpty() || parents[0] !is LocalStorageAccessFolder) {
throw NoSuchCloudFileException(name)
}
parent = parents[0] as LocalStorageAccessFolder
} ?: throw ParentFolderIsNullException(parent.name)
}
val result: MutableList<LocalStorageAccessNode> = ArrayList()
try {
contentResolver() //
.query( //
DocumentsContract.buildChildDocumentsUriUsingTree( //
parent.uri, //
parent.documentId
), arrayOf(
DocumentsContract.Document.COLUMN_DISPLAY_NAME, // cursor position 0
DocumentsContract.Document.COLUMN_MIME_TYPE, // cursor position 1
DocumentsContract.Document.COLUMN_SIZE, // cursor position 2
DocumentsContract.Document.COLUMN_LAST_MODIFIED, // cursor position 3
DocumentsContract.Document.COLUMN_DOCUMENT_ID // cursor position 4
), //
null, //
null, //
null
)?.use {
while (it.moveToNext()) {
if (it.getString(0) == name) {
result.add(idCache.cache(from(parent, it)))
}
}
}
return result
} catch (e: IllegalArgumentException) {
if (e.message?.contains(FileNotFoundException::class.java.canonicalName!!) == true) {
throw NoSuchCloudFileException(name)
}
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
fun exists(node: LocalStorageAccessNode): Boolean {
node.parent?.let {
return try {
return listFilesWithNameFilter(it, node.name).getOrNull(0)?.also {
idCache.add(it)
} != null
} catch (e: NoSuchCloudFileException) {
false
}
} ?: throw ParentFolderIsNullException(node.name)
}
@Throws(BackendException::class)
fun list(folder: LocalStorageAccessFolder): List<LocalStorageAccessNode> {
val result: MutableList<LocalStorageAccessNode> = ArrayList()
contentResolver() //
.query( //
DocumentsContract.buildChildDocumentsUriUsingTree( //
folder.uri, //
folder.documentId
), arrayOf( //
DocumentsContract.Document.COLUMN_DISPLAY_NAME, // cursor position 0
DocumentsContract.Document.COLUMN_MIME_TYPE, // cursor position 1
DocumentsContract.Document.COLUMN_SIZE, // cursor position 2
DocumentsContract.Document.COLUMN_LAST_MODIFIED, // cursor position 3
DocumentsContract.Document.COLUMN_DOCUMENT_ID // cursor position 4
), null, null, null
)?.use {
while (it.moveToNext()) {
result.add(idCache.cache(from(folder, it)))
}
}
return result
}
@Throws(BackendException::class)
fun create(folder: LocalStorageAccessFolder): LocalStorageAccessFolder {
var folder = folder
folder.parent?.let { foldersParent ->
if (foldersParent.documentId == null) {
folder = LocalStorageAccessFolder( //
create(foldersParent),
folder.name, //
folder.path, //
null, //
null
)
}
} ?: throw ParentFolderIsNullException(folder.name)
folder.parent?.let { foldersParent ->
foldersParent.uri?.let { foldersParentUri ->
val createdDocument = try {
DocumentsContract.createDocument( //
contentResolver(), //
foldersParentUri,
DocumentsContract.Document.MIME_TYPE_DIR, //
folder.name
)
} catch (e: FileNotFoundException) {
throw NoSuchCloudFileException(folder.name)
} ?: throw FatalBackendException("Failed to create document for unknown reason")
return idCache.cache(folder(foldersParent, buildDocumentFile(createdDocument)))
} ?: throw FatalBackendException("FoldersParentsUri shouldn't be null")
} ?: throw ParentFolderIsNullException(folder.name)
}
@Throws(BackendException::class)
fun move(source: LocalStorageAccessNode, target: LocalStorageAccessNode): LocalStorageAccessNode {
source.parent?.let { sourcesParent ->
if (exists(target)) {
throw CloudNodeAlreadyExistsException(target.name)
}
idCache.remove(source)
idCache.remove(target)
val isRename = source.name != target.name
val isMove = sourcesParent != target.parent
var renamedSource = source
if (isRename) {
renamedSource = rename(source, target.name)
}
return if (isMove) {
idCache.cache(internalMove(renamedSource, target))
} else renamedSource
} ?: throw ParentFolderIsNullException(source.name)
}
@Throws(NoSuchCloudFileException::class)
private fun rename(source: LocalStorageAccessNode, name: String): LocalStorageAccessNode {
source.parent?.let { parent ->
var newUri = try {
DocumentsContract.renameDocument(contentResolver(), source.uri, name)
} catch (e: FileNotFoundException) {
/* Bug in Android 9 see #460 TLDR; In this renameDocument-method, Android 9 throws
a `FileNotFoundException` although the file exists and is also renamed. */
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.P) {
throw NoSuchCloudFileException(source.name)
}
null
}
/* Bug in Android 9 see #460 TLDR; In this renameDocument-method, Android 9 throws
a `FileNotFoundException` although the file exists and is also renamed. */
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
newUri = try {
listFilesWithNameFilter(parent, name).getOrNull(0)?.uri
} catch (e: BackendException) {
throw FatalBackendException("Failed to list file while move of ${source.name}", e)
} ?: throw FatalBackendException("Failed to list file while move of ${source.name} for unkown reason")
}
requireNotNull(newUri)
return from(parent, buildDocumentFile(newUri))
} ?: throw ParentFolderIsNullException(source.name)
}
@Throws(NoSuchCloudFileException::class)
private fun internalMove(source: LocalStorageAccessNode, target: LocalStorageAccessNode): LocalStorageAccessNode {
source.uri?.let { sourceUri ->
source.parent?.uri?.let { sourcesParentUri ->
target.parent?.let { targetsParent ->
target.parent?.uri?.let { targetsParentUri ->
val movedTargetUri = try {
DocumentsContract.moveDocument(contentResolver(), sourceUri, sourcesParentUri, targetsParentUri)
} catch (e: FileNotFoundException) {
throw NoSuchCloudFileException(source.name)
} ?: throw FatalBackendException("Move failed for unknown reason")
return from(targetsParent, buildDocumentFile(movedTargetUri))
} ?: throw FatalBackendException("Target parents uri shouldn't be null")
} ?: throw FatalBackendException("Targets parent shouldn't be null")
} ?: throw FatalBackendException("Source parents uri shouldn't be null")
} ?: throw FatalBackendException("Source uri shouldn't be null")
}
@Throws(IOException::class, BackendException::class)
fun write(file: LocalStorageAccessFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): LocalStorageAccessFile {
var file = file
progressAware.onProgress(Progress.started(UploadState.upload(file)))
val fileUri = existingFileUri(file)
if (!replace && fileUri != null) {
throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false")
}
if (file.parent.uri == null) {
file.parent.parent?.let {
val parent = listFilesWithNameFilter(it, file.parent.name)[0] as LocalStorageAccessFolder
val tmpFileUri = fileUri?.toString() ?: ""
file = LocalStorageAccessFile(parent, file.name, file.path, file.size, file.modified, file.documentId, tmpFileUri)
} ?: throw ParentFolderIsNullException(file.parent.name)
}
val tmpFile = file
val uploadUri: Uri = (fileUri ?: createNewDocumentSupplier(tmpFile).get()) ?: throw NotFoundException(tmpFile.name)
data.open(context)?.use { inputStream ->
contentResolver().openOutputStream(uploadUri)?.use { out ->
object : TransferredBytesAwareInputStream(inputStream) {
override fun bytesTransferred(transferred: Long) {
progressAware //
.onProgress(
Progress.progress(UploadState.upload(tmpFile)) //
.between(0) //
.and(size) //
.withValue(transferred)
)
}
}.use { inputStream ->
if (out is FileOutputStream) {
out.channel.truncate(0)
}
CopyStream.copyStreamToStream(inputStream, out)
}
} ?: throw FatalBackendException("OutputStream shouldn't bee null")
} ?: throw FatalBackendException("InputStream shouldn't bee null")
progressAware.onProgress(Progress.completed(UploadState.upload(file)))
return file(file.parent, buildDocumentFile(uploadUri))
}
private fun createNewDocumentSupplier(file: LocalStorageAccessFile): Supplier<Uri?> {
return Supplier {
val mimeType = if (mimeTypes.fromFilename(file.name) == null) MimeType.APPLICATION_OCTET_STREAM else mimeTypes.fromFilename(file.name)
try {
DocumentsContract.createDocument(contentResolver(), file.parent.uri, mimeType.toString(), file.name) // FIXME
} catch (e: FileNotFoundException) {
null
}
}
}
@Throws(BackendException::class)
private fun existingFileUri(file: LocalStorageAccessFile): Uri? {
return listFilesWithNameFilter(file.parent, file.name).getOrNull(0)?.uri
}
@Throws(IOException::class)
fun read(file: LocalStorageAccessFile, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
progressAware.onProgress(Progress.started(DownloadState.download(file)))
contentResolver().openInputStream(file.uri)?.use { inputStream ->
object : TransferredBytesAwareOutputStream(data) {
override fun bytesTransferred(transferred: Long) {
progressAware.onProgress(
Progress.progress(DownloadState.download(file)) //
.between(0) //
.and(file.size ?: Long.MAX_VALUE) //
.withValue(transferred)
)
}
}.use { out -> CopyStream.copyStreamToStream(inputStream, out) }
} ?: throw FatalBackendException("InputStream shouldn't bee null")
progressAware.onProgress(Progress.completed(DownloadState.download(file)))
}
@Throws(NoSuchCloudFileException::class)
fun delete(node: LocalStorageAccessNode) {
requireNotNull(node.uri)
try {
DocumentsContract.deleteDocument(contentResolver(), node.uri)
} catch (e: FileNotFoundException) {
throw NoSuchCloudFileException(node.name)
}
idCache.remove(node)
}
private fun buildDocumentFile(fileUri: Uri): DocumentFile {
// can only be zero on devices with pre-Kitkat, which is excluded by the minSDK
return DocumentFile.fromSingleUri(context, fileUri)!!
}
private fun contentResolver(): ContentResolver {
return context.contentResolver
}
init {
if (!hasUriPermissions(context, cloud.rootUri())) {
throw NoAuthenticationProvidedException(cloud)
}
this.context = context
this.root = RootLocalStorageAccessFolder(cloud)
idCache = documentIdCache
}
}

View File

@ -1,124 +0,0 @@
package org.cryptomator.data.cloud.local.storageaccessframework;
import android.database.Cursor;
import android.os.Build;
import android.provider.DocumentsContract;
import androidx.annotation.RequiresApi;
import androidx.documentfile.provider.DocumentFile;
import org.cryptomator.util.Optional;
import java.util.Date;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class LocalStorageAccessFrameworkNodeFactory {
public static LocalStorageAccessNode from(LocalStorageAccessFolder parent, Cursor cursor) {
if (isFolder(cursor)) {
return folder(parent, cursor);
} else {
return file(parent, cursor);
}
}
private static LocalStorageAccessFile file(LocalStorageAccessFolder parent, Cursor cursor) {
return new LocalStorageAccessFile( //
parent, //
cursor.getString(0), //
getNodePath(parent, cursor.getString(0)), //
Optional.of(cursor.getLong(2)), //
Optional.of(new Date(cursor.getLong(3))), //
cursor.getString(4), //
getDocumentUri(parent, cursor.getString(4)));
}
private static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, Cursor cursor) {
return new LocalStorageAccessFolder(parent, //
cursor.getString(0), //
getNodePath(parent, cursor.getString(0)), //
cursor.getString(4), //
getDocumentUri(parent, cursor.getString(4)));
}
public static LocalStorageAccessNode from(LocalStorageAccessFolder parent, DocumentFile documentFile) {
if (isFolder(documentFile)) {
return folder(parent, documentFile);
} else {
return file(parent, documentFile);
}
}
public static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, DocumentFile directory) {
return new LocalStorageAccessFolder(parent, //
directory.getName(), //
getNodePath(parent, directory.getName()), //
DocumentsContract.getDocumentId(directory.getUri()), //
directory.getUri().toString());
}
public static LocalStorageAccessFile file(LocalStorageAccessFolder parent, DocumentFile documentFile) {
return new LocalStorageAccessFile( //
parent, //
documentFile.getName(), //
getNodePath(parent, documentFile.getName()), //
Optional.of(documentFile.length()), //
Optional.of(new Date(documentFile.lastModified())), //
DocumentsContract.getTreeDocumentId(documentFile.getUri()), //
documentFile.getUri().toString());
}
public static LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, Optional<Long> size) {
return new LocalStorageAccessFile(//
parent, //
name, //
getNodePath(parent, name), //
size, //
Optional.empty(), //
null, //
null);
}
public static LocalStorageAccessFile file(LocalStorageAccessFolder parent, String name, String path, Optional<Long> size, String documentId) {
return new LocalStorageAccessFile(parent, //
name, //
path, //
size, //
Optional.empty(), //
documentId, //
getDocumentUri(parent, documentId));
}
public static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name) {
return new LocalStorageAccessFolder(parent, //
name, //
getNodePath(parent, name), //
null, //
null);
}
public static LocalStorageAccessFolder folder(LocalStorageAccessFolder parent, String name, String documentId) {
return new LocalStorageAccessFolder(parent, //
name, //
getNodePath(parent, name), //
documentId, //
getDocumentUri(parent, documentId));
}
private static String getDocumentUri(LocalStorageAccessFolder parent, String documentId) {
return DocumentsContract.buildDocumentUriUsingTree(parent.getUri(), documentId).toString();
}
private static boolean isFolder(DocumentFile file) {
return file.isDirectory();
}
private static boolean isFolder(Cursor cursor) {
return cursor.getString(1).equals(DocumentsContract.Document.MIME_TYPE_DIR);
}
public static String getNodePath(LocalStorageAccessFolder parent, String name) {
return parent.getPath() + "/" + name;
}
}

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