From aa1cedff58d4f4e159c7145e07f1869d0bc5e83b Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Sat, 24 Oct 2020 10:56:25 +0300 Subject: [PATCH 01/59] [Snyk] Security upgrade com.google.guava:guava from 29.0-android to 30.0-jre (#17) * fix: pom.xml to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JAVA-COMGOOGLEGUAVA-1015415 * Update pom.xml Co-authored-by: Sebastian Stenzel --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fe138fa..dcc4859 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ 2.8.6 - 29.0-android + 30.0-jre 1.4.0 1.7.30 UTF-8 From f83bba9ee2d7f89a85e8e26ae00f790588765b14 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 26 Nov 2020 08:55:46 +0100 Subject: [PATCH 02/59] updated dependencies, increase java language level to 8 and moved from travis to github actions --- .github/workflows/build.yml | 56 +++++++++++++++++++ .gitignore | 26 ++++----- .idea/.name | 1 + .idea/misc.xml | 11 ++++ .idea/vcs.xml | 6 +++ .travis.yml | 39 -------------- pom.xml | 105 +++++++++++++++++++++++------------- 7 files changed, 152 insertions(+), 92 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .idea/.name create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml delete mode 100644 .travis.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7734712 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,56 @@ +name: Build + +on: + [push] + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" + env: + BUILD_VERSION: SNAPSHOT + outputs: + artifact-version: ${{ steps.setversion.outputs.version }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 11 + server-id: bintray-jcenter + server-username: BINTRAY_USERNAME + server-password: BINTRAY_API_KEY + - uses: actions/cache@v1 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Ensure to use tagged version + run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} + if: startsWith(github.ref, 'refs/tags/') + - name: Export the project version to the job environment and fix it as an ouput of this job + id: setversion + run: | + v=$(mvn help:evaluate "-Dexpression=project.version" -q -DforceStdout) + echo "BUILD_VERSION=${v}" >> $GITHUB_ENV + echo "::set-output name=version::${v}" + - name: Build and Test + run: mvn -B clean install jacoco:report -Pcoverage,dependency-check + - name: Upload code coverage report + id: codacyCoverageReporter + run: bash <(curl -Ls https://coverage.codacy.com/get.sh) + env: + CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} + continue-on-error: true + - name: Upload snapshot artifact cryptolib-${{ env.BUILD_VERSION }}.jar + uses: actions/upload-artifact@v2 + with: + name: cryptolib-${{ env.BUILD_VERSION }} + path: target/cryptolib-${{ env.BUILD_VERSION }}.jar + - name: Build and deploy to jcenter + if: startsWith(github.ref, 'refs/tags/') + run: mvn -B deploy -DskipTests + env: + BINTRAY_USERNAME: cryptobot + BINTRAY_API_KEY: ${{ secrets.BINTRAY_API_KEY }} diff --git a/.gitignore b/.gitignore index b22a255..0f591ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,18 @@ *.class - -# Package Files # *.jar -*.war -*.ear - -# Eclipse Settings Files # -.settings -.project -.classpath -target/ -test-output/ # Maven # target/ pom.xml.versionsBackup -# IntelliJ Settings Files # -.idea/ -out/ -.idea_modules/ +# IntelliJ Settings Files (https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems) # +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries +.idea/**/libraries/ +.idea/jarRepositories.xml +.idea/encodings.xml +.idea/modules.xml +.idea/compiler.xml +.idea/inspectionProfiles/ *.iml -*.iws diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..84a26aa --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +cryptolib \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4d8efc6 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 65c1026..0000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -dist: bionic -language: java -sudo: false -jdk: -- openjdk11 -env: - global: - - secure: "I/yCjGHCtNMBezO2ylpKKXU1m/deYj3l7yBx/epXVFv9pPixzgMUaGaOxJrz3s82ZuigwYgcRUDgjx0lmKJCKJCM1IGESjxfDvJPvY+yAbQz1PUd9O+buHtNU1nZRHGrPSSlv9aeCzYCcOtcP3GPlrVAGXalOnpTNeA8tfjjFXRYRSFgybP9ENVyRuVdI4gL3wfJV1OEXQ+87WsgTSmaadQymJilphGRdbTgxP6rilZVc7OJOgFmfyE38oJObSUDo7rzU4pPYHQI8DPGOo6Q9il0HEcJ6Zrz77JLTbrjcVhe4jK1MV57AdB15YGpw8gWa6Mzr2sEKwDw+iJ5ONGgCgwJM4OrJI3swdZtDbttV2GGnMot8gVn/l9Et6SMn7s+GoLRdfltie2SWZy/Zej2VnAxSUQ9Ty2FlrpExPSPgUAkDpgWEe+HYh/wMCsOQyX0dPQdsyOeYMJHyHqooR0IkHq2JIhfxb90x3mplRCAi0CXT2MXHAVh1ymNcI56BIY6y1NWsToAgElFGGAh6ntqND82+SnEqZYs7eA0RQsYfd8E7utHVPeEi14sen/j+hV8pnkUwju5n/3oqaPoecuuR7vXAI6pm2m9XmZoxeDC5REmGNnLWc6AxkHzh6LNemni+1RCvNmX2ypQmy0WUt1iovxi7yXc+9gdpEWIbgQu5do=" # CODACY_PROJECT_TOKEN - - secure: "J2u0PFVLT9pOUUoyNXHfCXWji33iJZlt9UwUzDA9B3zypHyiRfpMYvaZsHgqWF/VjrGqzRWIC3YgM3b0FbLb8GsYLFQ4ltELCCvy/dC/dZ2PwuXbAi4gK55csBOf3EvIE3SOfFqfM+ljpZIL88bjlcch+8uksCuoGxaMlQP0gpaeT+WEWZZG+FUml7ts7TVZMPANy/6uVOvtGrCFD42p56AVFNDDlyyIU5PdLkWLVG0Gd1WbOUSTzloq2pCb5fdcH5ib3bf91poKYkWKWnQTpvF5PoAbpGf6cJ7/Gk49bnsCRjF91T0yZ/ZFlT31S0IjnFKHdTzClAY87KfPJBdtkgrL2ejJRYIdE8qjID8a3W+KBY9iHDU9qwxIBLTfsCosYHOUNeUgBEKQLB0NFmsysSX/M3KQLpZYLd+A1e69v8PSEUowYXDJXqF3t6PEmq9RgPd3zpi5SfpSuZupq24sJoME49Ul4qpJk6bVL8BSYz6osZdCrCdSykmGM1C9+nf2WBmYOwi5a/+Mr1apmA+KEQpDoBSP2ztt8KkUM6px5CHjqwdqD09QD0d+4k+XHEWfj5RNnMaESGAgEAX/cUhJ+oqGF+9MdUBvGZ1f/f2mndMPmVDIenhmmSBrqby+7f3elH0oFKmUwQbiluaNMHDJTBuxzoZS4n6IDZNuaRrLP7E=" # BINTRAY_API_KEY -install: -- curl -o $HOME/.m2/settings.xml https://gist.githubusercontent.com/cryptobot/cf5fbd909c4782aaeeeb7c7f4a1a43da/raw/e60ee486e34ee0c79f89f947abe2c83b4290c6bb/settings.xml -- mvn source:help javadoc:help dependency:go-offline -Pdependency-check,coverage,release -before_script: -- mvn --update-snapshots dependency-check:check -Pdependency-check -- if [ -n "$TRAVIS_TAG" ]; then mvn versions:set -DnewVersion=${TRAVIS_TAG}; fi -script: -- | - if [[ -n "$TRAVIS_TAG" ]]; then - mvn clean install jacoco:report verify -Pcoverage,release - elif [[ "$TRAVIS_BRANCH" =~ ^release/.* ]]; then - mvn clean install jacoco:report verify -Pcoverage,release - else - mvn clean test jacoco:report verify -Pcoverage - fi -after_success: -- curl -o ~/codacy-coverage-reporter.jar https://oss.sonatype.org/service/local/repositories/releases/content/com/codacy/codacy-coverage-reporter/6.0.7/codacy-coverage-reporter-6.0.7-assembly.jar -- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r target/site/jacoco/jacoco.xml -cache: - directories: - - $HOME/.m2 -deploy: -- provider: script - script: mvn deploy -Dmaven.install.skip=true -Dmaven.test.skip=true -Prelease - skip_cleanup: true - on: - repo: cryptomator/cryptolib - tags: true -after_deploy: - - "echo '{\"close\": \"1\"}' | curl --max-time 10 -X POST -u cryptobot:${BINTRAY_API_KEY} https://api.bintray.com/maven_central_sync/cryptomator/maven/cryptolib/versions/${TRAVIS_TAG}" \ No newline at end of file diff --git a/pom.xml b/pom.xml index dcc4859..bf2e14a 100644 --- a/pom.xml +++ b/pom.xml @@ -14,11 +14,19 @@ + UTF-8 + + 2.8.6 30.0-jre 1.4.0 1.7.30 - UTF-8 + + + 5.7.0 + 3.6.0 + 2.2 + 1.26 @@ -76,19 +84,19 @@ org.junit.jupiter junit-jupiter - 5.7.0 + ${junit.jupiter.version} test org.mockito mockito-core - 3.5.11 + ${mockito.version} test org.hamcrest hamcrest - 2.2 + ${hamcrest.version} test @@ -100,27 +108,45 @@ org.openjdk.jmh jmh-core - 1.25.2 + ${jmh.version} test org.openjdk.jmh jmh-generator-annprocess - 1.25.2 + ${jmh.version} test + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + enforce-java + + enforce + + + + + You need at least JDK 11.0.3 to build this project. + [11.0.3,) + + + + + + maven-compiler-plugin 3.8.1 - 7 - 1.7 - 1.7 - 8 + 8 UTF-8 true @@ -146,31 +172,6 @@ - - intellij - - - false - - idea.maven.embedder.version - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - 8 - 1.8 - 1.8 - - - - - - dependency-check @@ -178,7 +179,7 @@ org.owasp dependency-check-maven - 6.0.1 + 6.0.3 24 0 @@ -213,6 +214,12 @@ prepare-agent + + report + + report + + @@ -253,7 +260,31 @@ - 1.7 + + + + apiNote + a + API Note: + + + implSpec + a + Implementation Requirements: + + + implNote + a + Implementation Note: + + param + return + throws + since + version + serialData + see + From 14d7c57b91484ddd70efd10d6870d3a8e6a1d230 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 26 Nov 2020 09:02:07 +0100 Subject: [PATCH 03/59] Create codacy-analysis.yml --- .github/workflows/codacy-analysis.yml | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/codacy-analysis.yml diff --git a/.github/workflows/codacy-analysis.yml b/.github/workflows/codacy-analysis.yml new file mode 100644 index 0000000..2bc4fba --- /dev/null +++ b/.github/workflows/codacy-analysis.yml @@ -0,0 +1,46 @@ +# This workflow checks out code, performs a Codacy security scan +# and integrates the results with the +# GitHub Advanced Security code scanning feature. For more information on +# the Codacy security scan action usage and parameters, see +# https://github.com/codacy/codacy-analysis-cli-action. +# For more information on Codacy Analysis CLI in general, see +# https://github.com/codacy/codacy-analysis-cli. + +name: Codacy Security Scan + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + +jobs: + codacy-security-scan: + name: Codacy Security Scan + runs-on: ubuntu-latest + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout code + uses: actions/checkout@v2 + + # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis + - name: Run Codacy Analysis CLI + uses: codacy/codacy-analysis-cli-action@1.1.0 + with: + # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository + # You can also omit the token and run the tools that support default configurations + project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} + verbose: true + output: results.sarif + format: sarif + # Adjust severity of non-security issues + gh-code-scanning-compat: true + # Force 0 exit code to allow SARIF file generation + # This will handover control about PR rejection to the GitHub side + max-allowed-issues: 2147483647 + + # Upload the SARIF file generated in the previous step + - name: Upload SARIF results file + uses: github/codeql-action/upload-sarif@v1 + with: + sarif_file: results.sarif From dc2b2b8f4cdcd2ce76ec607a5667f36e117ec40f Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 26 Nov 2020 09:19:20 +0100 Subject: [PATCH 04/59] Revert "Create codacy-analysis.yml" This reverts commit 14d7c57b91484ddd70efd10d6870d3a8e6a1d230. --- .github/workflows/codacy-analysis.yml | 46 --------------------------- 1 file changed, 46 deletions(-) delete mode 100644 .github/workflows/codacy-analysis.yml diff --git a/.github/workflows/codacy-analysis.yml b/.github/workflows/codacy-analysis.yml deleted file mode 100644 index 2bc4fba..0000000 --- a/.github/workflows/codacy-analysis.yml +++ /dev/null @@ -1,46 +0,0 @@ -# This workflow checks out code, performs a Codacy security scan -# and integrates the results with the -# GitHub Advanced Security code scanning feature. For more information on -# the Codacy security scan action usage and parameters, see -# https://github.com/codacy/codacy-analysis-cli-action. -# For more information on Codacy Analysis CLI in general, see -# https://github.com/codacy/codacy-analysis-cli. - -name: Codacy Security Scan - -on: - push: - branches: [ develop ] - pull_request: - branches: [ develop ] - -jobs: - codacy-security-scan: - name: Codacy Security Scan - runs-on: ubuntu-latest - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout code - uses: actions/checkout@v2 - - # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis - - name: Run Codacy Analysis CLI - uses: codacy/codacy-analysis-cli-action@1.1.0 - with: - # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository - # You can also omit the token and run the tools that support default configurations - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - verbose: true - output: results.sarif - format: sarif - # Adjust severity of non-security issues - gh-code-scanning-compat: true - # Force 0 exit code to allow SARIF file generation - # This will handover control about PR rejection to the GitHub side - max-allowed-issues: 2147483647 - - # Upload the SARIF file generated in the previous step - - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@v1 - with: - sarif_file: results.sarif From 388953079286b99af1e84dd27377e10fa03a4077 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 27 Nov 2020 20:26:32 +0100 Subject: [PATCH 05/59] Added new Masterkey class to separate key derivation and encryption --- .../org/cryptomator/cryptolib/Cryptors.java | 11 +- .../DecryptingReadableByteChannel.java | 21 +- .../cryptolib/api/CryptoException.java | 2 +- .../cryptomator/cryptolib/api/Cryptor.java | 4 +- .../cryptomator/cryptolib/api/KeyFile.java | 1 + .../cryptomator/cryptolib/api/Masterkey.java | 88 ++++++++ .../cryptolib/api/MasterkeyLoader.java | 20 ++ .../api/MasterkeyLoadingFailedException.java | 13 ++ .../cryptolib/common/Destroyables.java | 19 ++ .../cryptolib/common/MasterkeyFile.java | 200 ++++++++++++++++++ .../cryptolib/common/MasterkeyFileLoader.java | 51 +++++ .../cryptolib/v1/CryptorProviderImpl.java | 26 ++- .../cryptolib/v2/FileContentCryptorImpl.java | 3 +- .../cryptolib/CryptorIntegrationTest.java | 8 +- .../DecryptingReadableByteChannelTest.java | 7 +- .../cryptolib/common/DestroyablesTest.java | 35 +++ .../common/MasterkeyFileLoaderTest.java | 33 +++ .../cryptolib/common/MasterkeyFileTest.java | 55 +++++ .../cryptolib/common/MasterkeyTest.java | 100 +++++++++ .../cryptolib/v1/CryptorProviderImplTest.java | 4 +- .../v1/FileContentCryptorImplTest.java | 15 +- .../v1/FileContentEncryptorTest.java | 5 +- .../v1/FileHeaderCryptorBenchmark.java | 3 +- .../v1/FileHeaderCryptorImplTest.java | 2 +- .../cryptolib/v1/FileNameCryptorImplTest.java | 8 +- .../cryptolib/v2/CryptorProviderImplTest.java | 4 +- .../v2/FileContentCryptorImplBenchmark.java | 3 +- .../v2/FileContentCryptorImplTest.java | 4 +- .../v2/FileHeaderCryptorBenchmark.java | 3 +- .../v2/FileHeaderCryptorImplTest.java | 2 +- .../cryptolib/v2/FileNameCryptorImplTest.java | 4 +- .../org.mockito.plugins.MockMaker | 1 + 32 files changed, 698 insertions(+), 57 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptolib/api/Masterkey.java create mode 100644 src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java create mode 100644 src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoadingFailedException.java create mode 100644 src/main/java/org/cryptomator/cryptolib/common/Destroyables.java create mode 100644 src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java create mode 100644 src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java create mode 100644 src/test/java/org/cryptomator/cryptolib/common/DestroyablesTest.java create mode 100644 src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderTest.java create mode 100644 src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java create mode 100644 src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java create mode 100644 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/src/main/java/org/cryptomator/cryptolib/Cryptors.java b/src/main/java/org/cryptomator/cryptolib/Cryptors.java index df10392..c094758 100644 --- a/src/main/java/org/cryptomator/cryptolib/Cryptors.java +++ b/src/main/java/org/cryptomator/cryptolib/Cryptors.java @@ -15,6 +15,7 @@ import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.KeyFile; +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; import org.cryptomator.cryptolib.common.ReseedingSecureRandom; import static com.google.common.base.Preconditions.checkArgument; @@ -93,7 +94,8 @@ public static long ciphertextSize(long cleartextSize, Cryptor cryptor) { * @see #changePassphrase(CryptorProvider, byte[], byte[], CharSequence, CharSequence) * @since 1.1.0 */ - public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] masterkey, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException { + @Deprecated + public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] masterkey, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException { return changePassphrase(cryptorProvider, masterkey, new byte[0], oldPassphrase, newPassphrase); } @@ -109,7 +111,8 @@ public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] ma * @throws InvalidPassphraseException If the wrong oldPassphrase has been supplied for the masterkey * @since 1.1.4 */ - public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] masterkey, byte[] pepper, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException { + @Deprecated + public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] masterkey, byte[] pepper, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException { final KeyFile keyFile = KeyFile.parse(masterkey); try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, oldPassphrase, pepper, keyFile.getVersion())) { return cryptor.writeKeysToMasterkeyFile(newPassphrase, pepper, keyFile.getVersion()).serialize(); @@ -127,7 +130,8 @@ public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] ma * @throws InvalidPassphraseException If the wrong passphrase has been supplied for the masterkey * @since 1.3.0 */ - public static byte[] exportRawKey(CryptorProvider cryptorProvider, byte[] masterkey, byte[] pepper, CharSequence passphrase) { + @Deprecated + public static byte[] exportRawKey(CryptorProvider cryptorProvider, byte[] masterkey, byte[] pepper, CharSequence passphrase) throws UnsupportedVaultFormatException, InvalidPassphraseException { final KeyFile keyFile = KeyFile.parse(masterkey); try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, pepper, keyFile.getVersion())) { return cryptor.getRawKey(); @@ -145,6 +149,7 @@ public static byte[] exportRawKey(CryptorProvider cryptorProvider, byte[] master * @return The json-encoded masterkey protected by the passphrase * @since 1.3.0 */ + @Deprecated public static byte[] restoreRawKey(CryptorProvider cryptorProvider, byte[] rawKey, byte[] pepper, CharSequence passphrase, int vaultVersion) { try (Cryptor cryptor = cryptorProvider.createFromRawKey(rawKey)) { return cryptor.writeKeysToMasterkeyFile(passphrase, pepper, vaultVersion).serialize(); diff --git a/src/main/java/org/cryptomator/cryptolib/DecryptingReadableByteChannel.java b/src/main/java/org/cryptomator/cryptolib/DecryptingReadableByteChannel.java index 4810a41..8087e52 100644 --- a/src/main/java/org/cryptomator/cryptolib/DecryptingReadableByteChannel.java +++ b/src/main/java/org/cryptomator/cryptolib/DecryptingReadableByteChannel.java @@ -13,6 +13,7 @@ import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.ByteBuffers; @@ -69,15 +70,19 @@ public void close() throws IOException { @Override public synchronized int read(ByteBuffer dst) throws IOException { - loadHeaderIfNecessary(); - if (reachedEof) { - return -1; - } else { - return readInternal(dst); + try { + loadHeaderIfNecessary(); + if (reachedEof) { + return -1; + } else { + return readInternal(dst); + } + } catch (AuthenticationFailedException e) { + throw new IOException("Unauthentic ciphertext", e); } } - private int readInternal(ByteBuffer dst) throws IOException { + private int readInternal(ByteBuffer dst) throws IOException, AuthenticationFailedException { assert header != null : "header must be initialized"; int result = 0; @@ -91,7 +96,7 @@ private int readInternal(ByteBuffer dst) throws IOException { return result; } - private void loadHeaderIfNecessary() throws IOException { + private void loadHeaderIfNecessary() throws IOException, AuthenticationFailedException { if (header == null) { ByteBuffer headerBuf = ByteBuffer.allocate(cryptor.fileHeaderCryptor().headerSize()); int read = ByteBuffers.fill(delegate, headerBuf); @@ -103,7 +108,7 @@ private void loadHeaderIfNecessary() throws IOException { } } - private boolean loadNextCleartextChunk() throws IOException { + private boolean loadNextCleartextChunk() throws IOException, AuthenticationFailedException { ByteBuffer ciphertextChunk = ByteBuffer.allocate(cryptor.fileContentCryptor().ciphertextChunkSize()); int read = ByteBuffers.fill(delegate, ciphertextChunk); if (read == 0) { diff --git a/src/main/java/org/cryptomator/cryptolib/api/CryptoException.java b/src/main/java/org/cryptomator/cryptolib/api/CryptoException.java index fe1958a..0a5f034 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/CryptoException.java +++ b/src/main/java/org/cryptomator/cryptolib/api/CryptoException.java @@ -8,7 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptolib.api; -public abstract class CryptoException extends RuntimeException { +public abstract class CryptoException extends Exception { protected CryptoException() { super(); diff --git a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java index 3673a6f..0a20271 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java @@ -50,6 +50,8 @@ public interface Cryptor extends Destroyable, AutoCloseable { * Calls {@link #destroy()}. */ @Override - void close(); + default void close() { + destroy(); + } } diff --git a/src/main/java/org/cryptomator/cryptolib/api/KeyFile.java b/src/main/java/org/cryptomator/cryptolib/api/KeyFile.java index 518cdfb..db4bc2a 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/KeyFile.java +++ b/src/main/java/org/cryptomator/cryptolib/api/KeyFile.java @@ -38,6 +38,7 @@ * Each version might have its own package-private subclass of this file, which adds further properties. * These properties must be annotated with {@link Expose} in order to be considered by {@link #serialize()}. */ +@Deprecated public abstract class KeyFile { private static final Charset UTF_8 = Charset.forName("UTF-8"); diff --git a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java new file mode 100644 index 0000000..c71b950 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java @@ -0,0 +1,88 @@ +package org.cryptomator.cryptolib.api; + +import org.cryptomator.cryptolib.common.Destroyables; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + +public class Masterkey implements AutoCloseable, SecretKey { + + public static final String ENC_ALG = "AES"; + public static final String MAC_ALG = "HmacSHA256"; + public static final int KEY_LEN_BYTES = 32; + + private final SecretKey encKey; + private final SecretKey macKey; + + public Masterkey(SecretKey encKey, SecretKey macKey) { + this.encKey = encKey; + this.macKey = macKey; + } + + public static Masterkey createNew(SecureRandom random) { + try { + KeyGenerator encKeyGen = KeyGenerator.getInstance(ENC_ALG); + encKeyGen.init(KEY_LEN_BYTES * Byte.SIZE, random); + SecretKey encKey = encKeyGen.generateKey(); + KeyGenerator macKeyGen = KeyGenerator.getInstance(MAC_ALG); + macKeyGen.init(KEY_LEN_BYTES * Byte.SIZE, random); + SecretKey macKey = macKeyGen.generateKey(); + return new Masterkey(encKey, macKey); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e); + } + } + + public SecretKey getEncKey() { + return encKey; + } + + public SecretKey getMacKey() { + return macKey; + } + + @Override + public String getAlgorithm() { + return "private"; + } + + @Override + public String getFormat() { + return "RAW"; + } + + @Override + public byte[] getEncoded() { + byte[] rawEncKey = encKey.getEncoded(); + byte[] rawMacKey = macKey.getEncoded(); + try { + byte[] rawKey = new byte[rawEncKey.length + rawMacKey.length]; + System.arraycopy(rawEncKey, 0, rawKey, 0, rawEncKey.length); + System.arraycopy(rawMacKey, 0, rawKey, rawEncKey.length, rawMacKey.length); + return rawKey; + } finally { + Arrays.fill(rawEncKey, (byte) 0x00); + Arrays.fill(rawMacKey, (byte) 0x00); + } + } + + @Override + public void close() { + destroy(); + } + + @Override + public boolean isDestroyed() { + return encKey.isDestroyed() && macKey.isDestroyed(); + } + + @Override + public void destroy() { + Destroyables.destroySilently(encKey); + Destroyables.destroySilently(macKey); + } + +} diff --git a/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java new file mode 100644 index 0000000..f2a0c2e --- /dev/null +++ b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java @@ -0,0 +1,20 @@ +package org.cryptomator.cryptolib.api; + +/** + * Masterkey loaders load keys to unlock Cryptomator vaults. + * + * @see org.cryptomator.cryptolib.common.MasterkeyFileLoader + */ +@FunctionalInterface +public interface MasterkeyLoader { + + /** + * Loads a master key. This might be a long-running operation, as it may require user input or expensive computations. + * + * @param keyId a string uniquely identifying the source of the key and its identity, if multiple keys can be obtained from the same source + * @return The raw key bytes. Must not be null + * @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request + */ + Masterkey loadKey(String keyId) throws MasterkeyLoadingFailedException; + +} diff --git a/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoadingFailedException.java b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoadingFailedException.java new file mode 100644 index 0000000..e1f1558 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoadingFailedException.java @@ -0,0 +1,13 @@ +package org.cryptomator.cryptolib.api; + +public class MasterkeyLoadingFailedException extends CryptoException { + + public MasterkeyLoadingFailedException(String message, Throwable cause) { + super(message, cause); + } + + public MasterkeyLoadingFailedException(String message) { + super(message); + } + +} diff --git a/src/main/java/org/cryptomator/cryptolib/common/Destroyables.java b/src/main/java/org/cryptomator/cryptolib/common/Destroyables.java new file mode 100644 index 0000000..163a00f --- /dev/null +++ b/src/main/java/org/cryptomator/cryptolib/common/Destroyables.java @@ -0,0 +1,19 @@ +package org.cryptomator.cryptolib.common; + +import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; + +public class Destroyables { + + public static void destroySilently(Destroyable destroyable) { + if (destroyable == null) { + return; + } + try { + destroyable.destroy(); + } catch (DestroyFailedException e) { + // no-op + } + } + +} diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java new file mode 100644 index 0000000..b0c9410 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java @@ -0,0 +1,200 @@ +package org.cryptomator.cryptolib.common; + +import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Optional; + +public class MasterkeyFile { + + private static final Gson GSON = new GsonBuilder() // + .setPrettyPrinting() // + .registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) // + .create(); + + private final Content content; + + private MasterkeyFile(Content content) { + Preconditions.checkArgument(content.isValid(), "Invalid content"); + this.content = content; + } + + public static MasterkeyFile withContentFromFile(Path path) throws IOException { + try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) { + return MasterkeyFile.withContent(in); + } + } + + public static MasterkeyFile withContent(InputStream in) throws IOException { + try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + Content content = GSON.fromJson(reader, Content.class); + return new MasterkeyFile(content); + } catch (JsonParseException e) { + throw new IOException("Unreadable JSON", e); + } catch (IllegalArgumentException e) { + throw new IOException("Invalid JSON content", e); + } + } + + public static byte[] lock(Masterkey masterkey, CharSequence passphrase, byte[] pepper, int vaultVersion) { + // TODO + return null; + } + +// +// public void changePw() { +// CharSequence oldPw = ""; +// CharSequence newPw = ""; +// try { +// save(load(oldPw).loadKey("asd"), newPw); +// } catch (KeyLoadingFailedException e) { +// e.printStackTrace(); +// } finally { +// +// } +// } + + public MasterkeyFileLoader unlock(CharSequence passphrase, byte[] pepper, Optional expectedVaultVersion) throws CryptoException { + boolean success = false; + SecretKey kek = null; + SecretKey encKey = null; + SecretKey macKey = null; + try { + // derive keys: + kek = scrypt(passphrase, pepper); + macKey = AesKeyWrap.unwrap(kek, content.macMasterKey, Masterkey.MAC_ALG); + encKey = AesKeyWrap.unwrap(kek, content.encMasterKey, Masterkey.ENC_ALG); + + // check MAC: + if (expectedVaultVersion.isPresent()) { + checkVaultVersion(content, macKey, expectedVaultVersion.get()); + } + + // construct key: + success = true; + return new MasterkeyFileLoader(encKey, macKey); + } catch (InvalidKeyException e) { + throw new InvalidPassphraseException(); + } finally { + Destroyables.destroySilently(kek); + if (!success) { + Destroyables.destroySilently(encKey); + Destroyables.destroySilently(macKey); + } + } + } + + private SecretKey scrypt(CharSequence passphrase, byte[] pepper) { + byte[] salt = content.scryptSalt; + byte[] saltAndPepper = new byte[salt.length + pepper.length]; + System.arraycopy(salt, 0, saltAndPepper, 0, salt.length); + System.arraycopy(pepper, 0, saltAndPepper, salt.length, pepper.length); + byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, content.scryptCostParam, content.scryptBlockSize, Masterkey.KEY_LEN_BYTES); + try { + return new SecretKeySpec(kekBytes, Masterkey.ENC_ALG); + } finally { + Arrays.fill(kekBytes, (byte) 0x00); + } + } + + private void checkVaultVersion(Content content, SecretKey macKey, int expectedVaultVersion) throws UnsupportedVaultFormatException { + Mac mac = MacSupplier.HMAC_SHA256.withKey(macKey); + byte[] expectedVaultVersionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(expectedVaultVersion).array()); + if (content.versionMac == null || !MessageDigest.isEqual(expectedVaultVersionMac, content.versionMac)) { + // attempted downgrade attack: versionMac doesn't match version. + throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, expectedVaultVersion); + } + } + + private static class Content { + + @SerializedName("version") + int version; + + @SerializedName("scryptSalt") + byte[] scryptSalt; + + @SerializedName("scryptCostParam") + int scryptCostParam; + + @SerializedName("scryptBlockSize") + int scryptBlockSize; + + @SerializedName("primaryMasterKey") + byte[] encMasterKey; + + @SerializedName("hmacMasterKey") + byte[] macMasterKey; + + @SerializedName("versionMac") + byte[] versionMac; + + /** + * Performs a very superficial validation of this object. + * + * @return true if not missing any values + */ + private boolean isValid() { + return version != 0 + && scryptSalt != null + && scryptCostParam > 1 + && scryptBlockSize > 0 + && encMasterKey != null + && macMasterKey != null + && versionMac != null; + } + + } + + private static class ByteArrayJsonAdapter extends TypeAdapter { + + private static final BaseEncoding BASE64 = BaseEncoding.base64(); + + @Override + public void write(JsonWriter writer, byte[] value) throws IOException { + if (value == null) { + writer.nullValue(); + } else { + writer.value(BASE64.encode(value)); + } + } + + @Override + public byte[] read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } else { + return BASE64.decode(reader.nextString()); + } + } + } + +} diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java new file mode 100644 index 0000000..627299c --- /dev/null +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java @@ -0,0 +1,51 @@ +package org.cryptomator.cryptolib.common; + +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.util.Optional; + +/** + * Instances of this class can be retrieved by {@link MasterkeyFile#unlock(CharSequence, byte[], Optional) unlocking} + * a Cryptomator masterkey file and then be used to {@link MasterkeyLoader#loadKey(String) load} a {@link Masterkey}. + * + *
+ * 	Masterkey masterkey;
+ * 	try (MasterkeyLoader loader = MasterkeyFile.withContent(in).unlock(pw, pepper, expectedVaultVersion)) {
+ * 		masterkey = loader.loadKey(MasterkeyFileLoader.KEY_ID);
+ * 	}
+ * 
+ */ +public class MasterkeyFileLoader implements MasterkeyLoader, AutoCloseable { + + public static final String KEY_ID = "MASTERKEY_FILE"; + private final SecretKey encKey; + private final SecretKey macKey; + + // intentionally package-private + MasterkeyFileLoader(SecretKey encKey, SecretKey macKey) { + this.encKey = encKey; + this.macKey = macKey; + } + + @Override + public Masterkey loadKey(String keyId) throws MasterkeyLoadingFailedException { + if (!KEY_ID.equals(keyId)) { + throw new MasterkeyLoadingFailedException("Unsupported key " + keyId); + } + // we need a copy to make sure we can use autocloseable destruction + SecretKey encKeyCopy = new SecretKeySpec(encKey.getEncoded(), encKey.getAlgorithm()); + SecretKey macKeyCopy = new SecretKeySpec(macKey.getEncoded(), macKey.getAlgorithm()); + return new Masterkey(encKeyCopy, macKeyCopy); + } + + @Override + public void close() { + Destroyables.destroySilently(encKey); + Destroyables.destroySilently(macKey); + } + +} diff --git a/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java index ee16e41..129d29c 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java @@ -8,21 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v1; -import java.nio.ByteBuffer; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Arrays; - -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - import com.google.common.base.Preconditions; -import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.KeyFile; @@ -33,6 +19,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + import static org.cryptomator.cryptolib.v1.Constants.ENC_ALG; import static org.cryptomator.cryptolib.v1.Constants.KEY_LEN_BYTES; import static org.cryptomator.cryptolib.v1.Constants.MAC_ALG; diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java index 466f298..0ea4b50 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java @@ -9,6 +9,7 @@ package org.cryptomator.cryptolib.v2; import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.FileContentCryptor; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.CipherSupplier; @@ -113,7 +114,7 @@ void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long ch } // visible for testing - void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long chunkNumber, byte[] headerNonce, SecretKey fileKey) { + void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long chunkNumber, byte[] headerNonce, SecretKey fileKey) throws AuthenticationFailedException { assert ciphertextChunk.remaining() >= GCM_NONCE_SIZE + GCM_TAG_SIZE; try { diff --git a/src/test/java/org/cryptomator/cryptolib/CryptorIntegrationTest.java b/src/test/java/org/cryptomator/cryptolib/CryptorIntegrationTest.java index c33ee18..3be186e 100644 --- a/src/test/java/org/cryptomator/cryptolib/CryptorIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptolib/CryptorIntegrationTest.java @@ -5,7 +5,9 @@ import org.cryptomator.cryptolib.api.FileContentCryptor; import org.cryptomator.cryptolib.api.FileHeaderCryptor; import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.KeyFile; +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -60,7 +62,7 @@ public void setup() { } @Test - public void changePassword() { + public void changePassword() throws UnsupportedVaultFormatException, InvalidPassphraseException { byte[] newMasterkey = Cryptors.changePassphrase(cryptorProvider, masterkey, "password", "betterPassw0rd!"); Assertions.assertFalse(Arrays.equals(masterkey, newMasterkey)); @@ -70,7 +72,7 @@ public void changePassword() { } @Test - public void testExportRawKey() { + public void testExportRawKey() throws UnsupportedVaultFormatException, InvalidPassphraseException { byte[] rawKey = Cryptors.exportRawKey(cryptorProvider, masterkey, pepper, passphrase); Assertions.assertNotNull(rawKey); } @@ -81,7 +83,7 @@ class WithExportedRawKey { byte[] rawKey; @BeforeEach - public void setup() { + public void setup() throws UnsupportedVaultFormatException, InvalidPassphraseException { rawKey = Cryptors.exportRawKey(cryptorProvider, masterkey, pepper, passphrase); } diff --git a/src/test/java/org/cryptomator/cryptolib/DecryptingReadableByteChannelTest.java b/src/test/java/org/cryptomator/cryptolib/DecryptingReadableByteChannelTest.java index d066c3c..f201762 100644 --- a/src/test/java/org/cryptomator/cryptolib/DecryptingReadableByteChannelTest.java +++ b/src/test/java/org/cryptomator/cryptolib/DecryptingReadableByteChannelTest.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptolib; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileContentCryptor; import org.cryptomator.cryptolib.api.FileHeader; @@ -36,7 +37,7 @@ public class DecryptingReadableByteChannelTest { private FileHeader header; @BeforeEach - public void setup() { + public void setup() throws AuthenticationFailedException { cryptor = Mockito.mock(Cryptor.class); contentCryptor = Mockito.mock(FileContentCryptor.class); headerCryptor = Mockito.mock(FileHeaderCryptor.class); @@ -55,7 +56,7 @@ public void setup() { } @Test - public void testDecryption() throws IOException { + public void testDecryption() throws IOException, AuthenticationFailedException { ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream("hhhhhTOPSECRET!TOPSECRET!".getBytes())); ByteBuffer result = ByteBuffer.allocate(30); try (DecryptingReadableByteChannel ch = new DecryptingReadableByteChannel(src, cryptor, true)) { @@ -70,7 +71,7 @@ public void testDecryption() throws IOException { } @Test - public void testRandomAccessDecryption() throws IOException { + public void testRandomAccessDecryption() throws IOException, AuthenticationFailedException { ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream("TOPSECRET!".getBytes())); ByteBuffer result = ByteBuffer.allocate(30); try (DecryptingReadableByteChannel ch = new DecryptingReadableByteChannel(src, cryptor, true, header, 1)) { diff --git a/src/test/java/org/cryptomator/cryptolib/common/DestroyablesTest.java b/src/test/java/org/cryptomator/cryptolib/common/DestroyablesTest.java new file mode 100644 index 0000000..d48c21d --- /dev/null +++ b/src/test/java/org/cryptomator/cryptolib/common/DestroyablesTest.java @@ -0,0 +1,35 @@ +package org.cryptomator.cryptolib.common; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; + +public class DestroyablesTest { + + @Test + public void testDestroySilently() throws DestroyFailedException { + Destroyable destroyable = Mockito.mock(Destroyable.class); + + Destroyables.destroySilently(destroyable); + + Mockito.verify(destroyable).destroy(); + } + + @Test + public void testDestroySilentlyIgnoresNull() { + Destroyables.destroySilently(null); + } + + @Test + public void testDestroySilentlySuppressesException() throws DestroyFailedException { + Destroyable destroyable = Mockito.mock(Destroyable.class); + Mockito.doThrow(new DestroyFailedException()).when(destroyable).destroy(); + + Destroyables.destroySilently(destroyable); + + Mockito.verify(destroyable).destroy(); + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderTest.java new file mode 100644 index 0000000..9e41ba0 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderTest.java @@ -0,0 +1,33 @@ +package org.cryptomator.cryptolib.common; + +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import javax.crypto.SecretKey; +import javax.security.auth.DestroyFailedException; + +public class MasterkeyFileLoaderTest { + + @Test + public void testLoadedKeySurvivesLoader() throws MasterkeyLoadingFailedException, DestroyFailedException { + SecretKey encKey = Mockito.mock(SecretKey.class); + SecretKey macKey = Mockito.mock(SecretKey.class); + Mockito.when(encKey.getEncoded()).thenReturn(new byte[32]); + Mockito.when(encKey.getAlgorithm()).thenReturn("AES"); + Mockito.when(macKey.getEncoded()).thenReturn(new byte[32]); + Mockito.when(macKey.getAlgorithm()).thenReturn("HmacSHA256"); + + Masterkey masterkey; + try (MasterkeyFileLoader loader = new MasterkeyFileLoader(encKey, macKey)) { + masterkey = loader.loadKey(MasterkeyFileLoader.KEY_ID); + } + + Mockito.verify(encKey).destroy(); + Mockito.verify(macKey).destroy(); + Assertions.assertFalse(masterkey.isDestroyed()); + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java new file mode 100644 index 0000000..2485e3b --- /dev/null +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java @@ -0,0 +1,55 @@ +package org.cryptomator.cryptolib.common; + +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Optional; + +public class MasterkeyFileTest { + + @Test + public void testParse() throws IOException { + final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; + + MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes())); + Assertions.assertNotNull(masterkeyFile); + } + + @Test + public void testParseInvalid() { + final String content = "{\"foo\": 42}"; + + Assertions.assertThrows(IOException.class, () -> { + MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes())); + }); + } + + @Test + public void testParseMalformed() { + final String content = "not even json"; + + Assertions.assertThrows(IOException.class, () -> { + MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes())); + }); + } + + @Test + public void testLoad() throws IOException, CryptoException { + final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; + + MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes())); + MasterkeyLoader keyLoader = masterkeyFile.unlock("asd", new byte[0], Optional.empty()); + Assertions.assertNotNull(keyLoader); + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java new file mode 100644 index 0000000..e8530bb --- /dev/null +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java @@ -0,0 +1,100 @@ +package org.cryptomator.cryptolib.common; + +import org.cryptomator.cryptolib.api.Masterkey; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mockito; + +import javax.crypto.SecretKey; +import javax.security.auth.DestroyFailedException; +import java.security.SecureRandom; + +public class MasterkeyTest { + + private SecretKey encKey; + private SecretKey macKey; + private Masterkey masterkey; + + @BeforeEach + public void setup() { + encKey = Mockito.mock(SecretKey.class); + macKey = Mockito.mock(SecretKey.class); + masterkey = new Masterkey(encKey, macKey); + } + + @Test + public void testCreateNew() { + SecureRandom csprng = Mockito.mock(SecureRandom.class); + + Masterkey masterkey = Masterkey.createNew(csprng); + + Mockito.verify(csprng, Mockito.atLeastOnce()).nextBytes(Mockito.any()); + Assertions.assertNotNull(masterkey); + } + + @Test + public void testGetEncKey() { + SecretKey encKey = masterkey.getEncKey(); + + Assertions.assertSame(this.encKey, encKey); + } + + @Test + public void testGetMacKey() { + SecretKey macKey = masterkey.getMacKey(); + + Assertions.assertSame(this.macKey, macKey); + } + + @Test + public void testDestroy() throws DestroyFailedException { + masterkey.destroy(); + + Mockito.verify(encKey).destroy(); + Mockito.verify(macKey).destroy(); + } + + @ParameterizedTest + @CsvSource(value = { + "false,true,false", + "true,false,false", + "false,false,false" + }) + public void testIsNotDestroyed(boolean k1, boolean k2) { + Mockito.when(encKey.isDestroyed()).thenReturn(k1); + Mockito.when(macKey.isDestroyed()).thenReturn(k2); + + boolean destroyed = masterkey.isDestroyed(); + + Assertions.assertFalse(destroyed); + } + + @Test + public void testIsDestroyed() { + Mockito.when(encKey.isDestroyed()).thenReturn(true); + Mockito.when(macKey.isDestroyed()).thenReturn(true); + + boolean destroyed = masterkey.isDestroyed(); + + Assertions.assertTrue(destroyed); + } + + @ParameterizedTest(name = "new Masterkey({0}, {1}).getEncoded() == {2}") + @CsvSource(value = { + "foo,bar,foobar", + "foo,barbaz,foobarbaz", + "foobar,baz,foobarbaz" + }) + public void testGetEncoded(String k1, String k2, String combined) { + Mockito.when(encKey.getEncoded()).thenReturn(k1.getBytes()); + Mockito.when(macKey.getEncoded()).thenReturn(k2.getBytes()); + + byte[] raw = masterkey.getEncoded(); + + Assertions.assertArrayEquals(combined.getBytes(), raw); + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java index 4f84c5b..3ec2580 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java @@ -65,7 +65,7 @@ public void testCreateFromInvalidRawKey() { } @Test - public void testCreateFromKeyWithCorrectPassphrase() { + public void testCreateFromKeyWithCorrectPassphrase() throws UnsupportedVaultFormatException, InvalidPassphraseException { final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // @@ -88,7 +88,7 @@ public void testCreateFromKeyWithWrongPassphrase() { } @Test - public void testCreateFromKeyWithPepper() { + public void testCreateFromKeyWithPepper() throws UnsupportedVaultFormatException, InvalidPassphraseException { final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + "\"primaryMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // + "\"hmacMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java index 4c2707c..61d9927 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java @@ -15,6 +15,8 @@ import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.cryptomator.cryptolib.common.SeekableByteChannelMock; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -149,7 +151,7 @@ public void testDecryptChunkOfInvalidSize(int size) { @Test @DisplayName("decrypt chunk") - public void testChunkDecryption() { + public void testChunkDecryption() throws AuthenticationFailedException { ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=")); ByteBuffer cleartext = fileContentCryptor.decryptChunk(ciphertext, 0, headerCryptor.create(), true); ByteBuffer expected = US_ASCII.encode("hello world"); @@ -213,9 +215,10 @@ public void testDecryptionWithUnauthenticNonce() throws InterruptedException, IO ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext)); try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) { - Assertions.assertThrows(AuthenticationFailedException.class, () -> { + IOException thrown = Assertions.assertThrows(IOException.class, () -> { cleartextCh.read(ByteBuffer.allocate(3)); }); + MatcherAssert.assertThat(thrown.getCause(), CoreMatchers.instanceOf(AuthenticationFailedException.class)); } } @@ -237,9 +240,10 @@ public void testDecryptionWithUnauthenticContent() throws InterruptedException, ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext)); try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) { - Assertions.assertThrows(AuthenticationFailedException.class, () -> { + IOException thrown = Assertions.assertThrows(IOException.class, () -> { cleartextCh.read(ByteBuffer.allocate(3)); }); + MatcherAssert.assertThat(thrown.getCause(), CoreMatchers.instanceOf(AuthenticationFailedException.class)); } } @@ -261,15 +265,16 @@ public void testDecryptionWithUnauthenticMac() throws InterruptedException, IOEx ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext)); try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) { - Assertions.assertThrows(AuthenticationFailedException.class, () -> { + IOException thrown = Assertions.assertThrows(IOException.class, () -> { cleartextCh.read(ByteBuffer.allocate(3)); }); + MatcherAssert.assertThat(thrown.getCause(), CoreMatchers.instanceOf(AuthenticationFailedException.class)); } } @Test @DisplayName("decrypt chunk with unauthentic MAC but skipping MAC verficiation") - public void testChunkDecryptionWithUnauthenticMacSkipAuth() { + public void testChunkDecryptionWithUnauthenticMacSkipAuth() throws AuthenticationFailedException { ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3OG=")); ByteBuffer cleartext = fileContentCryptor.decryptChunk(ciphertext, 0, headerCryptor.create(), false); ByteBuffer expected = US_ASCII.encode(CharBuffer.wrap("hello world")); diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorTest.java index 353f71c..a34b327 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorTest.java @@ -13,6 +13,8 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.cryptomator.cryptolib.common.SeekableByteChannelMock; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -78,9 +80,10 @@ public void testDecryptManipulatedEncrypted() throws IOException { ByteBuffer result = ByteBuffer.allocate(size + 1); try (ReadableByteChannel ch = new DecryptingReadableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor, true)) { - Assertions.assertThrows(AuthenticationFailedException.class, () -> { + IOException thrown = Assertions.assertThrows(IOException.class, () -> { ch.read(result); }); + MatcherAssert.assertThat(thrown.getCause(), CoreMatchers.instanceOf(AuthenticationFailedException.class)); } } diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java index ba50375..8ca42f5 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java @@ -16,6 +16,7 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; @@ -58,7 +59,7 @@ public void benchmarkEncryption() { } @Benchmark - public void benchmarkDecryption() throws AEADBadTagException { + public void benchmarkDecryption() throws AuthenticationFailedException { HEADER_CRYPTOR.decryptHeader(validHeaderCiphertextBuf); } diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java index d5a5c09..a07cd5f 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java @@ -63,7 +63,7 @@ public void testHeaderSize() { @Test @SuppressWarnings("deprecation") - public void testDecryption() { + public void testDecryption() throws AuthenticationFailedException { byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="); FileHeader header = headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext)); Assertions.assertEquals(header.getFilesize(), -1l); diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java index a2f589e..1246343 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java @@ -43,7 +43,7 @@ static Stream filenameGenerator() { @DisplayName("encrypt and decrypt file names") @ParameterizedTest(name = "decrypt(encrypt({0}))") @MethodSource("filenameGenerator") - public void testDeterministicEncryptionOfFilenames(String origName) { + public void testDeterministicEncryptionOfFilenames(String origName) throws AuthenticationFailedException { String encrypted1 = filenameCryptor.encryptFilename(origName); String encrypted2 = filenameCryptor.encryptFilename(origName); String decrypted = filenameCryptor.decryptFilename(encrypted1); @@ -55,7 +55,7 @@ public void testDeterministicEncryptionOfFilenames(String origName) { @DisplayName("encrypt and decrypt file names with AD and custom encoding") @ParameterizedTest(name = "decrypt(encrypt({0}))") @MethodSource("filenameGenerator") - public void testDeterministicEncryptionOfFilenamesWithCustomEncodingAndAssociatedData(String origName) { + public void testDeterministicEncryptionOfFilenamesWithCustomEncodingAndAssociatedData(String origName) throws AuthenticationFailedException { byte[] associdatedData = new byte[10]; String encrypted1 = filenameCryptor.encryptFilename(BaseEncoding.base64Url(), origName, associdatedData); String encrypted2 = filenameCryptor.encryptFilename(BaseEncoding.base64Url(), origName, associdatedData); @@ -67,7 +67,7 @@ public void testDeterministicEncryptionOfFilenamesWithCustomEncodingAndAssociate @Test @DisplayName("encrypt and decrypt 128 bit filename") - public void testDeterministicEncryptionOf128bitFilename() { + public void testDeterministicEncryptionOf128bitFilename() throws AuthenticationFailedException { // block size length file names String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii String encryptedPath3a = filenameCryptor.encryptFilename(originalPath3); @@ -118,7 +118,7 @@ public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() { @Test @DisplayName("decrypt ciphertext with correct AD") - public void testDeterministicEncryptionOfFilenamesWithAssociatedData() { + public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws AuthenticationFailedException { final String encrypted = filenameCryptor.encryptFilename("test", "ad".getBytes(UTF_8)); final String decrypted = filenameCryptor.decryptFilename(encrypted, "ad".getBytes(UTF_8)); Assertions.assertEquals("test", decrypted); diff --git a/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java index 34e85dc..5551286 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java @@ -64,7 +64,7 @@ public void testCreateFromInvalidRawKey() { } @Test - public void testCreateFromKeyWithCorrectPassphrase() { + public void testCreateFromKeyWithCorrectPassphrase() throws UnsupportedVaultFormatException, InvalidPassphraseException { final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // @@ -87,7 +87,7 @@ public void testCreateFromKeyWithWrongPassphrase() { } @Test - public void testCreateFromKeyWithPepper() { + public void testCreateFromKeyWithPepper() throws UnsupportedVaultFormatException, InvalidPassphraseException { final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + "\"primaryMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // + "\"hmacMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplBenchmark.java index ee21f1f..5271fff 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplBenchmark.java @@ -15,6 +15,7 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -67,7 +68,7 @@ public void benchmarkEncryption() { } @Benchmark - public void benchmarkDecryption() { + public void benchmarkDecryption() throws AuthenticationFailedException { fileContentCryptor.decryptChunk(ciphertextChunk, cleartextChunk, 0l, new byte[12], ENC_KEY); } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java index 851fed8..4441278 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java @@ -70,7 +70,7 @@ public void setup() { } @Test - public void testDecryptedEncryptedEqualsPlaintext() throws NoSuchAlgorithmException { + public void testDecryptedEncryptedEqualsPlaintext() throws AuthenticationFailedException { SecretKey fileKey = new SecretKeySpec(new byte[16], "AES"); ByteBuffer ciphertext = ByteBuffer.allocate(fileContentCryptor.ciphertextChunkSize()); ByteBuffer cleartext = ByteBuffer.allocate(fileContentCryptor.cleartextChunkSize()); @@ -149,7 +149,7 @@ public void testDecryptChunkOfInvalidSize(int size) { @Test @DisplayName("decrypt chunk") - public void testChunkDecryption() { + public void testChunkDecryption() throws AuthenticationFailedException { ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAApsIsUSJAHAF1IqG66PAqEvceoFIiAj5736Xq")); ByteBuffer cleartext = fileContentCryptor.decryptChunk(ciphertext, 0, header, true); ByteBuffer expected = US_ASCII.encode("hello world"); diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java index 0749353..502d34d 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java @@ -17,6 +17,7 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.mockito.Mockito; @@ -64,7 +65,7 @@ public void benchmarkEncryption() { } @Benchmark - public void benchmarkDecryption() { + public void benchmarkDecryption() throws AuthenticationFailedException { HEADER_CRYPTOR.decryptHeader(validHeaderCiphertextBuf); } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java index 1f1b8de..13a1807 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java @@ -70,7 +70,7 @@ public void testHeaderSize() { @Test @SuppressWarnings("deprecation") - public void testDecryption() { + public void testDecryption() throws AuthenticationFailedException { byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjU0="); FileHeader header = headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext)); Assertions.assertEquals(header.getFilesize(), -1l); diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java index 0786682..1100695 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java @@ -26,7 +26,7 @@ public class FileNameCryptorImplTest { private static final Charset UTF_8 = StandardCharsets.UTF_8; @Test - public void testDeterministicEncryptionOfFilenames() throws IOException { + public void testDeterministicEncryptionOfFilenames() throws IOException, AuthenticationFailedException { final byte[] keyBytes = new byte[32]; final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); final SecretKey macKey = new SecretKeySpec(keyBytes, "AES"); @@ -94,7 +94,7 @@ public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() { } @Test - public void testDeterministicEncryptionOfFilenamesWithAssociatedData() { + public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws AuthenticationFailedException { final byte[] keyBytes = new byte[32]; final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); final SecretKey macKey = new SecretKeySpec(keyBytes, "AES"); diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..ca6ee9c --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file From 2b3f8a13530f9b5d393c81250de0466b8dfa14ba Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 29 Nov 2020 01:29:22 +0100 Subject: [PATCH 06/59] Added new Masterkey-specific APIs --- .../org/cryptomator/cryptolib/Cryptors.java | 40 +++-- .../cryptomator/cryptolib/api/Cryptor.java | 3 + .../cryptolib/api/CryptorProvider.java | 38 +---- .../cryptomator/cryptolib/api/KeyFile.java | 135 ---------------- .../cryptomator/cryptolib/api/Masterkey.java | 9 ++ .../cryptolib/common/MasterkeyFile.java | 101 +++++++++--- .../cryptomator/cryptolib/v1/CryptorImpl.java | 82 ++++------ .../cryptolib/v1/CryptorProviderImpl.java | 110 +------------ .../cryptomator/cryptolib/v1/KeyFileImpl.java | 42 ----- .../cryptomator/cryptolib/v2/CryptorImpl.java | 71 +++------ .../cryptolib/v2/CryptorProviderImpl.java | 113 +------------- .../cryptomator/cryptolib/v2/KeyFileImpl.java | 41 ----- .../cryptolib/CryptorIntegrationTest.java | 100 ------------ .../cryptolib/api/KeyFileTest.java | 68 -------- .../cryptolib/common/MasterkeyFileTest.java | 145 ++++++++++++++++-- .../cryptolib/common/MasterkeyTest.java | 21 +++ .../cryptolib/v1/CryptorImplTest.java | 56 ++----- .../cryptolib/v1/CryptorProviderImplTest.java | 132 +--------------- .../v1/FileContentEncryptorBenchmark.java | 6 +- .../v1/FileContentEncryptorTest.java | 6 +- .../cryptolib/v2/CryptorImplTest.java | 56 ++----- .../cryptolib/v2/CryptorProviderImplTest.java | 131 +--------------- .../v2/FileContentEncryptorBenchmark.java | 20 +-- 23 files changed, 388 insertions(+), 1138 deletions(-) delete mode 100644 src/main/java/org/cryptomator/cryptolib/api/KeyFile.java delete mode 100644 src/main/java/org/cryptomator/cryptolib/v1/KeyFileImpl.java delete mode 100644 src/main/java/org/cryptomator/cryptolib/v2/KeyFileImpl.java delete mode 100644 src/test/java/org/cryptomator/cryptolib/CryptorIntegrationTest.java delete mode 100644 src/test/java/org/cryptomator/cryptolib/api/KeyFileTest.java diff --git a/src/main/java/org/cryptomator/cryptolib/Cryptors.java b/src/main/java/org/cryptomator/cryptolib/Cryptors.java index c094758..9cd0c42 100644 --- a/src/main/java/org/cryptomator/cryptolib/Cryptors.java +++ b/src/main/java/org/cryptomator/cryptolib/Cryptors.java @@ -8,16 +8,25 @@ *******************************************************************************/ package org.cryptomator.cryptolib; -import java.security.SecureRandom; - +import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.FileHeaderCryptor; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.KeyFile; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.cryptolib.common.MasterkeyFile; +import org.cryptomator.cryptolib.common.MasterkeyFileLoader; import org.cryptomator.cryptolib.common.ReseedingSecureRandom; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.SecureRandom; +import java.util.Optional; + import static com.google.common.base.Preconditions.checkArgument; public final class Cryptors { @@ -43,7 +52,7 @@ public static CryptorProvider version2(SecureRandom seeder) { /** * Calculates the size of the cleartext resulting from the given ciphertext decrypted with the given cryptor. * - * @param ciphertextSize Length of encrypted payload. Not including the {@link FileHeader#getFilesize() length of the header}. + * @param ciphertextSize Length of encrypted payload. Not including the {@link FileHeaderCryptor#headerSize() length of the header}. * @param cryptor The cryptor which defines the cleartext/ciphertext ratio * @return Cleartext length of a ciphertextSize-sized ciphertext decrypted with cryptor. */ @@ -95,7 +104,7 @@ public static long ciphertextSize(long cleartextSize, Cryptor cryptor) { * @since 1.1.0 */ @Deprecated - public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] masterkey, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException { + public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] masterkey, CharSequence oldPassphrase, CharSequence newPassphrase) throws CryptoException { return changePassphrase(cryptorProvider, masterkey, new byte[0], oldPassphrase, newPassphrase); } @@ -112,10 +121,14 @@ public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] ma * @since 1.1.4 */ @Deprecated - public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] masterkey, byte[] pepper, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException { + public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] masterkey, byte[] pepper, CharSequence oldPassphrase, CharSequence newPassphrase) throws CryptoException { final KeyFile keyFile = KeyFile.parse(masterkey); - try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, oldPassphrase, pepper, keyFile.getVersion())) { + try (MasterkeyFileLoader loader = MasterkeyFile.withContent(new ByteArrayInputStream(masterkey)).unlock(oldPassphrase, pepper, Optional.empty()); + Masterkey key = loader.loadKey(MasterkeyFileLoader.KEY_ID); + Cryptor cryptor = cryptorProvider.withKey(key)) { return cryptor.writeKeysToMasterkeyFile(newPassphrase, pepper, keyFile.getVersion()).serialize(); + } catch (IOException e) { + throw new UncheckedIOException(e); } } @@ -131,10 +144,12 @@ public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] ma * @since 1.3.0 */ @Deprecated - public static byte[] exportRawKey(CryptorProvider cryptorProvider, byte[] masterkey, byte[] pepper, CharSequence passphrase) throws UnsupportedVaultFormatException, InvalidPassphraseException { - final KeyFile keyFile = KeyFile.parse(masterkey); - try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, pepper, keyFile.getVersion())) { - return cryptor.getRawKey(); + public static byte[] exportRawKey(CryptorProvider cryptorProvider, byte[] masterkey, byte[] pepper, CharSequence passphrase) throws UnsupportedVaultFormatException, CryptoException { + try (MasterkeyFileLoader loader = MasterkeyFile.withContent(new ByteArrayInputStream(masterkey)).unlock(passphrase, pepper, Optional.empty()); + Masterkey key = loader.loadKey(MasterkeyFileLoader.KEY_ID)) { + return key.getEncoded(); + } catch (IOException e) { + throw new UncheckedIOException(e); } } @@ -145,13 +160,14 @@ public static byte[] exportRawKey(CryptorProvider cryptorProvider, byte[] master * @param rawKey The original JSON representation of the masterkey * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) * @param passphrase The passphrase - * @param vaultVersion The version of the vault for which to recreate a masterkey file + * @param vaultVersion The version of the vault for which to recreate a masterkey file * @return The json-encoded masterkey protected by the passphrase * @since 1.3.0 */ @Deprecated public static byte[] restoreRawKey(CryptorProvider cryptorProvider, byte[] rawKey, byte[] pepper, CharSequence passphrase, int vaultVersion) { - try (Cryptor cryptor = cryptorProvider.createFromRawKey(rawKey)) { + try (Masterkey key = Masterkey.createFromRaw(rawKey); + Cryptor cryptor = cryptorProvider.withKey(key)) { return cryptor.writeKeysToMasterkeyFile(passphrase, pepper, vaultVersion).serialize(); } } diff --git a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java index 0a20271..cf08c14 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java @@ -26,6 +26,7 @@ public interface Cryptor extends Destroyable, AutoCloseable { * @return Encrypted data that can be stored in insecure locations. * @see #writeKeysToMasterkeyFile(CharSequence, byte[], int) */ + @Deprecated KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, int vaultVersion); /** @@ -35,12 +36,14 @@ public interface Cryptor extends Destroyable, AutoCloseable { * @return Encrypted data that can be stored in insecure locations. * @since 1.1.0 */ + @Deprecated KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, byte[] pepper, int vaultVersion); /** * @return All key material of this cryptor * @since 1.3.0 */ + @Deprecated byte[] getRawKey(); @Override diff --git a/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java b/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java index f8f562a..63b36f3 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java +++ b/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java @@ -10,42 +10,6 @@ public interface CryptorProvider { - /** - * @return A new Cryptor instance using randomized keys - */ - Cryptor createNew(); - - /** - * @param rawKey The key to use for the new cryptor - * @return A new Cryptor instance using the given key - * @throws IllegalArgumentException if the key is of invalid length - * @since 1.3.0 - */ - Cryptor createFromRawKey(byte[] rawKey) throws IllegalArgumentException; - - /** - * Shortcut for {@link #createFromKeyFile(KeyFile, CharSequence, byte[], int)} with en empty pepper. - * - * @param keyFile The parsed key file - * @param passphrase The passphrase to use for decrypting the keyfile - * @param expectedVaultVersion The vault version expected in this file - * @return A new Cryptor instance using the keys from the supplied keyfile - * @throws UnsupportedVaultFormatException If the vault has been created with a different version than expectedVaultVersion - * @throws InvalidPassphraseException If the key derived from the passphrase could not be used to decrypt the keyfile. - * @see #createFromKeyFile(KeyFile, CharSequence, byte[], int) - */ - Cryptor createFromKeyFile(KeyFile keyFile, CharSequence passphrase, int expectedVaultVersion) throws UnsupportedVaultFormatException, InvalidPassphraseException; - - /** - * @param keyFile The parsed key file - * @param passphrase The passphrase to use for decrypting the keyfile - * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) - * @param expectedVaultVersion The vault version expected in this file - * @return A new Cryptor instance using the keys from the supplied keyfile - * @throws UnsupportedVaultFormatException If the vault has been created with a different version than expectedVaultVersion - * @throws InvalidPassphraseException If the key derived from the passphrase and pepper could not be used to decrypt the keyfile. - * @since 1.1.0 - */ - Cryptor createFromKeyFile(KeyFile keyFile, CharSequence passphrase, byte[] pepper, int expectedVaultVersion) throws UnsupportedVaultFormatException, InvalidPassphraseException; + Cryptor withKey(Masterkey masterkey); } diff --git a/src/main/java/org/cryptomator/cryptolib/api/KeyFile.java b/src/main/java/org/cryptomator/cryptolib/api/KeyFile.java deleted file mode 100644 index db4bc2a..0000000 --- a/src/main/java/org/cryptomator/cryptolib/api/KeyFile.java +++ /dev/null @@ -1,135 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.cryptolib.api; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.lang.reflect.Type; -import java.nio.charset.Charset; - -import com.google.common.io.BaseEncoding; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; -import com.google.gson.stream.JsonReader; - -/** - * OOP-JSON interface for the masterkey file.
- *
- * Each version might have its own package-private subclass of this file, which adds further properties. - * These properties must be annotated with {@link Expose} in order to be considered by {@link #serialize()}. - */ -@Deprecated -public abstract class KeyFile { - - private static final Charset UTF_8 = Charset.forName("UTF-8"); - private static final Gson GSON = new GsonBuilder().setPrettyPrinting() // - .registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) // - .setLenient() // - .disableHtmlEscaping() // - .excludeFieldsWithoutExposeAnnotation().create(); - - @Expose - @SerializedName("version") - private int version; - - private JsonObject jsonObj; - - /** - * @return Version (i.e. vault format) stored in the masterkey file. - * @see #setVersion(int) - */ - public int getVersion() { - return version; - } - - /** - * @param version The vault format used to distinguish different implementations needed to access encrypted contents. - */ - public void setVersion(int version) { - this.version = version; - } - - /** - * Parses a json keyfile. - * - * @param serialized Json content. - * @return A new KeyFile instance. - */ - public static KeyFile parse(byte[] serialized) { - try (InputStream in = new ByteArrayInputStream(serialized); // - Reader reader = new InputStreamReader(in, UTF_8)) { - JsonElement json = JsonParser.parseReader(reader); - if (json.isJsonObject()) { - KeyFile result = GSON.fromJson(json, GenericKeyFile.class); - result.jsonObj = json.getAsJsonObject(); - return result; - } else { - throw new IllegalArgumentException("Key file doesn't contain json object."); - } - } catch (IOException | JsonParseException e) { - throw new IllegalArgumentException("Unable to parse key file.", e); - } - } - - /** - * Creates a JSON representation of this instance. - * - * @return UTF-8-encoded byte array of the JSON representation. - */ - public byte[] serialize() { - return GSON.toJson(this).getBytes(UTF_8); - } - - /** - * Creates a new version-specific KeyFile instance from this instance. - * - * @param clazz Version-specific subclass of KeyFile. - * @param Specific KeyFile implementation type. - * @return New instance of the given class. - */ - public T as(Class clazz) { - T result = GSON.fromJson(jsonObj, clazz); - ((KeyFile) result).jsonObj = jsonObj; - return result; - } - - private static class GenericKeyFile extends KeyFile { - } - - private static class ByteArrayJsonAdapter implements JsonSerializer, JsonDeserializer { - - private static final BaseEncoding BASE64 = BaseEncoding.base64(); - - @Override - public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return BASE64.decode(json.getAsString()); - } - - @Override - public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(BASE64.encode(src)); - } - - } - -} diff --git a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java index c71b950..e568765 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java @@ -1,9 +1,11 @@ package org.cryptomator.cryptolib.api; +import com.google.common.base.Preconditions; import org.cryptomator.cryptolib.common.Destroyables; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; @@ -36,6 +38,13 @@ public static Masterkey createNew(SecureRandom random) { } } + public static Masterkey createFromRaw(byte[] encoded) { + Preconditions.checkArgument(encoded.length == KEY_LEN_BYTES + KEY_LEN_BYTES, "Invalid raw key length %s", encoded.length); + SecretKey encKey = new SecretKeySpec(encoded, 0, KEY_LEN_BYTES, ENC_ALG); + SecretKey macKey = new SecretKeySpec(encoded, KEY_LEN_BYTES, KEY_LEN_BYTES, MAC_ALG); + return new Masterkey(encKey, macKey); + } + public SecretKey getEncKey() { return encKey; } diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java index b0c9410..cdea319 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java @@ -18,6 +18,7 @@ import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -29,13 +30,18 @@ import java.nio.file.StandardOpenOption; import java.security.InvalidKeyException; import java.security.MessageDigest; +import java.security.SecureRandom; import java.util.Arrays; import java.util.Optional; public class MasterkeyFile { + private static final int DEFAULT_SCRYPT_SALT_LENGTH = 8; + private static final int DEFAULT_SCRYPT_COST_PARAM = 1 << 15; // 2^15 + private static final int DEFAULT_SCRYPT_BLOCK_SIZE = 8; private static final Gson GSON = new GsonBuilder() // .setPrettyPrinting() // + .disableHtmlEscaping() // .registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) // .create(); @@ -63,32 +69,26 @@ public static MasterkeyFile withContent(InputStream in) throws IOException { } } - public static byte[] lock(Masterkey masterkey, CharSequence passphrase, byte[] pepper, int vaultVersion) { - // TODO - return null; - } - -// -// public void changePw() { -// CharSequence oldPw = ""; -// CharSequence newPw = ""; -// try { -// save(load(oldPw).loadKey("asd"), newPw); -// } catch (KeyLoadingFailedException e) { -// e.printStackTrace(); -// } finally { -// -// } -// } - - public MasterkeyFileLoader unlock(CharSequence passphrase, byte[] pepper, Optional expectedVaultVersion) throws CryptoException { + /** + * Derives a KEK from the given passphrase and the params from this masterkey file using scrypt and unwraps the + * stored encryption and MAC keys. + * + * @param passphrase The passphrase used during key derivation + * @param pepper An optional application-specific pepper added to the scrypt's salt. Can be an empty array. + * @param expectedVaultVersion An optional expected vault version. + * @return A masterkey loader that can be used to access the unwrapped keys. Should be used in a try-with-resource statement. + * @throws UnsupportedVaultFormatException If the expectedVaultVersion is present and does not match the cryptographically signed version stored in the masterkey file. + * @throws InvalidPassphraseException If the provided passphrase can not be used to unwrap the stored keys. + * @throws CryptoException In case of any other cryptographic exceptions + */ + public MasterkeyFileLoader unlock(CharSequence passphrase, byte[] pepper, Optional expectedVaultVersion) throws UnsupportedVaultFormatException, InvalidPassphraseException, CryptoException { boolean success = false; SecretKey kek = null; SecretKey encKey = null; SecretKey macKey = null; try { // derive keys: - kek = scrypt(passphrase, pepper); + kek = scrypt(passphrase, content.scryptSalt, pepper, content.scryptCostParam, content.scryptBlockSize); macKey = AesKeyWrap.unwrap(kek, content.macMasterKey, Masterkey.MAC_ALG); encKey = AesKeyWrap.unwrap(kek, content.encMasterKey, Masterkey.ENC_ALG); @@ -111,12 +111,67 @@ public MasterkeyFileLoader unlock(CharSequence passphrase, byte[] pepper, Option } } - private SecretKey scrypt(CharSequence passphrase, byte[] pepper) { - byte[] salt = content.scryptSalt; + /** + * Derives a KEK from the given passphrase and wraps the key material from masterkey. + * Then serializes the encrypted keys as well as used key derivation parameters into a JSON representation + * that can be stored into a masterkey file. + * + * @param masterkey The key to protect + * @param passphrase The passphrase used during key derivation + * @param pepper An optional application-specific pepper added to the scrypt's salt. Can be an empty array. + * @param vaultVersion The vault version that should be stored in this masterkey file (for downwards compatibility) + * @param csprng A cryptographically secure RNG + * @return A JSON representation of the encrypted masterkey with its key derivation parameters. + */ + public static byte[] lock(Masterkey masterkey, CharSequence passphrase, byte[] pepper, int vaultVersion, SecureRandom csprng) { + Preconditions.checkArgument(!masterkey.isDestroyed(), "masterkey has been destroyed"); + + final byte[] salt = new byte[DEFAULT_SCRYPT_SALT_LENGTH]; + csprng.nextBytes(salt); + SecretKey kek = scrypt(passphrase, salt, pepper, DEFAULT_SCRYPT_COST_PARAM, DEFAULT_SCRYPT_BLOCK_SIZE); + try { + final Mac mac = MacSupplier.HMAC_SHA256.withKey(masterkey.getMacKey()); + final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(vaultVersion).array()); + Content content = new Content(); + content.version = vaultVersion; + content.versionMac = versionMac; + content.scryptSalt = salt; + content.scryptCostParam = DEFAULT_SCRYPT_COST_PARAM; + content.scryptBlockSize = DEFAULT_SCRYPT_BLOCK_SIZE; + content.encMasterKey = AesKeyWrap.wrap(kek, masterkey.getEncKey()); + content.macMasterKey = AesKeyWrap.wrap(kek, masterkey.getMacKey()); + return GSON.toJson(content).getBytes(StandardCharsets.UTF_8); + } finally { + Destroyables.destroySilently(kek); + } + } + + /** + * Reencrypts a masterkey with a new passphrase. + * + * @param masterkey The original JSON representation of the masterkey + * @param oldPassphrase The old passphrase + * @param newPassphrase The new passphrase + * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) + * @param csprng A cryptographically secure RNG + * @return A JSON representation of the masterkey, now encrypted with newPassphrase + * @throws IOException + * @throws InvalidPassphraseException If the wrong oldPassphrase has been supplied for the masterkey + * @throws CryptoException In case of other cryptographic exceptions. + */ + public static byte[] changePassphrase(byte[] masterkey, CharSequence oldPassphrase, CharSequence newPassphrase, byte[] pepper, SecureRandom csprng) throws IOException, InvalidPassphraseException, CryptoException { + MasterkeyFile orig = MasterkeyFile.withContent(new ByteArrayInputStream(masterkey)); + try (MasterkeyFileLoader loader = orig.unlock(oldPassphrase, pepper, Optional.empty()); + Masterkey key = loader.loadKey(MasterkeyFileLoader.KEY_ID)) { + return MasterkeyFile.lock(key, newPassphrase, pepper, orig.content.version, csprng); + } + } + + private static SecretKey scrypt(CharSequence passphrase, byte[] salt, byte[] pepper, int costParam, int blockSize) { byte[] saltAndPepper = new byte[salt.length + pepper.length]; System.arraycopy(salt, 0, saltAndPepper, 0, salt.length); System.arraycopy(pepper, 0, saltAndPepper, salt.length, pepper.length); - byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, content.scryptCostParam, content.scryptBlockSize, Masterkey.KEY_LEN_BYTES); + byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, costParam, blockSize, Masterkey.KEY_LEN_BYTES); try { return new SecretKeySpec(kekBytes, Masterkey.ENC_ALG); } finally { diff --git a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java index 06ff2cc..d32f615 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java @@ -8,31 +8,28 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v1; -import static org.cryptomator.cryptolib.v1.Constants.DEFAULT_SCRYPT_BLOCK_SIZE; -import static org.cryptomator.cryptolib.v1.Constants.DEFAULT_SCRYPT_COST_PARAM; -import static org.cryptomator.cryptolib.v1.Constants.DEFAULT_SCRYPT_SALT_LENGTH; -import static org.cryptomator.cryptolib.v1.Constants.KEY_LEN_BYTES; - -import java.nio.ByteBuffer; -import java.security.SecureRandom; -import java.util.Arrays; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import javax.security.auth.DestroyFailedException; -import javax.security.auth.Destroyable; - import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.KeyFile; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.AesKeyWrap; import org.cryptomator.cryptolib.common.MacSupplier; import org.cryptomator.cryptolib.common.Scrypt; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.Arrays; + +import static org.cryptomator.cryptolib.v1.Constants.DEFAULT_SCRYPT_BLOCK_SIZE; +import static org.cryptomator.cryptolib.v1.Constants.DEFAULT_SCRYPT_COST_PARAM; +import static org.cryptomator.cryptolib.v1.Constants.DEFAULT_SCRYPT_SALT_LENGTH; +import static org.cryptomator.cryptolib.v1.Constants.KEY_LEN_BYTES; + class CryptorImpl implements Cryptor { - private final SecretKey encKey; - private final SecretKey macKey; + private final Masterkey masterkey; private final SecureRandom random; private final FileContentCryptorImpl fileContentCryptor; private final FileHeaderCryptorImpl fileHeaderCryptor; @@ -40,15 +37,14 @@ class CryptorImpl implements Cryptor { /** * Package-private constructor. - * Use {@link CryptorProviderImpl#createNew()} or {@link CryptorProviderImpl#createFromKeyFile(KeyFile, CharSequence, int)} to obtain a Cryptor instance. + * Use {@link CryptorProviderImpl#withKey(Masterkey)} to obtain a Cryptor instance. */ - CryptorImpl(SecretKey encKey, SecretKey macKey, SecureRandom random) { - this.encKey = encKey; - this.macKey = macKey; + CryptorImpl(Masterkey masterkey, SecureRandom random) { + this.masterkey = masterkey; this.random = random; - this.fileHeaderCryptor = new FileHeaderCryptorImpl(encKey, macKey, random); - this.fileContentCryptor = new FileContentCryptorImpl(macKey, random); - this.fileNameCryptor = new FileNameCryptorImpl(encKey, macKey); + this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey.getEncKey(), masterkey.getMacKey(), random); + this.fileContentCryptor = new FileContentCryptorImpl(masterkey.getMacKey(), random); + this.fileNameCryptor = new FileNameCryptorImpl(masterkey.getEncKey(), masterkey.getMacKey()); } @Override @@ -71,12 +67,7 @@ public FileNameCryptorImpl fileNameCryptor() { @Override public boolean isDestroyed() { - // SecretKey did not implement Destroyable in Java 7: - if (encKey instanceof Destroyable && macKey instanceof Destroyable) { - return ((Destroyable) encKey).isDestroyed() || ((Destroyable) macKey).isDestroyed(); - } else { - return false; - } + return masterkey.isDestroyed(); } @Override @@ -86,8 +77,7 @@ public void close() { @Override public void destroy() { - destroyQuietly(encKey); - destroyQuietly(macKey); + masterkey.destroy(); } @Override @@ -109,13 +99,13 @@ public KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, byte[] pepper, final byte[] wrappedMacKey; try { final SecretKey kek = new SecretKeySpec(kekBytes, Constants.ENC_ALG); - wrappedEncryptionKey = AesKeyWrap.wrap(kek, encKey); - wrappedMacKey = AesKeyWrap.wrap(kek, macKey); + wrappedEncryptionKey = AesKeyWrap.wrap(kek, masterkey.getEncKey()); + wrappedMacKey = AesKeyWrap.wrap(kek, masterkey.getMacKey()); } finally { Arrays.fill(kekBytes, (byte) 0x00); } - final Mac mac = MacSupplier.HMAC_SHA256.withKey(macKey); + final Mac mac = MacSupplier.HMAC_SHA256.withKey(masterkey.getMacKey()); final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(vaultVersion).array()); final KeyFileImpl keyfile = new KeyFileImpl(); @@ -131,27 +121,7 @@ public KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, byte[] pepper, @Override public byte[] getRawKey() { - byte[] rawEncKey = encKey.getEncoded(); - byte[] rawMacKeyKey = macKey.getEncoded(); - try { - byte[] rawKey = new byte[rawEncKey.length + rawMacKeyKey.length]; - System.arraycopy(rawEncKey, 0, rawKey, 0, rawEncKey.length); - System.arraycopy(rawMacKeyKey, 0, rawKey, rawEncKey.length, rawMacKeyKey.length); - return rawKey; - } finally { - Arrays.fill(rawEncKey, (byte) 0x00); - Arrays.fill(rawMacKeyKey, (byte) 0x00); - } - } - - private void destroyQuietly(SecretKey key) { - try { - if (key instanceof Destroyable && !((Destroyable) key).isDestroyed()) { - ((Destroyable) key).destroy(); - } - } catch (DestroyFailedException e) { - // ignore - } + return masterkey.getEncoded(); } private void assertNotDestroyed() { diff --git a/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java index 129d29c..680462d 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java @@ -8,126 +8,22 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v1; -import com.google.common.base.Preconditions; 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 org.cryptomator.cryptolib.common.AesKeyWrap; -import org.cryptomator.cryptolib.common.MacSupplier; -import org.cryptomator.cryptolib.common.Scrypt; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.cryptomator.cryptolib.api.Masterkey; -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.nio.ByteBuffer; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.Arrays; - -import static org.cryptomator.cryptolib.v1.Constants.ENC_ALG; -import static org.cryptomator.cryptolib.v1.Constants.KEY_LEN_BYTES; -import static org.cryptomator.cryptolib.v1.Constants.MAC_ALG; public class CryptorProviderImpl implements CryptorProvider { - private static final Logger LOG = LoggerFactory.getLogger(CryptorProviderImpl.class); - private final SecureRandom random; - private final KeyGenerator encKeyGen; - private final KeyGenerator macKeyGen; public CryptorProviderImpl(SecureRandom random) { - assertRequiredKeyLengthIsAllowed(); this.random = random; - try { - this.encKeyGen = KeyGenerator.getInstance(ENC_ALG); - encKeyGen.init(KEY_LEN_BYTES * Byte.SIZE, random); - this.macKeyGen = KeyGenerator.getInstance(MAC_ALG); - macKeyGen.init(KEY_LEN_BYTES * Byte.SIZE, random); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e); - } - } - - private static void assertRequiredKeyLengthIsAllowed() { - if (!isRequiredKeyLengthAllowed()) { - LOG.error("Required key length not supported. See https://github.com/cryptomator/cryptolib/wiki/Restricted-Key-Size."); - throw new IllegalStateException("Required key length not supported."); - } - } - - private static boolean isRequiredKeyLengthAllowed() { - try { - int requiredKeyLengthBits = KEY_LEN_BYTES * Byte.SIZE; - int allowedKeyLengthBits = Cipher.getMaxAllowedKeyLength(ENC_ALG); - return allowedKeyLengthBits >= requiredKeyLengthBits; - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Hard-coded algorithm \"" + ENC_ALG + "\" not supported.", e); - } - } - - @Override - public CryptorImpl createNew() { - SecretKey encKey = encKeyGen.generateKey(); - SecretKey macKey = macKeyGen.generateKey(); - return new CryptorImpl(encKey, macKey, random); } @Override - public CryptorImpl createFromRawKey(byte[] rawKey) throws IllegalArgumentException { - Preconditions.checkArgument(rawKey.length == KEY_LEN_BYTES + KEY_LEN_BYTES, "Invalid raw key length %s", rawKey.length); - SecretKey encKey = new SecretKeySpec(rawKey, 0, KEY_LEN_BYTES, ENC_ALG); - SecretKey macKey = new SecretKeySpec(rawKey, KEY_LEN_BYTES, KEY_LEN_BYTES, MAC_ALG); - return new CryptorImpl(encKey, macKey, random); - } - - @Override - public CryptorImpl createFromKeyFile(KeyFile keyFile, CharSequence passphrase, int expectedVaultVersion) throws UnsupportedVaultFormatException, InvalidPassphraseException { - return createFromKeyFile(keyFile, passphrase, new byte[0], expectedVaultVersion); - } - - @Override - public CryptorImpl createFromKeyFile(KeyFile keyFile, CharSequence passphrase, byte[] pepper, int expectedVaultVersion) throws UnsupportedVaultFormatException, InvalidPassphraseException { - final KeyFileImpl keyFileImpl = keyFile.as(KeyFileImpl.class); - final byte[] salt = keyFileImpl.scryptSalt; - final byte[] saltAndPepper = new byte[salt.length + pepper.length]; - System.arraycopy(salt, 0, saltAndPepper, 0, salt.length); - System.arraycopy(pepper, 0, saltAndPepper, salt.length, pepper.length); - final byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, keyFileImpl.scryptCostParam, keyFileImpl.scryptBlockSize, KEY_LEN_BYTES); - try { - SecretKey kek = new SecretKeySpec(kekBytes, ENC_ALG); - return createFromKeyFile(keyFileImpl, kek, expectedVaultVersion); - } finally { - Arrays.fill(kekBytes, (byte) 0x00); - } - } - - private CryptorImpl createFromKeyFile(KeyFileImpl keyFile, SecretKey kek, int expectedVaultVersion) throws UnsupportedVaultFormatException, InvalidPassphraseException { - // check version - if (expectedVaultVersion != keyFile.getVersion()) { - throw new UnsupportedVaultFormatException(keyFile.getVersion(), expectedVaultVersion); - } - - try { - SecretKey macKey = AesKeyWrap.unwrap(kek, keyFile.macMasterKey, MAC_ALG); - Mac mac = MacSupplier.HMAC_SHA256.withKey(macKey); - byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(expectedVaultVersion).array()); - if (keyFile.versionMac == null || !MessageDigest.isEqual(versionMac, keyFile.versionMac)) { - // attempted downgrade attack: versionMac doesn't match version. - throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, expectedVaultVersion); - } - SecretKey encKey = AesKeyWrap.unwrap(kek, keyFile.encryptionMasterKey, ENC_ALG); - return new CryptorImpl(encKey, macKey, random); - } catch (InvalidKeyException e) { - throw new InvalidPassphraseException(); - } + public CryptorImpl withKey(Masterkey masterkey) { + return new CryptorImpl(masterkey, random); } } diff --git a/src/main/java/org/cryptomator/cryptolib/v1/KeyFileImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/KeyFileImpl.java deleted file mode 100644 index 9edabb4..0000000 --- a/src/main/java/org/cryptomator/cryptolib/v1/KeyFileImpl.java +++ /dev/null @@ -1,42 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2015, 2016 Sebastian Stenzel and others. - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.cryptolib.v1; - -import org.cryptomator.cryptolib.api.KeyFile; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -class KeyFileImpl extends KeyFile { - - @Expose - @SerializedName("scryptSalt") - byte[] scryptSalt; - - @Expose - @SerializedName("scryptCostParam") - int scryptCostParam; - - @Expose - @SerializedName("scryptBlockSize") - int scryptBlockSize; - - @Expose - @SerializedName("primaryMasterKey") - byte[] encryptionMasterKey; - - @Expose - @SerializedName("hmacMasterKey") - byte[] macMasterKey; - - @Expose - @SerializedName("versionMac") - byte[] versionMac; - -} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java index 8272c37..2a329aa 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java @@ -8,21 +8,20 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; -import java.nio.ByteBuffer; -import java.security.SecureRandom; -import java.util.Arrays; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import javax.security.auth.DestroyFailedException; -import javax.security.auth.Destroyable; - import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.KeyFile; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.AesKeyWrap; import org.cryptomator.cryptolib.common.MacSupplier; import org.cryptomator.cryptolib.common.Scrypt; +import org.cryptomator.cryptolib.v1.CryptorProviderImpl; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.Arrays; import static org.cryptomator.cryptolib.v2.Constants.DEFAULT_SCRYPT_BLOCK_SIZE; import static org.cryptomator.cryptolib.v2.Constants.DEFAULT_SCRYPT_COST_PARAM; @@ -31,8 +30,7 @@ class CryptorImpl implements Cryptor { - private final SecretKey encKey; - private final SecretKey macKey; + private final Masterkey masterkey; private final SecureRandom random; private final FileContentCryptorImpl fileContentCryptor; private final FileHeaderCryptorImpl fileHeaderCryptor; @@ -40,15 +38,14 @@ class CryptorImpl implements Cryptor { /** * Package-private constructor. - * Use {@link CryptorProviderImpl#createNew()} or {@link CryptorProviderImpl#createFromKeyFile(KeyFile, CharSequence, int)} to obtain a Cryptor instance. + * Use {@link CryptorProviderImpl#withKey(Masterkey)} to obtain a Cryptor instance. */ - CryptorImpl(SecretKey encKey, SecretKey macKey, SecureRandom random) { - this.encKey = encKey; - this.macKey = macKey; + CryptorImpl(Masterkey masterkey, SecureRandom random) { + this.masterkey = masterkey; this.random = random; - this.fileHeaderCryptor = new FileHeaderCryptorImpl(encKey, random); + this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey.getEncKey(), random); this.fileContentCryptor = new FileContentCryptorImpl(random); - this.fileNameCryptor = new FileNameCryptorImpl(encKey, macKey); + this.fileNameCryptor = new FileNameCryptorImpl(masterkey.getEncKey(), masterkey.getMacKey()); } @Override @@ -71,12 +68,7 @@ public FileNameCryptorImpl fileNameCryptor() { @Override public boolean isDestroyed() { - // SecretKey did not implement Destroyable in Java 7: - if (encKey instanceof Destroyable && macKey instanceof Destroyable) { - return ((Destroyable) encKey).isDestroyed() || ((Destroyable) macKey).isDestroyed(); - } else { - return false; - } + return masterkey.isDestroyed(); } @Override @@ -86,8 +78,7 @@ public void close() { @Override public void destroy() { - destroyQuietly(encKey); - destroyQuietly(macKey); + masterkey.destroy(); } @Override @@ -109,13 +100,13 @@ public KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, byte[] pepper, final byte[] wrappedMacKey; try { final SecretKey kek = new SecretKeySpec(kekBytes, Constants.ENC_ALG); - wrappedEncryptionKey = AesKeyWrap.wrap(kek, encKey); - wrappedMacKey = AesKeyWrap.wrap(kek, macKey); + wrappedEncryptionKey = AesKeyWrap.wrap(kek, masterkey.getEncKey()); + wrappedMacKey = AesKeyWrap.wrap(kek, masterkey.getMacKey()); } finally { Arrays.fill(kekBytes, (byte) 0x00); } - final Mac mac = MacSupplier.HMAC_SHA256.withKey(macKey); + final Mac mac = MacSupplier.HMAC_SHA256.withKey(masterkey.getMacKey()); final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(vaultVersion).array()); final KeyFileImpl keyfile = new KeyFileImpl(); @@ -131,27 +122,7 @@ public KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, byte[] pepper, @Override public byte[] getRawKey() { - byte[] rawEncKey = encKey.getEncoded(); - byte[] rawMacKeyKey = macKey.getEncoded(); - try { - byte[] rawKey = new byte[rawEncKey.length + rawMacKeyKey.length]; - System.arraycopy(rawEncKey, 0, rawKey, 0, rawEncKey.length); - System.arraycopy(rawMacKeyKey, 0, rawKey, rawEncKey.length, rawMacKeyKey.length); - return rawKey; - } finally { - Arrays.fill(rawEncKey, (byte) 0x00); - Arrays.fill(rawMacKeyKey, (byte) 0x00); - } - } - - private void destroyQuietly(SecretKey key) { - try { - if (key instanceof Destroyable && !((Destroyable) key).isDestroyed()) { - ((Destroyable) key).destroy(); - } - } catch (DestroyFailedException e) { - // ignore - } + return masterkey.getEncoded(); } private void assertNotDestroyed() { diff --git a/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java index fd95070..1d1658b 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java @@ -8,127 +8,22 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; -import java.nio.ByteBuffer; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Arrays; - -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import com.google.common.base.Preconditions; 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 org.cryptomator.cryptolib.common.AesKeyWrap; -import org.cryptomator.cryptolib.common.MacSupplier; -import org.cryptomator.cryptolib.common.Scrypt; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.cryptomator.cryptolib.api.Masterkey; -import static org.cryptomator.cryptolib.v2.Constants.ENC_ALG; -import static org.cryptomator.cryptolib.v2.Constants.KEY_LEN_BYTES; -import static org.cryptomator.cryptolib.v2.Constants.MAC_ALG; +import java.security.SecureRandom; public class CryptorProviderImpl implements CryptorProvider { - private static final Logger LOG = LoggerFactory.getLogger(CryptorProviderImpl.class); - private final SecureRandom random; - private final KeyGenerator encKeyGen; - private final KeyGenerator macKeyGen; public CryptorProviderImpl(SecureRandom random) { - assertRequiredKeyLengthIsAllowed(); this.random = random; - try { - this.encKeyGen = KeyGenerator.getInstance(ENC_ALG); - encKeyGen.init(KEY_LEN_BYTES * Byte.SIZE, random); - this.macKeyGen = KeyGenerator.getInstance(MAC_ALG); - macKeyGen.init(KEY_LEN_BYTES * Byte.SIZE, random); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e); - } - } - - private static void assertRequiredKeyLengthIsAllowed() { - if (!isRequiredKeyLengthAllowed()) { - LOG.error("Required key length not supported. See https://github.com/cryptomator/cryptolib/wiki/Restricted-Key-Size."); - throw new IllegalStateException("Required key length not supported."); - } - } - - private static boolean isRequiredKeyLengthAllowed() { - try { - int requiredKeyLengthBits = KEY_LEN_BYTES * Byte.SIZE; - int allowedKeyLengthBits = Cipher.getMaxAllowedKeyLength(ENC_ALG); - return allowedKeyLengthBits >= requiredKeyLengthBits; - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Hard-coded algorithm \"" + ENC_ALG + "\" not supported.", e); - } } @Override - public CryptorImpl createNew() { - SecretKey encKey = encKeyGen.generateKey(); - SecretKey macKey = macKeyGen.generateKey(); - return new CryptorImpl(encKey, macKey, random); - } - - @Override - public CryptorImpl createFromRawKey(byte[] rawKey) throws IllegalArgumentException { - Preconditions.checkArgument(rawKey.length == Constants.KEY_LEN_BYTES + Constants.KEY_LEN_BYTES, "Invalid raw key length %s", rawKey.length); - SecretKey encKey = new SecretKeySpec(rawKey, 0, Constants.KEY_LEN_BYTES, Constants.ENC_ALG); - SecretKey macKey = new SecretKeySpec(rawKey, Constants.KEY_LEN_BYTES, Constants.KEY_LEN_BYTES, Constants.MAC_ALG); - return new CryptorImpl(encKey, macKey, random); - } - - @Override - public CryptorImpl createFromKeyFile(KeyFile keyFile, CharSequence passphrase, int expectedVaultVersion) throws UnsupportedVaultFormatException, InvalidPassphraseException { - return createFromKeyFile(keyFile, passphrase, new byte[0], expectedVaultVersion); - } - - @Override - public CryptorImpl createFromKeyFile(KeyFile keyFile, CharSequence passphrase, byte[] pepper, int expectedVaultVersion) throws UnsupportedVaultFormatException, InvalidPassphraseException { - final KeyFileImpl keyFileImpl = keyFile.as(KeyFileImpl.class); - final byte[] salt = keyFileImpl.scryptSalt; - final byte[] saltAndPepper = new byte[salt.length + pepper.length]; - System.arraycopy(salt, 0, saltAndPepper, 0, salt.length); - System.arraycopy(pepper, 0, saltAndPepper, salt.length, pepper.length); - final byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, keyFileImpl.scryptCostParam, keyFileImpl.scryptBlockSize, KEY_LEN_BYTES); - try { - SecretKey kek = new SecretKeySpec(kekBytes, ENC_ALG); - return createFromKeyFile(keyFileImpl, kek, expectedVaultVersion); - } finally { - Arrays.fill(kekBytes, (byte) 0x00); - } - } - - private CryptorImpl createFromKeyFile(KeyFileImpl keyFile, SecretKey kek, int expectedVaultVersion) throws UnsupportedVaultFormatException, InvalidPassphraseException { - // check version - if (expectedVaultVersion != keyFile.getVersion()) { - throw new UnsupportedVaultFormatException(keyFile.getVersion(), expectedVaultVersion); - } - - try { - SecretKey macKey = AesKeyWrap.unwrap(kek, keyFile.macMasterKey, MAC_ALG); - Mac mac = MacSupplier.HMAC_SHA256.withKey(macKey); - byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(expectedVaultVersion).array()); - if (keyFile.versionMac == null || !MessageDigest.isEqual(versionMac, keyFile.versionMac)) { - // attempted downgrade attack: versionMac doesn't match version. - throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, expectedVaultVersion); - } - SecretKey encKey = AesKeyWrap.unwrap(kek, keyFile.encryptionMasterKey, ENC_ALG); - return new CryptorImpl(encKey, macKey, random); - } catch (InvalidKeyException e) { - throw new InvalidPassphraseException(); - } + public CryptorImpl withKey(Masterkey masterkey) { + return new CryptorImpl(masterkey, random); } } diff --git a/src/main/java/org/cryptomator/cryptolib/v2/KeyFileImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/KeyFileImpl.java deleted file mode 100644 index 81a2ebc..0000000 --- a/src/main/java/org/cryptomator/cryptolib/v2/KeyFileImpl.java +++ /dev/null @@ -1,41 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2015, 2016 Sebastian Stenzel and others. - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.cryptolib.v2; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; -import org.cryptomator.cryptolib.api.KeyFile; - -class KeyFileImpl extends KeyFile { - - @Expose - @SerializedName("scryptSalt") - byte[] scryptSalt; - - @Expose - @SerializedName("scryptCostParam") - int scryptCostParam; - - @Expose - @SerializedName("scryptBlockSize") - int scryptBlockSize; - - @Expose - @SerializedName("primaryMasterKey") - byte[] encryptionMasterKey; - - @Expose - @SerializedName("hmacMasterKey") - byte[] macMasterKey; - - @Expose - @SerializedName("versionMac") - byte[] versionMac; - -} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/CryptorIntegrationTest.java b/src/test/java/org/cryptomator/cryptolib/CryptorIntegrationTest.java deleted file mode 100644 index 3be186e..0000000 --- a/src/test/java/org/cryptomator/cryptolib/CryptorIntegrationTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.cryptomator.cryptolib; - -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.CryptorProvider; -import org.cryptomator.cryptolib.api.FileContentCryptor; -import org.cryptomator.cryptolib.api.FileHeaderCryptor; -import org.cryptomator.cryptolib.api.FileNameCryptor; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.api.KeyFile; -import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import java.security.SecureRandom; -import java.util.Arrays; - -class CryptorIntegrationTest { - - private SecureRandom seeder; - private CryptorProvider cryptorProvider; - - @BeforeEach - public void setup() { - seeder = Mockito.mock(SecureRandom.class); - Mockito.when(seeder.generateSeed(Mockito.anyInt())).then(invocation -> { - int numBytes = invocation.getArgument(0); - return new byte[numBytes]; - }); - cryptorProvider = Cryptors.version1(seeder); - Assertions.assertNotNull(cryptorProvider); - } - - @Test - public void testCreateCryptor() { - Cryptor cryptor = cryptorProvider.createNew(); - Assertions.assertNotNull(cryptor); - FileContentCryptor fileContentCryptor = cryptor.fileContentCryptor(); - FileHeaderCryptor fileHeaderCryptor = cryptor.fileHeaderCryptor(); - FileNameCryptor fileNameCryptor = cryptor.fileNameCryptor(); - Assertions.assertNotNull(fileContentCryptor); - Assertions.assertNotNull(fileHeaderCryptor); - Assertions.assertNotNull(fileNameCryptor); - } - - @Nested - class WithWrittenMasterkeyFile { - - private Cryptor cryptor; - private byte[] pepper; - private CharSequence passphrase; - private byte[] masterkey; - - @BeforeEach - public void setup() { - cryptor = cryptorProvider.createNew(); - pepper = new byte[0]; - passphrase = "password"; - masterkey = cryptor.writeKeysToMasterkeyFile(passphrase, 42).serialize(); - } - - @Test - public void changePassword() throws UnsupportedVaultFormatException, InvalidPassphraseException { - byte[] newMasterkey = Cryptors.changePassphrase(cryptorProvider, masterkey, "password", "betterPassw0rd!"); - Assertions.assertFalse(Arrays.equals(masterkey, newMasterkey)); - - Cryptor newCryptor = cryptorProvider.createFromKeyFile(KeyFile.parse(newMasterkey), "betterPassw0rd!", 42); - Assertions.assertNotNull(newCryptor); - Assertions.assertNotSame(cryptor, newCryptor); - } - - @Test - public void testExportRawKey() throws UnsupportedVaultFormatException, InvalidPassphraseException { - byte[] rawKey = Cryptors.exportRawKey(cryptorProvider, masterkey, pepper, passphrase); - Assertions.assertNotNull(rawKey); - } - - @Nested - class WithExportedRawKey { - - byte[] rawKey; - - @BeforeEach - public void setup() throws UnsupportedVaultFormatException, InvalidPassphraseException { - rawKey = Cryptors.exportRawKey(cryptorProvider, masterkey, pepper, passphrase); - } - - @Test - public void testRestoreRawKey() { - byte[] newMasterkey = Cryptors.restoreRawKey(cryptorProvider, rawKey, pepper, passphrase, 42); - Assertions.assertFalse(Arrays.equals(masterkey, newMasterkey)); - } - - } - - } - -} diff --git a/src/test/java/org/cryptomator/cryptolib/api/KeyFileTest.java b/src/test/java/org/cryptomator/cryptolib/api/KeyFileTest.java deleted file mode 100644 index a6ee54c..0000000 --- a/src/test/java/org/cryptomator/cryptolib/api/KeyFileTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.cryptolib.api; - -import com.google.gson.annotations.Expose; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - -public class KeyFileTest { - - private static final Charset UTF_8 = StandardCharsets.UTF_8; - - @Test - public void testParse() { - final String serialized = "{\"version\":42, \"foo\":\"AAAAAAAAAAA=\", \"hidden\": \"hello world\"}"; - KeyFile keyFile = KeyFile.parse(serialized.getBytes()); - Assertions.assertEquals(42, keyFile.getVersion()); - KeyFileImpl keyFileImpl = keyFile.as(KeyFileImpl.class); - Assertions.assertEquals(42, keyFileImpl.getVersion()); - Assertions.assertArrayEquals(new byte[8], keyFileImpl.foo); - Assertions.assertNull(keyFileImpl.hidden); - } - - @Test - public void testParseInvalid1() { - final String serialized = "{i don't know syntax}"; - Assertions.assertThrows(IllegalArgumentException.class, () -> { - KeyFile.parse(serialized.getBytes()); - }); - } - - @Test - public void testParseInvalid2() { - final byte[] serialized = new byte[10]; - Assertions.assertThrows(IllegalArgumentException.class, () -> { - KeyFile.parse(serialized); - }); - } - - @Test - public void testSerialize() { - KeyFileImpl keyFile = new KeyFileImpl(); - keyFile.foo = new byte[8]; - keyFile.hidden = "hello world"; - String serialized = new String(keyFile.serialize(), UTF_8); - MatcherAssert.assertThat(serialized, CoreMatchers.containsString("\"foo\": \"AAAAAAAAAAA=\"")); - MatcherAssert.assertThat(serialized, CoreMatchers.not(CoreMatchers.containsString("\"hidden\": \"hello world\""))); - } - - private static class KeyFileImpl extends KeyFile { - @Expose - byte[] foo; - - String hidden; - } - -} diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java index 2485e3b..d05a3d6 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java @@ -1,16 +1,29 @@ package org.cryptomator.cryptolib.common; import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.util.Optional; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsNot.not; + public class MasterkeyFileTest { + private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; + @Test public void testParse() throws IOException { final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // @@ -40,16 +53,130 @@ public void testParseMalformed() { }); } - @Test - public void testLoad() throws IOException, CryptoException { - final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; + @Nested + class Unlock { + + @Test + public void testUnlockWithCorrectPassword() throws IOException, CryptoException { + final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; + + MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes())); + MasterkeyLoader keyLoader = masterkeyFile.unlock("asd", new byte[0], Optional.of(3)); + Assertions.assertNotNull(keyLoader); + } + + @Test + public void testUnlockWithIncorrectPassword() throws IOException { + final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; + + MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes())); + Assertions.assertThrows(InvalidPassphraseException.class, () -> { + masterkeyFile.unlock("qwe", new byte[0], Optional.empty()); + }); + } + + @Test + public void testUnlockWithIncorrectPepper() throws IOException { + final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; + + MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes())); + Assertions.assertThrows(InvalidPassphraseException.class, () -> { + masterkeyFile.unlock("qwe", new byte[3], Optional.empty()); + }); + } + + @Test + public void testUnlockWithIncorrectVaultFormat() throws IOException { + final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; + + MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes())); + Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> { + masterkeyFile.unlock("asd", new byte[0], Optional.of(42)); + }); + } + + @Test + public void testUnlockWithIncorrectVersionMac() throws IOException { + final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"versionMac\":\"AAAA\"}"; + + MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes())); + Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> { + masterkeyFile.unlock("asd", new byte[0], Optional.of(3)); + }); + } + + @Test + public void testUnlockWithIgnoredVersionMac() throws IOException, CryptoException { + final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + + "\"versionMac\":\"AAAA\"}"; + + MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes())); + MasterkeyLoader keyLoader = masterkeyFile.unlock("asd", new byte[0], Optional.empty()); + Assertions.assertNotNull(keyLoader); + } - MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes())); - MasterkeyLoader keyLoader = masterkeyFile.unlock("asd", new byte[0], Optional.empty()); - Assertions.assertNotNull(keyLoader); } + @Nested + class Lock { + + @Test + public void testLock() { + byte[] serialized; + try (Masterkey masterkey = Masterkey.createFromRaw(new byte[64])) { + serialized = MasterkeyFile.lock(masterkey, "asd", new byte[0], 3, RANDOM_MOCK); + } + + String serializedStr = new String(serialized, StandardCharsets.UTF_8); + MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"version\": 3")); + MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptSalt\": \"AAAAAAAAAAA=\"")); + MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptCostParam\": 32768")); + MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptBlockSize\": 8")); + MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"primaryMasterKey\": \"bOuDTfSpTHJrM4G321gts1QL+TFAZ3I6S/QHwim39pz+t+/K9IYy6g==\"")); + MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"hmacMasterKey\": \"bOuDTfSpTHJrM4G321gts1QL+TFAZ3I6S/QHwim39pz+t+/K9IYy6g==\"")); + MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"versionMac\": \"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"")); + } + + @Test + public void testLockWithDifferentPeppers() { + byte[] serialized1, serialized2; + try (Masterkey masterkey = Masterkey.createFromRaw(new byte[64])) { + serialized1 = MasterkeyFile.lock(masterkey, "asd", new byte[] {(byte) 0x01}, 8, RANDOM_MOCK); + serialized2 = MasterkeyFile.lock(masterkey, "asd", new byte[] {(byte) 0x02}, 8, RANDOM_MOCK); + } + + MatcherAssert.assertThat(serialized1, not(equalTo(serialized2))); + } + + } + + @Test + public void testChangePassword() throws IOException, CryptoException { + Masterkey masterkey = Masterkey.createFromRaw(new byte[64]); + byte[] serialized1 = MasterkeyFile.lock(masterkey, "password", new byte[0], 42, RANDOM_MOCK); + byte[] serialized2 = MasterkeyFile.changePassphrase(serialized1, "password", "betterPassw0rd!", new byte[0], RANDOM_MOCK); + Masterkey unlocked1 = MasterkeyFile.withContent(new ByteArrayInputStream(serialized1)).unlock("password", new byte[0], Optional.of(42)).loadKey(MasterkeyFileLoader.KEY_ID); + Masterkey unlocked2 = MasterkeyFile.withContent(new ByteArrayInputStream(serialized2)).unlock("betterPassw0rd!", new byte[0], Optional.of(42)).loadKey(MasterkeyFileLoader.KEY_ID); + + MatcherAssert.assertThat(serialized1, not(equalTo(serialized2))); + Assertions.assertNotSame(unlocked1, unlocked2); + Assertions.assertArrayEquals(unlocked1.getEncoded(), unlocked2.getEncoded()); + } } \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java index e8530bb..cff298f 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java @@ -6,11 +6,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import javax.crypto.SecretKey; import javax.security.auth.DestroyFailedException; import java.security.SecureRandom; +import java.util.Random; +import java.util.stream.Stream; public class MasterkeyTest { @@ -35,6 +38,24 @@ public void testCreateNew() { Assertions.assertNotNull(masterkey); } + @ParameterizedTest + @MethodSource("create64RandomBytes") + public void testCreateFromRawKey(byte[] encoded) { + Masterkey masterkey = Masterkey.createFromRaw(encoded); + + Assertions.assertNotNull(masterkey); + Assertions.assertArrayEquals(encoded, masterkey.getEncoded()); + } + + static Stream create64RandomBytes() { + Random rnd = new Random(42l); + return Stream.generate(() -> { + byte[] bytes = new byte[64]; + rnd.nextBytes(bytes); + return bytes; + }).limit(10); + } + @Test public void testGetEncKey() { SecretKey encKey = masterkey.getEncKey(); diff --git a/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java index be24058..908f3ee 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v1; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; @@ -32,19 +33,12 @@ public class CryptorImplTest { private static final Charset UTF_8 = StandardCharsets.UTF_8; private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; - private SecretKey encKey; - private SecretKey macKey; - - @BeforeEach - public void setup() { - encKey = new SecretKeySpec(new byte[32], "AES"); - macKey = new SecretKeySpec(new byte[32], "HmacSHA256"); - } + private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]); @Test public void testWriteKeysToMasterkeyFile() { final byte[] serialized; - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { serialized = cryptor.writeKeysToMasterkeyFile("asd", 3).serialize(); } String serializedStr = new String(serialized, UTF_8); @@ -59,70 +53,52 @@ public void testWriteKeysToMasterkeyFile() { @Test public void testWriteKeysToMasterkeyFileWithPepper() { - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { byte[] serialized1 = cryptor.writeKeysToMasterkeyFile("asd", new byte[] {(byte) 0x01}, 3).serialize(); byte[] serialized2 = cryptor.writeKeysToMasterkeyFile("asd", new byte[] {(byte) 0x02}, 3).serialize(); MatcherAssert.assertThat(serialized1, not(equalTo(serialized2))); } } - - @Test - public void testGetRawKey() { - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { - byte[] rawKey = cryptor.getRawKey(); - Assertions.assertArrayEquals(Arrays.copyOf(rawKey, Constants.KEY_LEN_BYTES), encKey.getEncoded()); - Assertions.assertArrayEquals(Arrays.copyOfRange(rawKey, Constants.KEY_LEN_BYTES, 2*Constants.KEY_LEN_BYTES), macKey.getEncoded()); - } - } @Test public void testGetFileContentCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileContentCryptor(), CoreMatchers.instanceOf(FileContentCryptorImpl.class)); } } @Test public void testGetFileHeaderCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileHeaderCryptor(), CoreMatchers.instanceOf(FileHeaderCryptorImpl.class)); } } @Test public void testGetFileNameCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileNameCryptor(), CoreMatchers.instanceOf(FileNameCryptorImpl.class)); } } @Test - public void testExplicitDestruction() throws DestroyFailedException { - DestroyableSecretKey encKey = Mockito.mock(DestroyableSecretKey.class); - DestroyableSecretKey macKey = Mockito.mock(DestroyableSecretKey.class); - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + public void testExplicitDestruction() { + Masterkey masterkey = Mockito.mock(Masterkey.class); + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { cryptor.destroy(); - Mockito.verify(encKey).destroy(); - Mockito.verify(macKey).destroy(); - Mockito.when(encKey.isDestroyed()).thenReturn(true); - Mockito.when(macKey.isDestroyed()).thenReturn(true); + Mockito.verify(masterkey).destroy(); + Mockito.when(masterkey.isDestroyed()).thenReturn(true); Assertions.assertTrue(cryptor.isDestroyed()); } } @Test - public void testImplicitDestruction() throws DestroyFailedException { - DestroyableSecretKey encKey = Mockito.mock(DestroyableSecretKey.class); - DestroyableSecretKey macKey = Mockito.mock(DestroyableSecretKey.class); - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + public void testImplicitDestruction() { + Masterkey masterkey = Mockito.mock(Masterkey.class); + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { Assertions.assertFalse(cryptor.isDestroyed()); } - Mockito.verify(encKey).destroy(); - Mockito.verify(macKey).destroy(); - } - - private interface DestroyableSecretKey extends SecretKey, Destroyable { - // In Java7 SecretKey doesn't implement Destroyable... + Mockito.verify(masterkey).destroy(); } } diff --git a/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java index 3ec2580..cc877e9 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java @@ -8,20 +8,14 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v1; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.api.KeyFile; -import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; import java.security.SecureRandom; -import java.util.Random; -import java.util.stream.Stream; public class CryptorProviderImplTest { @@ -35,126 +29,10 @@ public void setup() { } @Test - public void testCreateNew() { - CryptorImpl cryptor = cryptorProvider.createNew(); + public void testWithKey() { + Masterkey masterkey = Mockito.mock(Masterkey.class); + CryptorImpl cryptor = cryptorProvider.withKey(masterkey); Assertions.assertNotNull(cryptor); } - @ParameterizedTest - @MethodSource("create64RandomBytes") - public void testCreateFromRawKey(byte[] rawKey) { - CryptorImpl cryptor = cryptorProvider.createFromRawKey(rawKey); - Assertions.assertNotNull(cryptor); - Assertions.assertArrayEquals(rawKey, cryptor.getRawKey()); - } - - static Stream create64RandomBytes() { - Random rnd = new Random(42l); - return Stream.generate(() -> { - byte[] bytes = new byte[64]; - rnd.nextBytes(bytes); - return bytes; - }).limit(10); - } - - @Test - public void testCreateFromInvalidRawKey() { - Assertions.assertThrows(IllegalArgumentException.class, () -> { - cryptorProvider.createFromRawKey(new byte[3]); - }); - } - - @Test - public void testCreateFromKeyWithCorrectPassphrase() throws UnsupportedVaultFormatException, InvalidPassphraseException { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - CryptorImpl cryptor = cryptorProvider.createFromKeyFile(keyFile, "asd", 3); - Assertions.assertNotNull(cryptor); - } - - @Test - public void testCreateFromKeyWithWrongPassphrase() { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - Assertions.assertThrows(InvalidPassphraseException.class, () -> { - cryptorProvider.createFromKeyFile(keyFile, "qwe", 3); - }); - } - - @Test - public void testCreateFromKeyWithPepper() throws UnsupportedVaultFormatException, InvalidPassphraseException { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // - + "\"hmacMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - CryptorImpl cryptor = cryptorProvider.createFromKeyFile(keyFile, "asd", new byte[]{(byte) 0x01}, 3); - Assertions.assertNotNull(cryptor); - } - - @Test - public void testCreateFromKeyWithWrongPepper() { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // - + "\"hmacMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - Assertions.assertThrows(InvalidPassphraseException.class, () -> { - cryptorProvider.createFromKeyFile(keyFile, "asd", new byte[]{(byte) 0x02}, 3); - }); - } - - @Test - public void testCreateFromKeyWithWrongVaultFormat() { - final String testMasterKey = "{\"version\":-1,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - UnsupportedVaultFormatException exception = Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> { - cryptorProvider.createFromKeyFile(keyFile, "asd", 3); - }); - Assertions.assertTrue(exception.isVaultOlderThanSoftware()); - Assertions.assertFalse(exception.isSoftwareOlderThanVault()); - Assertions.assertEquals(-1, exception.getDetectedVersion()); - Assertions.assertEquals(3, exception.getSupportedVersion()); - } - - @Test - public void testCreateFromKeyWithMissingVersionMac() { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - UnsupportedVaultFormatException exception = Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> { - cryptorProvider.createFromKeyFile(keyFile, "asd", 3); - }); - Assertions.assertFalse(exception.isVaultOlderThanSoftware()); - Assertions.assertTrue(exception.isSoftwareOlderThanVault()); - Assertions.assertEquals(Integer.MAX_VALUE, exception.getDetectedVersion()); - Assertions.assertEquals(3, exception.getSupportedVersion()); - } - - @Test - public void testCreateFromKeyWithWrongVersionMac() { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLa=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - UnsupportedVaultFormatException exception = Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> { - cryptorProvider.createFromKeyFile(keyFile, "asd", 3); - }); - Assertions.assertFalse(exception.isVaultOlderThanSoftware()); - Assertions.assertTrue(exception.isSoftwareOlderThanVault()); - Assertions.assertEquals(Integer.MAX_VALUE, exception.getDetectedVersion()); - Assertions.assertEquals(3, exception.getSupportedVersion()); - } - } diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java index e475a10..9bd881a 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java @@ -19,6 +19,7 @@ import javax.crypto.spec.SecretKeySpec; import org.cryptomator.cryptolib.EncryptingWritableByteChannel; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -42,14 +43,13 @@ public class FileContentEncryptorBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final SecretKey ENC_KEY = new SecretKeySpec(new byte[32], "AES"); - private static final SecretKey MAC_KEY = new SecretKeySpec(new byte[32], "HmacSHA256"); + private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]); private CryptorImpl cryptor; @Setup(Level.Iteration) public void shuffleData() { - cryptor = new CryptorImpl(ENC_KEY, MAC_KEY, RANDOM_MOCK); + cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK); } @Benchmark diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorTest.java index a34b327..880b4c0 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorTest.java @@ -11,6 +11,7 @@ import org.cryptomator.cryptolib.DecryptingReadableByteChannel; import org.cryptomator.cryptolib.EncryptingWritableByteChannel; import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.cryptomator.cryptolib.common.SeekableByteChannelMock; import org.hamcrest.CoreMatchers; @@ -31,14 +32,13 @@ public class FileContentEncryptorTest { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final SecretKey ENC_KEY = new SecretKeySpec(new byte[32], "AES"); - private static final SecretKey MAC_KEY = new SecretKeySpec(new byte[32], "HmacSHA256"); + private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]); private CryptorImpl cryptor; @BeforeEach public void setup() { - cryptor = new CryptorImpl(ENC_KEY, MAC_KEY, RANDOM_MOCK); + cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK); } @Test diff --git a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java index 0e916a6..410b7e5 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; @@ -32,19 +33,12 @@ public class CryptorImplTest { private static final Charset UTF_8 = StandardCharsets.UTF_8; private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; - private SecretKey encKey; - private SecretKey macKey; - - @BeforeEach - public void setup() { - encKey = new SecretKeySpec(new byte[32], "AES"); - macKey = new SecretKeySpec(new byte[32], "HmacSHA256"); - } + private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]); @Test public void testWriteKeysToMasterkeyFile() { final byte[] serialized; - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { serialized = cryptor.writeKeysToMasterkeyFile("asd", 3).serialize(); } String serializedStr = new String(serialized, UTF_8); @@ -59,70 +53,52 @@ public void testWriteKeysToMasterkeyFile() { @Test public void testWriteKeysToMasterkeyFileWithPepper() { - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { byte[] serialized1 = cryptor.writeKeysToMasterkeyFile("asd", new byte[] {(byte) 0x01}, 3).serialize(); byte[] serialized2 = cryptor.writeKeysToMasterkeyFile("asd", new byte[] {(byte) 0x02}, 3).serialize(); MatcherAssert.assertThat(serialized1, not(equalTo(serialized2))); } } - - @Test - public void testGetRawKey() { - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { - byte[] rawKey = cryptor.getRawKey(); - Assertions.assertArrayEquals(Arrays.copyOf(rawKey, Constants.KEY_LEN_BYTES), encKey.getEncoded()); - Assertions.assertArrayEquals(Arrays.copyOfRange(rawKey, Constants.KEY_LEN_BYTES, 2* Constants.KEY_LEN_BYTES), macKey.getEncoded()); - } - } @Test public void testGetFileContentCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileContentCryptor(), CoreMatchers.instanceOf(FileContentCryptorImpl.class)); } } @Test public void testGetFileHeaderCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileHeaderCryptor(), CoreMatchers.instanceOf(FileHeaderCryptorImpl.class)); } } @Test public void testGetFileNameCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileNameCryptor(), CoreMatchers.instanceOf(FileNameCryptorImpl.class)); } } @Test - public void testExplicitDestruction() throws DestroyFailedException { - DestroyableSecretKey encKey = Mockito.mock(DestroyableSecretKey.class); - DestroyableSecretKey macKey = Mockito.mock(DestroyableSecretKey.class); - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + public void testExplicitDestruction() { + Masterkey masterkey = Mockito.mock(Masterkey.class); + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { cryptor.destroy(); - Mockito.verify(encKey).destroy(); - Mockito.verify(macKey).destroy(); - Mockito.when(encKey.isDestroyed()).thenReturn(true); - Mockito.when(macKey.isDestroyed()).thenReturn(true); + Mockito.verify(masterkey).destroy(); + Mockito.when(masterkey.isDestroyed()).thenReturn(true); Assertions.assertTrue(cryptor.isDestroyed()); } } @Test - public void testImplicitDestruction() throws DestroyFailedException { - DestroyableSecretKey encKey = Mockito.mock(DestroyableSecretKey.class); - DestroyableSecretKey macKey = Mockito.mock(DestroyableSecretKey.class); - try (CryptorImpl cryptor = new CryptorImpl(encKey, macKey, RANDOM_MOCK)) { + public void testImplicitDestruction() { + Masterkey masterkey = Mockito.mock(Masterkey.class); + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { Assertions.assertFalse(cryptor.isDestroyed()); } - Mockito.verify(encKey).destroy(); - Mockito.verify(macKey).destroy(); - } - - private interface DestroyableSecretKey extends SecretKey, Destroyable { - // In Java7 SecretKey doesn't implement Destroyable... + Mockito.verify(masterkey).destroy(); } } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java index 5551286..15c263f 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java @@ -8,19 +8,14 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.api.KeyFile; -import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; import java.security.SecureRandom; -import java.util.Random; -import java.util.stream.Stream; public class CryptorProviderImplTest { @@ -34,126 +29,10 @@ public void setup() { } @Test - public void testCreateNew() { - CryptorImpl cryptor = cryptorProvider.createNew(); + public void testWithKey() { + Masterkey masterkey = Mockito.mock(Masterkey.class); + CryptorImpl cryptor = cryptorProvider.withKey(masterkey); Assertions.assertNotNull(cryptor); } - @ParameterizedTest - @MethodSource("create64RandomBytes") - public void testCreateFromRawKey(byte[] rawKey) { - CryptorImpl cryptor = cryptorProvider.createFromRawKey(rawKey); - Assertions.assertNotNull(cryptor); - Assertions.assertArrayEquals(rawKey, cryptor.getRawKey()); - } - - static Stream create64RandomBytes() { - Random rnd = new Random(42l); - return Stream.generate(() -> { - byte[] bytes = new byte[64]; - rnd.nextBytes(bytes); - return bytes; - }).limit(10); - } - - @Test - public void testCreateFromInvalidRawKey() { - Assertions.assertThrows(IllegalArgumentException.class, () -> { - cryptorProvider.createFromRawKey(new byte[3]); - }); - } - - @Test - public void testCreateFromKeyWithCorrectPassphrase() throws UnsupportedVaultFormatException, InvalidPassphraseException { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - CryptorImpl cryptor = cryptorProvider.createFromKeyFile(keyFile, "asd", 3); - Assertions.assertNotNull(cryptor); - } - - @Test - public void testCreateFromKeyWithWrongPassphrase() { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - Assertions.assertThrows(InvalidPassphraseException.class, () -> { - cryptorProvider.createFromKeyFile(keyFile, "qwe", 3); - }); - } - - @Test - public void testCreateFromKeyWithPepper() throws UnsupportedVaultFormatException, InvalidPassphraseException { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // - + "\"hmacMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - CryptorImpl cryptor = cryptorProvider.createFromKeyFile(keyFile, "asd", new byte[]{(byte) 0x01}, 3); - Assertions.assertNotNull(cryptor); - } - - @Test - public void testCreateFromKeyWithWrongPepper() { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // - + "\"hmacMasterKey\":\"jkF3rc0WQsntEMlvXSLkquBLPlSYfOUDXDg90VHcj6irG4X/TOGJhA==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - Assertions.assertThrows(InvalidPassphraseException.class, () -> { - cryptorProvider.createFromKeyFile(keyFile, "asd", new byte[]{(byte) 0x02}, 3); - }); - } - - @Test - public void testCreateFromKeyWithWrongVaultFormat() { - final String testMasterKey = "{\"version\":-1,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - UnsupportedVaultFormatException exception = Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> { - cryptorProvider.createFromKeyFile(keyFile, "asd", 3); - }); - Assertions.assertTrue(exception.isVaultOlderThanSoftware()); - Assertions.assertFalse(exception.isSoftwareOlderThanVault()); - Assertions.assertEquals(-1, exception.getDetectedVersion()); - Assertions.assertEquals(3, exception.getSupportedVersion()); - } - - @Test - public void testCreateFromKeyWithMissingVersionMac() { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - UnsupportedVaultFormatException exception = Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> { - cryptorProvider.createFromKeyFile(keyFile, "asd", 3); - }); - Assertions.assertFalse(exception.isVaultOlderThanSoftware()); - Assertions.assertTrue(exception.isSoftwareOlderThanVault()); - Assertions.assertEquals(Integer.MAX_VALUE, exception.getDetectedVersion()); - Assertions.assertEquals(3, exception.getSupportedVersion()); - } - - @Test - public void testCreateFromKeyWithWrongVersionMac() { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLa=\"}"; - KeyFile keyFile = KeyFile.parse(testMasterKey.getBytes()); - UnsupportedVaultFormatException exception = Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> { - cryptorProvider.createFromKeyFile(keyFile, "asd", 3); - }); - Assertions.assertFalse(exception.isVaultOlderThanSoftware()); - Assertions.assertTrue(exception.isSoftwareOlderThanVault()); - Assertions.assertEquals(Integer.MAX_VALUE, exception.getDetectedVersion()); - Assertions.assertEquals(3, exception.getSupportedVersion()); - } - } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java index 42a000b..12f48b1 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java @@ -19,6 +19,7 @@ import javax.crypto.spec.SecretKeySpec; import org.cryptomator.cryptolib.EncryptingWritableByteChannel; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -42,14 +43,13 @@ public class FileContentEncryptorBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final SecretKey ENC_KEY = new SecretKeySpec(new byte[32], "AES"); - private static final SecretKey MAC_KEY = new SecretKeySpec(new byte[32], "HmacSHA256"); + private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]); private CryptorImpl cryptor; @Setup(Level.Iteration) public void shuffleData() { - cryptor = new CryptorImpl(ENC_KEY, MAC_KEY, RANDOM_MOCK); + cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK); } @Benchmark @@ -93,39 +93,39 @@ public boolean isOpen() { } @Override - public void close() throws IOException { + public void close() { open = false; } @Override - public int read(ByteBuffer dst) throws IOException { + public int read(ByteBuffer dst) { throw new UnsupportedOperationException(); } @Override - public int write(ByteBuffer src) throws IOException { + public int write(ByteBuffer src) { int delta = src.remaining(); src.position(src.position() + delta); return delta; } @Override - public long position() throws IOException { + public long position() { return 0; } @Override - public SeekableByteChannel position(long newPosition) throws IOException { + public SeekableByteChannel position(long newPosition) { return this; } @Override - public long size() throws IOException { + public long size() { return 0; } @Override - public SeekableByteChannel truncate(long size) throws IOException { + public SeekableByteChannel truncate(long size) { return this; } From 9c131cc9d8517d824c40482d7215dcb4e689898c Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 29 Nov 2020 01:30:01 +0100 Subject: [PATCH 07/59] Removed obsolete APIs --- .../org/cryptomator/cryptolib/Cryptors.java | 92 ------------------- .../cryptomator/cryptolib/api/Cryptor.java | 28 ------ .../cryptomator/cryptolib/package-info.java | 89 +++++++++++------- .../cryptomator/cryptolib/v1/CryptorImpl.java | 60 ------------ .../cryptomator/cryptolib/v2/CryptorImpl.java | 60 ------------ .../cryptomator/cryptolib/CryptorsTest.java | 12 --- .../cryptolib/v1/CryptorImplTest.java | 37 -------- .../cryptolib/v2/CryptorImplTest.java | 37 -------- 8 files changed, 56 insertions(+), 359 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/Cryptors.java b/src/main/java/org/cryptomator/cryptolib/Cryptors.java index 9cd0c42..f432e71 100644 --- a/src/main/java/org/cryptomator/cryptolib/Cryptors.java +++ b/src/main/java/org/cryptomator/cryptolib/Cryptors.java @@ -8,24 +8,13 @@ *******************************************************************************/ package org.cryptomator.cryptolib; -import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.api.FileHeaderCryptor; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.api.KeyFile; -import org.cryptomator.cryptolib.api.Masterkey; -import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; -import org.cryptomator.cryptolib.common.MasterkeyFile; -import org.cryptomator.cryptolib.common.MasterkeyFileLoader; import org.cryptomator.cryptolib.common.ReseedingSecureRandom; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.UncheckedIOException; import java.security.SecureRandom; -import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; @@ -91,85 +80,4 @@ public static long ciphertextSize(long cleartextSize, Cryptor cryptor) { return ciphertextChunkSize * numFullChunks + additionalCiphertextBytes; } - /** - * Reencrypts a masterkey with a new passphrase. - * - * @param cryptorProvider A suitable CryptorProvider instance, i.e. same version as the original masterkey has been created with. - * @param masterkey The original JSON representation of the masterkey - * @param oldPassphrase The old passphrase - * @param newPassphrase The new passphrase - * @return A JSON representation of the masterkey, now encrypted with newPassphrase - * @throws InvalidPassphraseException If the wrong oldPassphrase has been supplied for the masterkey - * @see #changePassphrase(CryptorProvider, byte[], byte[], CharSequence, CharSequence) - * @since 1.1.0 - */ - @Deprecated - public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] masterkey, CharSequence oldPassphrase, CharSequence newPassphrase) throws CryptoException { - return changePassphrase(cryptorProvider, masterkey, new byte[0], oldPassphrase, newPassphrase); - } - - /** - * Reencrypts a masterkey with a new passphrase. - * - * @param cryptorProvider A suitable CryptorProvider instance, i.e. same version as the original masterkey has been created with. - * @param masterkey The original JSON representation of the masterkey - * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) - * @param oldPassphrase The old passphrase - * @param newPassphrase The new passphrase - * @return A JSON representation of the masterkey, now encrypted with newPassphrase - * @throws InvalidPassphraseException If the wrong oldPassphrase has been supplied for the masterkey - * @since 1.1.4 - */ - @Deprecated - public static byte[] changePassphrase(CryptorProvider cryptorProvider, byte[] masterkey, byte[] pepper, CharSequence oldPassphrase, CharSequence newPassphrase) throws CryptoException { - final KeyFile keyFile = KeyFile.parse(masterkey); - try (MasterkeyFileLoader loader = MasterkeyFile.withContent(new ByteArrayInputStream(masterkey)).unlock(oldPassphrase, pepper, Optional.empty()); - Masterkey key = loader.loadKey(MasterkeyFileLoader.KEY_ID); - Cryptor cryptor = cryptorProvider.withKey(key)) { - return cryptor.writeKeysToMasterkeyFile(newPassphrase, pepper, keyFile.getVersion()).serialize(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - /** - * Decrypts the raw key from a given JSON-encoded masterkey file. - * - * @param cryptorProvider A suitable CryptorProvider instance, i.e. same version as the original masterkey has been created with. - * @param masterkey The original JSON representation of the masterkey - * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) - * @param passphrase The passphrase - * @return The raw key - * @throws InvalidPassphraseException If the wrong passphrase has been supplied for the masterkey - * @since 1.3.0 - */ - @Deprecated - public static byte[] exportRawKey(CryptorProvider cryptorProvider, byte[] masterkey, byte[] pepper, CharSequence passphrase) throws UnsupportedVaultFormatException, CryptoException { - try (MasterkeyFileLoader loader = MasterkeyFile.withContent(new ByteArrayInputStream(masterkey)).unlock(passphrase, pepper, Optional.empty()); - Masterkey key = loader.loadKey(MasterkeyFileLoader.KEY_ID)) { - return key.getEncoded(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - /** - * Encrypts a raw key to a JSON-encoded masterkey file. - * - * @param cryptorProvider A suitable CryptorProvider instance, i.e. same version as the original masterkey has been created with. - * @param rawKey The original JSON representation of the masterkey - * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) - * @param passphrase The passphrase - * @param vaultVersion The version of the vault for which to recreate a masterkey file - * @return The json-encoded masterkey protected by the passphrase - * @since 1.3.0 - */ - @Deprecated - public static byte[] restoreRawKey(CryptorProvider cryptorProvider, byte[] rawKey, byte[] pepper, CharSequence passphrase, int vaultVersion) { - try (Masterkey key = Masterkey.createFromRaw(rawKey); - Cryptor cryptor = cryptorProvider.withKey(key)) { - return cryptor.writeKeysToMasterkeyFile(passphrase, pepper, vaultVersion).serialize(); - } - } - } diff --git a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java index cf08c14..5e79c2c 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java @@ -18,34 +18,6 @@ public interface Cryptor extends Destroyable, AutoCloseable { FileNameCryptor fileNameCryptor(); - /** - * Shortcut for {@link #writeKeysToMasterkeyFile(CharSequence, byte[], int)} with en empty pepper. - * - * @param passphrase The passphrase used to encrypt the key material. - * @param vaultVersion Will be checked upon decryption of this masterkey. - * @return Encrypted data that can be stored in insecure locations. - * @see #writeKeysToMasterkeyFile(CharSequence, byte[], int) - */ - @Deprecated - KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, int vaultVersion); - - /** - * @param passphrase The passphrase used to encrypt the key material. - * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) - * @param vaultVersion Will be checked upon decryption of this masterkey. - * @return Encrypted data that can be stored in insecure locations. - * @since 1.1.0 - */ - @Deprecated - KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, byte[] pepper, int vaultVersion); - - /** - * @return All key material of this cryptor - * @since 1.3.0 - */ - @Deprecated - byte[] getRawKey(); - @Override void destroy(); diff --git a/src/main/java/org/cryptomator/cryptolib/package-info.java b/src/main/java/org/cryptomator/cryptolib/package-info.java index 588b58a..ff44ae7 100644 --- a/src/main/java/org/cryptomator/cryptolib/package-info.java +++ b/src/main/java/org/cryptomator/cryptolib/package-info.java @@ -1,54 +1,77 @@ -/******************************************************************************* - Cryptomator Crypto Library - Copyright (C) 2016 Sebastian Stenzel and others. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - *******************************************************************************/ /** * High-level encryption library used in Cryptomator. *

* Example Usage: * *

- * // Create new cryptor and save to masterkey file:
- * String password = "dadada";
- * {@link org.cryptomator.cryptolib.api.Cryptor Cryptor} cryptor = {@link org.cryptomator.cryptolib.Cryptors#version1(java.security.SecureRandom) Cryptors.version1(SecureRandom.getInstanceStrong())}.{@link org.cryptomator.cryptolib.api.CryptorProvider#createNew() createNew()};
- * KeyFile keyFile = cryptor.{@link org.cryptomator.cryptolib.api.Cryptor#writeKeysToMasterkeyFile(CharSequence, int) writeKeysToMasterkeyFile(password, 42)};
- * byte[] masterkeyFileContents = keyFile.{@link org.cryptomator.cryptolib.api.KeyFile#serialize() serialize()};
- * Files.write(pathToMasterkeyJsonFile, masterkeyFileContents, WRITE, CREATE, TRUNCATE_EXISTING);
- * 
- * // Create Cryptor from existing masterkey file:
- * byte[] masterkeyFileContents = Files.readAllBytes(pathToMasterkeyJsonFile);
- * String password = "dadada";
- * KeyFile keyFile = KeyFile.{@link org.cryptomator.cryptolib.api.KeyFile#parse(byte[]) parse(masterkeyFileContents)}
- * Cryptor cryptor = {@link org.cryptomator.cryptolib.api.CryptorProvider#createFromKeyFile(org.cryptomator.cryptolib.api.KeyFile, java.lang.CharSequence, int) CryptorProvider.createFromKeyFile(keyFile, password, 42)};
- * 
+ * // Create new masterkey and safe it to a file:
+ * SecureRandom csprng = SecureRandom.getInstanceStrong();
+ * Masterkey masterkey = {@link org.cryptomator.cryptolib.api.Masterkey#createNew(java.security.SecureRandom) Masterkey.createNew(csprng)};
+ * byte[] json = {@link org.cryptomator.cryptolib.common.MasterkeyFile#lock(org.cryptomator.cryptolib.api.Masterkey, java.lang.CharSequence, byte[], int, java.security.SecureRandom) MasterkeyFile.lock(masterkey, passphrase, pepper, vaultVersion, csprng)};
+ * Files.write(path, json);
+ *
+ * // Load a masterkey from a file:
+ * MasterkeyFileLoader loader = {@link org.cryptomator.cryptolib.common.MasterkeyFile#withContentFromFile(java.nio.file.Path) MasterkeyFile.withContentsFromFile(path)}.{@link org.cryptomator.cryptolib.common.MasterkeyFile#unlock(java.lang.CharSequence, byte[], java.util.Optional) unlock(passphrase, pepper, Optional.of(vaultVersion))};
+ * Masterkey masterkey = loader.load(MasterkeyFileLoader.KEY_ID);
+ *
+ * // Create new cryptor:
+ * {@link org.cryptomator.cryptolib.api.Cryptor Cryptor} cryptor = {@link org.cryptomator.cryptolib.Cryptors#version1(java.security.SecureRandom) Cryptors.version1(SecureRandom.getInstanceStrong())}.{@link org.cryptomator.cryptolib.api.CryptorProvider#withKey(org.cryptomator.cryptolib.api.Masterkey) withKey(masterkey)};
+ *
  * // Each directory needs a (relatively) unique ID, which affects the encryption/decryption of child names:
  * String uniqueIdOfDirectory = UUID.randomUUID().toString();
- * 
+ *
  * // Encrypt and decrypt file name:
  * String cleartextFileName = "foo.txt";
  * String encryptedName = cryptor.{@link org.cryptomator.cryptolib.api.Cryptor#fileNameCryptor() fileNameCryptor()}.{@link org.cryptomator.cryptolib.api.FileNameCryptor#encryptFilename(String, byte[][])  encryptFilename(cleartextFileName, uniqueIdOfDirectory.getBytes())};
  * String decryptedName = cryptor.fileNameCryptor().{@link org.cryptomator.cryptolib.api.FileNameCryptor#decryptFilename(String, byte[][])  decryptFilename(encryptedName, uniqueIdOfDirectory.getBytes())};
- * 
+ *
  * // Encrypt file contents:
  * ByteBuffer plaintext = ...;
  * SeekableByteChannel ciphertextOut = ...;
  * try (WritableByteChannel ch = new {@link org.cryptomator.cryptolib.EncryptingWritableByteChannel EncryptingWritableByteChannel}(ciphertextOut, cryptor)) {
  * 	ch.write(plaintext);
  * }
- * 
+ *
+ * // Decrypt file contents:
+ * ReadableByteChannel ciphertextIn = ...;
+ * try (ReadableByteChannel ch = new {@link org.cryptomator.cryptolib.DecryptingReadableByteChannel DecryptingReadableByteChannel}(ciphertextOut, cryptor, true)) {
+ * 	ch.read(plaintext);
+ * }
+ * 
+ */ +/** + * High-level encryption library used in Cryptomator. + *

+ * Example Usage: + * + *

+ * // Create new masterkey and safe it to a file:
+ * SecureRandom csprng = SecureRandom.getInstanceStrong();
+ * Masterkey masterkey = {@link org.cryptomator.cryptolib.api.Masterkey#createNew(java.security.SecureRandom) Masterkey.createNew(csprng)};
+ * byte[] json = {@link org.cryptomator.cryptolib.common.MasterkeyFile#lock(org.cryptomator.cryptolib.api.Masterkey, java.lang.CharSequence, byte[], int, java.security.SecureRandom) MasterkeyFile.lock(masterkey, passphrase, pepper, vaultVersion, csprng)};
+ * Files.write(path, json);
+ *
+ * // Load a masterkey from a file:
+ * MasterkeyFileLoader loader = {@link org.cryptomator.cryptolib.common.MasterkeyFile#withContentFromFile(java.nio.file.Path) MasterkeyFile.withContentsFromFile(path)}.{@link org.cryptomator.cryptolib.common.MasterkeyFile#unlock(java.lang.CharSequence, byte[], java.util.Optional) unlock(passphrase, pepper, Optional.of(vaultVersion))};
+ * Masterkey masterkey = loader.load(MasterkeyFileLoader.KEY_ID);
+ *
+ * // Create new cryptor:
+ * {@link org.cryptomator.cryptolib.api.Cryptor Cryptor} cryptor = {@link org.cryptomator.cryptolib.Cryptors#version1(java.security.SecureRandom) Cryptors.version1(SecureRandom.getInstanceStrong())}.{@link org.cryptomator.cryptolib.api.CryptorProvider#withKey(org.cryptomator.cryptolib.api.Masterkey) withKey(masterkey)};
+ * // Each directory needs a (relatively) unique ID, which affects the encryption/decryption of child names:
+ * String uniqueIdOfDirectory = UUID.randomUUID().toString();
+ *
+ * // Encrypt and decrypt file name:
+ * String cleartextFileName = "foo.txt";
+ * String encryptedName = cryptor.{@link org.cryptomator.cryptolib.api.Cryptor#fileNameCryptor() fileNameCryptor()}.{@link org.cryptomator.cryptolib.api.FileNameCryptor#encryptFilename(String, byte[][])  encryptFilename(cleartextFileName, uniqueIdOfDirectory.getBytes())};
+ * String decryptedName = cryptor.fileNameCryptor().{@link org.cryptomator.cryptolib.api.FileNameCryptor#decryptFilename(String, byte[][])  decryptFilename(encryptedName, uniqueIdOfDirectory.getBytes())};
+ *
+ * // Encrypt file contents:
+ * ByteBuffer plaintext = ...;
+ * SeekableByteChannel ciphertextOut = ...;
+ * try (WritableByteChannel ch = new {@link org.cryptomator.cryptolib.EncryptingWritableByteChannel EncryptingWritableByteChannel}(ciphertextOut, cryptor)) {
+ * 	ch.write(plaintext);
+ * }
+ *
  * // Decrypt file contents:
  * ReadableByteChannel ciphertextIn = ...;
  * try (ReadableByteChannel ch = new {@link org.cryptomator.cryptolib.DecryptingReadableByteChannel DecryptingReadableByteChannel}(ciphertextOut, cryptor, true)) {
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java
index d32f615..41cb84c 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java
@@ -9,28 +9,13 @@
 package org.cryptomator.cryptolib.v1;
 
 import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.KeyFile;
 import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.common.AesKeyWrap;
-import org.cryptomator.cryptolib.common.MacSupplier;
-import org.cryptomator.cryptolib.common.Scrypt;
 
-import javax.crypto.Mac;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-import java.nio.ByteBuffer;
 import java.security.SecureRandom;
-import java.util.Arrays;
-
-import static org.cryptomator.cryptolib.v1.Constants.DEFAULT_SCRYPT_BLOCK_SIZE;
-import static org.cryptomator.cryptolib.v1.Constants.DEFAULT_SCRYPT_COST_PARAM;
-import static org.cryptomator.cryptolib.v1.Constants.DEFAULT_SCRYPT_SALT_LENGTH;
-import static org.cryptomator.cryptolib.v1.Constants.KEY_LEN_BYTES;
 
 class CryptorImpl implements Cryptor {
 
 	private final Masterkey masterkey;
-	private final SecureRandom random;
 	private final FileContentCryptorImpl fileContentCryptor;
 	private final FileHeaderCryptorImpl fileHeaderCryptor;
 	private final FileNameCryptorImpl fileNameCryptor;
@@ -41,7 +26,6 @@ class CryptorImpl implements Cryptor {
 	 */
 	CryptorImpl(Masterkey masterkey, SecureRandom random) {
 		this.masterkey = masterkey;
-		this.random = random;
 		this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey.getEncKey(), masterkey.getMacKey(), random);
 		this.fileContentCryptor = new FileContentCryptorImpl(masterkey.getMacKey(), random);
 		this.fileNameCryptor = new FileNameCryptorImpl(masterkey.getEncKey(), masterkey.getMacKey());
@@ -80,50 +64,6 @@ public void destroy() {
 		masterkey.destroy();
 	}
 
-	@Override
-	public KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, int vaultVersion) {
-		return writeKeysToMasterkeyFile(passphrase, new byte[0], vaultVersion);
-	}
-
-	@Override
-	public KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, byte[] pepper, int vaultVersion) {
-		assertNotDestroyed();
-		final byte[] salt = new byte[DEFAULT_SCRYPT_SALT_LENGTH];
-		random.nextBytes(salt);
-		final byte[] saltAndPepper = new byte[salt.length + pepper.length];
-		System.arraycopy(salt, 0, saltAndPepper, 0, salt.length);
-		System.arraycopy(pepper, 0, saltAndPepper, salt.length, pepper.length);
-
-		final byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, DEFAULT_SCRYPT_COST_PARAM, DEFAULT_SCRYPT_BLOCK_SIZE, KEY_LEN_BYTES);
-		final byte[] wrappedEncryptionKey;
-		final byte[] wrappedMacKey;
-		try {
-			final SecretKey kek = new SecretKeySpec(kekBytes, Constants.ENC_ALG);
-			wrappedEncryptionKey = AesKeyWrap.wrap(kek, masterkey.getEncKey());
-			wrappedMacKey = AesKeyWrap.wrap(kek, masterkey.getMacKey());
-		} finally {
-			Arrays.fill(kekBytes, (byte) 0x00);
-		}
-
-		final Mac mac = MacSupplier.HMAC_SHA256.withKey(masterkey.getMacKey());
-		final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(vaultVersion).array());
-
-		final KeyFileImpl keyfile = new KeyFileImpl();
-		keyfile.setVersion(vaultVersion);
-		keyfile.scryptSalt = salt;
-		keyfile.scryptCostParam = DEFAULT_SCRYPT_COST_PARAM;
-		keyfile.scryptBlockSize = DEFAULT_SCRYPT_BLOCK_SIZE;
-		keyfile.encryptionMasterKey = wrappedEncryptionKey;
-		keyfile.macMasterKey = wrappedMacKey;
-		keyfile.versionMac = versionMac;
-		return keyfile;
-	}
-
-	@Override
-	public byte[] getRawKey() {
-		return masterkey.getEncoded();
-	}
-
 	private void assertNotDestroyed() {
 		if (isDestroyed()) {
 			throw new IllegalStateException("Cryptor destroyed.");
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java
index 2a329aa..1ba2ab5 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java
@@ -9,29 +9,14 @@
 package org.cryptomator.cryptolib.v2;
 
 import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.KeyFile;
 import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.common.AesKeyWrap;
-import org.cryptomator.cryptolib.common.MacSupplier;
-import org.cryptomator.cryptolib.common.Scrypt;
 import org.cryptomator.cryptolib.v1.CryptorProviderImpl;
 
-import javax.crypto.Mac;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-import java.nio.ByteBuffer;
 import java.security.SecureRandom;
-import java.util.Arrays;
-
-import static org.cryptomator.cryptolib.v2.Constants.DEFAULT_SCRYPT_BLOCK_SIZE;
-import static org.cryptomator.cryptolib.v2.Constants.DEFAULT_SCRYPT_COST_PARAM;
-import static org.cryptomator.cryptolib.v2.Constants.DEFAULT_SCRYPT_SALT_LENGTH;
-import static org.cryptomator.cryptolib.v2.Constants.KEY_LEN_BYTES;
 
 class CryptorImpl implements Cryptor {
 
 	private final Masterkey masterkey;
-	private final SecureRandom random;
 	private final FileContentCryptorImpl fileContentCryptor;
 	private final FileHeaderCryptorImpl fileHeaderCryptor;
 	private final FileNameCryptorImpl fileNameCryptor;
@@ -42,7 +27,6 @@ class CryptorImpl implements Cryptor {
 	 */
 	CryptorImpl(Masterkey masterkey, SecureRandom random) {
 		this.masterkey = masterkey;
-		this.random = random;
 		this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey.getEncKey(), random);
 		this.fileContentCryptor = new FileContentCryptorImpl(random);
 		this.fileNameCryptor = new FileNameCryptorImpl(masterkey.getEncKey(), masterkey.getMacKey());
@@ -81,50 +65,6 @@ public void destroy() {
 		masterkey.destroy();
 	}
 
-	@Override
-	public KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, int vaultVersion) {
-		return writeKeysToMasterkeyFile(passphrase, new byte[0], vaultVersion);
-	}
-
-	@Override
-	public KeyFile writeKeysToMasterkeyFile(CharSequence passphrase, byte[] pepper, int vaultVersion) {
-		assertNotDestroyed();
-		final byte[] salt = new byte[DEFAULT_SCRYPT_SALT_LENGTH];
-		random.nextBytes(salt);
-		final byte[] saltAndPepper = new byte[salt.length + pepper.length];
-		System.arraycopy(salt, 0, saltAndPepper, 0, salt.length);
-		System.arraycopy(pepper, 0, saltAndPepper, salt.length, pepper.length);
-
-		final byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, DEFAULT_SCRYPT_COST_PARAM, DEFAULT_SCRYPT_BLOCK_SIZE, KEY_LEN_BYTES);
-		final byte[] wrappedEncryptionKey;
-		final byte[] wrappedMacKey;
-		try {
-			final SecretKey kek = new SecretKeySpec(kekBytes, Constants.ENC_ALG);
-			wrappedEncryptionKey = AesKeyWrap.wrap(kek, masterkey.getEncKey());
-			wrappedMacKey = AesKeyWrap.wrap(kek, masterkey.getMacKey());
-		} finally {
-			Arrays.fill(kekBytes, (byte) 0x00);
-		}
-
-		final Mac mac = MacSupplier.HMAC_SHA256.withKey(masterkey.getMacKey());
-		final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(vaultVersion).array());
-
-		final KeyFileImpl keyfile = new KeyFileImpl();
-		keyfile.setVersion(vaultVersion);
-		keyfile.scryptSalt = salt;
-		keyfile.scryptCostParam = DEFAULT_SCRYPT_COST_PARAM;
-		keyfile.scryptBlockSize = DEFAULT_SCRYPT_BLOCK_SIZE;
-		keyfile.encryptionMasterKey = wrappedEncryptionKey;
-		keyfile.macMasterKey = wrappedMacKey;
-		keyfile.versionMac = versionMac;
-		return keyfile;
-	}
-
-	@Override
-	public byte[] getRawKey() {
-		return masterkey.getEncoded();
-	}
-
 	private void assertNotDestroyed() {
 		if (isDestroyed()) {
 			throw new IllegalStateException("Cryptor destroyed.");
diff --git a/src/test/java/org/cryptomator/cryptolib/CryptorsTest.java b/src/test/java/org/cryptomator/cryptolib/CryptorsTest.java
index a2cf89c..23ca512 100644
--- a/src/test/java/org/cryptomator/cryptolib/CryptorsTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/CryptorsTest.java
@@ -9,25 +9,13 @@
 package org.cryptomator.cryptolib;
 
 import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.CryptorProvider;
 import org.cryptomator.cryptolib.api.FileContentCryptor;
-import org.cryptomator.cryptolib.api.FileHeaderCryptor;
-import org.cryptomator.cryptolib.api.FileNameCryptor;
-import org.cryptomator.cryptolib.api.KeyFile;
 import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.Mockito;
 
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.Collection;
-
 public class CryptorsTest {
 
 	@ParameterizedTest(name = "cleartextSize({1}) == {0}")
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java
index 908f3ee..c591655 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java
@@ -13,53 +13,16 @@
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-import javax.security.auth.DestroyFailedException;
-import javax.security.auth.Destroyable;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
 import java.security.SecureRandom;
-import java.util.Arrays;
-
-import static org.hamcrest.core.IsEqual.equalTo;
-import static org.hamcrest.core.IsNot.not;
 
 public class CryptorImplTest {
 
-	private static final Charset UTF_8 = StandardCharsets.UTF_8;
 	private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
 	private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]);
 
-	@Test
-	public void testWriteKeysToMasterkeyFile() {
-		final byte[] serialized;
-		try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) {
-			serialized = cryptor.writeKeysToMasterkeyFile("asd", 3).serialize();
-		}
-		String serializedStr = new String(serialized, UTF_8);
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"version\": 3"));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptSalt\": \"AAAAAAAAAAA=\""));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptCostParam\": 32768"));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptBlockSize\": 8"));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"primaryMasterKey\": \"bOuDTfSpTHJrM4G321gts1QL+TFAZ3I6S/QHwim39pz+t+/K9IYy6g==\""));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"hmacMasterKey\": \"bOuDTfSpTHJrM4G321gts1QL+TFAZ3I6S/QHwim39pz+t+/K9IYy6g==\""));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"versionMac\": \"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\""));
-	}
-
-	@Test
-	public void testWriteKeysToMasterkeyFileWithPepper() {
-		try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) {
-			byte[] serialized1 = cryptor.writeKeysToMasterkeyFile("asd", new byte[] {(byte) 0x01}, 3).serialize();
-			byte[] serialized2 = cryptor.writeKeysToMasterkeyFile("asd", new byte[] {(byte) 0x02}, 3).serialize();
-			MatcherAssert.assertThat(serialized1, not(equalTo(serialized2)));
-		}
-	}
-
 	@Test
 	public void testGetFileContentCryptor() {
 		try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) {
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java
index 410b7e5..b008537 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java
@@ -13,53 +13,16 @@
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-import javax.security.auth.DestroyFailedException;
-import javax.security.auth.Destroyable;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
 import java.security.SecureRandom;
-import java.util.Arrays;
-
-import static org.hamcrest.core.IsEqual.equalTo;
-import static org.hamcrest.core.IsNot.not;
 
 public class CryptorImplTest {
 
-	private static final Charset UTF_8 = StandardCharsets.UTF_8;
 	private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
 	private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]);
 
-	@Test
-	public void testWriteKeysToMasterkeyFile() {
-		final byte[] serialized;
-		try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) {
-			serialized = cryptor.writeKeysToMasterkeyFile("asd", 3).serialize();
-		}
-		String serializedStr = new String(serialized, UTF_8);
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"version\": 3"));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptSalt\": \"AAAAAAAAAAA=\""));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptCostParam\": 32768"));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptBlockSize\": 8"));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"primaryMasterKey\": \"bOuDTfSpTHJrM4G321gts1QL+TFAZ3I6S/QHwim39pz+t+/K9IYy6g==\""));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"hmacMasterKey\": \"bOuDTfSpTHJrM4G321gts1QL+TFAZ3I6S/QHwim39pz+t+/K9IYy6g==\""));
-		MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"versionMac\": \"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\""));
-	}
-
-	@Test
-	public void testWriteKeysToMasterkeyFileWithPepper() {
-		try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) {
-			byte[] serialized1 = cryptor.writeKeysToMasterkeyFile("asd", new byte[] {(byte) 0x01}, 3).serialize();
-			byte[] serialized2 = cryptor.writeKeysToMasterkeyFile("asd", new byte[] {(byte) 0x02}, 3).serialize();
-			MatcherAssert.assertThat(serialized1, not(equalTo(serialized2)));
-		}
-	}
-
 	@Test
 	public void testGetFileContentCryptor() {
 		try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) {

From 95eeab6a98e6fea89bf5eca4c8d01a8393da4ce0 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel 
Date: Mon, 30 Nov 2020 10:43:53 +0100
Subject: [PATCH 08/59] updated README [ci skip]

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 18999cf..35c2292 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![Build Status](https://travis-ci.org/cryptomator/cryptolib.svg?branch=master)](https://travis-ci.org/cryptomator/cryptolib)
+[![Build](https://github.com/cryptomator/cryptolib/workflows/Build/badge.svg)](https://github.com/cryptomator/cryptolib/actions?query=workflow%3ABuild)
 [![Codacy Badge](https://api.codacy.com/project/badge/Grade/9d736fe3e9e14dfb8a65949abbe8f712)](https://www.codacy.com/app/cryptomator/cryptolib)
 [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/9d736fe3e9e14dfb8a65949abbe8f712)](https://www.codacy.com/app/cryptomator/cryptolib?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptolib&utm_campaign=Badge_Coverage)
 [![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptolib/badge.svg)](https://snyk.io/test/github/cryptomator/cryptolib)
@@ -15,7 +15,7 @@ This library contains all cryptographic functions that are used by Cryptomator.
 
 | Finding | Comment |
 |---|---|
-| 1u1-22-001 | The GPG key is used exclusively for the Maven repositories, is designed for signing only and is protected by a 30-character generated password (alphabet size: 96 chars). It is iterated and salted (SHA1 with 20971520 iterations). An offline attack is also very unattractive. Apart from that, this finding has no influence on the Tresor apps[1](#footnote-tresor-apps). This was not known to Cure53 at the time of reporting. |
+| 1u1-22-001 | The now revoked GPG key has been used exclusively for the Maven repositories, was designed for signing only and was protected by a 30-character generated password (alphabet size: 96 chars). It was iterated and salted (SHA1 with 20971520 iterations), making even offline attacks very unattractive. Apart from that, this finding has no influence on the Tresor apps[1](#footnote-tresor-apps). This was not known to Cure53 at the time of reporting. |
 | 1u1-22-002 | This issue is related to [siv-mode](https://github.com/cryptomator/siv-mode/). |
 
 ## License

From 51ea9aca9d3ca0c7a720efb72751e6cd91d0d12e Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel 
Date: Mon, 30 Nov 2020 11:35:24 +0100
Subject: [PATCH 09/59] moved test to parent package

---
 .../cryptolib/api/FileContentCryptor.java     |  7 +++
 .../cryptolib/v1/FileContentCryptorImpl.java  |  5 ++
 .../cryptolib/v2/FileContentCryptorImpl.java  |  5 ++
 ...est.java => CryptoLibIntegrationTest.java} | 46 ++++++++++---------
 4 files changed, 42 insertions(+), 21 deletions(-)
 rename src/test/java/org/cryptomator/cryptolib/{v1/FileContentEncryptorTest.java => CryptoLibIntegrationTest.java} (72%)

diff --git a/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java b/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java
index 5e3a627..d04983b 100644
--- a/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java
+++ b/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java
@@ -12,6 +12,11 @@
 
 public interface FileContentCryptor {
 
+	/**
+	 * @return true if it is technically possible to decrypt unauthentic ciphertext
+	 */
+	boolean canSkipAuthentication();
+
 	/**
 	 * @return The number of cleartext bytes per chunk.
 	 */
@@ -51,6 +56,7 @@ public interface FileContentCryptor {
 	 * @param authenticate Skip authentication by setting this flag to false. Should always be true by default.
 	 * @return Decrypted content. Position is set to 0 and limit to the end of the chunk.
 	 * @throws AuthenticationFailedException If authenticate is true and the given chunk does not match its MAC.
+	 * @throws UnsupportedOperationException If authenticate is false but this cryptor {@link #canSkipAuthentication() can not skip authentication}.
 	 */
 	ByteBuffer decryptChunk(ByteBuffer ciphertextChunk, long chunkNumber, FileHeader header, boolean authenticate) throws AuthenticationFailedException;
 
@@ -63,6 +69,7 @@ public interface FileContentCryptor {
 	 * @param header Header of the file, this chunk belongs to
 	 * @param authenticate Skip authentication by setting this flag to false. Should always be true by default.
 	 * @throws AuthenticationFailedException If authenticate is true and the given chunk does not match its MAC.
+	 * @throws UnsupportedOperationException If authenticate is false but this cryptor {@link #canSkipAuthentication() can not skip authentication}.
 	 */
 	void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long chunkNumber, FileHeader header, boolean authenticate) throws AuthenticationFailedException;
 
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java
index 62c3dd1..0463ca9 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java
@@ -41,6 +41,11 @@ class FileContentCryptorImpl implements FileContentCryptor {
 		this.random = random;
 	}
 
+	@Override
+	public boolean canSkipAuthentication() {
+		return true;
+	}
+
 	@Override
 	public int cleartextChunkSize() {
 		return PAYLOAD_SIZE;
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java
index 0ea4b50..d4741ac 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java
@@ -38,6 +38,11 @@ class FileContentCryptorImpl implements FileContentCryptor {
 		this.random = random;
 	}
 
+	@Override
+	public boolean canSkipAuthentication() {
+		return false;
+	}
+
 	@Override
 	public int cleartextChunkSize() {
 		return PAYLOAD_SIZE;
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorTest.java b/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java
similarity index 72%
rename from src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorTest.java
rename to src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java
index 880b4c0..35bf187 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java
@@ -6,43 +6,44 @@
  * Contributors:
  *     Sebastian Stenzel - initial API and implementation
  *******************************************************************************/
-package org.cryptomator.cryptolib.v1;
+package org.cryptomator.cryptolib;
 
-import org.cryptomator.cryptolib.DecryptingReadableByteChannel;
-import org.cryptomator.cryptolib.EncryptingWritableByteChannel;
 import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.Masterkey;
 import org.cryptomator.cryptolib.common.SecureRandomMock;
 import org.cryptomator.cryptolib.common.SeekableByteChannelMock;
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.WritableByteChannel;
 import java.security.SecureRandom;
 import java.util.Arrays;
+import java.util.stream.Stream;
 
-public class FileContentEncryptorTest {
+public class CryptoLibIntegrationTest {
 
 	private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
-	private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]);
+	private static final Masterkey MASTERKEY = Masterkey.createNew(RANDOM_MOCK);
 
-	private CryptorImpl cryptor;
-
-	@BeforeEach
-	public void setup() {
-		cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK);
+	private static Stream getCryptors() {
+		return Stream.of(
+				Cryptors.version1(RANDOM_MOCK).withKey(MASTERKEY),
+				Cryptors.version2(RANDOM_MOCK).withKey(MASTERKEY)
+		);
 	}
 
-	@Test
-	public void testDecryptEncrypted() throws IOException {
+	@ParameterizedTest
+	@MethodSource("getCryptors")
+	public void testDecryptEncrypted(Cryptor cryptor) throws IOException {
 		int size = 1 * 1024 * 1024;
 		ByteBuffer ciphertextBuffer = ByteBuffer.allocate(2 * size);
 
@@ -63,8 +64,9 @@ public void testDecryptEncrypted() throws IOException {
 		Assertions.assertArrayEquals(cleartext.array(), Arrays.copyOfRange(result.array(), 0, size));
 	}
 
-	@Test
-	public void testDecryptManipulatedEncrypted() throws IOException {
+	@ParameterizedTest
+	@MethodSource("getCryptors")
+	public void testDecryptManipulatedEncrypted(Cryptor cryptor) throws IOException {
 		int size = 1 * 1024 * 1024;
 		ByteBuffer ciphertextBuffer = ByteBuffer.allocate(2 * size);
 
@@ -75,7 +77,7 @@ public void testDecryptManipulatedEncrypted() throws IOException {
 		}
 
 		ciphertextBuffer.position(0);
-		int firstByteOfFirstChunk = FileHeaderImpl.SIZE + 1; // not inside chunk MAC
+		int firstByteOfFirstChunk = cryptor.fileHeaderCryptor().headerSize() + 1; // not inside chunk MAC
 		ciphertextBuffer.put(firstByteOfFirstChunk, (byte) ~ciphertextBuffer.get(firstByteOfFirstChunk));
 
 		ByteBuffer result = ByteBuffer.allocate(size + 1);
@@ -87,8 +89,10 @@ public void testDecryptManipulatedEncrypted() throws IOException {
 		}
 	}
 
-	@Test
-	public void testDecryptManipulatedEncryptedSkipAuth() throws InterruptedException, IOException {
+	@ParameterizedTest
+	@MethodSource("getCryptors")
+	public void testDecryptManipulatedEncryptedSkipAuth(Cryptor cryptor) throws IOException {
+		Assumptions.assumeTrue(cryptor.fileContentCryptor().canSkipAuthentication(), "cryptor doesn't support decryption of unauthentic ciphertext");
 		int size = 1 * 1024 * 1024;
 		ByteBuffer ciphertextBuffer = ByteBuffer.allocate(2 * size);
 
@@ -99,7 +103,7 @@ public void testDecryptManipulatedEncryptedSkipAuth() throws InterruptedExceptio
 		}
 
 		ciphertextBuffer.flip();
-		int lastByteOfFirstChunk = FileHeaderImpl.SIZE + Constants.CHUNK_SIZE - 1; // inside chunk MAC
+		int lastByteOfFirstChunk = cryptor.fileHeaderCryptor().headerSize() + cryptor.fileContentCryptor().ciphertextChunkSize() - 1; // inside chunk MAC
 		ciphertextBuffer.put(lastByteOfFirstChunk, (byte) ~ciphertextBuffer.get(lastByteOfFirstChunk));
 
 		ByteBuffer result = ByteBuffer.allocate(size + 1);

From aeea4196f107f03334aad513e847666e2598bce2 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel 
Date: Mon, 30 Nov 2020 11:36:02 +0100
Subject: [PATCH 10/59] enabled some more unit tests for GCM-based content
 cryptor

---
 .../cryptolib/common/SecureRandomMock.java    |   9 +-
 .../v2/FileContentCryptorImplTest.java        | 184 +++++++++---------
 2 files changed, 90 insertions(+), 103 deletions(-)

diff --git a/src/test/java/org/cryptomator/cryptolib/common/SecureRandomMock.java b/src/test/java/org/cryptomator/cryptolib/common/SecureRandomMock.java
index 7eda256..fbca490 100644
--- a/src/test/java/org/cryptomator/cryptolib/common/SecureRandomMock.java
+++ b/src/test/java/org/cryptomator/cryptolib/common/SecureRandomMock.java
@@ -20,14 +20,7 @@
 
 public class SecureRandomMock extends SecureRandom {
 
-	private static final ByteFiller NULL_FILLER = new ByteFiller() {
-
-		@Override
-		public void fill(byte[] bytes) {
-			Arrays.fill(bytes, (byte) 0x00);
-		}
-
-	};
+	private static final ByteFiller NULL_FILLER = bytes -> Arrays.fill(bytes, (byte) 0x00);
 	public static final SecureRandomMock NULL_RANDOM = new SecureRandomMock(NULL_FILLER);
 	private static final ByteFiller PRNG_FILLER = new ByteFiller() {
 
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java
index 4441278..494c44d 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java
@@ -10,11 +10,14 @@
 
 import com.google.common.io.BaseEncoding;
 import org.cryptomator.cryptolib.DecryptingReadableByteChannel;
+import org.cryptomator.cryptolib.EncryptingWritableByteChannel;
 import org.cryptomator.cryptolib.api.AuthenticationFailedException;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.FileHeader;
-import org.cryptomator.cryptolib.common.CipherSupplier;
 import org.cryptomator.cryptolib.common.SecureRandomMock;
+import org.cryptomator.cryptolib.common.SeekableByteChannelMock;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
@@ -25,7 +28,6 @@
 import org.mockito.Mockito;
 
 import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
 import java.io.ByteArrayInputStream;
 import java.io.EOFException;
@@ -34,20 +36,18 @@
 import java.nio.CharBuffer;
 import java.nio.channels.Channels;
 import java.nio.channels.ReadableByteChannel;
-import java.nio.charset.Charset;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.channels.WritableByteChannel;
 import java.nio.charset.StandardCharsets;
-import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
+import java.util.Arrays;
 
 import static org.cryptomator.cryptolib.v2.Constants.GCM_NONCE_SIZE;
 import static org.cryptomator.cryptolib.v2.Constants.GCM_TAG_SIZE;
 
 public class FileContentCryptorImplTest {
 
-	private static final Charset US_ASCII = Charset.forName("US-ASCII");
-	private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
-	private static final SecureRandom ANTI_REUSE_PRNG = SecureRandomMock.cycle((byte) 0x13, (byte) 0x37);
-
+	private SecureRandom csprng;
 	private FileHeaderImpl header;
 	private FileHeaderCryptorImpl headerCryptor;
 	private FileContentCryptorImpl fileContentCryptor;
@@ -55,18 +55,14 @@ public class FileContentCryptorImplTest {
 
 	@BeforeEach
 	public void setup() {
+		csprng = SecureRandomMock.cycle((byte) 0x55, (byte) 0x77); // AES-GCM implementation requires non-repeating nonces, still we need deterministic nonces for testing
 		SecretKey encKey = new SecretKeySpec(new byte[32], "AES");
 		header = new FileHeaderImpl(new byte[12], new byte[32]);
-		headerCryptor = new FileHeaderCryptorImpl(encKey, RANDOM_MOCK);
-		fileContentCryptor = new FileContentCryptorImpl(RANDOM_MOCK);
+		headerCryptor = new FileHeaderCryptorImpl(encKey, csprng);
+		fileContentCryptor = new FileContentCryptorImpl(csprng);
 		cryptor = Mockito.mock(Cryptor.class);
 		Mockito.when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor);
 		Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(headerCryptor);
-
-		// init cipher with distinct IV to avoid cipher-internal anti-reuse checking
-		byte[] nonce = new byte[GCM_NONCE_SIZE];
-		ANTI_REUSE_PRNG.nextBytes(nonce);
-		CipherSupplier.AES_GCM.forEncryption(encKey, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce));
 	}
 
 	@Test
@@ -74,7 +70,7 @@ public void testDecryptedEncryptedEqualsPlaintext() throws AuthenticationFailedE
 		SecretKey fileKey = new SecretKeySpec(new byte[16], "AES");
 		ByteBuffer ciphertext = ByteBuffer.allocate(fileContentCryptor.ciphertextChunkSize());
 		ByteBuffer cleartext = ByteBuffer.allocate(fileContentCryptor.cleartextChunkSize());
-		fileContentCryptor.encryptChunk(StandardCharsets.UTF_8.encode("asd"), ciphertext,42l, new byte[12], fileKey);
+		fileContentCryptor.encryptChunk(StandardCharsets.UTF_8.encode("asd"), ciphertext, 42l, new byte[12], fileKey);
 		ciphertext.flip();
 		fileContentCryptor.decryptChunk(ciphertext, cleartext, 42l, new byte[12], fileKey);
 		cleartext.flip();
@@ -98,38 +94,37 @@ public void testEncryptChunkOfInvalidSize(int size) {
 		@Test
 		@DisplayName("encrypt chunk")
 		public void testChunkEncryption() {
-			ByteBuffer cleartext = US_ASCII.encode(CharBuffer.wrap("hello world"));
+			ByteBuffer cleartext = StandardCharsets.US_ASCII.encode(CharBuffer.wrap("hello world"));
 			ByteBuffer ciphertext = fileContentCryptor.encryptChunk(cleartext, 0, header);
-			// echo -n "hello world" | openssl enc -aes-256-gcm -K 0 -iv 0 -a
-			byte[] expected = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAApsIsUSJAHAF1IqG66PAqEvceoFIiAj5736Xq");
+			// echo -n "hello world" | openssl enc -aes-256-gcm -K 0 -iv 555555555555555555555555 -a
+			byte[] expected = BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVnHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHv");
 			Assertions.assertEquals(ByteBuffer.wrap(expected), ciphertext);
 		}
 
 		@Test
 		@DisplayName("encrypt chunk with too small ciphertext buffer")
 		public void testChunkEncryptionWithBufferUnderflow() {
-			ByteBuffer cleartext = US_ASCII.encode(CharBuffer.wrap("hello world"));
+			ByteBuffer cleartext = StandardCharsets.US_ASCII.encode(CharBuffer.wrap("hello world"));
 			ByteBuffer ciphertext = ByteBuffer.allocate(Constants.CHUNK_SIZE - 1);
 			Assertions.assertThrows(IllegalArgumentException.class, () -> {
 				fileContentCryptor.encryptChunk(cleartext, ciphertext, 0, header);
 			});
 		}
 
-//		@Test
-//		@DisplayName("encrypt file")
-//		public void testFileEncryption() throws IOException {
-//			ByteBuffer dst = ByteBuffer.allocate(200);
-//			SeekableByteChannel dstCh = new SeekableByteChannelMock(dst);
-//			try (WritableByteChannel ch = new EncryptingWritableByteChannel(dstCh, cryptor)) {
-//				ch.write(US_ASCII.encode("hello world"));
-//			}
-//			byte[] ciphertext = new byte[147];
-//			dst.position(0);
-//			dst.get(ciphertext);
-//			byte[] expected = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfM" //
-//					+ "dzvHF3f9EE0LSnRLSsu6ps3IRcJgAAAAAAAAAAAAAAAAAAAAC08KwUzWD+5t8kxipYZGvVj719S6Z+RGH1cZcl9SEoEV7XUZ4rzO6+hdzo");
-//			Assertions.assertArrayEquals(expected, ciphertext);
-//		}
+		@Test
+		@DisplayName("encrypt file")
+		public void testFileEncryption() throws IOException {
+			ByteBuffer dst = ByteBuffer.allocate(200);
+			SeekableByteChannel dstCh = new SeekableByteChannelMock(dst);
+			try (WritableByteChannel ch = new EncryptingWritableByteChannel(dstCh, cryptor)) {
+				ch.write(StandardCharsets.US_ASCII.encode("hello world"));
+			}
+			dst.flip();
+			byte[] ciphertext = new byte[dst.remaining()];
+			dst.get(ciphertext);
+			byte[] expected = BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVC+/OFHHE8UvKYTOPlrMO5rCRLAI7/zk8Hjoisja03+yi9ugeeMz1evZhxDExrawl93vf9DKQPx5VVVVVVVVVVVVVVVVSxe6Nf7RO8orsVTzHAmXlNSy1oJpDrg9coV0=");
+			Assertions.assertArrayEquals(expected, ciphertext);
+		}
 
 	}
 
@@ -150,37 +145,36 @@ public void testDecryptChunkOfInvalidSize(int size) {
 		@Test
 		@DisplayName("decrypt chunk")
 		public void testChunkDecryption() throws AuthenticationFailedException {
-			ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAApsIsUSJAHAF1IqG66PAqEvceoFIiAj5736Xq"));
+			ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVnHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHv"));
 			ByteBuffer cleartext = fileContentCryptor.decryptChunk(ciphertext, 0, header, true);
-			ByteBuffer expected = US_ASCII.encode("hello world");
+			ByteBuffer expected = StandardCharsets.US_ASCII.encode("hello world");
 			Assertions.assertEquals(expected, cleartext);
 		}
 
 		@Test
 		@DisplayName("decrypt chunk with too small cleartext buffer")
 		public void testChunkDecryptionWithBufferUnderflow() {
-			ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAApsIsUSJAHAF1IqG66PAqEvceoFIiAj5736Xq"));
+			ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVnHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHv"));
 			ByteBuffer cleartext = ByteBuffer.allocate(Constants.PAYLOAD_SIZE - 1);
 			Assertions.assertThrows(IllegalArgumentException.class, () -> {
 				fileContentCryptor.decryptChunk(ciphertext, cleartext, 0, header, true);
 			});
 		}
 
-//		@Test
-//		@DisplayName("decrypt file")
-//		public void testFileDecryption() throws IOException {
-//			byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImCrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga27XjlTjFxC1VCqZa+" //
-//					+ "L2eH+xWVgrSLX+JkG35ZJxk5xXswAAAAAAAAAAAAAAAAAAAAC08KwUzWD+5t8kxipYZGvVj719S6Z+RGH1cZcl9SEoEV7XUZ4rzO6+hdzo");
-//			ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext));
-//
-//			ByteBuffer result = ByteBuffer.allocate(20);
-//			try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) {
-//				int read = cleartextCh.read(result);
-//				Assertions.assertEquals(11, read);
-//				byte[] expected = "hello world".getBytes(US_ASCII);
-//				Assertions.assertArrayEquals(expected, Arrays.copyOfRange(result.array(), 0, read));
-//			}
-//		}
+		@Test
+		@DisplayName("decrypt file")
+		public void testFileDecryption() throws IOException {
+			byte[] ciphertext = BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVC+/OFHHE8UvKYTOPlrMO5rCRLAI7/zk8Hjoisja03+yi9ugeeMz1evZhxDExrawl93vf9DKQPx5VVVVVVVVVVVVVVVVSxe6Nf7RO8orsVTzHAmXlNSy1oJpDrg9coV0=");
+			ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext));
+
+			ByteBuffer result = ByteBuffer.allocate(20);
+			try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) {
+				int read = cleartextCh.read(result);
+				Assertions.assertEquals(11, read);
+				byte[] expected = "hello world".getBytes(StandardCharsets.US_ASCII);
+				Assertions.assertArrayEquals(expected, Arrays.copyOfRange(result.array(), 0, read));
+			}
+		}
 
 		@Test
 		@DisplayName("decrypt file with unauthentic file header")
@@ -198,74 +192,74 @@ public void testDecryptionWithTooShortHeader() throws InterruptedException, IOEx
 		@Test
 		@DisplayName("decrypt chunk with unauthentic NONCE")
 		public void testChunkDecryptionWithUnauthenticNonce() {
-			ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("aAAAAAAAAAAAAAAApsIsUSJAHAF1IqG66PAqEvceoFIiAj5736Xq"));
+			ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("vVVVVVVVVVVVVVVVnHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHv"));
 
 			Assertions.assertThrows(AuthenticationFailedException.class, () -> {
 				fileContentCryptor.decryptChunk(ciphertext, 0, header, true);
 			});
 		}
 
-//		@Test
-//		@DisplayName("decrypt file with unauthentic NONCE in first chunk")
-//		public void testDecryptionWithUnauthenticNonce() throws InterruptedException, IOException {
-//			byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImCrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga27XjlTjFxC1VCqZa+" //
-//					+ "L2eH+xWVgrSLX+JkG35ZJxk5xXswAAAAAAAAAAABAAAAAAAAC08KwUzWD+5t8kxipYZGvVj719S6Z+RGH1cZcl9SEoEV7XUZ4rzO6+hdzo");
-//			ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext));
-//
-//			try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) {
-//				Assertions.assertThrows(AuthenticationFailedException.class, () -> {
-//					cleartextCh.read(ByteBuffer.allocate(3));
-//				});
-//			}
-//		}
+		@Test
+		@DisplayName("decrypt file with unauthentic NONCE in first chunk")
+		public void testDecryptionWithUnauthenticNonce() throws InterruptedException, IOException {
+			byte[] ciphertext = BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVC+/OFHHE8UvKYTOPlrMO5rCRLAI7/zk8Hjoisja03+yi9ugeeMz1evZhxDExrawl93vf9DKQPx5vVVVVVVVvVVVVVVVSxe6Nf7RO8orsVTzHAmXlNSy1oJpDrg9coV0=");
+			ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext));
+
+			try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) {
+				IOException thrown = Assertions.assertThrows(IOException.class, () -> {
+					cleartextCh.read(ByteBuffer.allocate(3));
+				});
+				MatcherAssert.assertThat(thrown.getCause(), CoreMatchers.instanceOf(AuthenticationFailedException.class));
+			}
+		}
 
 		@Test
 		@DisplayName("decrypt chunk with unauthentic CONTENT")
 		public void testChunkDecryptionWithUnauthenticContent() {
-			ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAApsIsUSJAHAF1IqqqqqqqEvceoFIiAj5736Xq"));
+			ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVNHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHv"));
 
 			Assertions.assertThrows(AuthenticationFailedException.class, () -> {
 				fileContentCryptor.decryptChunk(ciphertext, 0, header, true);
 			});
 		}
 
-//		@Test
-//		@DisplayName("decrypt file with unauthentic CONTENT in first chunk")
-//		public void testDecryptionWithUnauthenticContent() throws InterruptedException, IOException {
-//			byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImCrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga27XjlTjFxC1VCqZa+" //
-//					+ "L2eH+xWVgrSLX+JkG35ZJxk5xXswAAAAAAAAAAAAAAAAAAAAC08KwUZWD+5t8kxipYZGvVj719S6Z+RGH1cZcl9SEoEV7XUZ4rzO6+hdzo");
-//			ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext));
-//
-//			try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) {
-//				Assertions.assertThrows(AuthenticationFailedException.class, () -> {
-//					cleartextCh.read(ByteBuffer.allocate(3));
-//				});
-//			}
-//		}
+		@Test
+		@DisplayName("decrypt file with unauthentic CONTENT in first chunk")
+		public void testDecryptionWithUnauthenticContent() throws IOException {
+			byte[] ciphertext = BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVC+/OFHHE8UvKYTOPlrMO5rCRLAI7/zk8Hjoisja03+yi9ugeeMz1evZhxDExrawl93vf9DKQPx5VVVVVVVVvVVVVVVVsxe6Nf7RO8orsVTzHAmXlNSy1oJpDrg9coV0=");
+			ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext));
+
+			try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) {
+				IOException thrown = Assertions.assertThrows(IOException.class, () -> {
+					cleartextCh.read(ByteBuffer.allocate(3));
+				});
+				MatcherAssert.assertThat(thrown.getCause(), CoreMatchers.instanceOf(AuthenticationFailedException.class));
+			}
+		}
 
 		@Test
 		@DisplayName("decrypt chunk with unauthentic tag")
 		public void testChunkDecryptionWithUnauthenticTag() {
-			ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAApsIsUSJAHAF1IqG66PAqEvceoFIiAj5736XQ"));
+			ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVnHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHV"));
 
 			Assertions.assertThrows(AuthenticationFailedException.class, () -> {
 				fileContentCryptor.decryptChunk(ciphertext, 0, header, true);
 			});
 		}
 
-//		@Test
-//		@DisplayName("decrypt file with unauthentic MAC in first chunk")
-//		public void testDecryptionWithUnauthenticMac() throws InterruptedException, IOException {
-//			byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImCrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga27XjlTjFxC1VCqZa+" //
-//					+ "L2eH+xWVgrSLX+JkG35ZJxk5xXswAAAAAAAAAAAAAAAAAAAAC08KwUzWD+5t8kxipYZGvVj719S6Z+RGH1cZcl9SEoEV7XUZ4rzO6+hdzO");
-//			ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext));
-//
-//			try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) {
-//				Assertions.assertThrows(AuthenticationFailedException.class, () -> {
-//					cleartextCh.read(ByteBuffer.allocate(3));
-//				});
-//			}
-//		}
+		@Test
+		@DisplayName("decrypt file with unauthentic tag in first chunk")
+		public void testDecryptionWithUnauthenticTag() throws IOException {
+			byte[] ciphertext = BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVC+/OFHHE8UvKYTOPlrMO5rCRLAI7/zk8Hjoisja03+yi9ugeeMz1evZhxDExrawl93vf9DKQPx5VVVVVVVVVVVVVVVVSxe6Nf7RO8orsVTzHAmXlNSy1oJpDrg9coVx=");
+			ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext));
+
+			try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) {
+				IOException thrown = Assertions.assertThrows(IOException.class, () -> {
+					cleartextCh.read(ByteBuffer.allocate(3));
+				});
+				MatcherAssert.assertThat(thrown.getCause(), CoreMatchers.instanceOf(AuthenticationFailedException.class));
+			}
+		}
 
 		@Test
 		@DisplayName("decrypt chunk with unauthentic tag but skipping authentication")

From 6e8f91389971b1ce6f75b90735187387efeba0ae Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel 
Date: Thu, 3 Dec 2020 10:00:51 +0100
Subject: [PATCH 11/59] removed dead code [ci skip]

---
 src/main/java/org/cryptomator/cryptolib/v1/Constants.java | 8 +-------
 .../java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java | 2 +-
 src/main/java/org/cryptomator/cryptolib/v2/Constants.java | 8 +-------
 .../java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java | 2 +-
 4 files changed, 4 insertions(+), 16 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptolib/v1/Constants.java b/src/main/java/org/cryptomator/cryptolib/v1/Constants.java
index 57f4c4b..e4cc9d5 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/Constants.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/Constants.java
@@ -10,13 +10,7 @@
 
 final class Constants {
 
-	static final String ENC_ALG = "AES";
-	static final String MAC_ALG = "HmacSHA256";
-
-	static final int KEY_LEN_BYTES = 32;
-	static final int DEFAULT_SCRYPT_SALT_LENGTH = 8;
-	static final int DEFAULT_SCRYPT_COST_PARAM = 1 << 15; // 2^15
-	static final int DEFAULT_SCRYPT_BLOCK_SIZE = 8;
+	static final String CONTENT_ENC_ALG = "AES";
 
 	static final int NONCE_SIZE = 16;
 	static final int PAYLOAD_SIZE = 32 * 1024;
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java
index 9ca31bc..146e182 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java
@@ -91,7 +91,7 @@ private Payload(byte[] contentKeyBytes) {
 				throw new IllegalArgumentException("Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")");
 			}
 			this.contentKeyBytes = contentKeyBytes;
-			this.contentKey = new SecretKeySpec(contentKeyBytes, Constants.ENC_ALG);
+			this.contentKey = new SecretKeySpec(contentKeyBytes, Constants.CONTENT_ENC_ALG);
 		}
 
 		private long getFilesize() {
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/Constants.java b/src/main/java/org/cryptomator/cryptolib/v2/Constants.java
index 1671c49..b0a7380 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/Constants.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/Constants.java
@@ -10,13 +10,7 @@
 
 final class Constants {
 
-	static final String ENC_ALG = "AES";
-	static final String MAC_ALG = "HmacSHA256";
-
-	static final int KEY_LEN_BYTES = 32;
-	static final int DEFAULT_SCRYPT_SALT_LENGTH = 8;
-	static final int DEFAULT_SCRYPT_COST_PARAM = 1 << 15; // 2^15
-	static final int DEFAULT_SCRYPT_BLOCK_SIZE = 8;
+	static final String CONTENT_ENC_ALG = "AES";
 
 	static final int GCM_NONCE_SIZE = 12; // 96 bit IVs strongly recommended for GCM
 	static final int PAYLOAD_SIZE = 32 * 1024;
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java
index 2df08cc..6c1c11d 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java
@@ -91,7 +91,7 @@ private Payload(byte[] contentKeyBytes) {
 				throw new IllegalArgumentException("Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")");
 			}
 			this.contentKeyBytes = contentKeyBytes;
-			this.contentKey = new SecretKeySpec(contentKeyBytes, Constants.ENC_ALG);
+			this.contentKey = new SecretKeySpec(contentKeyBytes, Constants.CONTENT_ENC_ALG);
 		}
 
 		private long getFilesize() {

From 15c4f86ab62734897e1f296c8964a73a0e9db461 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel 
Date: Thu, 3 Dec 2020 10:11:25 +0100
Subject: [PATCH 12/59] skip codeql analysis on [ci skip]

---
 .github/workflows/codeql-analysis.yml | 2 +-
 README.md                             | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 96422c2..440f187 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -13,7 +13,7 @@ jobs:
   analyse:
     name: Analyse
     runs-on: ubuntu-latest
-
+    if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')"
     steps:
     - name: Checkout repository
       uses: actions/checkout@v2
diff --git a/README.md b/README.md
index 35c2292..0b96c79 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 [![Build](https://github.com/cryptomator/cryptolib/workflows/Build/badge.svg)](https://github.com/cryptomator/cryptolib/actions?query=workflow%3ABuild)
 [![Codacy Badge](https://api.codacy.com/project/badge/Grade/9d736fe3e9e14dfb8a65949abbe8f712)](https://www.codacy.com/app/cryptomator/cryptolib)
-[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/9d736fe3e9e14dfb8a65949abbe8f712)](https://www.codacy.com/app/cryptomator/cryptolib?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptolib&utm_campaign=Badge_Coverage)
+[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/9d736fe3e9e14dfb8a65949abbe8f712)](https://www.codacy.com/gh/cryptomator/cryptolib/dashboard)
 [![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptolib/badge.svg)](https://snyk.io/test/github/cryptomator/cryptolib)
 [![Maven Central](https://img.shields.io/maven-central/v/org.cryptomator/cryptolib.svg?maxAge=86400)](https://repo1.maven.org/maven2/org/cryptomator/cryptolib/)
 [![Javadocs](http://www.javadoc.io/badge/org.cryptomator/cryptolib.svg)](http://www.javadoc.io/doc/org.cryptomator/cryptolib)

From ae219a1edf1edeb2f29e9129755661ca11574b88 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel 
Date: Thu, 3 Dec 2020 10:15:57 +0100
Subject: [PATCH 13/59] Prepare version 2.0.0

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index bf2e14a..78b168a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
 	4.0.0
 	org.cryptomator
 	cryptolib
-	1.5.0-SNAPSHOT
+	2.0.0
 	Cryptomator Crypto Library
 	This library contains all cryptographic functions that are used by Cryptomator.
 	https://github.com/cryptomator/cryptolib

From 041a28f257b4e850c29fa4b88a08672e00ce1794 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel 
Date: Thu, 3 Dec 2020 13:18:32 +0100
Subject: [PATCH 14/59] add distribution config

---
 pom.xml | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/pom.xml b/pom.xml
index 78b168a..b57d938 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,6 +55,13 @@
 		
 	
 
+    
+        
+            bintray-jcenter
+            https://api.bintray.com/maven/cryptomator/maven/cryptolib/;publish=1
+        
+    
+
 	
 		
 			org.cryptomator

From b5d52623beea76d85d4a7847568797f9e577306b Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel 
Date: Fri, 4 Dec 2020 09:22:03 +0100
Subject: [PATCH 15/59] added convenience methods

---
 .../cryptolib/common/MasterkeyFile.java       |  7 ++++
 .../cryptolib/common/MasterkeyFileLoader.java | 32 +++++++++++++++++--
 2 files changed, 36 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java
index cdea319..2972d81 100644
--- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java
+++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java
@@ -69,6 +69,13 @@ public static MasterkeyFile withContent(InputStream in) throws IOException {
 		}
 	}
 
+	/**
+	 * @return The unverified vault version (MAC not checked)
+	 */
+	public int allegedVaultVersion() {
+		return content.version;
+	}
+
 	/**
 	 * Derives a KEK from the given passphrase and the params from this masterkey file using scrypt and unwraps the
 	 * stored encryption and MAC keys.
diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java
index 627299c..c105e30 100644
--- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java
+++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java
@@ -13,9 +13,8 @@
  * a Cryptomator masterkey file and then be used to {@link MasterkeyLoader#loadKey(String) load} a {@link Masterkey}.
  *
  * 
- * 	Masterkey masterkey;
- * 	try (MasterkeyLoader loader = MasterkeyFile.withContent(in).unlock(pw, pepper, expectedVaultVersion)) {
- * 		masterkey = loader.loadKey(MasterkeyFileLoader.KEY_ID);
+ * 	try (Masterkey masterkey = MasterkeyFile.withContent(in).unlock(pw, pepper, expectedVaultVersion).loadKeyAndClose()) {
+ * 		// use masterkey
  * 	}
  * 
*/ @@ -31,11 +30,38 @@ public class MasterkeyFileLoader implements MasterkeyLoader, AutoCloseable { this.macKey = macKey; } + /** + * Loads the key and closes this MasterkeyFileLoader immediately, if reuse is not required. + * + * @return The masterkey loaded from this masterkey file. + */ + public Masterkey loadKeyAndClose() { + try { + return loadKey(); + } finally { + close(); + } + } + + /** + * @return The masterkey loaded from this masterkey file. + */ + public Masterkey loadKey() { + try { + return loadKey(KEY_ID); + } catch (MasterkeyLoadingFailedException e) { + throw new IllegalStateException("Should have been able to load " + KEY_ID); + } + } + @Override public Masterkey loadKey(String keyId) throws MasterkeyLoadingFailedException { if (!KEY_ID.equals(keyId)) { throw new MasterkeyLoadingFailedException("Unsupported key " + keyId); } + if (encKey.isDestroyed() || macKey.isDestroyed()) { + throw new MasterkeyLoadingFailedException("MasterkeyFileLoader already closed."); + } // we need a copy to make sure we can use autocloseable destruction SecretKey encKeyCopy = new SecretKeySpec(encKey.getEncoded(), encKey.getAlgorithm()); SecretKey macKeyCopy = new SecretKeySpec(macKey.getEncoded(), macKey.getAlgorithm()); From bee23bdae6676486fd1ac3b921c43a2aced1be53 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 22 Jan 2021 19:38:38 +0100 Subject: [PATCH 16/59] refactored most of MasterkeyFile-specific classes --- .../api/InvalidPassphraseException.java | 5 +- .../cryptolib/api/MasterkeyLoader.java | 16 +- .../cryptolib/common/MasterkeyFile.java | 262 ---------------- .../cryptolib/common/MasterkeyFileAccess.java | 296 ++++++++++++++++++ .../cryptolib/common/MasterkeyFileLoader.java | 76 +---- .../common/VaultRootAwareContext.java | 13 + .../cryptomator/cryptolib/package-info.java | 49 +-- .../common/MasterkeyFileAccessTest.java | 195 ++++++++++++ .../common/MasterkeyFileLoaderTest.java | 26 -- .../cryptolib/common/MasterkeyFileTest.java | 182 ----------- 10 files changed, 541 insertions(+), 579 deletions(-) delete mode 100644 src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java create mode 100644 src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java create mode 100644 src/main/java/org/cryptomator/cryptolib/common/VaultRootAwareContext.java create mode 100644 src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java delete mode 100644 src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java diff --git a/src/main/java/org/cryptomator/cryptolib/api/InvalidPassphraseException.java b/src/main/java/org/cryptomator/cryptolib/api/InvalidPassphraseException.java index a41e26a..c7d765b 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/InvalidPassphraseException.java +++ b/src/main/java/org/cryptomator/cryptolib/api/InvalidPassphraseException.java @@ -8,6 +8,9 @@ *******************************************************************************/ package org.cryptomator.cryptolib.api; -public class InvalidPassphraseException extends CryptoException { +public class InvalidPassphraseException extends MasterkeyLoadingFailedException { + public InvalidPassphraseException() { + super(null); + } } diff --git a/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java index f2a0c2e..7d27db8 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java +++ b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java @@ -1,20 +1,26 @@ package org.cryptomator.cryptolib.api; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; + +import java.net.URI; + /** * Masterkey loaders load keys to unlock Cryptomator vaults. * - * @see org.cryptomator.cryptolib.common.MasterkeyFileLoader + * @see MasterkeyFileAccess */ -@FunctionalInterface -public interface MasterkeyLoader { +public interface MasterkeyLoader { + + boolean supportsScheme(String scheme); /** * Loads a master key. This might be a long-running operation, as it may require user input or expensive computations. * - * @param keyId a string uniquely identifying the source of the key and its identity, if multiple keys can be obtained from the same source + * @param keyId An URI uniquely identifying the source and identity of the key + * @param context An optional context containing additional information required during key retrieval * @return The raw key bytes. Must not be null * @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request */ - Masterkey loadKey(String keyId) throws MasterkeyLoadingFailedException; + Masterkey loadKey(URI keyId, C context) throws MasterkeyLoadingFailedException; } diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java deleted file mode 100644 index 2972d81..0000000 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java +++ /dev/null @@ -1,262 +0,0 @@ -package org.cryptomator.cryptolib.common; - -import com.google.common.base.Preconditions; -import com.google.common.io.BaseEncoding; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; -import com.google.gson.TypeAdapter; -import com.google.gson.annotations.SerializedName; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import org.cryptomator.cryptolib.api.CryptoException; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.api.Masterkey; -import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.Optional; - -public class MasterkeyFile { - - private static final int DEFAULT_SCRYPT_SALT_LENGTH = 8; - private static final int DEFAULT_SCRYPT_COST_PARAM = 1 << 15; // 2^15 - private static final int DEFAULT_SCRYPT_BLOCK_SIZE = 8; - private static final Gson GSON = new GsonBuilder() // - .setPrettyPrinting() // - .disableHtmlEscaping() // - .registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) // - .create(); - - private final Content content; - - private MasterkeyFile(Content content) { - Preconditions.checkArgument(content.isValid(), "Invalid content"); - this.content = content; - } - - public static MasterkeyFile withContentFromFile(Path path) throws IOException { - try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) { - return MasterkeyFile.withContent(in); - } - } - - public static MasterkeyFile withContent(InputStream in) throws IOException { - try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { - Content content = GSON.fromJson(reader, Content.class); - return new MasterkeyFile(content); - } catch (JsonParseException e) { - throw new IOException("Unreadable JSON", e); - } catch (IllegalArgumentException e) { - throw new IOException("Invalid JSON content", e); - } - } - - /** - * @return The unverified vault version (MAC not checked) - */ - public int allegedVaultVersion() { - return content.version; - } - - /** - * Derives a KEK from the given passphrase and the params from this masterkey file using scrypt and unwraps the - * stored encryption and MAC keys. - * - * @param passphrase The passphrase used during key derivation - * @param pepper An optional application-specific pepper added to the scrypt's salt. Can be an empty array. - * @param expectedVaultVersion An optional expected vault version. - * @return A masterkey loader that can be used to access the unwrapped keys. Should be used in a try-with-resource statement. - * @throws UnsupportedVaultFormatException If the expectedVaultVersion is present and does not match the cryptographically signed version stored in the masterkey file. - * @throws InvalidPassphraseException If the provided passphrase can not be used to unwrap the stored keys. - * @throws CryptoException In case of any other cryptographic exceptions - */ - public MasterkeyFileLoader unlock(CharSequence passphrase, byte[] pepper, Optional expectedVaultVersion) throws UnsupportedVaultFormatException, InvalidPassphraseException, CryptoException { - boolean success = false; - SecretKey kek = null; - SecretKey encKey = null; - SecretKey macKey = null; - try { - // derive keys: - kek = scrypt(passphrase, content.scryptSalt, pepper, content.scryptCostParam, content.scryptBlockSize); - macKey = AesKeyWrap.unwrap(kek, content.macMasterKey, Masterkey.MAC_ALG); - encKey = AesKeyWrap.unwrap(kek, content.encMasterKey, Masterkey.ENC_ALG); - - // check MAC: - if (expectedVaultVersion.isPresent()) { - checkVaultVersion(content, macKey, expectedVaultVersion.get()); - } - - // construct key: - success = true; - return new MasterkeyFileLoader(encKey, macKey); - } catch (InvalidKeyException e) { - throw new InvalidPassphraseException(); - } finally { - Destroyables.destroySilently(kek); - if (!success) { - Destroyables.destroySilently(encKey); - Destroyables.destroySilently(macKey); - } - } - } - - /** - * Derives a KEK from the given passphrase and wraps the key material from masterkey. - * Then serializes the encrypted keys as well as used key derivation parameters into a JSON representation - * that can be stored into a masterkey file. - * - * @param masterkey The key to protect - * @param passphrase The passphrase used during key derivation - * @param pepper An optional application-specific pepper added to the scrypt's salt. Can be an empty array. - * @param vaultVersion The vault version that should be stored in this masterkey file (for downwards compatibility) - * @param csprng A cryptographically secure RNG - * @return A JSON representation of the encrypted masterkey with its key derivation parameters. - */ - public static byte[] lock(Masterkey masterkey, CharSequence passphrase, byte[] pepper, int vaultVersion, SecureRandom csprng) { - Preconditions.checkArgument(!masterkey.isDestroyed(), "masterkey has been destroyed"); - - final byte[] salt = new byte[DEFAULT_SCRYPT_SALT_LENGTH]; - csprng.nextBytes(salt); - SecretKey kek = scrypt(passphrase, salt, pepper, DEFAULT_SCRYPT_COST_PARAM, DEFAULT_SCRYPT_BLOCK_SIZE); - try { - final Mac mac = MacSupplier.HMAC_SHA256.withKey(masterkey.getMacKey()); - final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(vaultVersion).array()); - Content content = new Content(); - content.version = vaultVersion; - content.versionMac = versionMac; - content.scryptSalt = salt; - content.scryptCostParam = DEFAULT_SCRYPT_COST_PARAM; - content.scryptBlockSize = DEFAULT_SCRYPT_BLOCK_SIZE; - content.encMasterKey = AesKeyWrap.wrap(kek, masterkey.getEncKey()); - content.macMasterKey = AesKeyWrap.wrap(kek, masterkey.getMacKey()); - return GSON.toJson(content).getBytes(StandardCharsets.UTF_8); - } finally { - Destroyables.destroySilently(kek); - } - } - - /** - * Reencrypts a masterkey with a new passphrase. - * - * @param masterkey The original JSON representation of the masterkey - * @param oldPassphrase The old passphrase - * @param newPassphrase The new passphrase - * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) - * @param csprng A cryptographically secure RNG - * @return A JSON representation of the masterkey, now encrypted with newPassphrase - * @throws IOException - * @throws InvalidPassphraseException If the wrong oldPassphrase has been supplied for the masterkey - * @throws CryptoException In case of other cryptographic exceptions. - */ - public static byte[] changePassphrase(byte[] masterkey, CharSequence oldPassphrase, CharSequence newPassphrase, byte[] pepper, SecureRandom csprng) throws IOException, InvalidPassphraseException, CryptoException { - MasterkeyFile orig = MasterkeyFile.withContent(new ByteArrayInputStream(masterkey)); - try (MasterkeyFileLoader loader = orig.unlock(oldPassphrase, pepper, Optional.empty()); - Masterkey key = loader.loadKey(MasterkeyFileLoader.KEY_ID)) { - return MasterkeyFile.lock(key, newPassphrase, pepper, orig.content.version, csprng); - } - } - - private static SecretKey scrypt(CharSequence passphrase, byte[] salt, byte[] pepper, int costParam, int blockSize) { - byte[] saltAndPepper = new byte[salt.length + pepper.length]; - System.arraycopy(salt, 0, saltAndPepper, 0, salt.length); - System.arraycopy(pepper, 0, saltAndPepper, salt.length, pepper.length); - byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, costParam, blockSize, Masterkey.KEY_LEN_BYTES); - try { - return new SecretKeySpec(kekBytes, Masterkey.ENC_ALG); - } finally { - Arrays.fill(kekBytes, (byte) 0x00); - } - } - - private void checkVaultVersion(Content content, SecretKey macKey, int expectedVaultVersion) throws UnsupportedVaultFormatException { - Mac mac = MacSupplier.HMAC_SHA256.withKey(macKey); - byte[] expectedVaultVersionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(expectedVaultVersion).array()); - if (content.versionMac == null || !MessageDigest.isEqual(expectedVaultVersionMac, content.versionMac)) { - // attempted downgrade attack: versionMac doesn't match version. - throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, expectedVaultVersion); - } - } - - private static class Content { - - @SerializedName("version") - int version; - - @SerializedName("scryptSalt") - byte[] scryptSalt; - - @SerializedName("scryptCostParam") - int scryptCostParam; - - @SerializedName("scryptBlockSize") - int scryptBlockSize; - - @SerializedName("primaryMasterKey") - byte[] encMasterKey; - - @SerializedName("hmacMasterKey") - byte[] macMasterKey; - - @SerializedName("versionMac") - byte[] versionMac; - - /** - * Performs a very superficial validation of this object. - * - * @return true if not missing any values - */ - private boolean isValid() { - return version != 0 - && scryptSalt != null - && scryptCostParam > 1 - && scryptBlockSize > 0 - && encMasterKey != null - && macMasterKey != null - && versionMac != null; - } - - } - - private static class ByteArrayJsonAdapter extends TypeAdapter { - - private static final BaseEncoding BASE64 = BaseEncoding.base64(); - - @Override - public void write(JsonWriter writer, byte[] value) throws IOException { - if (value == null) { - writer.nullValue(); - } else { - writer.value(BASE64.encode(value)); - } - } - - @Override - public byte[] read(JsonReader reader) throws IOException { - if (reader.peek() == JsonToken.NULL) { - reader.nextNull(); - return null; - } else { - return BASE64.decode(reader.nextString()); - } - } - } - -} diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java new file mode 100644 index 0000000..44eb05d --- /dev/null +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -0,0 +1,296 @@ +package org.cryptomator.cryptolib.common; + +import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.security.InvalidKeyException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.function.Function; + +/** + * Allow loading and persisting of {@link Masterkey masterkeys} from and to encrypted json files. + *

+ * Requires a passphrase for derivation of a KEK. + * + *

+ * 	MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(pepper, csprng);
+ * 	try (Masterkey masterkey = masterkeyFileAccess.load(path, passphrase) {
+ * 		// use masterkey
+ *  }
+ * 
+ */ +public class MasterkeyFileAccess { + private static final int DEFAULT_SCRYPT_SALT_LENGTH = 8; + private static final int DEFAULT_SCRYPT_COST_PARAM = 1 << 15; // 2^15 + private static final int DEFAULT_SCRYPT_BLOCK_SIZE = 8; + private static final Gson GSON = new GsonBuilder() // + .setPrettyPrinting() // + .disableHtmlEscaping() // + .registerTypeHierarchyAdapter(byte[].class, new MasterkeyFileAccess.ByteArrayJsonAdapter()) // + .create(); + + private final byte[] pepper; + private final SecureRandom csprng; + + public MasterkeyFileAccess(byte[] pepper, SecureRandom csprng) { + this.pepper = pepper; + this.csprng = csprng; + } + + /** + * Reencrypts a masterkey with a new passphrase. + * + * @param masterkey The original JSON representation of the masterkey + * @param oldPassphrase The old passphrase + * @param newPassphrase The new passphrase + * @return A JSON representation of the masterkey, now encrypted with newPassphrase + * @throws IOException If failing to read, parse or write JSON + * @throws InvalidPassphraseException If the wrong oldPassphrase has been supplied for the masterkey + */ + public byte[] changePassphrase(byte[] masterkey, CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException { + try (ByteArrayInputStream in = new ByteArrayInputStream(masterkey); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); + Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { + MasterkeyFile original = GSON.fromJson(reader, MasterkeyFile.class); + MasterkeyFile updated = changePassphrase(original, oldPassphrase, newPassphrase); + GSON.toJson(updated, writer); + return out.toByteArray(); + } catch (JsonParseException e) { + throw new IOException("Unreadable JSON", e); + } catch (IllegalArgumentException e) { + throw new IOException("Invalid JSON content", e); + } + } + + // visible for testing + MasterkeyFile changePassphrase(MasterkeyFile masterkey, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException { + try (Masterkey key = unlock(masterkey, oldPassphrase)) { + return lock(key, newPassphrase, masterkey.version, masterkey.scryptCostParam); + } + } + + /** + * Loads the JSON contents from the given file and derives a KEK from the given passphrase to + * unwrap the contained keys. + * + * @param filePath Which file to load + * @param passphrase The passphrase used during key derivation + * @return A new masterkey. Should be used in a try-with-resource statement. + * @throws InvalidPassphraseException If the provided passphrase can not be used to unwrap the stored keys. + * @throws MasterkeyLoadingFailedException + */ + public Masterkey load(Path filePath, CharSequence passphrase) throws MasterkeyLoadingFailedException { + try (InputStream in = Files.newInputStream(filePath, StandardOpenOption.READ)) { + return load(in, passphrase); + } catch (IOException e) { + throw new MasterkeyLoadingFailedException("I/O error", e); + } + } + + Masterkey load(InputStream in, CharSequence passphrase) throws MasterkeyLoadingFailedException, IOException { + try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + MasterkeyFile parsedFile = GSON.fromJson(reader, MasterkeyFile.class); + if (parsedFile == null || !parsedFile.isValid()) { + throw new JsonParseException("Invalid key file"); + } else { + return unlock(parsedFile, passphrase); + } + } catch (JsonParseException e) { + throw new MasterkeyLoadingFailedException("Unreadable JSON", e); + } catch (IllegalArgumentException e) { + throw new MasterkeyLoadingFailedException("Invalid JSON content", e); + } + } + + // visible for testing + Masterkey unlock(MasterkeyFile parsedFile, CharSequence passphrase) throws InvalidPassphraseException { + Preconditions.checkNotNull(parsedFile); + Preconditions.checkArgument(parsedFile.isValid(), "Invalid masterkey file"); + Preconditions.checkNotNull(passphrase); + + SecretKey kek = scrypt(passphrase, parsedFile.scryptSalt, pepper, parsedFile.scryptCostParam, parsedFile.scryptBlockSize); + try { + SecretKey encKey = AesKeyWrap.unwrap(kek, parsedFile.encMasterKey, Masterkey.ENC_ALG); + SecretKey macKey = AesKeyWrap.unwrap(kek, parsedFile.macMasterKey, Masterkey.MAC_ALG); + return new Masterkey(encKey, macKey); + } catch (InvalidKeyException e) { + throw new InvalidPassphraseException(); + } finally { + Destroyables.destroySilently(kek); + } + } + + /** + * Derives a KEK from the given passphrase and wraps the key material from masterkey. + * Then serializes the encrypted keys as well as used key derivation parameters into a JSON representation + * that will be stored at the given filePath. + * + * @param masterkey The key to protect + * @param filePath Where to store the file (gets overwritten, parent dir must exist) + * @param passphrase The passphrase used during key derivation + * @param vaultVersion The vault version that should be stored in this masterkey file (for downwards compatibility) + * @throws IOException When unable to write to the given file + */ + public void persist(Masterkey masterkey, Path filePath, CharSequence passphrase, int vaultVersion) throws IOException { + Path tmpFilePath = filePath.resolveSibling(filePath.getFileName().toString() + ".tmp"); + try (OutputStream out = Files.newOutputStream(tmpFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { + persist(masterkey, out, passphrase, vaultVersion); + } + Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING); + } + + void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, int vaultVersion) throws IOException { + Preconditions.checkArgument(!masterkey.isDestroyed(), "masterkey has been destroyed"); + + MasterkeyFile fileContent = lock(masterkey, passphrase, vaultVersion, DEFAULT_SCRYPT_COST_PARAM); + try (Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { + GSON.toJson(fileContent, writer); + writer.flush(); + } catch (JsonIOException e) { + throw new IOException(e); + } + } + + // visible for testing + MasterkeyFile lock(Masterkey masterkey, CharSequence passphrase, int vaultVersion, int scryptCostParam) { + Preconditions.checkNotNull(masterkey); + Preconditions.checkNotNull(passphrase); + Preconditions.checkArgument(!masterkey.isDestroyed(), "masterkey has been destroyed"); + + final byte[] salt = new byte[DEFAULT_SCRYPT_SALT_LENGTH]; + csprng.nextBytes(salt); + SecretKey kek = scrypt(passphrase, salt, pepper, scryptCostParam, DEFAULT_SCRYPT_BLOCK_SIZE); + try { + final Mac mac = MacSupplier.HMAC_SHA256.withKey(masterkey.getMacKey()); + final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(vaultVersion).array()); + MasterkeyFile result = new MasterkeyFile(); + result.version = vaultVersion; + result.versionMac = versionMac; + result.scryptSalt = salt; + result.scryptCostParam = scryptCostParam; + result.scryptBlockSize = DEFAULT_SCRYPT_BLOCK_SIZE; + result.encMasterKey = AesKeyWrap.wrap(kek, masterkey.getEncKey()); + result.macMasterKey = AesKeyWrap.wrap(kek, masterkey.getMacKey()); + return result; + } finally { + Destroyables.destroySilently(kek); + } + } + + /** + * Creates a {@link MasterkeyLoader} able to load keys from masterkey JSON files using the same pepper as this. + * + * @param passphraseProvider A callback used to retrieve the passphrase used during key derivation + * @param The type of the context to use during passphrase retrieval. + * @return A new masterkey loader. + */ + public MasterkeyLoader keyLoader(Function passphraseProvider) { + return new MasterkeyFileLoader<>(this, passphraseProvider); + } + + private static SecretKey scrypt(CharSequence passphrase, byte[] salt, byte[] pepper, int costParam, int blockSize) { + byte[] saltAndPepper = new byte[salt.length + pepper.length]; + System.arraycopy(salt, 0, saltAndPepper, 0, salt.length); + System.arraycopy(pepper, 0, saltAndPepper, salt.length, pepper.length); + byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, costParam, blockSize, Masterkey.KEY_LEN_BYTES); + try { + return new SecretKeySpec(kekBytes, Masterkey.ENC_ALG); + } finally { + Arrays.fill(kekBytes, (byte) 0x00); + } + } + + // visible for testing + static class MasterkeyFile { + + @SerializedName("version") + int version; + + @SerializedName("scryptSalt") + byte[] scryptSalt; + + @SerializedName("scryptCostParam") + int scryptCostParam; + + @SerializedName("scryptBlockSize") + int scryptBlockSize; + + @SerializedName("primaryMasterKey") + byte[] encMasterKey; + + @SerializedName("hmacMasterKey") + byte[] macMasterKey; + + @SerializedName("versionMac") + byte[] versionMac; + + private boolean isValid() { + return version != 0 + && scryptSalt != null + && scryptCostParam > 1 + && scryptBlockSize > 0 + && encMasterKey != null + && macMasterKey != null + && versionMac != null; + } + + } + + private static class ByteArrayJsonAdapter extends TypeAdapter { + + private static final BaseEncoding BASE64 = BaseEncoding.base64(); + + @Override + public void write(JsonWriter writer, byte[] value) throws IOException { + if (value == null) { + writer.nullValue(); + } else { + writer.value(BASE64.encode(value)); + } + } + + @Override + public byte[] read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } else { + return BASE64.decode(reader.nextString()); + } + } + } + +} diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java index c105e30..07543a4 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java @@ -4,74 +4,32 @@ import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.util.Optional; +import java.net.URI; +import java.nio.file.Path; +import java.util.function.Function; -/** - * Instances of this class can be retrieved by {@link MasterkeyFile#unlock(CharSequence, byte[], Optional) unlocking} - * a Cryptomator masterkey file and then be used to {@link MasterkeyLoader#loadKey(String) load} a {@link Masterkey}. - * - *
- * 	try (Masterkey masterkey = MasterkeyFile.withContent(in).unlock(pw, pepper, expectedVaultVersion).loadKeyAndClose()) {
- * 		// use masterkey
- * 	}
- * 
- */ -public class MasterkeyFileLoader implements MasterkeyLoader, AutoCloseable { +public class MasterkeyFileLoader implements MasterkeyLoader { - public static final String KEY_ID = "MASTERKEY_FILE"; - private final SecretKey encKey; - private final SecretKey macKey; + private static final String SUPPORTED_SCHEME = "masterkeyfile"; + private final MasterkeyFileAccess masterkeyFileAccess; + private final Function passphraseProvider; - // intentionally package-private - MasterkeyFileLoader(SecretKey encKey, SecretKey macKey) { - this.encKey = encKey; - this.macKey = macKey; - } - - /** - * Loads the key and closes this MasterkeyFileLoader immediately, if reuse is not required. - * - * @return The masterkey loaded from this masterkey file. - */ - public Masterkey loadKeyAndClose() { - try { - return loadKey(); - } finally { - close(); - } - } - - /** - * @return The masterkey loaded from this masterkey file. - */ - public Masterkey loadKey() { - try { - return loadKey(KEY_ID); - } catch (MasterkeyLoadingFailedException e) { - throw new IllegalStateException("Should have been able to load " + KEY_ID); - } + MasterkeyFileLoader(MasterkeyFileAccess masterkeyFileAccess, Function passphraseProvider) { + this.masterkeyFileAccess = masterkeyFileAccess; + this.passphraseProvider = passphraseProvider; } @Override - public Masterkey loadKey(String keyId) throws MasterkeyLoadingFailedException { - if (!KEY_ID.equals(keyId)) { - throw new MasterkeyLoadingFailedException("Unsupported key " + keyId); - } - if (encKey.isDestroyed() || macKey.isDestroyed()) { - throw new MasterkeyLoadingFailedException("MasterkeyFileLoader already closed."); - } - // we need a copy to make sure we can use autocloseable destruction - SecretKey encKeyCopy = new SecretKeySpec(encKey.getEncoded(), encKey.getAlgorithm()); - SecretKey macKeyCopy = new SecretKeySpec(macKey.getEncoded(), macKey.getAlgorithm()); - return new Masterkey(encKeyCopy, macKeyCopy); + public boolean supportsScheme(String scheme) { + return SUPPORTED_SCHEME.equalsIgnoreCase(scheme); } @Override - public void close() { - Destroyables.destroySilently(encKey); - Destroyables.destroySilently(macKey); + public Masterkey loadKey(URI keyId, C context) throws MasterkeyLoadingFailedException { + assert SUPPORTED_SCHEME.equalsIgnoreCase(keyId.getScheme()); + Path filePath = context.getVaultRoot().resolve(keyId.getSchemeSpecificPart()); + CharSequence passphrase = passphraseProvider.apply(context); + return masterkeyFileAccess.load(filePath, passphrase); } } diff --git a/src/main/java/org/cryptomator/cryptolib/common/VaultRootAwareContext.java b/src/main/java/org/cryptomator/cryptolib/common/VaultRootAwareContext.java new file mode 100644 index 0000000..3013dec --- /dev/null +++ b/src/main/java/org/cryptomator/cryptolib/common/VaultRootAwareContext.java @@ -0,0 +1,13 @@ +package org.cryptomator.cryptolib.common; + +import java.nio.file.Path; + +@FunctionalInterface +public interface VaultRootAwareContext { + + /** + * @return The vault's root path + */ + Path getVaultRoot(); + +} diff --git a/src/main/java/org/cryptomator/cryptolib/package-info.java b/src/main/java/org/cryptomator/cryptolib/package-info.java index ff44ae7..755e545 100644 --- a/src/main/java/org/cryptomator/cryptolib/package-info.java +++ b/src/main/java/org/cryptomator/cryptolib/package-info.java @@ -4,59 +4,20 @@ * Example Usage: * *
- * // Create new masterkey and safe it to a file:
- * SecureRandom csprng = SecureRandom.getInstanceStrong();
- * Masterkey masterkey = {@link org.cryptomator.cryptolib.api.Masterkey#createNew(java.security.SecureRandom) Masterkey.createNew(csprng)};
- * byte[] json = {@link org.cryptomator.cryptolib.common.MasterkeyFile#lock(org.cryptomator.cryptolib.api.Masterkey, java.lang.CharSequence, byte[], int, java.security.SecureRandom) MasterkeyFile.lock(masterkey, passphrase, pepper, vaultVersion, csprng)};
- * Files.write(path, json);
- *
- * // Load a masterkey from a file:
- * MasterkeyFileLoader loader = {@link org.cryptomator.cryptolib.common.MasterkeyFile#withContentFromFile(java.nio.file.Path) MasterkeyFile.withContentsFromFile(path)}.{@link org.cryptomator.cryptolib.common.MasterkeyFile#unlock(java.lang.CharSequence, byte[], java.util.Optional) unlock(passphrase, pepper, Optional.of(vaultVersion))};
- * Masterkey masterkey = loader.load(MasterkeyFileLoader.KEY_ID);
+ * // Define a pepper used during JSON serialization:
+ * MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(pepper, csprng);
  *
- * // Create new cryptor:
- * {@link org.cryptomator.cryptolib.api.Cryptor Cryptor} cryptor = {@link org.cryptomator.cryptolib.Cryptors#version1(java.security.SecureRandom) Cryptors.version1(SecureRandom.getInstanceStrong())}.{@link org.cryptomator.cryptolib.api.CryptorProvider#withKey(org.cryptomator.cryptolib.api.Masterkey) withKey(masterkey)};
- *
- * // Each directory needs a (relatively) unique ID, which affects the encryption/decryption of child names:
- * String uniqueIdOfDirectory = UUID.randomUUID().toString();
- *
- * // Encrypt and decrypt file name:
- * String cleartextFileName = "foo.txt";
- * String encryptedName = cryptor.{@link org.cryptomator.cryptolib.api.Cryptor#fileNameCryptor() fileNameCryptor()}.{@link org.cryptomator.cryptolib.api.FileNameCryptor#encryptFilename(String, byte[][])  encryptFilename(cleartextFileName, uniqueIdOfDirectory.getBytes())};
- * String decryptedName = cryptor.fileNameCryptor().{@link org.cryptomator.cryptolib.api.FileNameCryptor#decryptFilename(String, byte[][])  decryptFilename(encryptedName, uniqueIdOfDirectory.getBytes())};
- *
- * // Encrypt file contents:
- * ByteBuffer plaintext = ...;
- * SeekableByteChannel ciphertextOut = ...;
- * try (WritableByteChannel ch = new {@link org.cryptomator.cryptolib.EncryptingWritableByteChannel EncryptingWritableByteChannel}(ciphertextOut, cryptor)) {
- * 	ch.write(plaintext);
- * }
- *
- * // Decrypt file contents:
- * ReadableByteChannel ciphertextIn = ...;
- * try (ReadableByteChannel ch = new {@link org.cryptomator.cryptolib.DecryptingReadableByteChannel DecryptingReadableByteChannel}(ciphertextOut, cryptor, true)) {
- * 	ch.read(plaintext);
- * }
- * 
- */ -/** - * High-level encryption library used in Cryptomator. - *

- * Example Usage: - * - *

  * // Create new masterkey and safe it to a file:
  * SecureRandom csprng = SecureRandom.getInstanceStrong();
  * Masterkey masterkey = {@link org.cryptomator.cryptolib.api.Masterkey#createNew(java.security.SecureRandom) Masterkey.createNew(csprng)};
- * byte[] json = {@link org.cryptomator.cryptolib.common.MasterkeyFile#lock(org.cryptomator.cryptolib.api.Masterkey, java.lang.CharSequence, byte[], int, java.security.SecureRandom) MasterkeyFile.lock(masterkey, passphrase, pepper, vaultVersion, csprng)};
- * Files.write(path, json);
+ * {@link org.cryptomator.cryptolib.common.MasterkeyFileAccess#persist(org.cryptomator.cryptolib.api.Masterkey, java.nio.file.Path, java.lang.CharSequence, int) masterkeyFileAccess.persist(masterkey, path, passphrase, vaultVersion)};
  *
  * // Load a masterkey from a file:
- * MasterkeyFileLoader loader = {@link org.cryptomator.cryptolib.common.MasterkeyFile#withContentFromFile(java.nio.file.Path) MasterkeyFile.withContentsFromFile(path)}.{@link org.cryptomator.cryptolib.common.MasterkeyFile#unlock(java.lang.CharSequence, byte[], java.util.Optional) unlock(passphrase, pepper, Optional.of(vaultVersion))};
- * Masterkey masterkey = loader.load(MasterkeyFileLoader.KEY_ID);
+ * Masterkey masterkey = {@link org.cryptomator.cryptolib.common.MasterkeyFileAccess#load(java.nio.file.Path, java.lang.CharSequence) masterkeyFileAccess.load(path, passphrase};
  *
  * // Create new cryptor:
  * {@link org.cryptomator.cryptolib.api.Cryptor Cryptor} cryptor = {@link org.cryptomator.cryptolib.Cryptors#version1(java.security.SecureRandom) Cryptors.version1(SecureRandom.getInstanceStrong())}.{@link org.cryptomator.cryptolib.api.CryptorProvider#withKey(org.cryptomator.cryptolib.api.Masterkey) withKey(masterkey)};
+ *
  * // Each directory needs a (relatively) unique ID, which affects the encryption/decryption of child names:
  * String uniqueIdOfDirectory = UUID.randomUUID().toString();
  *
diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java
new file mode 100644
index 0000000..34f91f3
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java
@@ -0,0 +1,195 @@
+package org.cryptomator.cryptolib.common;
+
+import com.google.common.io.BaseEncoding;
+import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.hamcrest.MatcherAssert;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.security.SecureRandom;
+import java.util.function.Function;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.IsNot.not;
+
+public class MasterkeyFileAccessTest {
+
+	private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
+	private static final byte[] DEFAULT_PEPPER = new byte[0];
+
+	private Masterkey key = Masterkey.createFromRaw(new byte[64]);
+	private MasterkeyFileAccess.MasterkeyFile keyFile = new MasterkeyFileAccess.MasterkeyFile();
+
+	@BeforeEach
+	public void setup() {
+		keyFile.version = 3;
+		keyFile.scryptSalt = new byte[8];
+		keyFile.scryptCostParam = 2;
+		keyFile.scryptBlockSize = 8;
+		keyFile.encMasterKey = BaseEncoding.base64().decode("mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==");
+		keyFile.macMasterKey = BaseEncoding.base64().decode("mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==");
+		keyFile.versionMac = BaseEncoding.base64().decode("iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=");
+	}
+
+	@Test
+	@DisplayName("keyLoader(...) does not load a key yet")
+	public void testCreateKeyLoader() {
+		Function pwProvider = Mockito.mock(Function.class);
+		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK);
+
+		MasterkeyLoader keyLoader = masterkeyFileAccess.keyLoader(pwProvider);
+
+		Assertions.assertNotNull(keyLoader);
+		Mockito.verify(pwProvider, Mockito.never()).apply(Mockito.any());
+	}
+
+	@Test
+	@DisplayName("changePassphrase()")
+	public void testChangePassphrase() throws CryptoException {
+		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK);
+
+		MasterkeyFileAccess.MasterkeyFile changed1 = masterkeyFileAccess.changePassphrase(keyFile, "asd", "qwe");
+		MasterkeyFileAccess.MasterkeyFile changed2 = masterkeyFileAccess.changePassphrase(changed1, "qwe", "asd");
+
+		MatcherAssert.assertThat(keyFile.encMasterKey, not(equalTo(changed1.encMasterKey)));
+		Assertions.assertArrayEquals(keyFile.encMasterKey, changed2.encMasterKey);
+	}
+
+	@Nested
+	@DisplayName("load()")
+	class Load {
+
+		MasterkeyFileAccess masterkeyFileAccess = Mockito.spy(new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK));
+
+		@Test
+		public void testParseInvalid() {
+			String content = "{\"foo\": 42}";
+			InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+
+			Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> {
+				masterkeyFileAccess.load(in, "asd");
+			});
+		}
+
+		@Test
+		public void testParseMalformed() {
+			final String content = "not even json";
+			InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+
+			Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> {
+				masterkeyFileAccess.load(in, "asd");
+			});
+		}
+
+	}
+
+	@Nested
+	@DisplayName("unlock()")
+	class Unlock {
+
+		@Test
+		@DisplayName("with correct password")
+		public void testUnlockWithCorrectPassword() throws CryptoException {
+			MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK);
+
+			Masterkey key = masterkeyFileAccess.unlock(keyFile, "asd");
+
+			Assertions.assertNotNull(key);
+		}
+
+		@Test
+		@DisplayName("with invalid password")
+		public void testUnlockWithIncorrectPassword() {
+			MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK);
+
+			Assertions.assertThrows(InvalidPassphraseException.class, () -> {
+				masterkeyFileAccess.unlock(keyFile, "qwe");
+			});
+		}
+
+		@Test
+		@DisplayName("with correct password but invalid pepper")
+		public void testUnlockWithIncorrectPepper() {
+			MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[1], RANDOM_MOCK);
+
+			Assertions.assertThrows(InvalidPassphraseException.class, () -> {
+				masterkeyFileAccess.unlock(keyFile, "qwe");
+			});
+		}
+
+	}
+
+	@Nested
+	@DisplayName("lock()")
+	class Lock {
+
+		@Test
+		@DisplayName("creates expected values")
+		public void testLock() {
+			MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK);
+
+			MasterkeyFileAccess.MasterkeyFile keyFile = masterkeyFileAccess.lock(key, "asd", 3, 2);
+
+			Assertions.assertEquals(3, keyFile.version);
+			Assertions.assertArrayEquals(new byte[8], keyFile.scryptSalt);
+			Assertions.assertEquals(2, keyFile.scryptCostParam);
+			Assertions.assertEquals(8, keyFile.scryptBlockSize);
+			Assertions.assertArrayEquals(BaseEncoding.base64().decode("mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q=="), keyFile.encMasterKey);
+			Assertions.assertArrayEquals(BaseEncoding.base64().decode("mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q=="), keyFile.macMasterKey);
+			Assertions.assertArrayEquals(BaseEncoding.base64().decode("iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA="), keyFile.versionMac);
+		}
+
+		@Test
+		@DisplayName("different passwords -> different wrapped keys")
+		public void testLockWithDifferentPasswords() {
+			MasterkeyFileAccess masterkeyFileAccess1 = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK);
+
+			MasterkeyFileAccess.MasterkeyFile keyFile1 = masterkeyFileAccess1.lock(key, "asd", 8, 2);
+			MasterkeyFileAccess.MasterkeyFile keyFile2 = masterkeyFileAccess1.lock(key, "qwe", 8, 2);
+
+			MatcherAssert.assertThat(keyFile1.encMasterKey, not(equalTo(keyFile2.encMasterKey)));
+		}
+
+		@Test
+		@DisplayName("different peppers -> different wrapped keys")
+		public void testLockWithDifferentPeppers() {
+			byte[] pepper1 = new byte[]{(byte) 0x01};
+			byte[] pepper2 = new byte[]{(byte) 0x02};
+			MasterkeyFileAccess masterkeyFileAccess1 = new MasterkeyFileAccess(pepper1, RANDOM_MOCK);
+			MasterkeyFileAccess masterkeyFileAccess2 = new MasterkeyFileAccess(pepper2, RANDOM_MOCK);
+
+			MasterkeyFileAccess.MasterkeyFile keyFile1 = masterkeyFileAccess1.lock(key, "asd", 8, 2);
+			MasterkeyFileAccess.MasterkeyFile keyFile2 = masterkeyFileAccess2.lock(key, "asd", 8, 2);
+
+			MatcherAssert.assertThat(keyFile1.encMasterKey, not(equalTo(keyFile2.encMasterKey)));
+		}
+
+	}
+
+	@Test
+	@DisplayName("persist and load")
+	public void testPersistAndLoad(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
+		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK);
+		Path masterkeyFile = tmpDir.resolve("masterkey.cryptomator");
+
+		masterkeyFileAccess.persist(key, masterkeyFile, "asd", 999);
+		Masterkey loaded = masterkeyFileAccess.load(masterkeyFile, "asd");
+
+		Assertions.assertArrayEquals(key.getEncoded(), loaded.getEncoded());
+	}
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderTest.java
index 9e41ba0..a593242 100644
--- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderTest.java
@@ -1,33 +1,7 @@
 package org.cryptomator.cryptolib.common;
 
-import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
-import org.cryptomator.cryptolib.api.Masterkey;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-import javax.crypto.SecretKey;
-import javax.security.auth.DestroyFailedException;
-
 public class MasterkeyFileLoaderTest {
 
-	@Test
-	public void testLoadedKeySurvivesLoader() throws MasterkeyLoadingFailedException, DestroyFailedException {
-		SecretKey encKey = Mockito.mock(SecretKey.class);
-		SecretKey macKey = Mockito.mock(SecretKey.class);
-		Mockito.when(encKey.getEncoded()).thenReturn(new byte[32]);
-		Mockito.when(encKey.getAlgorithm()).thenReturn("AES");
-		Mockito.when(macKey.getEncoded()).thenReturn(new byte[32]);
-		Mockito.when(macKey.getAlgorithm()).thenReturn("HmacSHA256");
-
-		Masterkey masterkey;
-		try (MasterkeyFileLoader loader = new MasterkeyFileLoader(encKey, macKey)) {
-			masterkey = loader.loadKey(MasterkeyFileLoader.KEY_ID);
-		}
 
-		Mockito.verify(encKey).destroy();
-		Mockito.verify(macKey).destroy();
-		Assertions.assertFalse(masterkey.isDestroyed());
-	}
 
 }
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java
deleted file mode 100644
index d05a3d6..0000000
--- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java
+++ /dev/null
@@ -1,182 +0,0 @@
-package org.cryptomator.cryptolib.common;
-
-import org.cryptomator.cryptolib.api.CryptoException;
-import org.cryptomator.cryptolib.api.InvalidPassphraseException;
-import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.api.MasterkeyLoader;
-import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
-import org.hamcrest.CoreMatchers;
-import org.hamcrest.MatcherAssert;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.security.SecureRandom;
-import java.util.Optional;
-
-import static org.hamcrest.core.IsEqual.equalTo;
-import static org.hamcrest.core.IsNot.not;
-
-public class MasterkeyFileTest {
-
-	private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
-
-	@Test
-	public void testParse() throws IOException {
-		final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
-				+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-				+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-				+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
-
-		MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes()));
-		Assertions.assertNotNull(masterkeyFile);
-	}
-
-	@Test
-	public void testParseInvalid() {
-		final String content = "{\"foo\": 42}";
-
-		Assertions.assertThrows(IOException.class, () -> {
-			MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes()));
-		});
-	}
-
-	@Test
-	public void testParseMalformed() {
-		final String content = "not even json";
-
-		Assertions.assertThrows(IOException.class, () -> {
-			MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes()));
-		});
-	}
-
-	@Nested
-	class Unlock {
-
-		@Test
-		public void testUnlockWithCorrectPassword() throws IOException, CryptoException {
-			final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
-					+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
-
-			MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes()));
-			MasterkeyLoader keyLoader = masterkeyFile.unlock("asd", new byte[0], Optional.of(3));
-			Assertions.assertNotNull(keyLoader);
-		}
-
-		@Test
-		public void testUnlockWithIncorrectPassword() throws IOException {
-			final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
-					+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
-
-			MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes()));
-			Assertions.assertThrows(InvalidPassphraseException.class, () -> {
-				masterkeyFile.unlock("qwe", new byte[0], Optional.empty());
-			});
-		}
-
-		@Test
-		public void testUnlockWithIncorrectPepper() throws IOException {
-			final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
-					+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
-
-			MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes()));
-			Assertions.assertThrows(InvalidPassphraseException.class, () -> {
-				masterkeyFile.unlock("qwe", new byte[3], Optional.empty());
-			});
-		}
-
-		@Test
-		public void testUnlockWithIncorrectVaultFormat() throws IOException {
-			final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
-					+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
-
-			MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes()));
-			Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> {
-				masterkeyFile.unlock("asd", new byte[0], Optional.of(42));
-			});
-		}
-
-		@Test
-		public void testUnlockWithIncorrectVersionMac() throws IOException {
-			final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
-					+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"versionMac\":\"AAAA\"}";
-
-			MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes()));
-			Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> {
-				masterkeyFile.unlock("asd", new byte[0], Optional.of(3));
-			});
-		}
-
-		@Test
-		public void testUnlockWithIgnoredVersionMac() throws IOException, CryptoException {
-			final String content = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
-					+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
-					+ "\"versionMac\":\"AAAA\"}";
-
-			MasterkeyFile masterkeyFile = MasterkeyFile.withContent(new ByteArrayInputStream(content.getBytes()));
-			MasterkeyLoader keyLoader = masterkeyFile.unlock("asd", new byte[0], Optional.empty());
-			Assertions.assertNotNull(keyLoader);
-		}
-
-	}
-
-	@Nested
-	class Lock {
-
-		@Test
-		public void testLock() {
-			byte[] serialized;
-			try (Masterkey masterkey = Masterkey.createFromRaw(new byte[64])) {
-				serialized = MasterkeyFile.lock(masterkey, "asd", new byte[0], 3, RANDOM_MOCK);
-			}
-
-			String serializedStr = new String(serialized, StandardCharsets.UTF_8);
-			MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"version\": 3"));
-			MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptSalt\": \"AAAAAAAAAAA=\""));
-			MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptCostParam\": 32768"));
-			MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"scryptBlockSize\": 8"));
-			MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"primaryMasterKey\": \"bOuDTfSpTHJrM4G321gts1QL+TFAZ3I6S/QHwim39pz+t+/K9IYy6g==\""));
-			MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"hmacMasterKey\": \"bOuDTfSpTHJrM4G321gts1QL+TFAZ3I6S/QHwim39pz+t+/K9IYy6g==\""));
-			MatcherAssert.assertThat(serializedStr, CoreMatchers.containsString("\"versionMac\": \"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\""));
-		}
-
-		@Test
-		public void testLockWithDifferentPeppers() {
-			byte[] serialized1, serialized2;
-			try (Masterkey masterkey = Masterkey.createFromRaw(new byte[64])) {
-				serialized1 = MasterkeyFile.lock(masterkey, "asd", new byte[] {(byte) 0x01}, 8, RANDOM_MOCK);
-				serialized2 = MasterkeyFile.lock(masterkey, "asd", new byte[] {(byte) 0x02}, 8, RANDOM_MOCK);
-			}
-
-			MatcherAssert.assertThat(serialized1, not(equalTo(serialized2)));
-		}
-
-	}
-
-	@Test
-	public void testChangePassword() throws IOException, CryptoException {
-		Masterkey masterkey = Masterkey.createFromRaw(new byte[64]);
-		byte[] serialized1 = MasterkeyFile.lock(masterkey, "password", new byte[0], 42, RANDOM_MOCK);
-		byte[] serialized2 = MasterkeyFile.changePassphrase(serialized1, "password", "betterPassw0rd!", new byte[0], RANDOM_MOCK);
-		Masterkey unlocked1 = MasterkeyFile.withContent(new ByteArrayInputStream(serialized1)).unlock("password", new byte[0], Optional.of(42)).loadKey(MasterkeyFileLoader.KEY_ID);
-		Masterkey unlocked2 = MasterkeyFile.withContent(new ByteArrayInputStream(serialized2)).unlock("betterPassw0rd!", new byte[0], Optional.of(42)).loadKey(MasterkeyFileLoader.KEY_ID);
-
-		MatcherAssert.assertThat(serialized1, not(equalTo(serialized2)));
-		Assertions.assertNotSame(unlocked1, unlocked2);
-		Assertions.assertArrayEquals(unlocked1.getEncoded(), unlocked2.getEncoded());
-	}
-}
\ No newline at end of file

From e3ee3de00010ee7e4cdf538333e3ae0d99c82b08 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel 
Date: Fri, 22 Jan 2021 19:39:10 +0100
Subject: [PATCH 17/59] run CI release builds using the correct profile

---
 .github/workflows/build.yml | 2 +-
 pom.xml                     | 7 -------
 2 files changed, 1 insertion(+), 8 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7734712..2368bc4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -50,7 +50,7 @@ jobs:
           path: target/cryptolib-${{ env.BUILD_VERSION }}.jar
       - name: Build and deploy to jcenter
         if: startsWith(github.ref, 'refs/tags/')
-        run: mvn -B deploy -DskipTests
+        run: mvn -B deploy -DskipTests -Prelease
         env:
           BINTRAY_USERNAME: cryptobot
           BINTRAY_API_KEY: ${{ secrets.BINTRAY_API_KEY }}
diff --git a/pom.xml b/pom.xml
index b57d938..78b168a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,13 +55,6 @@
 		
 	
 
-    
-        
-            bintray-jcenter
-            https://api.bintray.com/maven/cryptomator/maven/cryptolib/;publish=1
-        
-    
-
 	
 		
 			org.cryptomator

From ef75f24c3a78964ba472af076e896ea664e0bac0 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel 
Date: Mon, 25 Jan 2021 18:26:01 +0100
Subject: [PATCH 18/59] Refactored MasterkeyFile-related API

---
 .../cryptolib/api/CryptoException.java        |  2 +-
 .../cryptolib/api/MasterkeyLoader.java        |  5 +-
 .../cryptolib/common/MasterkeyFileAccess.java | 40 ++++++++++---
 .../cryptolib/common/MasterkeyFileLoader.java | 57 +++++++++++++++----
 .../common/MasterkeyFileLoaderContext.java    | 23 ++++++++
 .../common/VaultRootAwareContext.java         | 13 -----
 .../common/MasterkeyFileAccessTest.java       | 19 +++++--
 7 files changed, 120 insertions(+), 39 deletions(-)
 create mode 100644 src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java
 delete mode 100644 src/main/java/org/cryptomator/cryptolib/common/VaultRootAwareContext.java

diff --git a/src/main/java/org/cryptomator/cryptolib/api/CryptoException.java b/src/main/java/org/cryptomator/cryptolib/api/CryptoException.java
index 0a5f034..fe1958a 100644
--- a/src/main/java/org/cryptomator/cryptolib/api/CryptoException.java
+++ b/src/main/java/org/cryptomator/cryptolib/api/CryptoException.java
@@ -8,7 +8,7 @@
  *******************************************************************************/
 package org.cryptomator.cryptolib.api;
 
-public abstract class CryptoException extends Exception {
+public abstract class CryptoException extends RuntimeException {
 
 	protected CryptoException() {
 		super();
diff --git a/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java
index 7d27db8..6065bca 100644
--- a/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java
+++ b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java
@@ -9,7 +9,7 @@
  *
  * @see MasterkeyFileAccess
  */
-public interface MasterkeyLoader {
+public interface MasterkeyLoader {
 
 	boolean supportsScheme(String scheme);
 
@@ -17,10 +17,9 @@ public interface MasterkeyLoader {
 	 * Loads a master key. This might be a long-running operation, as it may require user input or expensive computations.
 	 *
 	 * @param keyId An URI uniquely identifying the source and identity of the key
-	 * @param context An optional context containing additional information required during key retrieval
 	 * @return The raw key bytes. Must not be null
 	 * @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request
 	 */
-	Masterkey loadKey(URI keyId, C context) throws MasterkeyLoadingFailedException;
+	Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException;
 
 }
diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java
index 44eb05d..8f5ae29 100644
--- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java
+++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java
@@ -28,6 +28,7 @@
 import java.io.OutputStreamWriter;
 import java.io.Reader;
 import java.io.Writer;
+import java.net.URI;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -52,6 +53,8 @@
  * 
*/ public class MasterkeyFileAccess { + + private static final int DEFAULT_MASTERKEY_FILE_VERSION = 999; // legacy field. dropped with vault format 8 private static final int DEFAULT_SCRYPT_SALT_LENGTH = 8; private static final int DEFAULT_SCRYPT_COST_PARAM = 1 << 15; // 2^15 private static final int DEFAULT_SCRYPT_BLOCK_SIZE = 8; @@ -69,6 +72,26 @@ public MasterkeyFileAccess(byte[] pepper, SecureRandom csprng) { this.csprng = csprng; } + /** + * Parses the given masterkey file contents and returns the alleged vault version without verifying the version MAC. + * @param masterkey The file contents of a masterkey file. + * @return The (unverified) vault version + * @throws IOException In case of errors, such as unparseable JSON. + * @deprecated Starting with vault format 8, the vault version is no longer stored inside the masterkey file. + */ + @Deprecated + public static int readAllegedVaultVersion(byte[] masterkey) throws IOException { + try (ByteArrayInputStream in = new ByteArrayInputStream(masterkey); + Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + MasterkeyFile parsedFile = GSON.fromJson(reader, MasterkeyFile.class); + return parsedFile.version; + } catch (JsonParseException e) { + throw new IOException("Unreadable JSON", e); + } catch (IllegalArgumentException e) { + throw new IOException("Invalid JSON content", e); + } + } + /** * Reencrypts a masterkey with a new passphrase. * @@ -161,10 +184,13 @@ Masterkey unlock(MasterkeyFile parsedFile, CharSequence passphrase) throws Inval * @param masterkey The key to protect * @param filePath Where to store the file (gets overwritten, parent dir must exist) * @param passphrase The passphrase used during key derivation - * @param vaultVersion The vault version that should be stored in this masterkey file (for downwards compatibility) * @throws IOException When unable to write to the given file */ - public void persist(Masterkey masterkey, Path filePath, CharSequence passphrase, int vaultVersion) throws IOException { + public void persist(Masterkey masterkey, Path filePath, CharSequence passphrase) throws IOException { + persist(masterkey, filePath, passphrase, DEFAULT_MASTERKEY_FILE_VERSION); + } + + public void persist(Masterkey masterkey, Path filePath, CharSequence passphrase, @Deprecated int vaultVersion) throws IOException { Path tmpFilePath = filePath.resolveSibling(filePath.getFileName().toString() + ".tmp"); try (OutputStream out = Files.newOutputStream(tmpFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { persist(masterkey, out, passphrase, vaultVersion); @@ -172,7 +198,7 @@ public void persist(Masterkey masterkey, Path filePath, CharSequence passphrase, Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING); } - void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, int vaultVersion) throws IOException { + void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @Deprecated int vaultVersion) throws IOException { Preconditions.checkArgument(!masterkey.isDestroyed(), "masterkey has been destroyed"); MasterkeyFile fileContent = lock(masterkey, passphrase, vaultVersion, DEFAULT_SCRYPT_COST_PARAM); @@ -213,12 +239,12 @@ MasterkeyFile lock(Masterkey masterkey, CharSequence passphrase, int vaultVersio /** * Creates a {@link MasterkeyLoader} able to load keys from masterkey JSON files using the same pepper as this. * - * @param passphraseProvider A callback used to retrieve the passphrase used during key derivation - * @param The type of the context to use during passphrase retrieval. + * @param vaultRoot The path to a vault for which a masterkey should be loaded. + * @param context A context providing information required by the key loader. * @return A new masterkey loader. */ - public MasterkeyLoader keyLoader(Function passphraseProvider) { - return new MasterkeyFileLoader<>(this, passphraseProvider); + public MasterkeyFileLoader keyLoader(Path vaultRoot, MasterkeyFileLoaderContext context) { + return new MasterkeyFileLoader(vaultRoot, this, context); } private static SecretKey scrypt(CharSequence passphrase, byte[] salt, byte[] pepper, int costParam, int blockSize) { diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java index 07543a4..9752447 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java @@ -5,30 +5,65 @@ import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.function.Function; -public class MasterkeyFileLoader implements MasterkeyLoader { +/** + * A {@link MasterkeyLoader} for keys with the {@value #SCHEME} scheme. + *

+ * Instances of this class are {@link MasterkeyFileLoaderContext context}-specific and should be obtained + * via {@link MasterkeyFileAccess#keyLoader(MasterkeyFileLoaderContext)} + *

+ * This key loader {@link #loadKey(URI) loads} a vault's masterkey by interpreting the key ID as a path, + * either absolute or relative to the root directory of the vault, pointing to a masterkey file containing + * information that (paired with the correct passphrase) can be used to derive the masterkey. + */ +public class MasterkeyFileLoader implements MasterkeyLoader { - private static final String SUPPORTED_SCHEME = "masterkeyfile"; + public static final String SCHEME = "masterkeyfile"; + + private final Path vaultRoot; private final MasterkeyFileAccess masterkeyFileAccess; - private final Function passphraseProvider; + private final MasterkeyFileLoaderContext context; - MasterkeyFileLoader(MasterkeyFileAccess masterkeyFileAccess, Function passphraseProvider) { + MasterkeyFileLoader(Path vaultRoot, MasterkeyFileAccess masterkeyFileAccess, MasterkeyFileLoaderContext context) { + this.vaultRoot = vaultRoot; this.masterkeyFileAccess = masterkeyFileAccess; - this.passphraseProvider = passphraseProvider; + this.context = context; + } + + /** + * @param masterkeyFilePath Vault-relative or absolute path to a masterkey file. + * @return A new URI that can be used as key ID + */ + public static URI keyId(String masterkeyFilePath) { + try { + return new URI(SCHEME, masterkeyFilePath, null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't create URI from " + SCHEME + ":" + masterkeyFilePath, e); + } } @Override public boolean supportsScheme(String scheme) { - return SUPPORTED_SCHEME.equalsIgnoreCase(scheme); + return SCHEME.equalsIgnoreCase(scheme); } @Override - public Masterkey loadKey(URI keyId, C context) throws MasterkeyLoadingFailedException { - assert SUPPORTED_SCHEME.equalsIgnoreCase(keyId.getScheme()); - Path filePath = context.getVaultRoot().resolve(keyId.getSchemeSpecificPart()); - CharSequence passphrase = passphraseProvider.apply(context); + public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException { + assert SCHEME.equalsIgnoreCase(keyId.getScheme()); + Path filePath = vaultRoot.resolve(keyId.getSchemeSpecificPart()); + if (!Files.exists(filePath)) { + filePath = context.getMasterkeyFilePath(keyId.getSchemeSpecificPart()); + } + if (filePath == null) { + throw new MasterkeyLoadingFailedException("No masterkey file provided."); + } + CharSequence passphrase = context.getPassphrase(filePath); + if (passphrase == null) { + throw new MasterkeyLoadingFailedException("No passphrase provided."); + } return masterkeyFileAccess.load(filePath, passphrase); } diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java new file mode 100644 index 0000000..bddd11a --- /dev/null +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java @@ -0,0 +1,23 @@ +package org.cryptomator.cryptolib.common; + +import java.nio.file.Path; + +public interface MasterkeyFileLoaderContext { + + /** + * Provides the path of a masterkey file, if it could not be resolved automatically. + * + * @param incorrectPath The path as denoted by the key ID + * @return The correct path to a masterkey file or null to abort key loading. + */ + Path getMasterkeyFilePath(String incorrectPath); + + /** + * Provides the password for a given masterkey file. + * + * @param masterkeyFile For what masterkey file + * @return The passphrase or null to abort key loading. + */ + CharSequence getPassphrase(Path masterkeyFile); + +} diff --git a/src/main/java/org/cryptomator/cryptolib/common/VaultRootAwareContext.java b/src/main/java/org/cryptomator/cryptolib/common/VaultRootAwareContext.java deleted file mode 100644 index 3013dec..0000000 --- a/src/main/java/org/cryptomator/cryptolib/common/VaultRootAwareContext.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.cryptomator.cryptolib.common; - -import java.nio.file.Path; - -@FunctionalInterface -public interface VaultRootAwareContext { - - /** - * @return The vault's root path - */ - Path getVaultRoot(); - -} diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java index 34f91f3..9324a99 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java @@ -48,13 +48,14 @@ public void setup() { @Test @DisplayName("keyLoader(...) does not load a key yet") public void testCreateKeyLoader() { - Function pwProvider = Mockito.mock(Function.class); + Path path = Mockito.mock(Path.class); + MasterkeyFileLoaderContext keyLoaderContext = Mockito.mock(MasterkeyFileLoaderContext.class); MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK); - MasterkeyLoader keyLoader = masterkeyFileAccess.keyLoader(pwProvider); + MasterkeyLoader keyLoader = masterkeyFileAccess.keyLoader(path, keyLoaderContext); Assertions.assertNotNull(keyLoader); - Mockito.verify(pwProvider, Mockito.never()).apply(Mockito.any()); + Mockito.verifyNoInteractions(keyLoaderContext); } @Test @@ -69,6 +70,16 @@ public void testChangePassphrase() throws CryptoException { Assertions.assertArrayEquals(keyFile.encMasterKey, changed2.encMasterKey); } + @Test + @DisplayName("readAllegedVaultVersion()") + public void testReadAllegedVaultVersion() throws IOException { + byte[] content = "{\"version\": 1337}".getBytes(StandardCharsets.UTF_8); + + int version = MasterkeyFileAccess.readAllegedVaultVersion(content); + + Assertions.assertEquals(1337, version); + } + @Nested @DisplayName("load()") class Load { @@ -186,7 +197,7 @@ public void testPersistAndLoad(@TempDir Path tmpDir) throws IOException, Masterk MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK); Path masterkeyFile = tmpDir.resolve("masterkey.cryptomator"); - masterkeyFileAccess.persist(key, masterkeyFile, "asd", 999); + masterkeyFileAccess.persist(key, masterkeyFile, "asd"); Masterkey loaded = masterkeyFileAccess.load(masterkeyFile, "asd"); Assertions.assertArrayEquals(key.getEncoded(), loaded.getEncoded()); From 7829cbfef11fe8817ceb02bf3be438962cb6e17e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 25 Jan 2021 18:48:31 +0100 Subject: [PATCH 19/59] fixed JavaDoc --- .../cryptomator/cryptolib/common/ByteBuffers.java | 9 +++++---- .../cryptolib/common/MasterkeyFileAccess.java | 13 ++++++------- .../cryptolib/common/MasterkeyFileLoader.java | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/common/ByteBuffers.java b/src/main/java/org/cryptomator/cryptolib/common/ByteBuffers.java index a49d90a..d30ee36 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/ByteBuffers.java +++ b/src/main/java/org/cryptomator/cryptolib/common/ByteBuffers.java @@ -17,8 +17,8 @@ public class ByteBuffers { /** * Copies as many bytes as possible from the given source to the destination buffer. * The position of both buffers will be incremented by as many bytes as have been copied. - * - * @param source ByteBuffer from which bytes are read + * + * @param source ByteBuffer from which bytes are read * @param destination ByteBuffer into which bytes are written * @return number of bytes copied, i.e. {@link ByteBuffer#remaining() source.remaining()} or {@link ByteBuffer#remaining() destination.remaining()}, whatever is less. */ @@ -33,11 +33,12 @@ public static int copy(ByteBuffer source, ByteBuffer destination) { /** * Fills the given buffer by reading from the given source until either reaching EOF - * or buffer has no more {@link ByteBuffer#hasRemaining() remaining space}. + * or buffer has no more {@link ByteBuffer#hasRemaining() remaining space}. + * * @param source The channel to read from * @param buffer The buffer to fill * @return Number of bytes read. Will only be less than remaining space in buffer if reaching EOF. - * @throws IOException + * @throws IOException In case of I/O errors */ public static int fill(ReadableByteChannel source, ByteBuffer buffer) throws IOException { final int requested = buffer.remaining(); diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index 8f5ae29..022a6a1 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -28,7 +28,6 @@ import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; -import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -38,7 +37,6 @@ import java.security.InvalidKeyException; import java.security.SecureRandom; import java.util.Arrays; -import java.util.function.Function; /** * Allow loading and persisting of {@link Masterkey masterkeys} from and to encrypted json files. @@ -74,6 +72,7 @@ public MasterkeyFileAccess(byte[] pepper, SecureRandom csprng) { /** * Parses the given masterkey file contents and returns the alleged vault version without verifying the version MAC. + * * @param masterkey The file contents of a masterkey file. * @return The (unverified) vault version * @throws IOException In case of errors, such as unparseable JSON. @@ -133,7 +132,7 @@ MasterkeyFile changePassphrase(MasterkeyFile masterkey, CharSequence oldPassphra * @param passphrase The passphrase used during key derivation * @return A new masterkey. Should be used in a try-with-resource statement. * @throws InvalidPassphraseException If the provided passphrase can not be used to unwrap the stored keys. - * @throws MasterkeyLoadingFailedException + * @throws MasterkeyLoadingFailedException If reading the masterkey file fails */ public Masterkey load(Path filePath, CharSequence passphrase) throws MasterkeyLoadingFailedException { try (InputStream in = Files.newInputStream(filePath, StandardOpenOption.READ)) { @@ -181,9 +180,9 @@ Masterkey unlock(MasterkeyFile parsedFile, CharSequence passphrase) throws Inval * Then serializes the encrypted keys as well as used key derivation parameters into a JSON representation * that will be stored at the given filePath. * - * @param masterkey The key to protect - * @param filePath Where to store the file (gets overwritten, parent dir must exist) - * @param passphrase The passphrase used during key derivation + * @param masterkey The key to protect + * @param filePath Where to store the file (gets overwritten, parent dir must exist) + * @param passphrase The passphrase used during key derivation * @throws IOException When unable to write to the given file */ public void persist(Masterkey masterkey, Path filePath, CharSequence passphrase) throws IOException { @@ -240,7 +239,7 @@ MasterkeyFile lock(Masterkey masterkey, CharSequence passphrase, int vaultVersio * Creates a {@link MasterkeyLoader} able to load keys from masterkey JSON files using the same pepper as this. * * @param vaultRoot The path to a vault for which a masterkey should be loaded. - * @param context A context providing information required by the key loader. + * @param context A context providing information required by the key loader. * @return A new masterkey loader. */ public MasterkeyFileLoader keyLoader(Path vaultRoot, MasterkeyFileLoaderContext context) { diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java index 9752447..14e0758 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java @@ -13,7 +13,7 @@ * A {@link MasterkeyLoader} for keys with the {@value #SCHEME} scheme. *

* Instances of this class are {@link MasterkeyFileLoaderContext context}-specific and should be obtained - * via {@link MasterkeyFileAccess#keyLoader(MasterkeyFileLoaderContext)} + * via {@link MasterkeyFileAccess#keyLoader(Path, MasterkeyFileLoaderContext)} *

* This key loader {@link #loadKey(URI) loads} a vault's masterkey by interpreting the key ID as a path, * either absolute or relative to the root directory of the vault, pointing to a masterkey file containing From e5fad3d027d8902297deb4f3f3ae2b660387b261 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 25 Jan 2021 22:13:57 +0100 Subject: [PATCH 20/59] fixed changePassphrase --- .../cryptolib/common/MasterkeyFileAccess.java | 8 ++- .../common/MasterkeyFileAccessTest.java | 62 ++++++++++++------- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index 022a6a1..4445155 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -109,6 +109,7 @@ public byte[] changePassphrase(byte[] masterkey, CharSequence oldPassphrase, Cha MasterkeyFile original = GSON.fromJson(reader, MasterkeyFile.class); MasterkeyFile updated = changePassphrase(original, oldPassphrase, newPassphrase); GSON.toJson(updated, writer); + writer.flush(); return out.toByteArray(); } catch (JsonParseException e) { throw new IOException("Unreadable JSON", e); @@ -198,9 +199,14 @@ public void persist(Masterkey masterkey, Path filePath, CharSequence passphrase, } void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @Deprecated int vaultVersion) throws IOException { + persist(masterkey, out, passphrase, vaultVersion, DEFAULT_SCRYPT_COST_PARAM); + } + + // visible for testing + void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @Deprecated int vaultVersion, int scryptCostParam) throws IOException { Preconditions.checkArgument(!masterkey.isDestroyed(), "masterkey has been destroyed"); - MasterkeyFile fileContent = lock(masterkey, passphrase, vaultVersion, DEFAULT_SCRYPT_COST_PARAM); + MasterkeyFile fileContent = lock(masterkey, passphrase, vaultVersion, scryptCostParam); try (Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { GSON.toJson(fileContent, writer); writer.flush(); diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java index 9324a99..1b2230d 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java @@ -16,12 +16,12 @@ import org.mockito.Mockito; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.security.SecureRandom; -import java.util.function.Function; import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsNot.not; @@ -33,6 +33,7 @@ public class MasterkeyFileAccessTest { private Masterkey key = Masterkey.createFromRaw(new byte[64]); private MasterkeyFileAccess.MasterkeyFile keyFile = new MasterkeyFileAccess.MasterkeyFile(); + private MasterkeyFileAccess masterkeyFileAccess = Mockito.spy(new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK)); @BeforeEach public void setup() { @@ -50,7 +51,6 @@ public void setup() { public void testCreateKeyLoader() { Path path = Mockito.mock(Path.class); MasterkeyFileLoaderContext keyLoaderContext = Mockito.mock(MasterkeyFileLoaderContext.class); - MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK); MasterkeyLoader keyLoader = masterkeyFileAccess.keyLoader(path, keyLoaderContext); @@ -59,10 +59,8 @@ public void testCreateKeyLoader() { } @Test - @DisplayName("changePassphrase()") - public void testChangePassphrase() throws CryptoException { - MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK); - + @DisplayName("changePassphrase(MasterkeyFile, ...)") + public void testChangePassphraseWithMasterkeyFile() throws CryptoException { MasterkeyFileAccess.MasterkeyFile changed1 = masterkeyFileAccess.changePassphrase(keyFile, "asd", "qwe"); MasterkeyFileAccess.MasterkeyFile changed2 = masterkeyFileAccess.changePassphrase(changed1, "qwe", "asd"); @@ -81,13 +79,41 @@ public void testReadAllegedVaultVersion() throws IOException { } @Nested - @DisplayName("load()") - class Load { + @DisplayName("with serialized keyfile") + class WithSerializedKeyFile { + + private byte[] serializedKeyFile; - MasterkeyFileAccess masterkeyFileAccess = Mockito.spy(new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK)); + @BeforeEach + public void setup() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + masterkeyFileAccess.persist(key, out, "asd", 999, 2); + serializedKeyFile = out.toByteArray(); + } @Test - public void testParseInvalid() { + @DisplayName("changePassphrase(byte[], ...)") + public void testChangePassphraseWithRawBytes() throws CryptoException, IOException { + byte[] changed = masterkeyFileAccess.changePassphrase(serializedKeyFile, "asd", "qwe"); + byte[] restored = masterkeyFileAccess.changePassphrase(changed, "qwe", "asd"); + + MatcherAssert.assertThat(changed, not(equalTo(serializedKeyFile))); + Assertions.assertArrayEquals(serializedKeyFile, restored); + } + + @Test + @DisplayName("load()") + public void testLoad() throws IOException { + InputStream in = new ByteArrayInputStream(serializedKeyFile); + + Masterkey loaded = masterkeyFileAccess.load(in, "asd"); + + Assertions.assertArrayEquals(key.getEncoded(), loaded.getEncoded()); + } + + @Test + @DisplayName("load() unrelated json file") + public void testLoadInvalid() { String content = "{\"foo\": 42}"; InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); @@ -97,7 +123,8 @@ public void testParseInvalid() { } @Test - public void testParseMalformed() { + @DisplayName("load() non-json file") + public void testLoadMalformed() { final String content = "not even json"; InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); @@ -115,8 +142,6 @@ class Unlock { @Test @DisplayName("with correct password") public void testUnlockWithCorrectPassword() throws CryptoException { - MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK); - Masterkey key = masterkeyFileAccess.unlock(keyFile, "asd"); Assertions.assertNotNull(key); @@ -125,8 +150,6 @@ public void testUnlockWithCorrectPassword() throws CryptoException { @Test @DisplayName("with invalid password") public void testUnlockWithIncorrectPassword() { - MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK); - Assertions.assertThrows(InvalidPassphraseException.class, () -> { masterkeyFileAccess.unlock(keyFile, "qwe"); }); @@ -151,8 +174,6 @@ class Lock { @Test @DisplayName("creates expected values") public void testLock() { - MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK); - MasterkeyFileAccess.MasterkeyFile keyFile = masterkeyFileAccess.lock(key, "asd", 3, 2); Assertions.assertEquals(3, keyFile.version); @@ -167,10 +188,8 @@ public void testLock() { @Test @DisplayName("different passwords -> different wrapped keys") public void testLockWithDifferentPasswords() { - MasterkeyFileAccess masterkeyFileAccess1 = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK); - - MasterkeyFileAccess.MasterkeyFile keyFile1 = masterkeyFileAccess1.lock(key, "asd", 8, 2); - MasterkeyFileAccess.MasterkeyFile keyFile2 = masterkeyFileAccess1.lock(key, "qwe", 8, 2); + MasterkeyFileAccess.MasterkeyFile keyFile1 = masterkeyFileAccess.lock(key, "asd", 8, 2); + MasterkeyFileAccess.MasterkeyFile keyFile2 = masterkeyFileAccess.lock(key, "qwe", 8, 2); MatcherAssert.assertThat(keyFile1.encMasterKey, not(equalTo(keyFile2.encMasterKey))); } @@ -194,7 +213,6 @@ public void testLockWithDifferentPeppers() { @Test @DisplayName("persist and load") public void testPersistAndLoad(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException { - MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK); Path masterkeyFile = tmpDir.resolve("masterkey.cryptomator"); masterkeyFileAccess.persist(key, masterkeyFile, "asd"); From 427fd88899cdd24f5c8c9348959538d98cfae080 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 26 Jan 2021 15:47:48 +0100 Subject: [PATCH 21/59] added public methods for use with InputStream/OutputStream --- .../cryptolib/common/MasterkeyFileAccess.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index 4445155..6be9696 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -103,14 +103,19 @@ public static int readAllegedVaultVersion(byte[] masterkey) throws IOException { */ public byte[] changePassphrase(byte[] masterkey, CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException { try (ByteArrayInputStream in = new ByteArrayInputStream(masterkey); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); - Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + changePassphrase(in, out, oldPassphrase, newPassphrase); + return out.toByteArray(); + } + } + + public void changePassphrase(InputStream oldIn, OutputStream newOut, CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException { + try (Reader reader = new InputStreamReader(oldIn, StandardCharsets.UTF_8); + Writer writer = new OutputStreamWriter(newOut, StandardCharsets.UTF_8)) { MasterkeyFile original = GSON.fromJson(reader, MasterkeyFile.class); MasterkeyFile updated = changePassphrase(original, oldPassphrase, newPassphrase); GSON.toJson(updated, writer); writer.flush(); - return out.toByteArray(); } catch (JsonParseException e) { throw new IOException("Unreadable JSON", e); } catch (IllegalArgumentException e) { @@ -143,7 +148,7 @@ public Masterkey load(Path filePath, CharSequence passphrase) throws MasterkeyLo } } - Masterkey load(InputStream in, CharSequence passphrase) throws MasterkeyLoadingFailedException, IOException { + public Masterkey load(InputStream in, CharSequence passphrase) throws MasterkeyLoadingFailedException, IOException { try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { MasterkeyFile parsedFile = GSON.fromJson(reader, MasterkeyFile.class); if (parsedFile == null || !parsedFile.isValid()) { @@ -198,7 +203,7 @@ public void persist(Masterkey masterkey, Path filePath, CharSequence passphrase, Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING); } - void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @Deprecated int vaultVersion) throws IOException { + public void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @Deprecated int vaultVersion) throws IOException { persist(masterkey, out, passphrase, vaultVersion, DEFAULT_SCRYPT_COST_PARAM); } From 85468d467dd06d611862fe3e3bf4d337aabbf13a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 26 Jan 2021 15:49:05 +0100 Subject: [PATCH 22/59] Remove explicit writer.flush(), as the writer gets flushed implicitly during close() --- .../org/cryptomator/cryptolib/common/MasterkeyFileAccess.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index 6be9696..d9753ad 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -115,7 +115,6 @@ public void changePassphrase(InputStream oldIn, OutputStream newOut, CharSequenc MasterkeyFile original = GSON.fromJson(reader, MasterkeyFile.class); MasterkeyFile updated = changePassphrase(original, oldPassphrase, newPassphrase); GSON.toJson(updated, writer); - writer.flush(); } catch (JsonParseException e) { throw new IOException("Unreadable JSON", e); } catch (IllegalArgumentException e) { @@ -214,7 +213,6 @@ void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @De MasterkeyFile fileContent = lock(masterkey, passphrase, vaultVersion, scryptCostParam); try (Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { GSON.toJson(fileContent, writer); - writer.flush(); } catch (JsonIOException e) { throw new IOException(e); } From aac23e689f852feb7e323324f072eec1bc962d65 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 27 Jan 2021 11:22:28 +0100 Subject: [PATCH 23/59] Refined API of MasterkeyFileLoaderContext --- .../cryptolib/common/MasterkeyFileLoader.java | 8 +------- .../cryptolib/common/MasterkeyFileLoaderContext.java | 12 ++++++++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java index 14e0758..c967973 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java @@ -55,15 +55,9 @@ public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException { assert SCHEME.equalsIgnoreCase(keyId.getScheme()); Path filePath = vaultRoot.resolve(keyId.getSchemeSpecificPart()); if (!Files.exists(filePath)) { - filePath = context.getMasterkeyFilePath(keyId.getSchemeSpecificPart()); - } - if (filePath == null) { - throw new MasterkeyLoadingFailedException("No masterkey file provided."); + filePath = context.getCorrectMasterkeyFilePath(keyId.getSchemeSpecificPart()); } CharSequence passphrase = context.getPassphrase(filePath); - if (passphrase == null) { - throw new MasterkeyLoadingFailedException("No passphrase provided."); - } return masterkeyFileAccess.load(filePath, passphrase); } diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java index bddd11a..989a0b7 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java @@ -1,5 +1,7 @@ package org.cryptomator.cryptolib.common; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; + import java.nio.file.Path; public interface MasterkeyFileLoaderContext { @@ -8,16 +10,18 @@ public interface MasterkeyFileLoaderContext { * Provides the path of a masterkey file, if it could not be resolved automatically. * * @param incorrectPath The path as denoted by the key ID - * @return The correct path to a masterkey file or null to abort key loading. + * @return The correct path to a masterkey file, must not be null + * @throws MasterkeyLoadingFailedException If the context is unable to provide a correct paath */ - Path getMasterkeyFilePath(String incorrectPath); + Path getCorrectMasterkeyFilePath(String incorrectPath) throws MasterkeyLoadingFailedException; /** * Provides the password for a given masterkey file. * * @param masterkeyFile For what masterkey file - * @return The passphrase or null to abort key loading. + * @return The passphrase, must not be null + * @throws MasterkeyLoadingFailedException If the context is unable to provide a passphrase */ - CharSequence getPassphrase(Path masterkeyFile); + CharSequence getPassphrase(Path masterkeyFile) throws MasterkeyLoadingFailedException; } From 22a844081cb165361bc40de67fd83e13572c9efe Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 9 Feb 2021 08:26:16 +0100 Subject: [PATCH 24/59] moved from bintray to central --- .github/workflows/build.yml | 38 ++---- .github/workflows/codeql-analysis.yml | 31 ++--- .github/workflows/publish-central.yml | 37 ++++++ .github/workflows/publish-github.yml | 40 ++++++ pom.xml | 171 +++++++++++++++----------- 5 files changed, 200 insertions(+), 117 deletions(-) create mode 100644 .github/workflows/publish-central.yml create mode 100644 .github/workflows/publish-github.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7734712..36fde6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,41 +1,27 @@ name: Build - on: [push] - jobs: build: name: Build and Test runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" - env: - BUILD_VERSION: SNAPSHOT - outputs: - artifact-version: ${{ steps.setversion.outputs.version }} steps: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: java-version: 11 - server-id: bintray-jcenter - server-username: BINTRAY_USERNAME - server-password: BINTRAY_API_KEY - - uses: actions/cache@v1 + - uses: actions/cache@v2 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven- - name: Ensure to use tagged version - run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} if: startsWith(github.ref, 'refs/tags/') - - name: Export the project version to the job environment and fix it as an ouput of this job - id: setversion - run: | - v=$(mvn help:evaluate "-Dexpression=project.version" -q -DforceStdout) - echo "BUILD_VERSION=${v}" >> $GITHUB_ENV - echo "::set-output name=version::${v}" + run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} - name: Build and Test + id: buildAndTest run: mvn -B clean install jacoco:report -Pcoverage,dependency-check - name: Upload code coverage report id: codacyCoverageReporter @@ -43,14 +29,16 @@ jobs: env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} continue-on-error: true - - name: Upload snapshot artifact cryptolib-${{ env.BUILD_VERSION }}.jar - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v2 with: - name: cryptolib-${{ env.BUILD_VERSION }} - path: target/cryptolib-${{ env.BUILD_VERSION }}.jar - - name: Build and deploy to jcenter + name: artifacts + path: target/*.jar + - name: Create Release + uses: actions/create-release@v1 if: startsWith(github.ref, 'refs/tags/') - run: mvn -B deploy -DskipTests env: - BINTRAY_USERNAME: cryptobot - BINTRAY_API_KEY: ${{ secrets.BINTRAY_API_KEY }} + GITHUB_TOKEN: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} # release as "cryptobot" + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + prerelease: true \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 440f187..cd8fbb5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,13 +1,13 @@ + name: "CodeQL" on: push: - branches: [develop, master] + branches: [develop, main] pull_request: - # The branches below must be a subset of the branches above branches: [develop] schedule: - - cron: '0 0 * * 4' + - cron: '0 6 * * 0' jobs: analyse: @@ -15,30 +15,23 @@ jobs: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" steps: - - name: Checkout repository - uses: actions/checkout@v2 + - uses: actions/checkout@v2 with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. fetch-depth: 2 - uses: actions/setup-java@v1 with: java-version: 11 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Initializes the CodeQL tools for scanning. + - uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: java - - # Build project - name: Build and Test - run: mvn -B install - + run: mvn -B install -DskipTests - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v1 \ No newline at end of file diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml new file mode 100644 index 0000000..f22b7a9 --- /dev/null +++ b/.github/workflows/publish-central.yml @@ -0,0 +1,37 @@ +name: Publish to Maven Central +on: + workflow_dispatch: + inputs: + tag: + description: 'Tag' + required: true + default: '0.0.0' +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: "refs/tags/${{ github.event.inputs.tag }}" + - uses: actions/setup-java@v1 + with: + java-version: 11 + server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml + server-username: MAVEN_USERNAME # env variable for username in deploy + server-password: MAVEN_PASSWORD # env variable for token in deploy + gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import + gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase + - uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Enforce project version ${{ github.event.inputs.tag }} + run: mvn versions:set -B -DnewVersion=${{ github.event.inputs.tag }} + - name: Deploy + run: mvn deploy -B -DskipTests -Psign,deploy-central --no-transfer-progress + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} \ No newline at end of file diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml new file mode 100644 index 0000000..32a3041 --- /dev/null +++ b/.github/workflows/publish-github.yml @@ -0,0 +1,40 @@ +name: Publish to GitHub Packages +on: + release: + types: [published] +jobs: + publish: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') # only allow publishing tagged versions + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 11 + gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import + gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase + - uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Enforce project version ${{ github.event.release.tag_name }} + run: mvn versions:set -B -DnewVersion=${{ github.event.release.tag_name }} + - name: Deploy + run: mvn deploy -B -DskipTests -Psign,deploy-github --no-transfer-progress + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_USERNAME: 'Cryptobot' + SLACK_ICON: + SLACK_ICON_EMOJI: ':bot:' + SLACK_CHANNEL: 'cryptomator-desktop' + SLACK_TITLE: "Published ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}" + SLACK_MESSAGE: "Ready to ." + SLACK_FOOTER: + MSG_MINIMAL: true \ No newline at end of file diff --git a/pom.xml b/pom.xml index bf2e14a..32189fb 100644 --- a/pom.xml +++ b/pom.xml @@ -47,14 +47,6 @@ - - - bintray - bintray - https://jcenter.bintray.com - - - org.cryptomator @@ -146,7 +138,7 @@ maven-compiler-plugin 3.8.1 - 8 + 11 UTF-8 true @@ -168,6 +160,57 @@ + + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + maven-javadoc-plugin + 3.2.0 + + + attach-javadocs + + jar + + + + + + + + apiNote + a + API Note: + + + implSpec + a + Implementation Requirements: + + + implNote + a + Implementation Note: + + param + return + throws + since + version + serialData + see + + + @@ -179,7 +222,7 @@ org.owasp dependency-check-maven - 6.0.3 + 6.1.0 24 0 @@ -227,85 +270,67 @@ - release - - - bintray-jcenter - https://api.bintray.com/maven/cryptomator/maven/cryptolib/;publish=1 - - + sign - maven-source-plugin - 3.2.1 + maven-gpg-plugin + 1.6 - attach-sources + sign-artifacts + verify - jar-no-fork + sign + + + --pinentry-mode + loopback + + + + + + + + deploy-central + + + ossrh + Maven Central + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + - maven-javadoc-plugin - 3.2.0 - - - attach-javadocs - - jar - - - + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true - - - - apiNote - a - API Note: - - - implSpec - a - Implementation Requirements: - - - implNote - a - Implementation Note: - - param - return - throws - since - version - serialData - see - + ossrh + https://oss.sonatype.org/ + true - - maven-dependency-plugin - 3.1.2 - - - generate-dependency-list - prepare-package - - list - - - runtime - ${project.build.directory}/dependency-list.txt - - - - + + + deploy-github + + + github + GitHub Packages + https://maven.pkg.github.com/cryptomator/cryptolib + + + From cc96bd6d780784cc97cefd45499303cb405a7752 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 9 Feb 2021 08:32:48 +0100 Subject: [PATCH 25/59] dependency update --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 32189fb..8120c2d 100644 --- a/pom.xml +++ b/pom.xml @@ -18,15 +18,15 @@ 2.8.6 - 30.0-jre - 1.4.0 + 30.1-jre + 1.4.1 1.7.30 - 5.7.0 - 3.6.0 + 5.7.1 + 3.7.7 2.2 - 1.26 + 1.27 From 595c10a5b2e4ef2e604b710bad762a747046b0ce Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 9 Feb 2021 10:03:29 +0100 Subject: [PATCH 26/59] reset target JDK version to 8 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8120c2d..d728a87 100644 --- a/pom.xml +++ b/pom.xml @@ -138,7 +138,7 @@ maven-compiler-plugin 3.8.1 - 11 + 8 UTF-8 true From 1186fca3fa2b1ae9594dfe72b4e66ea95f680904 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 12 Feb 2021 16:23:01 +0100 Subject: [PATCH 27/59] dagger no longer used in this project --- suppression.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/suppression.xml b/suppression.xml index 507280a..c714921 100644 --- a/suppression.xml +++ b/suppression.xml @@ -1,9 +1,4 @@ - - - com.google.googlejavaformat:google-java-format:1.0 - CVE-2014-4555 - \ No newline at end of file From 20df782805462392bfd995d4d61c32cd59f1f57b Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 15 Mar 2021 17:09:42 +0100 Subject: [PATCH 28/59] Improved destruction of masterkey, fixes #19 --- .../cryptomator/cryptolib/api/Masterkey.java | 65 ++++--- .../common/DestroyableSecretKey.java | 115 ++++++++++++ .../cryptolib/common/MasterkeyFileAccess.java | 15 +- .../cryptomator/cryptolib/common/Scrypt.java | 78 ++------ .../cryptolib/v1/FileHeaderImpl.java | 15 +- .../cryptolib/v2/FileHeaderImpl.java | 15 +- .../common/DestroyableSecretKeyTest.java | 177 ++++++++++++++++++ .../cryptolib/common/MasterkeyTest.java | 22 ++- .../cryptolib/v1/CryptorImplTest.java | 15 +- .../cryptolib/v2/CryptorImplTest.java | 15 +- 10 files changed, 404 insertions(+), 128 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java create mode 100644 src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java diff --git a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java index e568765..c19ff85 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java @@ -1,50 +1,55 @@ package org.cryptomator.cryptolib.api; import com.google.common.base.Preconditions; -import org.cryptomator.cryptolib.common.Destroyables; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; -import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; +import java.util.Objects; -public class Masterkey implements AutoCloseable, SecretKey { +public class Masterkey implements AutoCloseable, SecretKey, Cloneable { public static final String ENC_ALG = "AES"; public static final String MAC_ALG = "HmacSHA256"; public static final int KEY_LEN_BYTES = 32; - private final SecretKey encKey; - private final SecretKey macKey; + private final DestroyableSecretKey encKey; + private final DestroyableSecretKey macKey; public Masterkey(SecretKey encKey, SecretKey macKey) { - this.encKey = encKey; - this.macKey = macKey; + this(DestroyableSecretKey.from(encKey), DestroyableSecretKey.from(macKey)); + } + + public Masterkey(DestroyableSecretKey encKey, DestroyableSecretKey macKey) { + this.encKey = Preconditions.checkNotNull(encKey); + this.macKey = Preconditions.checkNotNull(macKey); } public static Masterkey createNew(SecureRandom random) { - try { - KeyGenerator encKeyGen = KeyGenerator.getInstance(ENC_ALG); - encKeyGen.init(KEY_LEN_BYTES * Byte.SIZE, random); - SecretKey encKey = encKeyGen.generateKey(); - KeyGenerator macKeyGen = KeyGenerator.getInstance(MAC_ALG); - macKeyGen.init(KEY_LEN_BYTES * Byte.SIZE, random); - SecretKey macKey = macKeyGen.generateKey(); - return new Masterkey(encKey, macKey); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e); - } + DestroyableSecretKey encKey = DestroyableSecretKey.generate(random, ENC_ALG, KEY_LEN_BYTES); + DestroyableSecretKey macKey = DestroyableSecretKey.generate(random, MAC_ALG, KEY_LEN_BYTES); + return new Masterkey(encKey, macKey); } public static Masterkey createFromRaw(byte[] encoded) { Preconditions.checkArgument(encoded.length == KEY_LEN_BYTES + KEY_LEN_BYTES, "Invalid raw key length %s", encoded.length); - SecretKey encKey = new SecretKeySpec(encoded, 0, KEY_LEN_BYTES, ENC_ALG); - SecretKey macKey = new SecretKeySpec(encoded, KEY_LEN_BYTES, KEY_LEN_BYTES, MAC_ALG); + DestroyableSecretKey encKey = new DestroyableSecretKey(encoded, 0, KEY_LEN_BYTES, ENC_ALG); + DestroyableSecretKey macKey = new DestroyableSecretKey(encoded, KEY_LEN_BYTES, KEY_LEN_BYTES, MAC_ALG); return new Masterkey(encKey, macKey); } + /** + * Creates an exact deep copy of this Masterkey. + * The new instance is decoupled from this instance and will therefore survive if this gets destroyed. + * + * @return A new but equal Masterkey instance + */ + @Override + public Masterkey clone() { + return Masterkey.createFromRaw(getEncoded()); + } + public SecretKey getEncKey() { return encKey; } @@ -90,8 +95,20 @@ public boolean isDestroyed() { @Override public void destroy() { - Destroyables.destroySilently(encKey); - Destroyables.destroySilently(macKey); + encKey.destroy(); + macKey.destroy(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Masterkey masterkey = (Masterkey) o; + return encKey.equals(masterkey.encKey) && macKey.equals(masterkey.macKey); } + @Override + public int hashCode() { + return Objects.hash(encKey, macKey); + } } diff --git a/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java b/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java new file mode 100644 index 0000000..58a262a --- /dev/null +++ b/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java @@ -0,0 +1,115 @@ +package org.cryptomator.cryptolib.common; + +import com.google.common.base.Preconditions; + +import javax.crypto.SecretKey; +import javax.security.auth.Destroyable; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Objects; + +/** + * A {@link SecretKey} that (other than JDK's SecretKeySpec) + * actually implements {@link Destroyable}. + *

+ * Furthermore this key keeps track of any accesses via {@link #getEncoded()} and will destroy returned byte arrays as well. + */ +public class DestroyableSecretKey implements SecretKey, AutoCloseable { + + private transient final byte[] key; + private final String algorithm; + private boolean destroyed; + + public DestroyableSecretKey(byte[] key, String algorithm) { + this(key, 0, key.length, algorithm); + } + + public DestroyableSecretKey(byte[] key, int offset, int len, String algorithm) { + Preconditions.checkArgument(offset >= 0, "Invalid offset"); + Preconditions.checkArgument(len >= 0, "Invalid length"); + Preconditions.checkArgument(key.length >= offset+len, "Invalid offset/len"); + this.key = new byte[len]; + this.algorithm = Preconditions.checkNotNull(algorithm, "Algorithm must not be null"); + this.destroyed = false; + System.arraycopy(key, offset, this.key, 0, len); + } + + public static DestroyableSecretKey from(SecretKey secretKey) { + if (secretKey instanceof DestroyableSecretKey) { + return (DestroyableSecretKey) secretKey; + } else { + return new DestroyableSecretKey(secretKey.getEncoded(), secretKey.getAlgorithm()); + } + } + + /** + * Creates a new key of given length and for use with given algorithm using entropy from the given csprng. + * + * @param csprng A cryptographically secure random number source + * @param algorithm The {@link #getAlgorithm() key algorithm} + * @param keyLenBytes The length of the key (in bytes) + * @return A new secret key + */ + public static DestroyableSecretKey generate(SecureRandom csprng, String algorithm, int keyLenBytes) { + byte[] key = new byte[keyLenBytes]; + try { + csprng.nextBytes(key); + return new DestroyableSecretKey(key, algorithm); + } finally { + Arrays.fill(key, (byte) 0x00); + } + } + + @Override + public String getAlgorithm() { + Preconditions.checkState(!destroyed, "Key has been destroyed"); + return algorithm; + } + + @Override + public String getFormat() { + Preconditions.checkState(!destroyed, "Key has been destroyed"); + return "RAW"; + } + + @Override + public byte[] getEncoded() { + Preconditions.checkState(!destroyed, "Key has been destroyed"); + return key.clone(); + } + + @Override + public void destroy() { + Arrays.fill(key, (byte) 0x00); + destroyed = true; + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + /** + * Same as {@link #destroy()} + */ + @Override + public void close() { + destroy(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DestroyableSecretKey that = (DestroyableSecretKey) o; + return algorithm.equals(that.algorithm) && MessageDigest.isEqual(this.key, that.key); + } + + @Override + public int hashCode() { + int result = Objects.hash(algorithm); + result = 31 * result + Arrays.hashCode(key); + return result; + } +} diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index d9753ad..299d47a 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -18,7 +18,6 @@ import javax.crypto.Mac; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -168,15 +167,12 @@ Masterkey unlock(MasterkeyFile parsedFile, CharSequence passphrase) throws Inval Preconditions.checkArgument(parsedFile.isValid(), "Invalid masterkey file"); Preconditions.checkNotNull(passphrase); - SecretKey kek = scrypt(passphrase, parsedFile.scryptSalt, pepper, parsedFile.scryptCostParam, parsedFile.scryptBlockSize); - try { + try (DestroyableSecretKey kek = scrypt(passphrase, parsedFile.scryptSalt, pepper, parsedFile.scryptCostParam, parsedFile.scryptBlockSize)) { SecretKey encKey = AesKeyWrap.unwrap(kek, parsedFile.encMasterKey, Masterkey.ENC_ALG); SecretKey macKey = AesKeyWrap.unwrap(kek, parsedFile.macMasterKey, Masterkey.MAC_ALG); return new Masterkey(encKey, macKey); } catch (InvalidKeyException e) { throw new InvalidPassphraseException(); - } finally { - Destroyables.destroySilently(kek); } } @@ -226,8 +222,7 @@ MasterkeyFile lock(Masterkey masterkey, CharSequence passphrase, int vaultVersio final byte[] salt = new byte[DEFAULT_SCRYPT_SALT_LENGTH]; csprng.nextBytes(salt); - SecretKey kek = scrypt(passphrase, salt, pepper, scryptCostParam, DEFAULT_SCRYPT_BLOCK_SIZE); - try { + try (DestroyableSecretKey kek = scrypt(passphrase, salt, pepper, scryptCostParam, DEFAULT_SCRYPT_BLOCK_SIZE)) { final Mac mac = MacSupplier.HMAC_SHA256.withKey(masterkey.getMacKey()); final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(vaultVersion).array()); MasterkeyFile result = new MasterkeyFile(); @@ -239,8 +234,6 @@ MasterkeyFile lock(Masterkey masterkey, CharSequence passphrase, int vaultVersio result.encMasterKey = AesKeyWrap.wrap(kek, masterkey.getEncKey()); result.macMasterKey = AesKeyWrap.wrap(kek, masterkey.getMacKey()); return result; - } finally { - Destroyables.destroySilently(kek); } } @@ -255,13 +248,13 @@ public MasterkeyFileLoader keyLoader(Path vaultRoot, MasterkeyFileLoaderContext return new MasterkeyFileLoader(vaultRoot, this, context); } - private static SecretKey scrypt(CharSequence passphrase, byte[] salt, byte[] pepper, int costParam, int blockSize) { + private static DestroyableSecretKey scrypt(CharSequence passphrase, byte[] salt, byte[] pepper, int costParam, int blockSize) { byte[] saltAndPepper = new byte[salt.length + pepper.length]; System.arraycopy(salt, 0, saltAndPepper, 0, salt.length); System.arraycopy(pepper, 0, saltAndPepper, salt.length, pepper.length); byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, costParam, blockSize, Masterkey.KEY_LEN_BYTES); try { - return new SecretKeySpec(kekBytes, Masterkey.ENC_ALG); + return new DestroyableSecretKey(kekBytes, Masterkey.ENC_ALG); } finally { Arrays.fill(kekBytes, (byte) 0x00); } diff --git a/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java b/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java index 6dd8651..17364cc 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java +++ b/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java @@ -8,16 +8,11 @@ * Sebastian Stenzel - initial API and implementation *******************************************************************************/ +import javax.crypto.Mac; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.security.auth.Destroyable; public class Scrypt { @@ -75,71 +70,24 @@ public static byte[] scrypt(byte[] passphrase, byte[] salt, int costParam, int b throw new IllegalArgumentException("Parameter r is too large"); } - CascadingDestroyableSecretKey key = new CascadingDestroyableSecretKey(passphrase, "HmacSHA256"); - Mac mac = MacSupplier.HMAC_SHA256.withKey(key); - - byte[] DK = new byte[keyLengthInBytes]; - byte[] B = new byte[128 * blockSize * P]; - byte[] XY = new byte[256 * blockSize]; - byte[] V = new byte[128 * blockSize * costParam]; - - pbkdf2(mac, salt, 1, B, P * 128 * blockSize); - - for (int i = 0; i < P; i++) { - smix(B, i * 128 * blockSize, blockSize, costParam, V, XY); - } - - pbkdf2(mac, B, 1, DK, keyLengthInBytes); - - key.destroy(); - - return DK; - } - - private static class CascadingDestroyableSecretKey implements SecretKey, Destroyable { - - private final byte[] key; - private final String algorithm; - private final Collection clones = new ArrayList<>(); - private boolean destroyed; - - public CascadingDestroyableSecretKey(byte[] key, String algorithm) { - this.key = key; - this.algorithm = algorithm; - this.destroyed = false; - } - - @Override - public String getAlgorithm() { - return algorithm; - } + try (DestroyableSecretKey key = new DestroyableSecretKey(passphrase, "HmacSHA256")) { + Mac mac = MacSupplier.HMAC_SHA256.withKey(key); - @Override - public String getFormat() { - return "RAW"; - } + byte[] DK = new byte[keyLengthInBytes]; + byte[] B = new byte[128 * blockSize * P]; + byte[] XY = new byte[256 * blockSize]; + byte[] V = new byte[128 * blockSize * costParam]; - @Override - public byte[] getEncoded() { - byte[] clone = key.clone(); - clones.add(clone); - return clone; - } + pbkdf2(mac, salt, 1, B, P * 128 * blockSize); - @Override - public void destroy() { - for (byte[] clone : clones) { - Arrays.fill(clone, (byte) 0x00); + for (int i = 0; i < P; i++) { + smix(B, i * 128 * blockSize, blockSize, costParam, V, XY); } - Arrays.fill(key, (byte) 0x00); - destroyed = true; - } - @Override - public boolean isDestroyed() { - return destroyed; - } + pbkdf2(mac, B, 1, DK, keyLengthInBytes); + return DK; + } } /** diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java index 146e182..63fa53d 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java @@ -8,13 +8,12 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v1; -import java.util.Arrays; +import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import javax.security.auth.Destroyable; - -import org.cryptomator.cryptolib.api.FileHeader; +import java.util.Arrays; class FileHeaderImpl implements FileHeader, Destroyable { @@ -80,18 +79,17 @@ public static class Payload implements Destroyable { static final int CONTENT_KEY_POS = 8; static final int CONTENT_KEY_LEN = 32; static final int SIZE = FILESIZE_LEN + CONTENT_KEY_LEN; - private static final byte[] EMPTY_CONTENT_KEY = new byte[CONTENT_KEY_LEN]; private long filesize = -1L; private final byte[] contentKeyBytes; - private final SecretKey contentKey; + private final DestroyableSecretKey contentKey; private Payload(byte[] contentKeyBytes) { if (contentKeyBytes.length != CONTENT_KEY_LEN) { throw new IllegalArgumentException("Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")"); } this.contentKeyBytes = contentKeyBytes; - this.contentKey = new SecretKeySpec(contentKeyBytes, Constants.CONTENT_ENC_ALG); + this.contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG); } private long getFilesize() { @@ -112,11 +110,12 @@ byte[] getContentKeyBytes() { @Override public boolean isDestroyed() { - return Arrays.equals(contentKeyBytes, EMPTY_CONTENT_KEY); + return contentKey.isDestroyed(); } @Override public void destroy() { + contentKey.destroy(); Arrays.fill(contentKeyBytes, (byte) 0x00); } diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java index 6c1c11d..263f832 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java @@ -8,13 +8,12 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; -import java.util.Arrays; +import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import javax.security.auth.Destroyable; - -import org.cryptomator.cryptolib.api.FileHeader; +import java.util.Arrays; class FileHeaderImpl implements FileHeader, Destroyable { @@ -80,18 +79,17 @@ public static class Payload implements Destroyable { static final int CONTENT_KEY_POS = 8; static final int CONTENT_KEY_LEN = 32; static final int SIZE = FILESIZE_LEN + CONTENT_KEY_LEN; - private static final byte[] EMPTY_CONTENT_KEY = new byte[CONTENT_KEY_LEN]; private long filesize = -1L; private final byte[] contentKeyBytes; - private final SecretKey contentKey; + private final DestroyableSecretKey contentKey; private Payload(byte[] contentKeyBytes) { if (contentKeyBytes.length != CONTENT_KEY_LEN) { throw new IllegalArgumentException("Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")"); } this.contentKeyBytes = contentKeyBytes; - this.contentKey = new SecretKeySpec(contentKeyBytes, Constants.CONTENT_ENC_ALG); + this.contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG); } private long getFilesize() { @@ -112,11 +110,12 @@ byte[] getContentKeyBytes() { @Override public boolean isDestroyed() { - return Arrays.equals(contentKeyBytes, EMPTY_CONTENT_KEY); + return contentKey.isDestroyed(); } @Override public void destroy() { + contentKey.destroy(); Arrays.fill(contentKeyBytes, (byte) 0x00); } diff --git a/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java new file mode 100644 index 0000000..78da0d6 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java @@ -0,0 +1,177 @@ +package org.cryptomator.cryptolib.common; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Random; + +public class DestroyableSecretKeyTest { + + @DisplayName("DestroyableSecretKey.generate(...)") + @ParameterizedTest(name = "keylen = {0}") + @ValueSource(ints = {16, 32}) + public void testGenerateNew(int keylen) { + byte[] keySrc = new byte[keylen]; + new Random(42).nextBytes(keySrc); + SecureRandom csprng = Mockito.mock(SecureRandom.class); + Mockito.doAnswer(invocation -> { + byte[] keyDst = invocation.getArgument(0); + assert keySrc.length == keyDst.length; + System.arraycopy(keySrc, 0, keyDst, 0, keyDst.length); + return null; + }).when(csprng).nextBytes(Mockito.any()); + + DestroyableSecretKey key = DestroyableSecretKey.generate(csprng, "TEST", keylen); + + Assertions.assertNotNull(key); + Assertions.assertArrayEquals(keySrc, key.getEncoded()); + Mockito.verify(csprng).nextBytes(Mockito.any()); + } + + @Test + public void testConstructorFailsForInvalidAlgorithm() { + Assertions.assertThrows(NullPointerException.class, () -> { + new DestroyableSecretKey(new byte[16], null); + }); + } + + @Test + public void testConstructorFailsForInvalidLength() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new DestroyableSecretKey(new byte[16], 0, -1, "TEST"); + }); + } + + @Test + public void testConstructorFailsForInvalidOffset() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new DestroyableSecretKey(new byte[16], -1, 16, "TEST"); + }); + } + + @Test + public void testConstructorFailsForInvalidLengthAndOffset() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new DestroyableSecretKey(new byte[16], 8, 16, "TEST"); + }); + } + + @Test + public void testConstructorClonesKey() { + byte[] empty = new byte[32]; + byte[] rawKey = new byte[32]; + new Random(42).nextBytes(rawKey); + Assumptions.assumeFalse(Arrays.equals(empty, rawKey)); + + DestroyableSecretKey key = new DestroyableSecretKey(rawKey, "TEST"); + + Assertions.assertArrayEquals(rawKey, key.getEncoded()); + Arrays.fill(rawKey, (byte) 0x00); + Assertions.assertFalse(Arrays.equals(empty, key.getEncoded())); + } + + @Nested + @DisplayName("An undestroyed key...") + public class WithUndestroyed { + + private byte[] rawKey; + private DestroyableSecretKey key; + + @BeforeEach + public void setup() { + this.rawKey = new byte[32]; + new Random(42).nextBytes(rawKey); + this.key = new DestroyableSecretKey(rawKey, "EXAMPLE"); + } + + @Test + @DisplayName("isDestroyed() returns false") + public void testIsDestroyed() { + Assertions.assertFalse(key.isDestroyed()); + } + + @Test + @DisplayName("equals(empty key) returns false") + public void testEquals() { + DestroyableSecretKey emptyKey = new DestroyableSecretKey(new byte[32], "EXAMPLE"); + + // hashcode _may_ collide, though + Assertions.assertNotEquals(emptyKey, key); + } + + @Test + @DisplayName("getAlgorithm() returns algorithm") + public void testGetAlgorithm() { + Assertions.assertEquals("EXAMPLE", key.getAlgorithm()); + } + + @Test + @DisplayName("getFormat() returns 'RAW'") + public void testGetFormat() { + Assertions.assertEquals("RAW", key.getFormat()); + } + + @Test + @DisplayName("getEncoded() returns raw key") + public void testGetEncoded() { + Assertions.assertArrayEquals(rawKey, key.getEncoded()); + } + + @Nested + @DisplayName("After destroy()...") + public class WithDestroyed { + + @BeforeEach + public void setup() { + key.close(); + } + + @Test + @DisplayName("isDestroyed() returns true") + public void testIsDestroyed() { + Assertions.assertTrue(key.isDestroyed()); + } + + @Test + @DisplayName("equals(empty key) returns true") + public void testEquals() { + DestroyableSecretKey emptyKey = new DestroyableSecretKey(new byte[32], "EXAMPLE"); + + Assertions.assertEquals(emptyKey.hashCode(), key.hashCode()); + Assertions.assertEquals(emptyKey, key); + } + + @Test + @DisplayName("getAlgorithm() throws IllegalStateException") + public void testGetAlgorithm() { + Assertions.assertThrows(IllegalStateException.class, key::getAlgorithm); + } + + @Test + @DisplayName("getFormat() throws IllegalStateException") + public void testGetFormat() { + Assertions.assertThrows(IllegalStateException.class, key::getFormat); + } + + @Test + @DisplayName("getEncoded() throws IllegalStateException") + public void testGetEncoded() { + Assertions.assertThrows(IllegalStateException.class, key::getEncoded); + } + + } + + } + + + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java index cff298f..372f589 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java @@ -12,19 +12,20 @@ import javax.crypto.SecretKey; import javax.security.auth.DestroyFailedException; import java.security.SecureRandom; +import java.util.Arrays; import java.util.Random; import java.util.stream.Stream; public class MasterkeyTest { - private SecretKey encKey; - private SecretKey macKey; + private DestroyableSecretKey encKey; + private DestroyableSecretKey macKey; private Masterkey masterkey; @BeforeEach public void setup() { - encKey = Mockito.mock(SecretKey.class); - macKey = Mockito.mock(SecretKey.class); + encKey = Mockito.mock(DestroyableSecretKey.class); + macKey = Mockito.mock(DestroyableSecretKey.class); masterkey = new Masterkey(encKey, macKey); } @@ -118,4 +119,17 @@ public void testGetEncoded(String k1, String k2, String combined) { Assertions.assertArrayEquals(combined.getBytes(), raw); } + @Test + public void testClone() { + byte[] raw = new byte[64]; + Arrays.fill(raw, (byte) 0x55); + Masterkey original = Masterkey.createFromRaw(raw); + + Masterkey clone = original.clone(); + + Assertions.assertEquals(original, clone); + clone.destroy(); + Assertions.assertNotEquals(original, clone); + } + } \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java index c591655..9fc7b4d 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java @@ -13,6 +13,7 @@ import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -21,25 +22,31 @@ public class CryptorImplTest { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; - private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]); + + private Masterkey masterkey; + + @BeforeEach + public void setup() { + this.masterkey = Masterkey.createFromRaw(new byte[64]); + } @Test public void testGetFileContentCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileContentCryptor(), CoreMatchers.instanceOf(FileContentCryptorImpl.class)); } } @Test public void testGetFileHeaderCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileHeaderCryptor(), CoreMatchers.instanceOf(FileHeaderCryptorImpl.class)); } } @Test public void testGetFileNameCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileNameCryptor(), CoreMatchers.instanceOf(FileNameCryptorImpl.class)); } } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java index b008537..4208465 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java @@ -13,6 +13,7 @@ import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -21,25 +22,31 @@ public class CryptorImplTest { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; - private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]); + + private Masterkey masterkey; + + @BeforeEach + public void setup() { + this.masterkey = Masterkey.createFromRaw(new byte[64]); + } @Test public void testGetFileContentCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileContentCryptor(), CoreMatchers.instanceOf(FileContentCryptorImpl.class)); } } @Test public void testGetFileHeaderCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileHeaderCryptor(), CoreMatchers.instanceOf(FileHeaderCryptorImpl.class)); } } @Test public void testGetFileNameCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { MatcherAssert.assertThat(cryptor.fileNameCryptor(), CoreMatchers.instanceOf(FileNameCryptorImpl.class)); } } From ad31beaab1fe107dc73b716b5a28372dec5322b9 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 15 Mar 2021 18:05:43 +0100 Subject: [PATCH 29/59] support scoped, secure and short-lived access to key material by adding Cloneable to DestroyableSecretKey --- .../cryptomator/cryptolib/api/Masterkey.java | 24 ++++++--------- .../common/DestroyableSecretKey.java | 30 ++++++++++++++++--- .../common/DestroyableSecretKeyTest.java | 17 +++++++++-- .../cryptolib/common/MasterkeyTest.java | 10 +++++-- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java index c19ff85..4e155bf 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java @@ -5,7 +5,6 @@ import javax.crypto.SecretKey; import java.security.SecureRandom; -import java.util.Arrays; import java.util.Objects; public class Masterkey implements AutoCloseable, SecretKey, Cloneable { @@ -47,15 +46,15 @@ public static Masterkey createFromRaw(byte[] encoded) { */ @Override public Masterkey clone() { - return Masterkey.createFromRaw(getEncoded()); + return new Masterkey(getEncKey(), getMacKey()); } - public SecretKey getEncKey() { - return encKey; + public DestroyableSecretKey getEncKey() { + return encKey.clone(); } - public SecretKey getMacKey() { - return macKey; + public DestroyableSecretKey getMacKey() { + return macKey.clone(); } @Override @@ -72,15 +71,10 @@ public String getFormat() { public byte[] getEncoded() { byte[] rawEncKey = encKey.getEncoded(); byte[] rawMacKey = macKey.getEncoded(); - try { - byte[] rawKey = new byte[rawEncKey.length + rawMacKey.length]; - System.arraycopy(rawEncKey, 0, rawKey, 0, rawEncKey.length); - System.arraycopy(rawMacKey, 0, rawKey, rawEncKey.length, rawMacKey.length); - return rawKey; - } finally { - Arrays.fill(rawEncKey, (byte) 0x00); - Arrays.fill(rawMacKey, (byte) 0x00); - } + byte[] rawKey = new byte[rawEncKey.length + rawMacKey.length]; + System.arraycopy(rawEncKey, 0, rawKey, 0, rawEncKey.length); + System.arraycopy(rawMacKey, 0, rawKey, rawEncKey.length, rawMacKey.length); + return rawKey; } @Override diff --git a/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java b/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java index 58a262a..9079836 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java +++ b/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java @@ -12,10 +12,21 @@ /** * A {@link SecretKey} that (other than JDK's SecretKeySpec) * actually implements {@link Destroyable}. - *

- * Furthermore this key keeps track of any accesses via {@link #getEncoded()} and will destroy returned byte arrays as well. + * + * Furthermore, this implementation will not create copies when accessing {@link #getEncoded()}. + * Instead it implements {@link AutoCloseable} and {@link Cloneable} in an exception-free manner. To prevent mutation of the exposed key, + * you would want to make sure to always work on scoped copies, such as in this example: + * + *

+ *     // clone "key" to protect it from unwanted modifications:
+ *     try (DestroyableSecretKey k = key.clone()) {
+ *         // use "k":
+ *         Cipher cipher = Cipher.init(k, ...)
+ *         cipher.doFinal(...)
+ *     } // "k" will get destroyed here
+ * 
*/ -public class DestroyableSecretKey implements SecretKey, AutoCloseable { +public class DestroyableSecretKey implements SecretKey, AutoCloseable, Cloneable { private transient final byte[] key; private final String algorithm; @@ -73,10 +84,21 @@ public String getFormat() { return "RAW"; } + /** + * Returns the key as raw byte array. Do not mutate this, unless you know what you're doing! + * If in doubt, make your local copy, first. + * @return The secret key bytes + */ @Override public byte[] getEncoded() { Preconditions.checkState(!destroyed, "Key has been destroyed"); - return key.clone(); + return key; + } + + @Override + public DestroyableSecretKey clone() { + Preconditions.checkState(!destroyed, "Key has been destroyed"); + return new DestroyableSecretKey(key, algorithm); } @Override diff --git a/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java index 78da0d6..8011460 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java @@ -126,6 +126,15 @@ public void testGetEncoded() { Assertions.assertArrayEquals(rawKey, key.getEncoded()); } + @Test + @DisplayName("clone() returns equal copy") + public void testClone() { + DestroyableSecretKey clone = key.clone(); + + Assertions.assertEquals(key, clone); + Assertions.assertNotSame(key, clone); + } + @Nested @DisplayName("After destroy()...") public class WithDestroyed { @@ -168,10 +177,14 @@ public void testGetEncoded() { Assertions.assertThrows(IllegalStateException.class, key::getEncoded); } + @Test + @DisplayName("clone() throws IllegalStateException") + public void testClone() { + Assertions.assertThrows(IllegalStateException.class, key::clone); + } + } } - - } \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java index 372f589..4b7f51e 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java @@ -59,16 +59,22 @@ static Stream create64RandomBytes() { @Test public void testGetEncKey() { + DestroyableSecretKey clonedEncKey = Mockito.mock(DestroyableSecretKey.class); + Mockito.when(encKey.clone()).thenReturn(clonedEncKey); + SecretKey encKey = masterkey.getEncKey(); - Assertions.assertSame(this.encKey, encKey); + Assertions.assertSame(clonedEncKey, encKey); } @Test public void testGetMacKey() { + DestroyableSecretKey clonedMacKey = Mockito.mock(DestroyableSecretKey.class); + Mockito.when(macKey.clone()).thenReturn(clonedMacKey); + SecretKey macKey = masterkey.getMacKey(); - Assertions.assertSame(this.macKey, macKey); + Assertions.assertSame(clonedMacKey, macKey); } @Test From a2e40f3fac4cb146210af8ecf5cea59142e5fd27 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 15 Mar 2021 18:20:11 +0100 Subject: [PATCH 30/59] Use scoped keys in v1 cryptor impl --- .../cryptolib/v1/FileContentCryptorImpl.java | 38 ++++++------- .../cryptolib/v1/FileHeaderCryptorImpl.java | 53 ++++++++++--------- .../cryptolib/v1/FileHeaderImpl.java | 3 +- .../cryptolib/v1/FileNameCryptorImpl.java | 38 ++++++------- .../v1/FileContentCryptorImplBenchmark.java | 5 +- .../v1/FileContentCryptorImplTest.java | 13 ++--- .../v1/FileHeaderCryptorBenchmark.java | 17 +++--- .../v1/FileHeaderCryptorImplTest.java | 8 ++- .../cryptolib/v1/FileNameCryptorImplTest.java | 5 +- 9 files changed, 91 insertions(+), 89 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java index 0463ca9..0ce1206 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java @@ -12,13 +12,13 @@ import org.cryptomator.cryptolib.api.FileContentCryptor; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.CipherSupplier; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.MacSupplier; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.Mac; -import javax.crypto.SecretKey; import javax.crypto.ShortBufferException; import javax.crypto.spec.IvParameterSpec; import java.nio.ByteBuffer; @@ -33,10 +33,10 @@ class FileContentCryptorImpl implements FileContentCryptor { - private final SecretKey macKey; + private final DestroyableSecretKey macKey; private final SecureRandom random; - FileContentCryptorImpl(SecretKey macKey, SecureRandom random) { + FileContentCryptorImpl(DestroyableSecretKey macKey, SecureRandom random) { this.macKey = macKey; this.random = random; } @@ -101,14 +101,14 @@ public void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, } // visible for testing - void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long chunkNumber, byte[] headerNonce, SecretKey fileKey) { - try { + void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long chunkNumber, byte[] headerNonce, DestroyableSecretKey fileKey) { + try (DestroyableSecretKey fk = fileKey.clone(); DestroyableSecretKey mk = macKey.clone()) { // nonce: byte[] nonce = new byte[NONCE_SIZE]; random.nextBytes(nonce); // payload: - final Cipher cipher = CipherSupplier.AES_CTR.forEncryption(fileKey, new IvParameterSpec(nonce)); + final Cipher cipher = CipherSupplier.AES_CTR.forEncryption(fk, new IvParameterSpec(nonce)); ciphertextChunk.put(nonce); assert ciphertextChunk.remaining() >= cipher.getOutputSize(cleartextChunk.remaining()) + MAC_SIZE; int bytesEncrypted = cipher.doFinal(cleartextChunk, ciphertextChunk); @@ -116,7 +116,7 @@ void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long ch // mac: final ByteBuffer ciphertextBuf = ciphertextChunk.asReadOnlyBuffer(); ciphertextBuf.position(NONCE_SIZE).limit(NONCE_SIZE + bytesEncrypted); - byte[] authenticationCode = calcChunkMac(macKey, headerNonce, chunkNumber, nonce, ciphertextBuf); + byte[] authenticationCode = calcChunkMac(mk, headerNonce, chunkNumber, nonce, ciphertextBuf); assert authenticationCode.length == MAC_SIZE; ciphertextChunk.put(authenticationCode); } catch (ShortBufferException e) { @@ -127,10 +127,10 @@ void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long ch } // visible for testing - void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, SecretKey fileKey) { + void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, DestroyableSecretKey fileKey) { assert ciphertextChunk.remaining() >= NONCE_SIZE + MAC_SIZE; - try { + try (DestroyableSecretKey fk = fileKey.clone()) { // nonce: final byte[] nonce = new byte[NONCE_SIZE]; final ByteBuffer chunkNonceBuf = ciphertextChunk.asReadOnlyBuffer(); @@ -142,7 +142,7 @@ void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, SecretK payloadBuf.position(NONCE_SIZE).limit(ciphertextChunk.limit() - MAC_SIZE); // payload: - final Cipher cipher = CipherSupplier.AES_CTR.forDecryption(fileKey, new IvParameterSpec(nonce)); + final Cipher cipher = CipherSupplier.AES_CTR.forDecryption(fk, new IvParameterSpec(nonce)); assert cleartextChunk.remaining() >= cipher.getOutputSize(payloadBuf.remaining()); cipher.doFinal(payloadBuf, cleartextChunk); } catch (ShortBufferException e) { @@ -179,14 +179,16 @@ boolean checkChunkMac(byte[] headerNonce, long chunkNumber, ByteBuffer chunkBuf) return MessageDigest.isEqual(expectedMac, calculatedMac); } - private static byte[] calcChunkMac(SecretKey macKey, byte[] headerNonce, long chunkNumber, byte[] chunkNonce, ByteBuffer ciphertext) { - final byte[] chunkNumberBigEndian = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.BIG_ENDIAN).putLong(chunkNumber).array(); - final Mac mac = MacSupplier.HMAC_SHA256.withKey(macKey); - mac.update(headerNonce); - mac.update(chunkNumberBigEndian); - mac.update(chunkNonce); - mac.update(ciphertext); - return mac.doFinal(); + private static byte[] calcChunkMac(DestroyableSecretKey macKey, byte[] headerNonce, long chunkNumber, byte[] chunkNonce, ByteBuffer ciphertext) { + try (DestroyableSecretKey mk = macKey.clone()) { + final byte[] chunkNumberBigEndian = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.BIG_ENDIAN).putLong(chunkNumber).array(); + final Mac mac = MacSupplier.HMAC_SHA256.withKey(mk); + mac.update(headerNonce); + mac.update(chunkNumberBigEndian); + mac.update(chunkNonce); + mac.update(ciphertext); + return mac.doFinal(); + } } } diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java index ccdf02a..cc613e3 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java @@ -8,32 +8,31 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v1; -import java.nio.ByteBuffer; -import java.security.MessageDigest; -import java.security.SecureRandom; -import java.util.Arrays; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.FileHeaderCryptor; +import org.cryptomator.cryptolib.common.CipherSupplier; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; +import org.cryptomator.cryptolib.common.MacSupplier; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.Mac; -import javax.crypto.SecretKey; import javax.crypto.ShortBufferException; import javax.crypto.spec.IvParameterSpec; - -import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.cryptomator.cryptolib.api.FileHeader; -import org.cryptomator.cryptolib.api.FileHeaderCryptor; -import org.cryptomator.cryptolib.common.CipherSupplier; -import org.cryptomator.cryptolib.common.MacSupplier; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Arrays; class FileHeaderCryptorImpl implements FileHeaderCryptor { - private final SecretKey headerKey; - private final SecretKey macKey; + private final DestroyableSecretKey headerKey; + private final DestroyableSecretKey macKey; private final SecureRandom random; - FileHeaderCryptorImpl(SecretKey headerKey, SecretKey macKey, SecureRandom random) { + FileHeaderCryptorImpl(DestroyableSecretKey headerKey, DestroyableSecretKey macKey, SecureRandom random) { this.headerKey = headerKey; this.macKey = macKey; this.random = random; @@ -60,19 +59,19 @@ public ByteBuffer encryptHeader(FileHeader header) { payloadCleartextBuf.putLong(-1l); payloadCleartextBuf.put(headerImpl.getPayload().getContentKeyBytes()); payloadCleartextBuf.flip(); - try { + try (DestroyableSecretKey hk = headerKey.clone(); DestroyableSecretKey mk = macKey.clone()) { ByteBuffer result = ByteBuffer.allocate(FileHeaderImpl.SIZE); result.put(headerImpl.getNonce()); // encrypt payload: - Cipher cipher = CipherSupplier.AES_CTR.forEncryption(headerKey, new IvParameterSpec(headerImpl.getNonce())); + Cipher cipher = CipherSupplier.AES_CTR.forEncryption(hk, new IvParameterSpec(headerImpl.getNonce())); int encrypted = cipher.doFinal(payloadCleartextBuf, result); assert encrypted == FileHeaderImpl.Payload.SIZE; // mac nonce and ciphertext: ByteBuffer nonceAndCiphertextBuf = result.duplicate(); nonceAndCiphertextBuf.flip(); - Mac mac = MacSupplier.HMAC_SHA256.withKey(macKey); + Mac mac = MacSupplier.HMAC_SHA256.withKey(mk); mac.update(nonceAndCiphertextBuf); result.put(mac.doFinal()); @@ -104,19 +103,21 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic buf.get(expectedMac); // check mac: - ByteBuffer nonceAndCiphertextBuf = buf.duplicate(); - nonceAndCiphertextBuf.position(FileHeaderImpl.NONCE_POS).limit(FileHeaderImpl.NONCE_POS + FileHeaderImpl.NONCE_LEN + FileHeaderImpl.PAYLOAD_LEN); - Mac mac = MacSupplier.HMAC_SHA256.withKey(macKey); - mac.update(nonceAndCiphertextBuf); - byte[] calculatedMac = mac.doFinal(); - if (!MessageDigest.isEqual(expectedMac, calculatedMac)) { - throw new AuthenticationFailedException("Header MAC doesn't match."); + try (DestroyableSecretKey mk = macKey.clone()) { + ByteBuffer nonceAndCiphertextBuf = buf.duplicate(); + nonceAndCiphertextBuf.position(FileHeaderImpl.NONCE_POS).limit(FileHeaderImpl.NONCE_POS + FileHeaderImpl.NONCE_LEN + FileHeaderImpl.PAYLOAD_LEN); + Mac mac = MacSupplier.HMAC_SHA256.withKey(mk); + mac.update(nonceAndCiphertextBuf); + byte[] calculatedMac = mac.doFinal(); + if (!MessageDigest.isEqual(expectedMac, calculatedMac)) { + throw new AuthenticationFailedException("Header MAC doesn't match."); + } } ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE); - try { + try (DestroyableSecretKey hk = headerKey.clone()) { // decrypt payload: - Cipher cipher = CipherSupplier.AES_CTR.forDecryption(headerKey, new IvParameterSpec(nonce)); + Cipher cipher = CipherSupplier.AES_CTR.forDecryption(hk, new IvParameterSpec(nonce)); int decrypted = cipher.doFinal(ByteBuffer.wrap(ciphertextPayload), payloadCleartextBuf); assert decrypted == FileHeaderImpl.Payload.SIZE; diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java index 63fa53d..6004ccf 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java @@ -11,7 +11,6 @@ import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.DestroyableSecretKey; -import javax.crypto.SecretKey; import javax.security.auth.Destroyable; import java.util.Arrays; @@ -100,7 +99,7 @@ private void setFilesize(long filesize) { this.filesize = filesize; } - SecretKey getContentKey() { + DestroyableSecretKey getContentKey() { return contentKey; } diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java index 79e16ff..f6acb2d 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java @@ -8,18 +8,16 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v1; -import java.nio.charset.Charset; - -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.SecretKey; - +import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.cryptomator.siv.SivMode; import org.cryptomator.siv.UnauthenticCiphertextException; -import com.google.common.io.BaseEncoding; +import javax.crypto.IllegalBlockSizeException; +import java.nio.charset.Charset; class FileNameCryptorImpl implements FileNameCryptor { @@ -32,20 +30,22 @@ protected SivMode initialValue() { }; }; - private final SecretKey encryptionKey; - private final SecretKey macKey; + private final DestroyableSecretKey encryptionKey; + private final DestroyableSecretKey macKey; - FileNameCryptorImpl(SecretKey encryptionKey, SecretKey macKey) { + FileNameCryptorImpl(DestroyableSecretKey encryptionKey, DestroyableSecretKey macKey) { this.encryptionKey = encryptionKey; this.macKey = macKey; } @Override public String hashDirectoryId(String cleartextDirectoryId) { - byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); - byte[] encryptedBytes = AES_SIV.get().encrypt(encryptionKey, macKey, cleartextBytes); - byte[] hashedBytes = MessageDigestSupplier.SHA1.get().digest(encryptedBytes); - return BASE32.encode(hashedBytes); + try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); + byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes); + byte[] hashedBytes = MessageDigestSupplier.SHA1.get().digest(encryptedBytes); + return BASE32.encode(hashedBytes); + } } @Override @@ -55,9 +55,11 @@ public String encryptFilename(String cleartextName, byte[]... associatedData) { @Override public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) { - byte[] cleartextBytes = cleartextName.getBytes(UTF_8); - byte[] encryptedBytes = AES_SIV.get().encrypt(encryptionKey, macKey, cleartextBytes, associatedData); - return encoding.encode(encryptedBytes); + try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + byte[] cleartextBytes = cleartextName.getBytes(UTF_8); + byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes, associatedData); + return encoding.encode(encryptedBytes); + } } @Override @@ -67,9 +69,9 @@ public String decryptFilename(String ciphertextName, byte[]... associatedData) t @Override public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { - try { + try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { byte[] encryptedBytes = encoding.decode(ciphertextName); - byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData); + byte[] cleartextBytes = AES_SIV.get().decrypt(ek, mk, encryptedBytes, associatedData); return new String(cleartextBytes, UTF_8); } catch (UnauthenticCiphertextException | IllegalArgumentException | IllegalBlockSizeException e) { throw new AuthenticationFailedException("Invalid Ciphertext.", e); diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java index d476e78..846c7f1 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java @@ -15,6 +15,7 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -38,8 +39,8 @@ public class FileContentCryptorImplBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final SecretKey ENC_KEY = new SecretKeySpec(new byte[16], "AES"); - private static final SecretKey MAC_KEY = new SecretKeySpec(new byte[16], "HmacSHA256"); + private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES"); + private static final DestroyableSecretKey MAC_KEY = new DestroyableSecretKey(new byte[16], "HmacSHA256"); private final byte[] headerNonce = new byte[Constants.NONCE_SIZE]; private final ByteBuffer cleartextChunk = ByteBuffer.allocate(Constants.PAYLOAD_SIZE); private final ByteBuffer ciphertextChunk = ByteBuffer.allocate(Constants.CHUNK_SIZE); diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java index 61d9927..5d7265a 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java @@ -13,6 +13,7 @@ import org.cryptomator.cryptolib.EncryptingWritableByteChannel; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.cryptomator.cryptolib.common.SeekableByteChannelMock; import org.hamcrest.CoreMatchers; @@ -53,8 +54,8 @@ public class FileContentCryptorImplTest { @BeforeEach public void setup() { - SecretKey encKey = new SecretKeySpec(new byte[32], "AES"); - SecretKey macKey = new SecretKeySpec(new byte[32], "HmacSHA256"); + DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES"); + DestroyableSecretKey macKey = new DestroyableSecretKey(new byte[32], "HmacSHA256"); headerCryptor = new FileHeaderCryptorImpl(encKey, macKey, RANDOM_MOCK); fileContentCryptor = new FileContentCryptorImpl(macKey, RANDOM_MOCK); cryptor = Mockito.mock(Cryptor.class); @@ -64,9 +65,9 @@ public void setup() { @Test public void testMacIsValidAfterEncryption() throws NoSuchAlgorithmException { - SecretKey fileKey = new SecretKeySpec(new byte[16], "AES"); + DestroyableSecretKey fileKey = new DestroyableSecretKey(new byte[16], "AES"); ByteBuffer ciphertext = ByteBuffer.allocate(fileContentCryptor.ciphertextChunkSize()); - fileContentCryptor.encryptChunk(StandardCharsets.UTF_8.encode("asd"), ciphertext,42l, new byte[16], fileKey); + fileContentCryptor.encryptChunk(StandardCharsets.UTF_8.encode("asd"), ciphertext, 42l, new byte[16], fileKey); ciphertext.flip(); Assertions.assertTrue(fileContentCryptor.checkChunkMac(new byte[16], 42l, ciphertext)); Assertions.assertFalse(fileContentCryptor.checkChunkMac(new byte[16], 43l, ciphertext)); @@ -74,10 +75,10 @@ public void testMacIsValidAfterEncryption() throws NoSuchAlgorithmException { @Test public void testDecryptedEncryptedEqualsPlaintext() throws NoSuchAlgorithmException { - SecretKey fileKey = new SecretKeySpec(new byte[16], "AES"); + DestroyableSecretKey fileKey = new DestroyableSecretKey(new byte[16], "AES"); ByteBuffer ciphertext = ByteBuffer.allocate(fileContentCryptor.ciphertextChunkSize()); ByteBuffer cleartext = ByteBuffer.allocate(fileContentCryptor.cleartextChunkSize()); - fileContentCryptor.encryptChunk(StandardCharsets.UTF_8.encode("asd"), ciphertext,42l, new byte[12], fileKey); + fileContentCryptor.encryptChunk(StandardCharsets.UTF_8.encode("asd"), ciphertext, 42l, new byte[12], fileKey); ciphertext.flip(); fileContentCryptor.decryptChunk(ciphertext, cleartext, fileKey); cleartext.flip(); diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java index 8ca42f5..9b1e9a8 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java @@ -8,16 +8,9 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v1; -import java.nio.ByteBuffer; -import java.security.SecureRandom; -import java.util.concurrent.TimeUnit; - -import javax.crypto.AEADBadTagException; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -30,6 +23,10 @@ import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Warmup; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.concurrent.TimeUnit; + /** * Needs to be compiled via maven as the JMH annotation processor needs to do stuff... */ @@ -41,8 +38,8 @@ public class FileHeaderCryptorBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final SecretKey ENC_KEY = new SecretKeySpec(new byte[16], "AES"); - private static final SecretKey MAC_KEY = new SecretKeySpec(new byte[16], "HmacSHA256"); + private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES"); + private static final DestroyableSecretKey MAC_KEY = new DestroyableSecretKey(new byte[16], "HmacSHA256"); private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(ENC_KEY, MAC_KEY, RANDOM_MOCK); private FileHeader header; private ByteBuffer validHeaderCiphertextBuf; diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java index a07cd5f..c68a0e9 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java @@ -11,14 +11,12 @@ import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import javax.crypto.AEADBadTagException; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import java.nio.ByteBuffer; import java.security.SecureRandom; @@ -29,8 +27,8 @@ public class FileHeaderCryptorImplTest { @BeforeEach public void setup() { - SecretKey encKey = new SecretKeySpec(new byte[32], "AES"); - SecretKey macKey = new SecretKeySpec(new byte[32], "HmacSHA256"); + DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES"); + DestroyableSecretKey macKey = new DestroyableSecretKey(new byte[32], "HmacSHA256"); headerCryptor = new FileHeaderCryptorImpl(encKey, macKey, RANDOM_MOCK); } diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java index 1246343..8eac407 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java @@ -10,6 +10,7 @@ import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.siv.UnauthenticCiphertextException; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; @@ -32,8 +33,8 @@ public class FileNameCryptorImplTest { private static final Charset UTF_8 = StandardCharsets.UTF_8; final byte[] keyBytes = new byte[32]; - final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); - final SecretKey macKey = new SecretKeySpec(keyBytes, "AES"); + final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); + final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); static Stream filenameGenerator() { From 717f11944550bdf74b5b3bc4a0a573c0ab043e32 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 15 Mar 2021 18:28:02 +0100 Subject: [PATCH 31/59] Use scoped keys in v2 cryptor impl --- .../cryptolib/v2/FileContentCryptorImpl.java | 15 ++++---- .../cryptolib/v2/FileHeaderCryptorImpl.java | 33 +++++++--------- .../cryptolib/v2/FileHeaderImpl.java | 3 +- .../cryptolib/v2/FileNameCryptorImpl.java | 37 +++++++++--------- .../v2/FileContentCryptorImplBenchmark.java | 14 +++---- .../v2/FileContentCryptorImplTest.java | 7 ++-- .../v2/FileHeaderCryptorBenchmark.java | 17 +++------ .../v2/FileHeaderCryptorImplTest.java | 5 +-- .../cryptolib/v2/FileNameCryptorImplTest.java | 38 +++++++++---------- 9 files changed, 77 insertions(+), 92 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java index d4741ac..25ec9c4 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java @@ -9,16 +9,15 @@ package org.cryptomator.cryptolib.v2; import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.FileContentCryptor; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.CipherSupplier; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import javax.crypto.AEADBadTagException; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; -import javax.crypto.SecretKey; import javax.crypto.ShortBufferException; import javax.crypto.spec.GCMParameterSpec; import java.nio.ByteBuffer; @@ -97,14 +96,14 @@ public void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, } // visible for testing - void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long chunkNumber, byte[] headerNonce, SecretKey fileKey) { - try { + void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long chunkNumber, byte[] headerNonce, DestroyableSecretKey fileKey) { + try (DestroyableSecretKey fk = fileKey.clone()) { // nonce: byte[] nonce = new byte[GCM_NONCE_SIZE]; random.nextBytes(nonce); // payload: - final Cipher cipher = CipherSupplier.AES_GCM.forEncryption(fileKey, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); + final Cipher cipher = CipherSupplier.AES_GCM.forEncryption(fk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); final byte[] chunkNumberBigEndian = longToBigEndianByteArray(chunkNumber); cipher.updateAAD(chunkNumberBigEndian); cipher.updateAAD(headerNonce); @@ -119,10 +118,10 @@ void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long ch } // visible for testing - void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long chunkNumber, byte[] headerNonce, SecretKey fileKey) throws AuthenticationFailedException { + void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long chunkNumber, byte[] headerNonce, DestroyableSecretKey fileKey) throws AuthenticationFailedException { assert ciphertextChunk.remaining() >= GCM_NONCE_SIZE + GCM_TAG_SIZE; - try { + try (DestroyableSecretKey fk = fileKey.clone()) { // nonce: final byte[] nonce = new byte[GCM_NONCE_SIZE]; final ByteBuffer chunkNonceBuf = ciphertextChunk.asReadOnlyBuffer(); @@ -135,7 +134,7 @@ void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long ch assert payloadBuf.remaining() >= GCM_TAG_SIZE; // payload: - final Cipher cipher = CipherSupplier.AES_GCM.forDecryption(fileKey, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); + final Cipher cipher = CipherSupplier.AES_GCM.forDecryption(fk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); final byte[] chunkNumberBigEndian = longToBigEndianByteArray(chunkNumber); cipher.updateAAD(chunkNumberBigEndian); cipher.updateAAD(headerNonce); diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java index 541e93c..729a627 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java @@ -8,35 +8,30 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; -import java.nio.ByteBuffer; -import java.security.MessageDigest; -import java.security.SecureRandom; -import java.util.Arrays; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.FileHeaderCryptor; +import org.cryptomator.cryptolib.common.CipherSupplier; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import javax.crypto.AEADBadTagException; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; -import javax.crypto.Mac; -import javax.crypto.SecretKey; import javax.crypto.ShortBufferException; import javax.crypto.spec.GCMParameterSpec; -import javax.crypto.spec.IvParameterSpec; - -import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.cryptomator.cryptolib.api.FileHeader; -import org.cryptomator.cryptolib.api.FileHeaderCryptor; -import org.cryptomator.cryptolib.common.CipherSupplier; -import org.cryptomator.cryptolib.common.MacSupplier; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.Arrays; import static org.cryptomator.cryptolib.v2.Constants.GCM_TAG_SIZE; class FileHeaderCryptorImpl implements FileHeaderCryptor { - private final SecretKey headerKey; + private final DestroyableSecretKey headerKey; private final SecureRandom random; - FileHeaderCryptorImpl(SecretKey headerKey, SecureRandom random) { + FileHeaderCryptorImpl(DestroyableSecretKey headerKey, SecureRandom random) { this.headerKey = headerKey; this.random = random; } @@ -62,12 +57,12 @@ public ByteBuffer encryptHeader(FileHeader header) { payloadCleartextBuf.putLong(-1l); payloadCleartextBuf.put(headerImpl.getPayload().getContentKeyBytes()); payloadCleartextBuf.flip(); - try { + try (DestroyableSecretKey hk = headerKey.clone()) { ByteBuffer result = ByteBuffer.allocate(FileHeaderImpl.SIZE); result.put(headerImpl.getNonce()); // encrypt payload: - Cipher cipher = CipherSupplier.AES_GCM.forEncryption(headerKey, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce())); + Cipher cipher = CipherSupplier.AES_GCM.forEncryption(hk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce())); int encrypted = cipher.doFinal(payloadCleartextBuf, result); assert encrypted == FileHeaderImpl.PAYLOAD_LEN + FileHeaderImpl.TAG_LEN; @@ -96,9 +91,9 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic buf.get(ciphertextAndTag); ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE); - try { + try (DestroyableSecretKey hk = headerKey.clone()) { // decrypt payload: - Cipher cipher = CipherSupplier.AES_GCM.forDecryption(headerKey, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); + Cipher cipher = CipherSupplier.AES_GCM.forDecryption(hk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); int decrypted = cipher.doFinal(ByteBuffer.wrap(ciphertextAndTag), payloadCleartextBuf); assert decrypted == FileHeaderImpl.Payload.SIZE; diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java index 263f832..bc0ad35 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java @@ -11,7 +11,6 @@ import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.DestroyableSecretKey; -import javax.crypto.SecretKey; import javax.security.auth.Destroyable; import java.util.Arrays; @@ -100,7 +99,7 @@ private void setFilesize(long filesize) { this.filesize = filesize; } - SecretKey getContentKey() { + DestroyableSecretKey getContentKey() { return contentKey; } diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java index db79244..6410d5e 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java @@ -8,18 +8,17 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; -import java.nio.charset.Charset; - -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.SecretKey; - import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.cryptomator.siv.SivMode; import org.cryptomator.siv.UnauthenticCiphertextException; +import javax.crypto.IllegalBlockSizeException; +import java.nio.charset.Charset; + class FileNameCryptorImpl implements FileNameCryptor { private static final Charset UTF_8 = Charset.forName("UTF-8"); @@ -31,20 +30,22 @@ protected SivMode initialValue() { }; }; - private final SecretKey encryptionKey; - private final SecretKey macKey; + private final DestroyableSecretKey encryptionKey; + private final DestroyableSecretKey macKey; - FileNameCryptorImpl(SecretKey encryptionKey, SecretKey macKey) { + FileNameCryptorImpl(DestroyableSecretKey encryptionKey, DestroyableSecretKey macKey) { this.encryptionKey = encryptionKey; this.macKey = macKey; } @Override public String hashDirectoryId(String cleartextDirectoryId) { - byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); - byte[] encryptedBytes = AES_SIV.get().encrypt(encryptionKey, macKey, cleartextBytes); - byte[] hashedBytes = MessageDigestSupplier.SHA1.get().digest(encryptedBytes); - return BASE32.encode(hashedBytes); + try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); + byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes); + byte[] hashedBytes = MessageDigestSupplier.SHA1.get().digest(encryptedBytes); + return BASE32.encode(hashedBytes); + } } @Override @@ -54,9 +55,11 @@ public String encryptFilename(String cleartextName, byte[]... associatedData) { @Override public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) { - byte[] cleartextBytes = cleartextName.getBytes(UTF_8); - byte[] encryptedBytes = AES_SIV.get().encrypt(encryptionKey, macKey, cleartextBytes, associatedData); - return encoding.encode(encryptedBytes); + try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + byte[] cleartextBytes = cleartextName.getBytes(UTF_8); + byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes, associatedData); + return encoding.encode(encryptedBytes); + } } @Override @@ -66,9 +69,9 @@ public String decryptFilename(String ciphertextName, byte[]... associatedData) t @Override public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { - try { + try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { byte[] encryptedBytes = encoding.decode(ciphertextName); - byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData); + byte[] cleartextBytes = AES_SIV.get().decrypt(ek, mk, encryptedBytes, associatedData); return new String(cleartextBytes, UTF_8); } catch (UnauthenticCiphertextException | IllegalBlockSizeException e) { throw new AuthenticationFailedException("Invalid Ciphertext.", e); diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplBenchmark.java index 5271fff..2797651 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplBenchmark.java @@ -8,14 +8,8 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; -import java.nio.ByteBuffer; -import java.security.SecureRandom; -import java.util.concurrent.TimeUnit; - -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -28,6 +22,10 @@ import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Warmup; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.concurrent.TimeUnit; + /** * Needs to be compiled via maven as the JMH annotation processor needs to do stuff... */ @@ -39,7 +37,7 @@ public class FileContentCryptorImplBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final SecretKey ENC_KEY = new SecretKeySpec(new byte[16], "AES"); + private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES"); private final byte[] headerNonce = new byte[FileHeaderImpl.NONCE_LEN]; private final ByteBuffer cleartextChunk = ByteBuffer.allocate(Constants.PAYLOAD_SIZE); private final ByteBuffer ciphertextChunk = ByteBuffer.allocate(Constants.CHUNK_SIZE); diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java index 494c44d..09259b0 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java @@ -14,6 +14,7 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.cryptomator.cryptolib.common.SeekableByteChannelMock; import org.hamcrest.CoreMatchers; @@ -27,8 +28,6 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; @@ -56,7 +55,7 @@ public class FileContentCryptorImplTest { @BeforeEach public void setup() { csprng = SecureRandomMock.cycle((byte) 0x55, (byte) 0x77); // AES-GCM implementation requires non-repeating nonces, still we need deterministic nonces for testing - SecretKey encKey = new SecretKeySpec(new byte[32], "AES"); + DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES"); header = new FileHeaderImpl(new byte[12], new byte[32]); headerCryptor = new FileHeaderCryptorImpl(encKey, csprng); fileContentCryptor = new FileContentCryptorImpl(csprng); @@ -67,7 +66,7 @@ public void setup() { @Test public void testDecryptedEncryptedEqualsPlaintext() throws AuthenticationFailedException { - SecretKey fileKey = new SecretKeySpec(new byte[16], "AES"); + DestroyableSecretKey fileKey = new DestroyableSecretKey(new byte[16], "AES"); ByteBuffer ciphertext = ByteBuffer.allocate(fileContentCryptor.ciphertextChunkSize()); ByteBuffer cleartext = ByteBuffer.allocate(fileContentCryptor.cleartextChunkSize()); fileContentCryptor.encryptChunk(StandardCharsets.UTF_8.encode("asd"), ciphertext, 42l, new byte[12], fileKey); diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java index 502d34d..84e2267 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java @@ -8,19 +8,10 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; -import java.nio.ByteBuffer; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.concurrent.TimeUnit; - -import javax.crypto.AEADBadTagException; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; -import org.mockito.Mockito; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Level; @@ -32,6 +23,10 @@ import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Warmup; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.concurrent.TimeUnit; + /** * Needs to be compiled via maven as the JMH annotation processor needs to do stuff... */ @@ -43,7 +38,7 @@ public class FileHeaderCryptorBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final SecretKey ENC_KEY = new SecretKeySpec(new byte[16], "AES"); + private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES"); private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(ENC_KEY, RANDOM_MOCK); private ByteBuffer validHeaderCiphertextBuf; diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java index 2ccb63b..2af9cc3 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java @@ -12,15 +12,14 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.CipherSupplier; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import javax.crypto.Cipher; -import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; -import javax.crypto.spec.SecretKeySpec; import java.nio.ByteBuffer; import java.security.SecureRandom; @@ -36,7 +35,7 @@ public class FileHeaderCryptorImplTest { @BeforeEach public void setup() { - SecretKey encKey = new SecretKeySpec(new byte[32], "AES"); + DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES"); headerCryptor = new FileHeaderCryptorImpl(encKey, RANDOM_MOCK); // create new (unused) cipher, just to cipher.init() internally. This is an attempt to avoid diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java index 1100695..b94c858 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java @@ -8,28 +8,26 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.common.DestroyableSecretKey; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.UUID; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - public class FileNameCryptorImplTest { private static final Charset UTF_8 = StandardCharsets.UTF_8; @Test - public void testDeterministicEncryptionOfFilenames() throws IOException, AuthenticationFailedException { + public void testDeterministicEncryptionOfFilenames() throws AuthenticationFailedException { final byte[] keyBytes = new byte[32]; - final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); - final SecretKey macKey = new SecretKeySpec(keyBytes, "AES"); + final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); + final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); // some random @@ -54,8 +52,8 @@ public void testDeterministicEncryptionOfFilenames() throws IOException, Authent @Test public void testDeterministicHashingOfDirectoryIds() throws IOException { final byte[] keyBytes = new byte[32]; - final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); - final SecretKey macKey = new SecretKeySpec(keyBytes, "AES"); + final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); + final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); // some random @@ -70,8 +68,8 @@ public void testDeterministicHashingOfDirectoryIds() throws IOException { @Test public void testDecryptionOfManipulatedFilename() { final byte[] keyBytes = new byte[32]; - final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); - final SecretKey macKey = new SecretKeySpec(keyBytes, "AES"); + final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); + final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); final byte[] encrypted = filenameCryptor.encryptFilename("test").getBytes(UTF_8); @@ -84,8 +82,8 @@ public void testDecryptionOfManipulatedFilename() { @Test public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() { final byte[] keyBytes = new byte[32]; - final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); - final SecretKey macKey = new SecretKeySpec(keyBytes, "AES"); + final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); + final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); final String encrypted1 = filenameCryptor.encryptFilename("test", "ad1".getBytes(UTF_8)); @@ -96,8 +94,8 @@ public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() { @Test public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws AuthenticationFailedException { final byte[] keyBytes = new byte[32]; - final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); - final SecretKey macKey = new SecretKeySpec(keyBytes, "AES"); + final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); + final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); final String encrypted = filenameCryptor.encryptFilename("test", "ad".getBytes(UTF_8)); @@ -108,8 +106,8 @@ public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws Au @Test public void testDeterministicEncryptionOfFilenamesWithWrongAssociatedData() { final byte[] keyBytes = new byte[32]; - final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); - final SecretKey macKey = new SecretKeySpec(keyBytes, "AES"); + final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); + final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); final String encrypted = filenameCryptor.encryptFilename("test", "right".getBytes(UTF_8)); From 6060188673825308928fd9c8a3acbaa7d183d02a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 15 Mar 2021 19:24:31 +0100 Subject: [PATCH 32/59] fixed nonce reuse issues in reproducable tests --- .../v2/FileContentCryptorImplTest.java | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java index 09259b0..2a6d93f 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java @@ -46,7 +46,9 @@ public class FileContentCryptorImplTest { - private SecureRandom csprng; + // AES-GCM implementation requires non-repeating nonces, still we need deterministic nonces for testing + private static final SecureRandom CSPRNG = Mockito.spy(SecureRandomMock.cycle((byte) 0xF0, (byte) 0x0F)); + private FileHeaderImpl header; private FileHeaderCryptorImpl headerCryptor; private FileContentCryptorImpl fileContentCryptor; @@ -54,11 +56,10 @@ public class FileContentCryptorImplTest { @BeforeEach public void setup() { - csprng = SecureRandomMock.cycle((byte) 0x55, (byte) 0x77); // AES-GCM implementation requires non-repeating nonces, still we need deterministic nonces for testing DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES"); header = new FileHeaderImpl(new byte[12], new byte[32]); - headerCryptor = new FileHeaderCryptorImpl(encKey, csprng); - fileContentCryptor = new FileContentCryptorImpl(csprng); + headerCryptor = new FileHeaderCryptorImpl(encKey, CSPRNG); + fileContentCryptor = new FileContentCryptorImpl(CSPRNG); cryptor = Mockito.mock(Cryptor.class); Mockito.when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor); Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(headerCryptor); @@ -93,10 +94,15 @@ public void testEncryptChunkOfInvalidSize(int size) { @Test @DisplayName("encrypt chunk") public void testChunkEncryption() { + Mockito.doAnswer(invocation -> { + byte[] nonce = invocation.getArgument(0); + Arrays.fill(nonce, (byte) 0x33); + return null; + }).when(CSPRNG).nextBytes(Mockito.any()); ByteBuffer cleartext = StandardCharsets.US_ASCII.encode(CharBuffer.wrap("hello world")); ByteBuffer ciphertext = fileContentCryptor.encryptChunk(cleartext, 0, header); - // echo -n "hello world" | openssl enc -aes-256-gcm -K 0 -iv 555555555555555555555555 -a - byte[] expected = BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVnHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHv"); + // echo -n "hello world" | openssl enc -aes-256-gcm -K 0 -iv 333333333333333333333333 -a + byte[] expected = BaseEncoding.base64().decode("MzMzMzMzMzMzMzMzbYvL7CusRmzk70Kn1QxFA5WQg/hgKeba4bln"); Assertions.assertEquals(ByteBuffer.wrap(expected), ciphertext); } @@ -113,6 +119,19 @@ public void testChunkEncryptionWithBufferUnderflow() { @Test @DisplayName("encrypt file") public void testFileEncryption() throws IOException { + Mockito.doAnswer(invocation -> { + byte[] nonce = invocation.getArgument(0); + Arrays.fill(nonce, (byte) 0x55); + return null; + }).doAnswer(invocation -> { + byte[] nonce = invocation.getArgument(0); + Arrays.fill(nonce, (byte) 0x77); + return null; + }).doAnswer(invocation -> { + byte[] nonce = invocation.getArgument(0); + Arrays.fill(nonce, (byte) 0x55); + return null; + }).when(CSPRNG).nextBytes(Mockito.any()); ByteBuffer dst = ByteBuffer.allocate(200); SeekableByteChannel dstCh = new SeekableByteChannelMock(dst); try (WritableByteChannel ch = new EncryptingWritableByteChannel(dstCh, cryptor)) { From 2e80d1a93b737a2654bd28c2808ceb7146efac99 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 15 Mar 2021 19:24:58 +0100 Subject: [PATCH 33/59] cleanup --- .../cryptolib/common/DestroyableSecretKeyTest.java | 10 +++++++++- .../cryptolib/v1/FileContentCryptorImplTest.java | 2 -- .../cryptolib/v1/FileNameCryptorImplTest.java | 3 --- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java index 8011460..3c7418e 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java @@ -18,7 +18,7 @@ public class DestroyableSecretKeyTest { @DisplayName("DestroyableSecretKey.generate(...)") @ParameterizedTest(name = "keylen = {0}") - @ValueSource(ints = {16, 32}) + @ValueSource(ints = {0, 16, 24, 32}) public void testGenerateNew(int keylen) { byte[] keySrc = new byte[keylen]; new Random(42).nextBytes(keySrc); @@ -135,6 +135,14 @@ public void testClone() { Assertions.assertNotSame(key, clone); } + @Test + @DisplayName("close() destroys key") + public void testClose() { + key.close(); + + Assertions.assertTrue(key.isDestroyed()); + } + @Nested @DisplayName("After destroy()...") public class WithDestroyed { diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java index 5d7265a..e1b2639 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java @@ -27,8 +27,6 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java index 8eac407..97738d5 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java @@ -20,12 +20,9 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.UUID; -import java.util.stream.IntStream; import java.util.stream.Stream; public class FileNameCryptorImplTest { From 1de90392ffe9eafaec786d9cee9acc6b573e77a7 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 16 Mar 2021 11:28:07 +0100 Subject: [PATCH 34/59] cleanup and documentation --- .../common/DestroyableSecretKey.java | 39 +++++++++++++++---- .../common/DestroyableSecretKeyTest.java | 12 +++++- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java b/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java index 9079836..30982c9 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java +++ b/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java @@ -12,7 +12,7 @@ /** * A {@link SecretKey} that (other than JDK's SecretKeySpec) * actually implements {@link Destroyable}. - * + *

* Furthermore, this implementation will not create copies when accessing {@link #getEncoded()}. * Instead it implements {@link AutoCloseable} and {@link Cloneable} in an exception-free manner. To prevent mutation of the exposed key, * you would want to make sure to always work on scoped copies, such as in this example: @@ -32,20 +32,40 @@ public class DestroyableSecretKey implements SecretKey, AutoCloseable, Cloneable private final String algorithm; private boolean destroyed; + /** + * Convenience constructor for {@link #DestroyableSecretKey(byte[], int, int, String)} + * + * @param key The raw key data (will get copied) + * @param algorithm The {@link #getAlgorithm() algorithm name} + */ public DestroyableSecretKey(byte[] key, String algorithm) { this(key, 0, key.length, algorithm); } + /** + * Creates a new destroyable secret key, copying of the provided raw key bytes. + * + * @param key A byte[] holding the key material (relevant part will get copied) + * @param offset The offset within key where the key starts + * @param len The number of bytes beginning at offset to read from key + * @param algorithm The {@link #getAlgorithm() algorithm name} + */ public DestroyableSecretKey(byte[] key, int offset, int len, String algorithm) { Preconditions.checkArgument(offset >= 0, "Invalid offset"); Preconditions.checkArgument(len >= 0, "Invalid length"); - Preconditions.checkArgument(key.length >= offset+len, "Invalid offset/len"); + Preconditions.checkArgument(key.length >= offset + len, "Invalid offset/len"); this.key = new byte[len]; this.algorithm = Preconditions.checkNotNull(algorithm, "Algorithm must not be null"); this.destroyed = false; System.arraycopy(key, offset, this.key, 0, len); } + /** + * Casts or converts a given {@link SecretKey} to a DestroyableSecretKey + * + * @param secretKey The secret key + * @return Either the provided or a new key, depending on whether the provided key is already a DestroyableSecretKey + */ public static DestroyableSecretKey from(SecretKey secretKey) { if (secretKey instanceof DestroyableSecretKey) { return (DestroyableSecretKey) secretKey; @@ -57,8 +77,8 @@ public static DestroyableSecretKey from(SecretKey secretKey) { /** * Creates a new key of given length and for use with given algorithm using entropy from the given csprng. * - * @param csprng A cryptographically secure random number source - * @param algorithm The {@link #getAlgorithm() key algorithm} + * @param csprng A cryptographically secure random number source + * @param algorithm The {@link #getAlgorithm() key algorithm} * @param keyLenBytes The length of the key (in bytes) * @return A new secret key */ @@ -85,9 +105,12 @@ public String getFormat() { } /** - * Returns the key as raw byte array. Do not mutate this, unless you know what you're doing! - * If in doubt, make your local copy, first. - * @return The secret key bytes + * Returns the raw key bytes this instance wraps. + *

+ * Important: Any change to the returned array will reflect in this key. Make sure to + * {@link #clone() make a local copy} if you can't rule out mutations. + * + * @return A byte array holding the secret key */ @Override public byte[] getEncoded() { @@ -98,7 +121,7 @@ public byte[] getEncoded() { @Override public DestroyableSecretKey clone() { Preconditions.checkState(!destroyed, "Key has been destroyed"); - return new DestroyableSecretKey(key, algorithm); + return new DestroyableSecretKey(key, algorithm); // key will get copied by the constructor as per contract } @Override diff --git a/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java index 3c7418e..a1f099a 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java @@ -16,9 +16,9 @@ public class DestroyableSecretKeyTest { - @DisplayName("DestroyableSecretKey.generate(...)") + @DisplayName("generate(...)") @ParameterizedTest(name = "keylen = {0}") - @ValueSource(ints = {0, 16, 24, 32}) + @ValueSource(ints = {0, 16, 24, 32, 64, 777}) public void testGenerateNew(int keylen) { byte[] keySrc = new byte[keylen]; new Random(42).nextBytes(keySrc); @@ -65,6 +65,14 @@ public void testConstructorFailsForInvalidLengthAndOffset() { }); } + @Test + public void testConstructorCreatesLocalCopy() { + byte[] orig = "hello".getBytes(); + DestroyableSecretKey key = new DestroyableSecretKey(orig, "TEST"); + Arrays.fill(orig, (byte) 0x00); + Assertions.assertArrayEquals("hello".getBytes(), key.getEncoded()); + } + @Test public void testConstructorClonesKey() { byte[] empty = new byte[32]; From 3395becc5eda300c1be147069b3e92f14c2f4823 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 16 Mar 2021 12:13:16 +0100 Subject: [PATCH 35/59] Refactored Masterkey (now extending DestroyableSecretKey) related to #19 --- .../cryptomator/cryptolib/api/Masterkey.java | 100 ++++------------- .../cryptolib/common/MasterkeyFileAccess.java | 16 ++- .../cryptomator/cryptolib/package-info.java | 2 +- .../cryptolib/CryptoLibIntegrationTest.java | 3 +- .../common/MasterkeyFileAccessTest.java | 2 +- .../cryptolib/common/MasterkeyTest.java | 101 +++--------------- .../cryptolib/v1/CryptorImplTest.java | 2 +- .../v1/FileContentEncryptorBenchmark.java | 2 +- .../cryptolib/v2/CryptorImplTest.java | 2 +- .../v2/FileContentEncryptorBenchmark.java | 2 +- 10 files changed, 51 insertions(+), 181 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java index 4e155bf..13ebbcf 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java @@ -3,106 +3,46 @@ import com.google.common.base.Preconditions; import org.cryptomator.cryptolib.common.DestroyableSecretKey; -import javax.crypto.SecretKey; import java.security.SecureRandom; -import java.util.Objects; +import java.util.Arrays; -public class Masterkey implements AutoCloseable, SecretKey, Cloneable { +public class Masterkey extends DestroyableSecretKey { + private static final String KEY_ALGORITHM = "MASTERKEY"; public static final String ENC_ALG = "AES"; public static final String MAC_ALG = "HmacSHA256"; - public static final int KEY_LEN_BYTES = 32; + public static final int SUBKEY_LEN_BYTES = 32; - private final DestroyableSecretKey encKey; - private final DestroyableSecretKey macKey; - - public Masterkey(SecretKey encKey, SecretKey macKey) { - this(DestroyableSecretKey.from(encKey), DestroyableSecretKey.from(macKey)); - } - - public Masterkey(DestroyableSecretKey encKey, DestroyableSecretKey macKey) { - this.encKey = Preconditions.checkNotNull(encKey); - this.macKey = Preconditions.checkNotNull(macKey); + public Masterkey(byte[] key) { + super(checkKeyLength(key), KEY_ALGORITHM); } - public static Masterkey createNew(SecureRandom random) { - DestroyableSecretKey encKey = DestroyableSecretKey.generate(random, ENC_ALG, KEY_LEN_BYTES); - DestroyableSecretKey macKey = DestroyableSecretKey.generate(random, MAC_ALG, KEY_LEN_BYTES); - return new Masterkey(encKey, macKey); + private static byte[] checkKeyLength(byte[] key) { + Preconditions.checkArgument(key.length == SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES, "Invalid raw key length %s", key.length); + return key; } - public static Masterkey createFromRaw(byte[] encoded) { - Preconditions.checkArgument(encoded.length == KEY_LEN_BYTES + KEY_LEN_BYTES, "Invalid raw key length %s", encoded.length); - DestroyableSecretKey encKey = new DestroyableSecretKey(encoded, 0, KEY_LEN_BYTES, ENC_ALG); - DestroyableSecretKey macKey = new DestroyableSecretKey(encoded, KEY_LEN_BYTES, KEY_LEN_BYTES, MAC_ALG); - return new Masterkey(encKey, macKey); + public static Masterkey generate(SecureRandom csprng) { + byte[] key = new byte[SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES]; + try { + csprng.nextBytes(key); + return new Masterkey(key); + } finally { + Arrays.fill(key, (byte) 0x00); + } } - /** - * Creates an exact deep copy of this Masterkey. - * The new instance is decoupled from this instance and will therefore survive if this gets destroyed. - * - * @return A new but equal Masterkey instance - */ @Override public Masterkey clone() { - return new Masterkey(getEncKey(), getMacKey()); + return new Masterkey(getEncoded()); } public DestroyableSecretKey getEncKey() { - return encKey.clone(); + return new DestroyableSecretKey(getEncoded(), 0, SUBKEY_LEN_BYTES, ENC_ALG); } public DestroyableSecretKey getMacKey() { - return macKey.clone(); - } - - @Override - public String getAlgorithm() { - return "private"; - } - - @Override - public String getFormat() { - return "RAW"; - } - - @Override - public byte[] getEncoded() { - byte[] rawEncKey = encKey.getEncoded(); - byte[] rawMacKey = macKey.getEncoded(); - byte[] rawKey = new byte[rawEncKey.length + rawMacKey.length]; - System.arraycopy(rawEncKey, 0, rawKey, 0, rawEncKey.length); - System.arraycopy(rawMacKey, 0, rawKey, rawEncKey.length, rawMacKey.length); - return rawKey; - } - - @Override - public void close() { - destroy(); + return new DestroyableSecretKey(getEncoded(), SUBKEY_LEN_BYTES, SUBKEY_LEN_BYTES, MAC_ALG); } - @Override - public boolean isDestroyed() { - return encKey.isDestroyed() && macKey.isDestroyed(); - } - - @Override - public void destroy() { - encKey.destroy(); - macKey.destroy(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Masterkey masterkey = (Masterkey) o; - return encKey.equals(masterkey.encKey) && macKey.equals(masterkey.macKey); - } - - @Override - public int hashCode() { - return Objects.hash(encKey, macKey); - } } diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index 299d47a..97cb161 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -167,12 +167,20 @@ Masterkey unlock(MasterkeyFile parsedFile, CharSequence passphrase) throws Inval Preconditions.checkArgument(parsedFile.isValid(), "Invalid masterkey file"); Preconditions.checkNotNull(passphrase); + byte[] encKey = new byte[0], macKey = new byte[0], combined = new byte[0]; try (DestroyableSecretKey kek = scrypt(passphrase, parsedFile.scryptSalt, pepper, parsedFile.scryptCostParam, parsedFile.scryptBlockSize)) { - SecretKey encKey = AesKeyWrap.unwrap(kek, parsedFile.encMasterKey, Masterkey.ENC_ALG); - SecretKey macKey = AesKeyWrap.unwrap(kek, parsedFile.macMasterKey, Masterkey.MAC_ALG); - return new Masterkey(encKey, macKey); + encKey = AesKeyWrap.unwrap(kek, parsedFile.encMasterKey, Masterkey.ENC_ALG).getEncoded(); + macKey = AesKeyWrap.unwrap(kek, parsedFile.macMasterKey, Masterkey.MAC_ALG).getEncoded(); + combined = new byte[encKey.length + macKey.length]; + System.arraycopy(encKey, 0, combined, 0, encKey.length); + System.arraycopy(macKey, 0, combined, encKey.length, macKey.length); + return new Masterkey(combined); } catch (InvalidKeyException e) { throw new InvalidPassphraseException(); + } finally { + Arrays.fill(encKey, (byte) 0x00); + Arrays.fill(macKey, (byte) 0x00); + Arrays.fill(combined, (byte) 0x00); } } @@ -252,7 +260,7 @@ private static DestroyableSecretKey scrypt(CharSequence passphrase, byte[] salt, byte[] saltAndPepper = new byte[salt.length + pepper.length]; System.arraycopy(salt, 0, saltAndPepper, 0, salt.length); System.arraycopy(pepper, 0, saltAndPepper, salt.length, pepper.length); - byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, costParam, blockSize, Masterkey.KEY_LEN_BYTES); + byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, costParam, blockSize, Masterkey.SUBKEY_LEN_BYTES); try { return new DestroyableSecretKey(kekBytes, Masterkey.ENC_ALG); } finally { diff --git a/src/main/java/org/cryptomator/cryptolib/package-info.java b/src/main/java/org/cryptomator/cryptolib/package-info.java index 755e545..16c444b 100644 --- a/src/main/java/org/cryptomator/cryptolib/package-info.java +++ b/src/main/java/org/cryptomator/cryptolib/package-info.java @@ -9,7 +9,7 @@ * * // Create new masterkey and safe it to a file: * SecureRandom csprng = SecureRandom.getInstanceStrong(); - * Masterkey masterkey = {@link org.cryptomator.cryptolib.api.Masterkey#createNew(java.security.SecureRandom) Masterkey.createNew(csprng)}; + * Masterkey masterkey = {@link org.cryptomator.cryptolib.api.Masterkey#generate(java.security.SecureRandom) Masterkey.generate(csprng)}; * {@link org.cryptomator.cryptolib.common.MasterkeyFileAccess#persist(org.cryptomator.cryptolib.api.Masterkey, java.nio.file.Path, java.lang.CharSequence, int) masterkeyFileAccess.persist(masterkey, path, passphrase, vaultVersion)}; * * // Load a masterkey from a file: diff --git a/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java b/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java index 35bf187..5cef884 100644 --- a/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java @@ -17,7 +17,6 @@ import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -32,7 +31,7 @@ public class CryptoLibIntegrationTest { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final Masterkey MASTERKEY = Masterkey.createNew(RANDOM_MOCK); + private static final Masterkey MASTERKEY = Masterkey.generate(RANDOM_MOCK); private static Stream getCryptors() { return Stream.of( diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java index 1b2230d..0a56ec6 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java @@ -31,7 +31,7 @@ public class MasterkeyFileAccessTest { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; private static final byte[] DEFAULT_PEPPER = new byte[0]; - private Masterkey key = Masterkey.createFromRaw(new byte[64]); + private Masterkey key = new Masterkey(new byte[64]); private MasterkeyFileAccess.MasterkeyFile keyFile = new MasterkeyFileAccess.MasterkeyFile(); private MasterkeyFileAccess masterkeyFileAccess = Mockito.spy(new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK)); diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java index 4b7f51e..356dfab 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java @@ -4,132 +4,55 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import javax.crypto.SecretKey; -import javax.security.auth.DestroyFailedException; import java.security.SecureRandom; import java.util.Arrays; -import java.util.Random; -import java.util.stream.Stream; public class MasterkeyTest { - private DestroyableSecretKey encKey; - private DestroyableSecretKey macKey; + private byte[] raw; private Masterkey masterkey; @BeforeEach public void setup() { - encKey = Mockito.mock(DestroyableSecretKey.class); - macKey = Mockito.mock(DestroyableSecretKey.class); - masterkey = new Masterkey(encKey, macKey); + raw = new byte[64]; + for (byte b=0; b create64RandomBytes() { - Random rnd = new Random(42l); - return Stream.generate(() -> { - byte[] bytes = new byte[64]; - rnd.nextBytes(bytes); - return bytes; - }).limit(10); - } - @Test public void testGetEncKey() { - DestroyableSecretKey clonedEncKey = Mockito.mock(DestroyableSecretKey.class); - Mockito.when(encKey.clone()).thenReturn(clonedEncKey); - SecretKey encKey = masterkey.getEncKey(); - Assertions.assertSame(clonedEncKey, encKey); + Assertions.assertArrayEquals(Arrays.copyOfRange(raw, 0,32), encKey.getEncoded()); } @Test public void testGetMacKey() { - DestroyableSecretKey clonedMacKey = Mockito.mock(DestroyableSecretKey.class); - Mockito.when(macKey.clone()).thenReturn(clonedMacKey); - - SecretKey macKey = masterkey.getMacKey(); - - Assertions.assertSame(clonedMacKey, macKey); - } - - @Test - public void testDestroy() throws DestroyFailedException { - masterkey.destroy(); - - Mockito.verify(encKey).destroy(); - Mockito.verify(macKey).destroy(); - } - - @ParameterizedTest - @CsvSource(value = { - "false,true,false", - "true,false,false", - "false,false,false" - }) - public void testIsNotDestroyed(boolean k1, boolean k2) { - Mockito.when(encKey.isDestroyed()).thenReturn(k1); - Mockito.when(macKey.isDestroyed()).thenReturn(k2); - - boolean destroyed = masterkey.isDestroyed(); - - Assertions.assertFalse(destroyed); - } - - @Test - public void testIsDestroyed() { - Mockito.when(encKey.isDestroyed()).thenReturn(true); - Mockito.when(macKey.isDestroyed()).thenReturn(true); - - boolean destroyed = masterkey.isDestroyed(); - - Assertions.assertTrue(destroyed); - } - - @ParameterizedTest(name = "new Masterkey({0}, {1}).getEncoded() == {2}") - @CsvSource(value = { - "foo,bar,foobar", - "foo,barbaz,foobarbaz", - "foobar,baz,foobarbaz" - }) - public void testGetEncoded(String k1, String k2, String combined) { - Mockito.when(encKey.getEncoded()).thenReturn(k1.getBytes()); - Mockito.when(macKey.getEncoded()).thenReturn(k2.getBytes()); - - byte[] raw = masterkey.getEncoded(); + SecretKey encKey = masterkey.getMacKey(); - Assertions.assertArrayEquals(combined.getBytes(), raw); + Assertions.assertArrayEquals(Arrays.copyOfRange(raw, 32, 64), encKey.getEncoded()); } @Test public void testClone() { byte[] raw = new byte[64]; Arrays.fill(raw, (byte) 0x55); - Masterkey original = Masterkey.createFromRaw(raw); + Masterkey original = new Masterkey(raw); Masterkey clone = original.clone(); diff --git a/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java index 9fc7b4d..9bfb5bb 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java @@ -27,7 +27,7 @@ public class CryptorImplTest { @BeforeEach public void setup() { - this.masterkey = Masterkey.createFromRaw(new byte[64]); + this.masterkey = new Masterkey(new byte[64]); } @Test diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java index 9bd881a..66b754d 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java @@ -43,7 +43,7 @@ public class FileContentEncryptorBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]); + private static final Masterkey MASTERKEY = new Masterkey(new byte[64]); private CryptorImpl cryptor; diff --git a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java index 4208465..ea33b48 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java @@ -27,7 +27,7 @@ public class CryptorImplTest { @BeforeEach public void setup() { - this.masterkey = Masterkey.createFromRaw(new byte[64]); + this.masterkey = new Masterkey(new byte[64]); } @Test diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java index 12f48b1..14ca1ef 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java @@ -43,7 +43,7 @@ public class FileContentEncryptorBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final Masterkey MASTERKEY = Masterkey.createFromRaw(new byte[64]); + private static final Masterkey MASTERKEY = new Masterkey(new byte[64]); private CryptorImpl cryptor; From ec554329298f678bf9a8065e62db07fa93cd1f34 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 16 Mar 2021 12:43:39 +0100 Subject: [PATCH 36/59] added javadoc [ci skip] --- .../java/org/cryptomator/cryptolib/api/Masterkey.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java index 13ebbcf..7d5ec69 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java @@ -37,10 +37,20 @@ public Masterkey clone() { return new Masterkey(getEncoded()); } + /** + * Get the encryption subkey. + * + * @return A new copy of the subkey used for encryption + */ public DestroyableSecretKey getEncKey() { return new DestroyableSecretKey(getEncoded(), 0, SUBKEY_LEN_BYTES, ENC_ALG); } + /** + * Get the MAC subkey. + * + * @return A new copy of the subkey used for message authentication + */ public DestroyableSecretKey getMacKey() { return new DestroyableSecretKey(getEncoded(), SUBKEY_LEN_BYTES, SUBKEY_LEN_BYTES, MAC_ALG); } From 178f06d1685c615c858180a78e46c38c19be814e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 16 Mar 2021 12:45:21 +0100 Subject: [PATCH 37/59] simplified key usage in v1 cryptor --- .../cryptomator/cryptolib/v1/CryptorImpl.java | 6 +++--- .../cryptolib/v1/FileContentCryptorImpl.java | 17 +++++++++-------- .../cryptolib/v1/FileHeaderCryptorImpl.java | 19 +++++++++---------- .../cryptolib/v1/FileNameCryptorImpl.java | 15 +++++++-------- .../v1/FileContentCryptorImplBenchmark.java | 10 +++++----- .../v1/FileContentCryptorImplTest.java | 8 ++++---- .../v1/FileHeaderCryptorBenchmark.java | 6 +++--- .../v1/FileHeaderCryptorImplTest.java | 6 +++--- .../cryptolib/v1/FileNameCryptorImplTest.java | 9 ++++----- 9 files changed, 47 insertions(+), 49 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java index 41cb84c..d4c8dc0 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java @@ -26,9 +26,9 @@ class CryptorImpl implements Cryptor { */ CryptorImpl(Masterkey masterkey, SecureRandom random) { this.masterkey = masterkey; - this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey.getEncKey(), masterkey.getMacKey(), random); - this.fileContentCryptor = new FileContentCryptorImpl(masterkey.getMacKey(), random); - this.fileNameCryptor = new FileNameCryptorImpl(masterkey.getEncKey(), masterkey.getMacKey()); + this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random); + this.fileContentCryptor = new FileContentCryptorImpl(masterkey, random); + this.fileNameCryptor = new FileNameCryptorImpl(masterkey); } @Override diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java index 0ce1206..0a68b32 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java @@ -11,6 +11,7 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileContentCryptor; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.CipherSupplier; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.MacSupplier; @@ -33,11 +34,11 @@ class FileContentCryptorImpl implements FileContentCryptor { - private final DestroyableSecretKey macKey; + private final Masterkey masterkey; private final SecureRandom random; - FileContentCryptorImpl(DestroyableSecretKey macKey, SecureRandom random) { - this.macKey = macKey; + FileContentCryptorImpl(Masterkey masterkey, SecureRandom random) { + this.masterkey = masterkey; this.random = random; } @@ -102,7 +103,7 @@ public void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, // visible for testing void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long chunkNumber, byte[] headerNonce, DestroyableSecretKey fileKey) { - try (DestroyableSecretKey fk = fileKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey fk = fileKey.clone()) { // nonce: byte[] nonce = new byte[NONCE_SIZE]; random.nextBytes(nonce); @@ -116,7 +117,7 @@ void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long ch // mac: final ByteBuffer ciphertextBuf = ciphertextChunk.asReadOnlyBuffer(); ciphertextBuf.position(NONCE_SIZE).limit(NONCE_SIZE + bytesEncrypted); - byte[] authenticationCode = calcChunkMac(mk, headerNonce, chunkNumber, nonce, ciphertextBuf); + byte[] authenticationCode = calcChunkMac(headerNonce, chunkNumber, nonce, ciphertextBuf); assert authenticationCode.length == MAC_SIZE; ciphertextChunk.put(authenticationCode); } catch (ShortBufferException e) { @@ -173,14 +174,14 @@ boolean checkChunkMac(byte[] headerNonce, long chunkNumber, ByteBuffer chunkBuf) expectedMacBuf.get(expectedMac); // get actual MAC: - final byte[] calculatedMac = calcChunkMac(macKey, headerNonce, chunkNumber, chunkNonce, payloadBuf); + final byte[] calculatedMac = calcChunkMac(headerNonce, chunkNumber, chunkNonce, payloadBuf); // time-constant equality check of two MACs: return MessageDigest.isEqual(expectedMac, calculatedMac); } - private static byte[] calcChunkMac(DestroyableSecretKey macKey, byte[] headerNonce, long chunkNumber, byte[] chunkNonce, ByteBuffer ciphertext) { - try (DestroyableSecretKey mk = macKey.clone()) { + private byte[] calcChunkMac(byte[] headerNonce, long chunkNumber, byte[] chunkNonce, ByteBuffer ciphertext) { + try (DestroyableSecretKey mk = masterkey.getMacKey()) { final byte[] chunkNumberBigEndian = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.BIG_ENDIAN).putLong(chunkNumber).array(); final Mac mac = MacSupplier.HMAC_SHA256.withKey(mk); mac.update(headerNonce); diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java index cc613e3..ac9d457 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java @@ -11,6 +11,7 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.api.FileHeaderCryptor; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.CipherSupplier; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.MacSupplier; @@ -28,13 +29,11 @@ class FileHeaderCryptorImpl implements FileHeaderCryptor { - private final DestroyableSecretKey headerKey; - private final DestroyableSecretKey macKey; + private final Masterkey masterkey; private final SecureRandom random; - FileHeaderCryptorImpl(DestroyableSecretKey headerKey, DestroyableSecretKey macKey, SecureRandom random) { - this.headerKey = headerKey; - this.macKey = macKey; + FileHeaderCryptorImpl(Masterkey masterkey, SecureRandom random) { + this.masterkey = masterkey; this.random = random; } @@ -59,12 +58,12 @@ public ByteBuffer encryptHeader(FileHeader header) { payloadCleartextBuf.putLong(-1l); payloadCleartextBuf.put(headerImpl.getPayload().getContentKeyBytes()); payloadCleartextBuf.flip(); - try (DestroyableSecretKey hk = headerKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { ByteBuffer result = ByteBuffer.allocate(FileHeaderImpl.SIZE); result.put(headerImpl.getNonce()); // encrypt payload: - Cipher cipher = CipherSupplier.AES_CTR.forEncryption(hk, new IvParameterSpec(headerImpl.getNonce())); + Cipher cipher = CipherSupplier.AES_CTR.forEncryption(ek, new IvParameterSpec(headerImpl.getNonce())); int encrypted = cipher.doFinal(payloadCleartextBuf, result); assert encrypted == FileHeaderImpl.Payload.SIZE; @@ -103,7 +102,7 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic buf.get(expectedMac); // check mac: - try (DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey mk = masterkey.getMacKey()) { ByteBuffer nonceAndCiphertextBuf = buf.duplicate(); nonceAndCiphertextBuf.position(FileHeaderImpl.NONCE_POS).limit(FileHeaderImpl.NONCE_POS + FileHeaderImpl.NONCE_LEN + FileHeaderImpl.PAYLOAD_LEN); Mac mac = MacSupplier.HMAC_SHA256.withKey(mk); @@ -115,9 +114,9 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic } ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE); - try (DestroyableSecretKey hk = headerKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey()) { // decrypt payload: - Cipher cipher = CipherSupplier.AES_CTR.forDecryption(hk, new IvParameterSpec(nonce)); + Cipher cipher = CipherSupplier.AES_CTR.forDecryption(ek, new IvParameterSpec(nonce)); int decrypted = cipher.doFinal(ByteBuffer.wrap(ciphertextPayload), payloadCleartextBuf); assert decrypted == FileHeaderImpl.Payload.SIZE; diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java index f6acb2d..b9274a5 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java @@ -11,6 +11,7 @@ import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.cryptomator.siv.SivMode; @@ -30,17 +31,15 @@ protected SivMode initialValue() { }; }; - private final DestroyableSecretKey encryptionKey; - private final DestroyableSecretKey macKey; + private final Masterkey masterkey; - FileNameCryptorImpl(DestroyableSecretKey encryptionKey, DestroyableSecretKey macKey) { - this.encryptionKey = encryptionKey; - this.macKey = macKey; + FileNameCryptorImpl(Masterkey masterkey) { + this.masterkey = masterkey; } @Override public String hashDirectoryId(String cleartextDirectoryId) { - try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes); byte[] hashedBytes = MessageDigestSupplier.SHA1.get().digest(encryptedBytes); @@ -55,7 +54,7 @@ public String encryptFilename(String cleartextName, byte[]... associatedData) { @Override public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) { - try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { byte[] cleartextBytes = cleartextName.getBytes(UTF_8); byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes, associatedData); return encoding.encode(encryptedBytes); @@ -69,7 +68,7 @@ public String decryptFilename(String ciphertextName, byte[]... associatedData) t @Override public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { - try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { byte[] encryptedBytes = encoding.decode(ciphertextName); byte[] cleartextBytes = AES_SIV.get().decrypt(ek, mk, encryptedBytes, associatedData); return new String(cleartextBytes, UTF_8); diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java index 846c7f1..d8902e3 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java @@ -15,6 +15,7 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; @@ -39,12 +40,11 @@ public class FileContentCryptorImplBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES"); - private static final DestroyableSecretKey MAC_KEY = new DestroyableSecretKey(new byte[16], "HmacSHA256"); + private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);; private final byte[] headerNonce = new byte[Constants.NONCE_SIZE]; private final ByteBuffer cleartextChunk = ByteBuffer.allocate(Constants.PAYLOAD_SIZE); private final ByteBuffer ciphertextChunk = ByteBuffer.allocate(Constants.CHUNK_SIZE); - private final FileContentCryptorImpl fileContentCryptor = new FileContentCryptorImpl(MAC_KEY, RANDOM_MOCK); + private final FileContentCryptorImpl fileContentCryptor = new FileContentCryptorImpl(MASTERKEY, RANDOM_MOCK); private long chunkNumber; @Setup(Level.Invocation) @@ -59,7 +59,7 @@ public void shuffleData() { @Benchmark public void benchmarkEncryption() { - fileContentCryptor.encryptChunk(cleartextChunk, ciphertextChunk, chunkNumber, headerNonce, ENC_KEY); + fileContentCryptor.encryptChunk(cleartextChunk, ciphertextChunk, chunkNumber, headerNonce, MASTERKEY.getEncKey()); } @Benchmark @@ -69,7 +69,7 @@ public void benchmarkAuthentication() { @Benchmark public void benchmarkDecryption() { - fileContentCryptor.decryptChunk(ciphertextChunk, ciphertextChunk, ENC_KEY); + fileContentCryptor.decryptChunk(ciphertextChunk, ciphertextChunk, MASTERKEY.getEncKey()); } } diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java index e1b2639..b5eab53 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java @@ -13,6 +13,7 @@ import org.cryptomator.cryptolib.EncryptingWritableByteChannel; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.cryptomator.cryptolib.common.SeekableByteChannelMock; @@ -52,10 +53,9 @@ public class FileContentCryptorImplTest { @BeforeEach public void setup() { - DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES"); - DestroyableSecretKey macKey = new DestroyableSecretKey(new byte[32], "HmacSHA256"); - headerCryptor = new FileHeaderCryptorImpl(encKey, macKey, RANDOM_MOCK); - fileContentCryptor = new FileContentCryptorImpl(macKey, RANDOM_MOCK); + Masterkey masterkey = new Masterkey(new byte[64]); + headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK); + fileContentCryptor = new FileContentCryptorImpl(masterkey, RANDOM_MOCK); cryptor = Mockito.mock(Cryptor.class); Mockito.when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor); Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(headerCryptor); diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java index 9b1e9a8..5a89bae 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java @@ -10,6 +10,7 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; @@ -38,9 +39,8 @@ public class FileHeaderCryptorBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES"); - private static final DestroyableSecretKey MAC_KEY = new DestroyableSecretKey(new byte[16], "HmacSHA256"); - private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(ENC_KEY, MAC_KEY, RANDOM_MOCK); + private static final Masterkey MASTERKEY = new Masterkey(new byte[64]); + private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK); private FileHeader header; private ByteBuffer validHeaderCiphertextBuf; diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java index c68a0e9..a3bf7cf 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java @@ -11,6 +11,7 @@ import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.junit.jupiter.api.Assertions; @@ -27,9 +28,8 @@ public class FileHeaderCryptorImplTest { @BeforeEach public void setup() { - DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES"); - DestroyableSecretKey macKey = new DestroyableSecretKey(new byte[32], "HmacSHA256"); - headerCryptor = new FileHeaderCryptorImpl(encKey, macKey, RANDOM_MOCK); + Masterkey masterkey = new Masterkey(new byte[64]); + headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK); } @Test diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java index 97738d5..b1551e2 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java @@ -10,6 +10,7 @@ import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.siv.UnauthenticCiphertextException; import org.hamcrest.CoreMatchers; @@ -29,12 +30,10 @@ public class FileNameCryptorImplTest { private static final Charset UTF_8 = StandardCharsets.UTF_8; - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); + private final Masterkey masterkey = new Masterkey(new byte[64]); + private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(masterkey); - static Stream filenameGenerator() { + private static Stream filenameGenerator() { return Stream.generate(UUID::randomUUID).map(UUID::toString).limit(100); } From 67720f758f27e05090ff435ff3087bfb8a38a806 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 16 Mar 2021 12:56:25 +0100 Subject: [PATCH 38/59] simplified key usage in v2 cryptor --- .../cryptomator/cryptolib/v2/CryptorImpl.java | 4 +- .../cryptolib/v2/FileHeaderCryptorImpl.java | 15 ++- .../cryptolib/v2/FileNameCryptorImpl.java | 17 ++- .../v2/FileContentCryptorImplTest.java | 5 +- .../v2/FileHeaderCryptorBenchmark.java | 5 +- .../v2/FileHeaderCryptorImplTest.java | 7 +- .../cryptolib/v2/FileNameCryptorImplTest.java | 127 ++++++++++-------- 7 files changed, 99 insertions(+), 81 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java index 1ba2ab5..fa8bd68 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java @@ -27,9 +27,9 @@ class CryptorImpl implements Cryptor { */ CryptorImpl(Masterkey masterkey, SecureRandom random) { this.masterkey = masterkey; - this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey.getEncKey(), random); + this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random); this.fileContentCryptor = new FileContentCryptorImpl(random); - this.fileNameCryptor = new FileNameCryptorImpl(masterkey.getEncKey(), masterkey.getMacKey()); + this.fileNameCryptor = new FileNameCryptorImpl(masterkey); } @Override diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java index 729a627..3c05f48 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java @@ -11,6 +11,7 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.api.FileHeaderCryptor; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.CipherSupplier; import org.cryptomator.cryptolib.common.DestroyableSecretKey; @@ -28,11 +29,11 @@ class FileHeaderCryptorImpl implements FileHeaderCryptor { - private final DestroyableSecretKey headerKey; + private final Masterkey masterkey; private final SecureRandom random; - FileHeaderCryptorImpl(DestroyableSecretKey headerKey, SecureRandom random) { - this.headerKey = headerKey; + FileHeaderCryptorImpl(Masterkey masterkey, SecureRandom random) { + this.masterkey = masterkey; this.random = random; } @@ -57,12 +58,12 @@ public ByteBuffer encryptHeader(FileHeader header) { payloadCleartextBuf.putLong(-1l); payloadCleartextBuf.put(headerImpl.getPayload().getContentKeyBytes()); payloadCleartextBuf.flip(); - try (DestroyableSecretKey hk = headerKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey()) { ByteBuffer result = ByteBuffer.allocate(FileHeaderImpl.SIZE); result.put(headerImpl.getNonce()); // encrypt payload: - Cipher cipher = CipherSupplier.AES_GCM.forEncryption(hk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce())); + Cipher cipher = CipherSupplier.AES_GCM.forEncryption(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce())); int encrypted = cipher.doFinal(payloadCleartextBuf, result); assert encrypted == FileHeaderImpl.PAYLOAD_LEN + FileHeaderImpl.TAG_LEN; @@ -91,9 +92,9 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic buf.get(ciphertextAndTag); ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE); - try (DestroyableSecretKey hk = headerKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey()) { // decrypt payload: - Cipher cipher = CipherSupplier.AES_GCM.forDecryption(hk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); + Cipher cipher = CipherSupplier.AES_GCM.forDecryption(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); int decrypted = cipher.doFinal(ByteBuffer.wrap(ciphertextAndTag), payloadCleartextBuf); assert decrypted == FileHeaderImpl.Payload.SIZE; diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java index 6410d5e..27756f6 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java @@ -11,6 +11,7 @@ import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.cryptomator.siv.SivMode; @@ -30,17 +31,15 @@ protected SivMode initialValue() { }; }; - private final DestroyableSecretKey encryptionKey; - private final DestroyableSecretKey macKey; + private final Masterkey masterkey; - FileNameCryptorImpl(DestroyableSecretKey encryptionKey, DestroyableSecretKey macKey) { - this.encryptionKey = encryptionKey; - this.macKey = macKey; + FileNameCryptorImpl(Masterkey masterkey) { + this.masterkey = masterkey; } @Override public String hashDirectoryId(String cleartextDirectoryId) { - try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes); byte[] hashedBytes = MessageDigestSupplier.SHA1.get().digest(encryptedBytes); @@ -55,7 +54,7 @@ public String encryptFilename(String cleartextName, byte[]... associatedData) { @Override public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) { - try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { byte[] cleartextBytes = cleartextName.getBytes(UTF_8); byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes, associatedData); return encoding.encode(encryptedBytes); @@ -69,11 +68,11 @@ public String decryptFilename(String ciphertextName, byte[]... associatedData) t @Override public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { - try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { byte[] encryptedBytes = encoding.decode(ciphertextName); byte[] cleartextBytes = AES_SIV.get().decrypt(ek, mk, encryptedBytes, associatedData); return new String(cleartextBytes, UTF_8); - } catch (UnauthenticCiphertextException | IllegalBlockSizeException e) { + } catch (IllegalArgumentException | UnauthenticCiphertextException | IllegalBlockSizeException e) { throw new AuthenticationFailedException("Invalid Ciphertext.", e); } } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java index 2a6d93f..566375e 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java @@ -14,6 +14,7 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.cryptomator.cryptolib.common.SeekableByteChannelMock; @@ -56,9 +57,9 @@ public class FileContentCryptorImplTest { @BeforeEach public void setup() { - DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES"); + Masterkey masterkey = new Masterkey(new byte[64]); header = new FileHeaderImpl(new byte[12], new byte[32]); - headerCryptor = new FileHeaderCryptorImpl(encKey, CSPRNG); + headerCryptor = new FileHeaderCryptorImpl(masterkey, CSPRNG); fileContentCryptor = new FileContentCryptorImpl(CSPRNG); cryptor = Mockito.mock(Cryptor.class); Mockito.when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor); diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java index 84e2267..1d2cd4b 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java @@ -10,6 +10,7 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; @@ -38,8 +39,8 @@ public class FileHeaderCryptorBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES"); - private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(ENC_KEY, RANDOM_MOCK); + private static final Masterkey MASTERKEY = new Masterkey(new byte[64]); + private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK); private ByteBuffer validHeaderCiphertextBuf; private FileHeader header; diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java index 2af9cc3..394de95 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java @@ -11,6 +11,7 @@ import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.CipherSupplier; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; @@ -35,14 +36,14 @@ public class FileHeaderCryptorImplTest { @BeforeEach public void setup() { - DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES"); - headerCryptor = new FileHeaderCryptorImpl(encKey, RANDOM_MOCK); + Masterkey masterkey = new Masterkey(new byte[64]); + headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK); // create new (unused) cipher, just to cipher.init() internally. This is an attempt to avoid // InvalidAlgorithmParameterExceptions due to IV-reuse, when the actual unit tests use constant IVs byte[] nonce = new byte[GCM_NONCE_SIZE]; ANTI_REUSE_PRNG.nextBytes(nonce); - Cipher cipher = CipherSupplier.AES_GCM.forEncryption(encKey, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); + Cipher cipher = CipherSupplier.AES_GCM.forEncryption(masterkey.getEncKey(), new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); Assertions.assertNotNull(cipher); } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java index b94c858..b9a3a34 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java @@ -8,109 +8,124 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; +import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.cryptomator.cryptolib.common.DestroyableSecretKey; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.siv.UnauthenticCiphertextException; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; -import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.UUID; +import java.util.stream.Stream; public class FileNameCryptorImplTest { private static final Charset UTF_8 = StandardCharsets.UTF_8; - @Test - public void testDeterministicEncryptionOfFilenames() throws AuthenticationFailedException { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - - // some random - for (int i = 0; i < 2000; i++) { - final String origName = UUID.randomUUID().toString(); - final String encrypted1 = filenameCryptor.encryptFilename(origName); - final String encrypted2 = filenameCryptor.encryptFilename(origName); - Assertions.assertEquals(encrypted1, encrypted2); - final String decrypted = filenameCryptor.decryptFilename(encrypted1); - Assertions.assertEquals(origName, decrypted); - } + private final Masterkey masterkey = new Masterkey(new byte[64]); + private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(masterkey); + + private static Stream filenameGenerator() { + return Stream.generate(UUID::randomUUID).map(UUID::toString).limit(100); + } + + @DisplayName("encrypt and decrypt file names") + @ParameterizedTest(name = "decrypt(encrypt({0}))") + @MethodSource("filenameGenerator") + public void testDeterministicEncryptionOfFilenames(String origName) throws AuthenticationFailedException { + String encrypted1 = filenameCryptor.encryptFilename(origName); + String encrypted2 = filenameCryptor.encryptFilename(origName); + String decrypted = filenameCryptor.decryptFilename(encrypted1); + + Assertions.assertEquals(encrypted1, encrypted2); + Assertions.assertEquals(origName, decrypted); + } + + @DisplayName("encrypt and decrypt file names with AD and custom encoding") + @ParameterizedTest(name = "decrypt(encrypt({0}))") + @MethodSource("filenameGenerator") + public void testDeterministicEncryptionOfFilenamesWithCustomEncodingAndAssociatedData(String origName) throws AuthenticationFailedException { + byte[] associdatedData = new byte[10]; + String encrypted1 = filenameCryptor.encryptFilename(BaseEncoding.base64Url(), origName, associdatedData); + String encrypted2 = filenameCryptor.encryptFilename(BaseEncoding.base64Url(), origName, associdatedData); + String decrypted = filenameCryptor.decryptFilename(BaseEncoding.base64Url(), encrypted1, associdatedData); + + Assertions.assertEquals(encrypted1, encrypted2); + Assertions.assertEquals(origName, decrypted); + } + @Test + @DisplayName("encrypt and decrypt 128 bit filename") + public void testDeterministicEncryptionOf128bitFilename() throws AuthenticationFailedException { // block size length file names - final String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii - final String encryptedPath3a = filenameCryptor.encryptFilename(originalPath3); - final String encryptedPath3b = filenameCryptor.encryptFilename(originalPath3); + String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii + String encryptedPath3a = filenameCryptor.encryptFilename(originalPath3); + String encryptedPath3b = filenameCryptor.encryptFilename(originalPath3); + String decryptedPath3 = filenameCryptor.decryptFilename(encryptedPath3a); + Assertions.assertEquals(encryptedPath3a, encryptedPath3b); - final String decryptedPath3 = filenameCryptor.decryptFilename(encryptedPath3a); Assertions.assertEquals(originalPath3, decryptedPath3); } + @DisplayName("hash directory id for random directory ids") + @ParameterizedTest(name = "hashDirectoryId({0})") + @MethodSource("filenameGenerator") + public void testDeterministicHashingOfDirectoryIds(String originalDirectoryId) { + final String hashedDirectory1 = filenameCryptor.hashDirectoryId(originalDirectoryId); + final String hashedDirectory2 = filenameCryptor.hashDirectoryId(originalDirectoryId); + Assertions.assertEquals(hashedDirectory1, hashedDirectory2); + } + @Test - public void testDeterministicHashingOfDirectoryIds() throws IOException { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - - // some random - for (int i = 0; i < 2000; i++) { - final String originalDirectoryId = UUID.randomUUID().toString(); - final String hashedDirectory1 = filenameCryptor.hashDirectoryId(originalDirectoryId); - final String hashedDirectory2 = filenameCryptor.hashDirectoryId(originalDirectoryId); - Assertions.assertEquals(hashedDirectory1, hashedDirectory2); - } + @DisplayName("decrypt non-ciphertext") + public void testDecryptionOfMalformedFilename() { + AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> { + filenameCryptor.decryptFilename("lol"); + }); + MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(IllegalArgumentException.class)); } @Test + @DisplayName("decrypt tampered ciphertext") public void testDecryptionOfManipulatedFilename() { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - final byte[] encrypted = filenameCryptor.encryptFilename("test").getBytes(UTF_8); encrypted[0] ^= (byte) 0x01; // change 1 bit in first byte - Assertions.assertThrows(AuthenticationFailedException.class, () -> { + + AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> { filenameCryptor.decryptFilename(new String(encrypted, UTF_8)); }); + MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(UnauthenticCiphertextException.class)); } @Test + @DisplayName("encrypt with different AD") public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - final String encrypted1 = filenameCryptor.encryptFilename("test", "ad1".getBytes(UTF_8)); final String encrypted2 = filenameCryptor.encryptFilename("test", "ad2".getBytes(UTF_8)); Assertions.assertNotEquals(encrypted1, encrypted2); } @Test + @DisplayName("decrypt ciphertext with correct AD") public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws AuthenticationFailedException { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - final String encrypted = filenameCryptor.encryptFilename("test", "ad".getBytes(UTF_8)); final String decrypted = filenameCryptor.decryptFilename(encrypted, "ad".getBytes(UTF_8)); Assertions.assertEquals("test", decrypted); } @Test + @DisplayName("decrypt ciphertext with incorrect AD") public void testDeterministicEncryptionOfFilenamesWithWrongAssociatedData() { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - final String encrypted = filenameCryptor.encryptFilename("test", "right".getBytes(UTF_8)); + Assertions.assertThrows(AuthenticationFailedException.class, () -> { filenameCryptor.decryptFilename(encrypted, "wrong".getBytes(UTF_8)); }); From 13b4c46a554b9727668b9fd80a2222e52018133e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 16 Mar 2021 13:32:48 +0100 Subject: [PATCH 39/59] updated slack notification [ci skip] --- .github/workflows/publish-github.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml index 32a3041..c74c504 100644 --- a/.github/workflows/publish-github.yml +++ b/.github/workflows/publish-github.yml @@ -35,6 +35,6 @@ jobs: SLACK_ICON_EMOJI: ':bot:' SLACK_CHANNEL: 'cryptomator-desktop' SLACK_TITLE: "Published ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}" - SLACK_MESSAGE: "Ready to ." + SLACK_MESSAGE: "Ready to ." SLACK_FOOTER: MSG_MINIMAL: true \ No newline at end of file From 4a5f21ce2dbdf497abf270acf7db2b6aba1b53fb Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 22 Mar 2021 12:25:47 +0100 Subject: [PATCH 40/59] Update README.md [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b96c79..09d91b2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Build](https://github.com/cryptomator/cryptolib/workflows/Build/badge.svg)](https://github.com/cryptomator/cryptolib/actions?query=workflow%3ABuild) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/9d736fe3e9e14dfb8a65949abbe8f712)](https://www.codacy.com/app/cryptomator/cryptolib) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/9d736fe3e9e14dfb8a65949abbe8f712)](https://www.codacy.com/gh/cryptomator/cryptolib/dashboard) [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/9d736fe3e9e14dfb8a65949abbe8f712)](https://www.codacy.com/gh/cryptomator/cryptolib/dashboard) [![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptolib/badge.svg)](https://snyk.io/test/github/cryptomator/cryptolib) [![Maven Central](https://img.shields.io/maven-central/v/org.cryptomator/cryptolib.svg?maxAge=86400)](https://repo1.maven.org/maven2/org/cryptomator/cryptolib/) From aaa76ac6f4f1482aab482ff98443f148072688f6 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 22 Apr 2021 21:56:34 +0200 Subject: [PATCH 41/59] removed MasterkeyFileLoader, simplified MasterkeyLoader --- .../cryptolib/api/MasterkeyLoader.java | 3 +- .../cryptolib/common/MasterkeyFileAccess.java | 11 ---- .../cryptolib/common/MasterkeyFileLoader.java | 64 ------------------- .../common/MasterkeyFileLoaderContext.java | 27 -------- .../common/MasterkeyFileAccessTest.java | 12 ---- 5 files changed, 1 insertion(+), 116 deletions(-) delete mode 100644 src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java delete mode 100644 src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java diff --git a/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java index 6065bca..04ac281 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java +++ b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java @@ -9,10 +9,9 @@ * * @see MasterkeyFileAccess */ +@FunctionalInterface public interface MasterkeyLoader { - boolean supportsScheme(String scheme); - /** * Loads a master key. This might be a long-running operation, as it may require user input or expensive computations. * diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index 97cb161..c83314c 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -245,17 +245,6 @@ MasterkeyFile lock(Masterkey masterkey, CharSequence passphrase, int vaultVersio } } - /** - * Creates a {@link MasterkeyLoader} able to load keys from masterkey JSON files using the same pepper as this. - * - * @param vaultRoot The path to a vault for which a masterkey should be loaded. - * @param context A context providing information required by the key loader. - * @return A new masterkey loader. - */ - public MasterkeyFileLoader keyLoader(Path vaultRoot, MasterkeyFileLoaderContext context) { - return new MasterkeyFileLoader(vaultRoot, this, context); - } - private static DestroyableSecretKey scrypt(CharSequence passphrase, byte[] salt, byte[] pepper, int costParam, int blockSize) { byte[] saltAndPepper = new byte[salt.length + pepper.length]; System.arraycopy(salt, 0, saltAndPepper, 0, salt.length); diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java deleted file mode 100644 index c967973..0000000 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoader.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.cryptomator.cryptolib.common; - -import org.cryptomator.cryptolib.api.Masterkey; -import org.cryptomator.cryptolib.api.MasterkeyLoader; -import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * A {@link MasterkeyLoader} for keys with the {@value #SCHEME} scheme. - *

- * Instances of this class are {@link MasterkeyFileLoaderContext context}-specific and should be obtained - * via {@link MasterkeyFileAccess#keyLoader(Path, MasterkeyFileLoaderContext)} - *

- * This key loader {@link #loadKey(URI) loads} a vault's masterkey by interpreting the key ID as a path, - * either absolute or relative to the root directory of the vault, pointing to a masterkey file containing - * information that (paired with the correct passphrase) can be used to derive the masterkey. - */ -public class MasterkeyFileLoader implements MasterkeyLoader { - - public static final String SCHEME = "masterkeyfile"; - - private final Path vaultRoot; - private final MasterkeyFileAccess masterkeyFileAccess; - private final MasterkeyFileLoaderContext context; - - MasterkeyFileLoader(Path vaultRoot, MasterkeyFileAccess masterkeyFileAccess, MasterkeyFileLoaderContext context) { - this.vaultRoot = vaultRoot; - this.masterkeyFileAccess = masterkeyFileAccess; - this.context = context; - } - - /** - * @param masterkeyFilePath Vault-relative or absolute path to a masterkey file. - * @return A new URI that can be used as key ID - */ - public static URI keyId(String masterkeyFilePath) { - try { - return new URI(SCHEME, masterkeyFilePath, null); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Can't create URI from " + SCHEME + ":" + masterkeyFilePath, e); - } - } - - @Override - public boolean supportsScheme(String scheme) { - return SCHEME.equalsIgnoreCase(scheme); - } - - @Override - public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException { - assert SCHEME.equalsIgnoreCase(keyId.getScheme()); - Path filePath = vaultRoot.resolve(keyId.getSchemeSpecificPart()); - if (!Files.exists(filePath)) { - filePath = context.getCorrectMasterkeyFilePath(keyId.getSchemeSpecificPart()); - } - CharSequence passphrase = context.getPassphrase(filePath); - return masterkeyFileAccess.load(filePath, passphrase); - } - -} diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java deleted file mode 100644 index 989a0b7..0000000 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileLoaderContext.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.cryptomator.cryptolib.common; - -import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; - -import java.nio.file.Path; - -public interface MasterkeyFileLoaderContext { - - /** - * Provides the path of a masterkey file, if it could not be resolved automatically. - * - * @param incorrectPath The path as denoted by the key ID - * @return The correct path to a masterkey file, must not be null - * @throws MasterkeyLoadingFailedException If the context is unable to provide a correct paath - */ - Path getCorrectMasterkeyFilePath(String incorrectPath) throws MasterkeyLoadingFailedException; - - /** - * Provides the password for a given masterkey file. - * - * @param masterkeyFile For what masterkey file - * @return The passphrase, must not be null - * @throws MasterkeyLoadingFailedException If the context is unable to provide a passphrase - */ - CharSequence getPassphrase(Path masterkeyFile) throws MasterkeyLoadingFailedException; - -} diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java index 0a56ec6..569502e 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java @@ -46,18 +46,6 @@ public void setup() { keyFile.versionMac = BaseEncoding.base64().decode("iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA="); } - @Test - @DisplayName("keyLoader(...) does not load a key yet") - public void testCreateKeyLoader() { - Path path = Mockito.mock(Path.class); - MasterkeyFileLoaderContext keyLoaderContext = Mockito.mock(MasterkeyFileLoaderContext.class); - - MasterkeyLoader keyLoader = masterkeyFileAccess.keyLoader(path, keyLoaderContext); - - Assertions.assertNotNull(keyLoader); - Mockito.verifyNoInteractions(keyLoaderContext); - } - @Test @DisplayName("changePassphrase(MasterkeyFile, ...)") public void testChangePassphraseWithMasterkeyFile() throws CryptoException { From 8852709d8daf0483e4ca5e17292d4233637c21c2 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 22 Apr 2021 22:13:45 +0200 Subject: [PATCH 42/59] update build plugin --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0675b36..1061887 100644 --- a/pom.xml +++ b/pom.xml @@ -222,7 +222,7 @@ org.owasp dependency-check-maven - 6.1.0 + 6.1.5 24 0 From fd69c2ff8a97d8056edb4bf1280731ad0c7e2b32 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Tue, 4 May 2021 13:07:58 +0200 Subject: [PATCH 43/59] improve documentation of the MasterkeyLoader interface --- .../cryptomator/cryptolib/api/MasterkeyLoader.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java index 04ac281..c12f2e9 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java +++ b/src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java @@ -14,9 +14,20 @@ public interface MasterkeyLoader { /** * Loads a master key. This might be a long-running operation, as it may require user input or expensive computations. + *

+ * It is the caller's responsibility to destroy the returned {@link Masterkey} after usage by calling {@link Masterkey#destroy()}. This can easily be done using a try-with-resource block: + *

+	 * {@code
+	 * Masterkeyloader keyLoader;
+	 * URI keyId;
+	 * try (Masterkey key = keyLoader.loadKey(keyId) ){
+	 *     // Do stuff with the key
+	 * }
+	 * }
+	 * 
* * @param keyId An URI uniquely identifying the source and identity of the key - * @return The raw key bytes. Must not be null + * @return a {@link Masterkey} object wrapping the raw key bytes. Must not be null * @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request */ Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException; From db0daf0e40305f6a18fb473a21dc44fc5f2ffb35 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Tue, 4 May 2021 13:09:16 +0200 Subject: [PATCH 44/59] Update pom.xml * bump dependencies * move used JDK to more prominent place * move non-default build plugin versions to properties section --- pom.xml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 1061887..bea119a 100644 --- a/pom.xml +++ b/pom.xml @@ -15,18 +15,24 @@ UTF-8 + 8 2.8.6 - 30.1-jre + 30.1.1-jre 1.4.1 1.7.30 5.7.1 - 3.7.7 + 3.9.0 2.2 - 1.27 + 1.29 + + + 6.1.6 + 0.8.7 + 1.6.8 @@ -138,7 +144,6 @@ maven-compiler-plugin 3.8.1 - 8 UTF-8 true @@ -222,7 +227,7 @@ org.owasp dependency-check-maven - 6.1.5 + ${dependency-check.version} 24 0 @@ -249,7 +254,7 @@ org.jacoco jacoco-maven-plugin - 0.8.6 + ${jacoco.version} prepare-agent @@ -310,7 +315,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.8 + ${nexus-staging.version} true ossrh From f5b7b76a10bfc3eb49e95201b96554b9558659cd Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Tue, 4 May 2021 13:15:06 +0200 Subject: [PATCH 45/59] use correct jacoco version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bea119a..a82cfb7 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ 6.1.6 - 0.8.7 + 0.8.6 1.6.8 From 0921eead63fe5910b734ee7750b7241e890b6b37 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 21 May 2021 18:04:10 +0200 Subject: [PATCH 46/59] added module-info --- pom.xml | 21 +++++++++++++++++++-- src/main/java9/module-info.java | 12 ++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 src/main/java9/module-info.java diff --git a/pom.xml b/pom.xml index a82cfb7..967f3b4 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 2.8.6 30.1.1-jre - 1.4.1 + 1.4.2 1.7.30 @@ -147,6 +147,22 @@ UTF-8 true + + + java9 + compile + + compile + + + 9 + + ${project.basedir}/src/main/java9 + + true + + + org.apache.maven.plugins @@ -160,7 +176,8 @@ - org.cryptomator.cryptolib + true + true diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java new file mode 100644 index 0000000..7111d6f --- /dev/null +++ b/src/main/java9/module-info.java @@ -0,0 +1,12 @@ +module org.cryptomator.cryptolib { + requires org.cryptomator.siv; + requires com.google.gson; + requires com.google.common; + requires org.slf4j; + + exports org.cryptomator.cryptolib; + exports org.cryptomator.cryptolib.api; + exports org.cryptomator.cryptolib.common; + + opens org.cryptomator.cryptolib.common to com.google.gson; +} \ No newline at end of file From cb41804f2bc4582fbef44a934f2263437b14ea5b Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 31 May 2021 13:34:51 +0200 Subject: [PATCH 47/59] Split masterkey-related methods into parsing and crypto --- .../cryptolib/common/MasterkeyFile.java | 91 +++++++++++++++++++ .../cryptolib/common/MasterkeyFileAccess.java | 83 +---------------- .../common/MasterkeyFileAccessTest.java | 17 ++-- .../cryptolib/common/MasterkeyFileTest.java | 32 +++++++ 4 files changed, 136 insertions(+), 87 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java create mode 100644 src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java new file mode 100644 index 0000000..5df021a --- /dev/null +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java @@ -0,0 +1,91 @@ +package org.cryptomator.cryptolib.common; + +import com.google.common.io.BaseEncoding; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; + +/** + * Representation of encrypted masterkey json file. Used by {@link MasterkeyFileAccess} to load and persist keys. + */ +public class MasterkeyFile { + + private static final Gson GSON = new GsonBuilder() // + .setPrettyPrinting() // + .disableHtmlEscaping() // + .registerTypeHierarchyAdapter(byte[].class, new MasterkeyFile.ByteArrayJsonAdapter()) // + .create(); + + @SerializedName("version") + public int version; + + @SerializedName("scryptSalt") + public byte[] scryptSalt; + + @SerializedName("scryptCostParam") + public int scryptCostParam; + + @SerializedName("scryptBlockSize") + public int scryptBlockSize; + + @SerializedName("primaryMasterKey") + public byte[] encMasterKey; + + @SerializedName("hmacMasterKey") + public byte[] macMasterKey; + + @SerializedName("versionMac") + public byte[] versionMac; + + public static MasterkeyFile read(Reader reader) throws JsonParseException { + return GSON.fromJson(reader, MasterkeyFile.class); + } + + public void write(Writer writer) throws JsonParseException { + GSON.toJson(this, writer); + } + + boolean isValid() { + return version != 0 + && scryptSalt != null + && scryptCostParam > 1 + && scryptBlockSize > 0 + && encMasterKey != null + && macMasterKey != null + && versionMac != null; + } + + private static class ByteArrayJsonAdapter extends TypeAdapter { + + private static final BaseEncoding BASE64 = BaseEncoding.base64(); + + @Override + public void write(JsonWriter writer, byte[] value) throws IOException { + if (value == null) { + writer.nullValue(); + } else { + writer.value(BASE64.encode(value)); + } + } + + @Override + public byte[] read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } else { + return BASE64.decode(reader.nextString()); + } + } + } + +} diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index c83314c..e171716 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -1,23 +1,13 @@ package org.cryptomator.cryptolib.common; import com.google.common.base.Preconditions; -import com.google.common.io.BaseEncoding; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.google.gson.JsonIOException; import com.google.gson.JsonParseException; -import com.google.gson.TypeAdapter; -import com.google.gson.annotations.SerializedName; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.Masterkey; -import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import javax.crypto.Mac; -import javax.crypto.SecretKey; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -55,11 +45,6 @@ public class MasterkeyFileAccess { private static final int DEFAULT_SCRYPT_SALT_LENGTH = 8; private static final int DEFAULT_SCRYPT_COST_PARAM = 1 << 15; // 2^15 private static final int DEFAULT_SCRYPT_BLOCK_SIZE = 8; - private static final Gson GSON = new GsonBuilder() // - .setPrettyPrinting() // - .disableHtmlEscaping() // - .registerTypeHierarchyAdapter(byte[].class, new MasterkeyFileAccess.ByteArrayJsonAdapter()) // - .create(); private final byte[] pepper; private final SecureRandom csprng; @@ -81,7 +66,7 @@ public MasterkeyFileAccess(byte[] pepper, SecureRandom csprng) { public static int readAllegedVaultVersion(byte[] masterkey) throws IOException { try (ByteArrayInputStream in = new ByteArrayInputStream(masterkey); Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { - MasterkeyFile parsedFile = GSON.fromJson(reader, MasterkeyFile.class); + MasterkeyFile parsedFile = MasterkeyFile.read(reader); return parsedFile.version; } catch (JsonParseException e) { throw new IOException("Unreadable JSON", e); @@ -111,9 +96,9 @@ public byte[] changePassphrase(byte[] masterkey, CharSequence oldPassphrase, Cha public void changePassphrase(InputStream oldIn, OutputStream newOut, CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException { try (Reader reader = new InputStreamReader(oldIn, StandardCharsets.UTF_8); Writer writer = new OutputStreamWriter(newOut, StandardCharsets.UTF_8)) { - MasterkeyFile original = GSON.fromJson(reader, MasterkeyFile.class); + MasterkeyFile original = MasterkeyFile.read(reader); MasterkeyFile updated = changePassphrase(original, oldPassphrase, newPassphrase); - GSON.toJson(updated, writer); + updated.write(writer); } catch (JsonParseException e) { throw new IOException("Unreadable JSON", e); } catch (IllegalArgumentException e) { @@ -148,7 +133,7 @@ public Masterkey load(Path filePath, CharSequence passphrase) throws MasterkeyLo public Masterkey load(InputStream in, CharSequence passphrase) throws MasterkeyLoadingFailedException, IOException { try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { - MasterkeyFile parsedFile = GSON.fromJson(reader, MasterkeyFile.class); + MasterkeyFile parsedFile = MasterkeyFile.read(reader); if (parsedFile == null || !parsedFile.isValid()) { throw new JsonParseException("Invalid key file"); } else { @@ -216,7 +201,7 @@ void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @De MasterkeyFile fileContent = lock(masterkey, passphrase, vaultVersion, scryptCostParam); try (Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { - GSON.toJson(fileContent, writer); + fileContent.write(writer); } catch (JsonIOException e) { throw new IOException(e); } @@ -257,64 +242,6 @@ private static DestroyableSecretKey scrypt(CharSequence passphrase, byte[] salt, } } - // visible for testing - static class MasterkeyFile { - - @SerializedName("version") - int version; - - @SerializedName("scryptSalt") - byte[] scryptSalt; - - @SerializedName("scryptCostParam") - int scryptCostParam; - - @SerializedName("scryptBlockSize") - int scryptBlockSize; - - @SerializedName("primaryMasterKey") - byte[] encMasterKey; - - @SerializedName("hmacMasterKey") - byte[] macMasterKey; - - @SerializedName("versionMac") - byte[] versionMac; - - private boolean isValid() { - return version != 0 - && scryptSalt != null - && scryptCostParam > 1 - && scryptBlockSize > 0 - && encMasterKey != null - && macMasterKey != null - && versionMac != null; - } - - } - - private static class ByteArrayJsonAdapter extends TypeAdapter { - - private static final BaseEncoding BASE64 = BaseEncoding.base64(); - @Override - public void write(JsonWriter writer, byte[] value) throws IOException { - if (value == null) { - writer.nullValue(); - } else { - writer.value(BASE64.encode(value)); - } - } - - @Override - public byte[] read(JsonReader reader) throws IOException { - if (reader.peek() == JsonToken.NULL) { - reader.nextNull(); - return null; - } else { - return BASE64.decode(reader.nextString()); - } - } - } } diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java index 569502e..0ff637e 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java @@ -4,7 +4,6 @@ import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.Masterkey; -import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; @@ -32,7 +31,7 @@ public class MasterkeyFileAccessTest { private static final byte[] DEFAULT_PEPPER = new byte[0]; private Masterkey key = new Masterkey(new byte[64]); - private MasterkeyFileAccess.MasterkeyFile keyFile = new MasterkeyFileAccess.MasterkeyFile(); + private MasterkeyFile keyFile = new MasterkeyFile(); private MasterkeyFileAccess masterkeyFileAccess = Mockito.spy(new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK)); @BeforeEach @@ -49,8 +48,8 @@ public void setup() { @Test @DisplayName("changePassphrase(MasterkeyFile, ...)") public void testChangePassphraseWithMasterkeyFile() throws CryptoException { - MasterkeyFileAccess.MasterkeyFile changed1 = masterkeyFileAccess.changePassphrase(keyFile, "asd", "qwe"); - MasterkeyFileAccess.MasterkeyFile changed2 = masterkeyFileAccess.changePassphrase(changed1, "qwe", "asd"); + MasterkeyFile changed1 = masterkeyFileAccess.changePassphrase(keyFile, "asd", "qwe"); + MasterkeyFile changed2 = masterkeyFileAccess.changePassphrase(changed1, "qwe", "asd"); MatcherAssert.assertThat(keyFile.encMasterKey, not(equalTo(changed1.encMasterKey))); Assertions.assertArrayEquals(keyFile.encMasterKey, changed2.encMasterKey); @@ -162,7 +161,7 @@ class Lock { @Test @DisplayName("creates expected values") public void testLock() { - MasterkeyFileAccess.MasterkeyFile keyFile = masterkeyFileAccess.lock(key, "asd", 3, 2); + MasterkeyFile keyFile = masterkeyFileAccess.lock(key, "asd", 3, 2); Assertions.assertEquals(3, keyFile.version); Assertions.assertArrayEquals(new byte[8], keyFile.scryptSalt); @@ -176,8 +175,8 @@ public void testLock() { @Test @DisplayName("different passwords -> different wrapped keys") public void testLockWithDifferentPasswords() { - MasterkeyFileAccess.MasterkeyFile keyFile1 = masterkeyFileAccess.lock(key, "asd", 8, 2); - MasterkeyFileAccess.MasterkeyFile keyFile2 = masterkeyFileAccess.lock(key, "qwe", 8, 2); + MasterkeyFile keyFile1 = masterkeyFileAccess.lock(key, "asd", 8, 2); + MasterkeyFile keyFile2 = masterkeyFileAccess.lock(key, "qwe", 8, 2); MatcherAssert.assertThat(keyFile1.encMasterKey, not(equalTo(keyFile2.encMasterKey))); } @@ -190,8 +189,8 @@ public void testLockWithDifferentPeppers() { MasterkeyFileAccess masterkeyFileAccess1 = new MasterkeyFileAccess(pepper1, RANDOM_MOCK); MasterkeyFileAccess masterkeyFileAccess2 = new MasterkeyFileAccess(pepper2, RANDOM_MOCK); - MasterkeyFileAccess.MasterkeyFile keyFile1 = masterkeyFileAccess1.lock(key, "asd", 8, 2); - MasterkeyFileAccess.MasterkeyFile keyFile2 = masterkeyFileAccess2.lock(key, "asd", 8, 2); + MasterkeyFile keyFile1 = masterkeyFileAccess1.lock(key, "asd", 8, 2); + MasterkeyFile keyFile2 = masterkeyFileAccess2.lock(key, "asd", 8, 2); MatcherAssert.assertThat(keyFile1.encMasterKey, not(equalTo(keyFile2.encMasterKey))); } diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java new file mode 100644 index 0000000..e1b9950 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java @@ -0,0 +1,32 @@ +package org.cryptomator.cryptolib.common; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.StringReader; +import java.io.StringWriter; + +public class MasterkeyFileTest { + + @Test + public void testRead() { + MasterkeyFile masterkeyFile = MasterkeyFile.read(new StringReader("{\"scryptSalt\": \"Zm9v\"}")); + + Assertions.assertArrayEquals("foo".getBytes(), masterkeyFile.scryptSalt); + } + + @Test + public void testWrite() { + MasterkeyFile masterkeyFile = new MasterkeyFile(); + masterkeyFile.scryptSalt = "foo".getBytes(); + + StringWriter jsonWriter = new StringWriter(); + masterkeyFile.write(jsonWriter); + String json = jsonWriter.toString(); + + MatcherAssert.assertThat(json, CoreMatchers.containsString("\"scryptSalt\": \"Zm9v\"")); + } + +} \ No newline at end of file From cebeb6cde4f93042fe90efd2fcb37d93b8f4cdf7 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 31 May 2021 22:40:20 +0200 Subject: [PATCH 48/59] don't expose GSON-API --- .../cryptolib/common/MasterkeyFile.java | 24 +++++++++++++++--- .../cryptolib/common/MasterkeyFileAccess.java | 25 +++---------------- .../common/MasterkeyFileAccessTest.java | 4 +-- .../cryptolib/common/MasterkeyFileTest.java | 5 ++-- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java index 5df021a..58f31e5 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java @@ -3,6 +3,7 @@ import com.google.common.io.BaseEncoding; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; import com.google.gson.JsonParseException; import com.google.gson.TypeAdapter; import com.google.gson.annotations.SerializedName; @@ -46,12 +47,27 @@ public class MasterkeyFile { @SerializedName("versionMac") public byte[] versionMac; - public static MasterkeyFile read(Reader reader) throws JsonParseException { - return GSON.fromJson(reader, MasterkeyFile.class); + public static MasterkeyFile read(Reader reader) throws IOException { + try { + MasterkeyFile result = GSON.fromJson(reader, MasterkeyFile.class); + if (result == null) { + throw new IOException("JSON EOF"); + } else { + return result; + } + } catch (JsonParseException e) { + throw new IOException("Unreadable JSON", e); + } catch (IllegalArgumentException e) { + throw new IOException("Invalid JSON content", e); + } } - public void write(Writer writer) throws JsonParseException { - GSON.toJson(this, writer); + public void write(Writer writer) throws IOException { + try { + GSON.toJson(this, writer); + } catch (JsonIOException e) { + throw new IOException(e); + } } boolean isValid() { diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index e171716..b2607d9 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -1,8 +1,6 @@ package org.cryptomator.cryptolib.common; import com.google.common.base.Preconditions; -import com.google.gson.JsonIOException; -import com.google.gson.JsonParseException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; @@ -68,10 +66,6 @@ public static int readAllegedVaultVersion(byte[] masterkey) throws IOException { Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { MasterkeyFile parsedFile = MasterkeyFile.read(reader); return parsedFile.version; - } catch (JsonParseException e) { - throw new IOException("Unreadable JSON", e); - } catch (IllegalArgumentException e) { - throw new IOException("Invalid JSON content", e); } } @@ -87,7 +81,7 @@ public static int readAllegedVaultVersion(byte[] masterkey) throws IOException { */ public byte[] changePassphrase(byte[] masterkey, CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException { try (ByteArrayInputStream in = new ByteArrayInputStream(masterkey); - ByteArrayOutputStream out = new ByteArrayOutputStream()) { + ByteArrayOutputStream out = new ByteArrayOutputStream()) { changePassphrase(in, out, oldPassphrase, newPassphrase); return out.toByteArray(); } @@ -99,10 +93,6 @@ public void changePassphrase(InputStream oldIn, OutputStream newOut, CharSequenc MasterkeyFile original = MasterkeyFile.read(reader); MasterkeyFile updated = changePassphrase(original, oldPassphrase, newPassphrase); updated.write(writer); - } catch (JsonParseException e) { - throw new IOException("Unreadable JSON", e); - } catch (IllegalArgumentException e) { - throw new IOException("Invalid JSON content", e); } } @@ -131,18 +121,14 @@ public Masterkey load(Path filePath, CharSequence passphrase) throws MasterkeyLo } } - public Masterkey load(InputStream in, CharSequence passphrase) throws MasterkeyLoadingFailedException, IOException { + public Masterkey load(InputStream in, CharSequence passphrase) throws IOException { try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { MasterkeyFile parsedFile = MasterkeyFile.read(reader); - if (parsedFile == null || !parsedFile.isValid()) { - throw new JsonParseException("Invalid key file"); + if (!parsedFile.isValid()) { + throw new IOException("Invalid key file"); } else { return unlock(parsedFile, passphrase); } - } catch (JsonParseException e) { - throw new MasterkeyLoadingFailedException("Unreadable JSON", e); - } catch (IllegalArgumentException e) { - throw new MasterkeyLoadingFailedException("Invalid JSON content", e); } } @@ -202,8 +188,6 @@ void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @De MasterkeyFile fileContent = lock(masterkey, passphrase, vaultVersion, scryptCostParam); try (Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { fileContent.write(writer); - } catch (JsonIOException e) { - throw new IOException(e); } } @@ -243,5 +227,4 @@ private static DestroyableSecretKey scrypt(CharSequence passphrase, byte[] salt, } - } diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java index 0ff637e..b3cc841 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java @@ -104,7 +104,7 @@ public void testLoadInvalid() { String content = "{\"foo\": 42}"; InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> { + Assertions.assertThrows(IOException.class, () -> { masterkeyFileAccess.load(in, "asd"); }); } @@ -115,7 +115,7 @@ public void testLoadMalformed() { final String content = "not even json"; InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); - Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> { + Assertions.assertThrows(IOException.class, () -> { masterkeyFileAccess.load(in, "asd"); }); } diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java index e1b9950..79ba682 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java @@ -5,20 +5,21 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; public class MasterkeyFileTest { @Test - public void testRead() { + public void testRead() throws IOException { MasterkeyFile masterkeyFile = MasterkeyFile.read(new StringReader("{\"scryptSalt\": \"Zm9v\"}")); Assertions.assertArrayEquals("foo".getBytes(), masterkeyFile.scryptSalt); } @Test - public void testWrite() { + public void testWrite() throws IOException { MasterkeyFile masterkeyFile = new MasterkeyFile(); masterkeyFile.scryptSalt = "foo".getBytes(); From d3b0423994d05a782f18dd8d7d4cfc7101ff1073 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 31 May 2021 22:40:54 +0200 Subject: [PATCH 49/59] add reflect-config for GraalVM --- .../cryptolib/reflect-config.json | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/main/resources/META-INF/native-image/org.cryptomator/cryptolib/reflect-config.json diff --git a/src/main/resources/META-INF/native-image/org.cryptomator/cryptolib/reflect-config.json b/src/main/resources/META-INF/native-image/org.cryptomator/cryptolib/reflect-config.json new file mode 100644 index 0000000..11bf234 --- /dev/null +++ b/src/main/resources/META-INF/native-image/org.cryptomator/cryptolib/reflect-config.json @@ -0,0 +1,46 @@ +[ + { + "name":"byte[]" + }, + { + "name":"com.sun.crypto.provider.AESWrapCipher$General", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"com.sun.crypto.provider.HmacCore$HmacSHA256", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"java.lang.reflect.AccessibleObject", + "fields":[{"name":"override"}] + }, + { + "name":"java.security.MessageDigestSpi" + }, + { + "name":"java.security.SecureRandomParameters" + }, + { + "name":"org.cryptomator.cryptolib.common.MasterkeyFile", + "allDeclaredFields":true, + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"sun.misc.Unsafe", + "fields":[{"name":"theUnsafe"}], + "methods":[ + {"name":"objectFieldOffset","parameterTypes":["java.lang.reflect.Field"] }, + {"name":"putBoolean","parameterTypes":["java.lang.Object","long","boolean"] } + ] + }, + { + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"sun.security.provider.SHA2$SHA256", + "methods":[ + {"name":"","parameterTypes":[] } + ] + } +] \ No newline at end of file From 83eaeb1bbbaa43ae83b5e8e5f72671dbdf65e485 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 14 Jun 2021 12:04:26 +0200 Subject: [PATCH 50/59] Replaced `Charset.forName("UTF-8")` with `StandardCharsets.UTF_8` Requires Android API Level 19+ --- .../cryptolib/DecryptingReadableByteChannel.java | 10 +++++----- .../cryptolib/common/MasterkeyFileAccess.java | 13 +++++++------ .../org/cryptomator/cryptolib/common/Scrypt.java | 4 ++-- .../cryptolib/v1/FileNameCryptorImpl.java | 4 ++-- .../cryptolib/v2/FileNameCryptorImpl.java | 4 ++-- .../DecryptingReadableByteChannelTest.java | 6 ++---- .../EncryptingReadableByteChannelTest.java | 5 ++--- .../EncryptingWritableByteChannelTest.java | 5 +++-- .../cryptolib/common/MasterkeyFileAccessTest.java | 8 ++++---- .../cryptomator/cryptolib/common/ScryptTest.java | 8 +++----- .../cryptolib/v1/FileContentCryptorImplTest.java | 12 ++++++------ .../cryptolib/v1/FileNameCryptorImplTest.java | 6 ++---- .../cryptolib/v2/FileContentCryptorImplTest.java | 5 +++-- .../cryptolib/v2/FileNameCryptorImplTest.java | 6 ++---- 14 files changed, 45 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/DecryptingReadableByteChannel.java b/src/main/java/org/cryptomator/cryptolib/DecryptingReadableByteChannel.java index 8087e52..f87f27e 100644 --- a/src/main/java/org/cryptomator/cryptolib/DecryptingReadableByteChannel.java +++ b/src/main/java/org/cryptomator/cryptolib/DecryptingReadableByteChannel.java @@ -8,16 +8,16 @@ *******************************************************************************/ package org.cryptomator.cryptolib; -import java.io.EOFException; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ReadableByteChannel; - import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.ByteBuffers; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; + public class DecryptingReadableByteChannel implements ReadableByteChannel { private final ReadableByteChannel delegate; diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index b2607d9..d8a3939 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -16,7 +16,6 @@ import java.io.Reader; import java.io.Writer; import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -25,6 +24,8 @@ import java.security.SecureRandom; import java.util.Arrays; +import static java.nio.charset.StandardCharsets.UTF_8; + /** * Allow loading and persisting of {@link Masterkey masterkeys} from and to encrypted json files. *

@@ -63,7 +64,7 @@ public MasterkeyFileAccess(byte[] pepper, SecureRandom csprng) { @Deprecated public static int readAllegedVaultVersion(byte[] masterkey) throws IOException { try (ByteArrayInputStream in = new ByteArrayInputStream(masterkey); - Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + Reader reader = new InputStreamReader(in, UTF_8)) { MasterkeyFile parsedFile = MasterkeyFile.read(reader); return parsedFile.version; } @@ -88,8 +89,8 @@ public byte[] changePassphrase(byte[] masterkey, CharSequence oldPassphrase, Cha } public void changePassphrase(InputStream oldIn, OutputStream newOut, CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException { - try (Reader reader = new InputStreamReader(oldIn, StandardCharsets.UTF_8); - Writer writer = new OutputStreamWriter(newOut, StandardCharsets.UTF_8)) { + try (Reader reader = new InputStreamReader(oldIn, UTF_8); + Writer writer = new OutputStreamWriter(newOut, UTF_8)) { MasterkeyFile original = MasterkeyFile.read(reader); MasterkeyFile updated = changePassphrase(original, oldPassphrase, newPassphrase); updated.write(writer); @@ -122,7 +123,7 @@ public Masterkey load(Path filePath, CharSequence passphrase) throws MasterkeyLo } public Masterkey load(InputStream in, CharSequence passphrase) throws IOException { - try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + try (Reader reader = new InputStreamReader(in, UTF_8)) { MasterkeyFile parsedFile = MasterkeyFile.read(reader); if (!parsedFile.isValid()) { throw new IOException("Invalid key file"); @@ -186,7 +187,7 @@ void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @De Preconditions.checkArgument(!masterkey.isDestroyed(), "masterkey has been destroyed"); MasterkeyFile fileContent = lock(masterkey, passphrase, vaultVersion, scryptCostParam); - try (Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { + try (Writer writer = new OutputStreamWriter(out, UTF_8)) { fileContent.write(writer); } } diff --git a/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java b/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java index 17364cc..3b30a76 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java +++ b/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java @@ -11,12 +11,12 @@ import javax.crypto.Mac; import java.nio.ByteBuffer; import java.nio.CharBuffer; -import java.nio.charset.Charset; import java.util.Arrays; +import static java.nio.charset.StandardCharsets.UTF_8; + public class Scrypt { - private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final int P = 1; // scrypt parallelization parameter /** diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java index b9274a5..089ec6e 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java @@ -18,11 +18,11 @@ import org.cryptomator.siv.UnauthenticCiphertextException; import javax.crypto.IllegalBlockSizeException; -import java.nio.charset.Charset; + +import static java.nio.charset.StandardCharsets.UTF_8; class FileNameCryptorImpl implements FileNameCryptor { - private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final BaseEncoding BASE32 = BaseEncoding.base32(); private static final ThreadLocal AES_SIV = new ThreadLocal() { @Override diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java index 27756f6..8c326e3 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java @@ -18,11 +18,11 @@ import org.cryptomator.siv.UnauthenticCiphertextException; import javax.crypto.IllegalBlockSizeException; -import java.nio.charset.Charset; + +import static java.nio.charset.StandardCharsets.UTF_8; class FileNameCryptorImpl implements FileNameCryptor { - private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final BaseEncoding BASE32 = BaseEncoding.base32(); private static final ThreadLocal AES_SIV = new ThreadLocal() { @Override diff --git a/src/test/java/org/cryptomator/cryptolib/DecryptingReadableByteChannelTest.java b/src/test/java/org/cryptomator/cryptolib/DecryptingReadableByteChannelTest.java index f201762..ae9a88d 100644 --- a/src/test/java/org/cryptomator/cryptolib/DecryptingReadableByteChannelTest.java +++ b/src/test/java/org/cryptomator/cryptolib/DecryptingReadableByteChannelTest.java @@ -23,13 +23,11 @@ import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Arrays; -public class DecryptingReadableByteChannelTest { +import static java.nio.charset.StandardCharsets.UTF_8; - private static final Charset UTF_8 = StandardCharsets.UTF_8; +public class DecryptingReadableByteChannelTest { private Cryptor cryptor; private FileContentCryptor contentCryptor; diff --git a/src/test/java/org/cryptomator/cryptolib/EncryptingReadableByteChannelTest.java b/src/test/java/org/cryptomator/cryptolib/EncryptingReadableByteChannelTest.java index 2c97f5a..0f1a1c1 100644 --- a/src/test/java/org/cryptomator/cryptolib/EncryptingReadableByteChannelTest.java +++ b/src/test/java/org/cryptomator/cryptolib/EncryptingReadableByteChannelTest.java @@ -15,12 +15,11 @@ import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; -import java.nio.charset.Charset; import java.util.Arrays; -class EncryptingReadableByteChannelTest { +import static java.nio.charset.StandardCharsets.UTF_8; - private static final Charset UTF_8 = Charset.forName("UTF-8"); +class EncryptingReadableByteChannelTest { private ByteBuffer dstFile; private ReadableByteChannel srcFileChannel; diff --git a/src/test/java/org/cryptomator/cryptolib/EncryptingWritableByteChannelTest.java b/src/test/java/org/cryptomator/cryptolib/EncryptingWritableByteChannelTest.java index bc5e0a2..f959ae4 100644 --- a/src/test/java/org/cryptomator/cryptolib/EncryptingWritableByteChannelTest.java +++ b/src/test/java/org/cryptomator/cryptolib/EncryptingWritableByteChannelTest.java @@ -22,11 +22,12 @@ import java.nio.ByteBuffer; import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Arrays; -public class EncryptingWritableByteChannelTest { +import static java.nio.charset.StandardCharsets.UTF_8; - private static final Charset UTF_8 = Charset.forName("UTF-8"); +public class EncryptingWritableByteChannelTest { private ByteBuffer dstFile; private WritableByteChannel dstFileChannel; diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java index b3cc841..e35d510 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java @@ -18,10 +18,10 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.security.SecureRandom; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsNot.not; @@ -58,7 +58,7 @@ public void testChangePassphraseWithMasterkeyFile() throws CryptoException { @Test @DisplayName("readAllegedVaultVersion()") public void testReadAllegedVaultVersion() throws IOException { - byte[] content = "{\"version\": 1337}".getBytes(StandardCharsets.UTF_8); + byte[] content = "{\"version\": 1337}".getBytes(UTF_8); int version = MasterkeyFileAccess.readAllegedVaultVersion(content); @@ -102,7 +102,7 @@ public void testLoad() throws IOException { @DisplayName("load() unrelated json file") public void testLoadInvalid() { String content = "{\"foo\": 42}"; - InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + InputStream in = new ByteArrayInputStream(content.getBytes(UTF_8)); Assertions.assertThrows(IOException.class, () -> { masterkeyFileAccess.load(in, "asd"); @@ -113,7 +113,7 @@ public void testLoadInvalid() { @DisplayName("load() non-json file") public void testLoadMalformed() { final String content = "not even json"; - InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + InputStream in = new ByteArrayInputStream(content.getBytes(UTF_8)); Assertions.assertThrows(IOException.class, () -> { masterkeyFileAccess.load(in, "asd"); diff --git a/src/test/java/org/cryptomator/cryptolib/common/ScryptTest.java b/src/test/java/org/cryptomator/cryptolib/common/ScryptTest.java index 3314457..45e695a 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/ScryptTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/ScryptTest.java @@ -3,18 +3,16 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import java.nio.charset.Charset; +import static java.nio.charset.StandardCharsets.US_ASCII; /** * Tests from https://tools.ietf.org/html/rfc7914#section-12 */ public class ScryptTest { - private static final Charset ASCII = Charset.forName("ASCII"); - @Test public void testEmptyString() { - byte[] key = Scrypt.scrypt("", "".getBytes(ASCII), 16, 1, 64); + byte[] key = Scrypt.scrypt("", "".getBytes(US_ASCII), 16, 1, 64); byte[] expected = new byte[] { // (byte) 0x77, (byte) 0xd6, (byte) 0x57, (byte) 0x62, (byte) 0x38, (byte) 0x65, (byte) 0x7b, (byte) 0x20, // (byte) 0x3b, (byte) 0x19, (byte) 0xca, (byte) 0x42, (byte) 0xc1, (byte) 0x8a, (byte) 0x04, (byte) 0x97, // @@ -30,7 +28,7 @@ public void testEmptyString() { @Test public void testPleaseLetMeInString() { - byte[] key = Scrypt.scrypt("pleaseletmein", "SodiumChloride".getBytes(ASCII), 16384, 8, 64); + byte[] key = Scrypt.scrypt("pleaseletmein", "SodiumChloride".getBytes(US_ASCII), 16384, 8, 64); byte[] expected = new byte[] { // (byte) 0x70, (byte) 0x23, (byte) 0xbd, (byte) 0xcb, (byte) 0x3a, (byte) 0xfd, (byte) 0x73, (byte) 0x48, // (byte) 0x46, (byte) 0x1c, (byte) 0x06, (byte) 0xcd, (byte) 0x81, (byte) 0xfd, (byte) 0x38, (byte) 0xeb, // diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java index b5eab53..48084eb 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java @@ -37,15 +37,15 @@ import java.nio.channels.ReadableByteChannel; import java.nio.channels.SeekableByteChannel; import java.nio.channels.WritableByteChannel; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_8; + public class FileContentCryptorImplTest { - private static final Charset US_ASCII = Charset.forName("US-ASCII"); private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; private FileHeaderCryptorImpl headerCryptor; private FileContentCryptorImpl fileContentCryptor; @@ -65,7 +65,7 @@ public void setup() { public void testMacIsValidAfterEncryption() throws NoSuchAlgorithmException { DestroyableSecretKey fileKey = new DestroyableSecretKey(new byte[16], "AES"); ByteBuffer ciphertext = ByteBuffer.allocate(fileContentCryptor.ciphertextChunkSize()); - fileContentCryptor.encryptChunk(StandardCharsets.UTF_8.encode("asd"), ciphertext, 42l, new byte[16], fileKey); + fileContentCryptor.encryptChunk(UTF_8.encode("asd"), ciphertext, 42l, new byte[16], fileKey); ciphertext.flip(); Assertions.assertTrue(fileContentCryptor.checkChunkMac(new byte[16], 42l, ciphertext)); Assertions.assertFalse(fileContentCryptor.checkChunkMac(new byte[16], 43l, ciphertext)); @@ -76,11 +76,11 @@ public void testDecryptedEncryptedEqualsPlaintext() throws NoSuchAlgorithmExcept DestroyableSecretKey fileKey = new DestroyableSecretKey(new byte[16], "AES"); ByteBuffer ciphertext = ByteBuffer.allocate(fileContentCryptor.ciphertextChunkSize()); ByteBuffer cleartext = ByteBuffer.allocate(fileContentCryptor.cleartextChunkSize()); - fileContentCryptor.encryptChunk(StandardCharsets.UTF_8.encode("asd"), ciphertext, 42l, new byte[12], fileKey); + fileContentCryptor.encryptChunk(UTF_8.encode("asd"), ciphertext, 42l, new byte[12], fileKey); ciphertext.flip(); fileContentCryptor.decryptChunk(ciphertext, cleartext, fileKey); cleartext.flip(); - Assertions.assertEquals(StandardCharsets.UTF_8.encode("asd"), cleartext); + Assertions.assertEquals(UTF_8.encode("asd"), cleartext); } @Nested diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java index b1551e2..20fb932 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java @@ -21,14 +21,12 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.UUID; import java.util.stream.Stream; -public class FileNameCryptorImplTest { +import static java.nio.charset.StandardCharsets.UTF_8; - private static final Charset UTF_8 = StandardCharsets.UTF_8; +public class FileNameCryptorImplTest { private final Masterkey masterkey = new Masterkey(new byte[64]); private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(masterkey); diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java index 566375e..160fd91 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java @@ -42,6 +42,7 @@ import java.security.SecureRandom; import java.util.Arrays; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.cryptomator.cryptolib.v2.Constants.GCM_NONCE_SIZE; import static org.cryptomator.cryptolib.v2.Constants.GCM_TAG_SIZE; @@ -71,11 +72,11 @@ public void testDecryptedEncryptedEqualsPlaintext() throws AuthenticationFailedE DestroyableSecretKey fileKey = new DestroyableSecretKey(new byte[16], "AES"); ByteBuffer ciphertext = ByteBuffer.allocate(fileContentCryptor.ciphertextChunkSize()); ByteBuffer cleartext = ByteBuffer.allocate(fileContentCryptor.cleartextChunkSize()); - fileContentCryptor.encryptChunk(StandardCharsets.UTF_8.encode("asd"), ciphertext, 42l, new byte[12], fileKey); + fileContentCryptor.encryptChunk(UTF_8.encode("asd"), ciphertext, 42l, new byte[12], fileKey); ciphertext.flip(); fileContentCryptor.decryptChunk(ciphertext, cleartext, 42l, new byte[12], fileKey); cleartext.flip(); - Assertions.assertEquals(StandardCharsets.UTF_8.encode("asd"), cleartext); + Assertions.assertEquals(UTF_8.encode("asd"), cleartext); } @Nested diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java index b9a3a34..ebfa3d2 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java @@ -20,15 +20,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.UUID; import java.util.stream.Stream; +import static java.nio.charset.StandardCharsets.UTF_8; -public class FileNameCryptorImplTest { - private static final Charset UTF_8 = StandardCharsets.UTF_8; +public class FileNameCryptorImplTest { private final Masterkey masterkey = new Masterkey(new byte[64]); private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(masterkey); From d32856f2b21f7ea88aed62d8245093fd18294f37 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 14 Jun 2021 12:22:23 +0200 Subject: [PATCH 51/59] remove legacy FileNameCryptor.encrypt/decryptFilename methods without explicit encoding --- .../cryptolib/api/FileNameCryptor.java | 15 --------- .../cryptomator/cryptolib/package-info.java | 4 +-- .../cryptolib/v1/FileNameCryptorImpl.java | 10 ------ .../cryptolib/v2/FileNameCryptorImpl.java | 10 ------ .../cryptolib/v1/FileNameCryptorImplTest.java | 33 ++++++++++--------- .../cryptolib/v2/FileNameCryptorImplTest.java | 32 +++++++++--------- 6 files changed, 36 insertions(+), 68 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/api/FileNameCryptor.java b/src/main/java/org/cryptomator/cryptolib/api/FileNameCryptor.java index 50b6181..e20cd87 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/FileNameCryptor.java +++ b/src/main/java/org/cryptomator/cryptolib/api/FileNameCryptor.java @@ -24,13 +24,6 @@ public interface FileNameCryptor { */ String hashDirectoryId(String cleartextDirectoryId); - /** - * @param cleartextName original filename including cleartext file extension - * @param associatedData optional associated data, that will not get encrypted but needs to be provided during decryption - * @return encrypted filename without any file extension, encoded in BASE32 - */ - String encryptFilename(String cleartextName, byte[]... associatedData); - /** * @param encoding Encoding to use to encode the returned ciphertext * @param cleartextName original filename including cleartext file extension @@ -39,14 +32,6 @@ public interface FileNameCryptor { */ String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData); - /** - * @param ciphertextName Ciphertext only, with any additional strings like file extensions stripped first, encoded in BASE32 - * @param associatedData the same associated data used during encryption, otherwise and {@link AuthenticationFailedException} will be thrown - * @return cleartext filename, probably including its cleartext file extension. - * @throws AuthenticationFailedException if the ciphertext is malformed - */ - String decryptFilename(String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException; - /** * @param encoding Encoding to use to decode ciphertextName * @param ciphertextName Ciphertext only, with any additional strings like file extensions stripped first. diff --git a/src/main/java/org/cryptomator/cryptolib/package-info.java b/src/main/java/org/cryptomator/cryptolib/package-info.java index 16c444b..c7b856a 100644 --- a/src/main/java/org/cryptomator/cryptolib/package-info.java +++ b/src/main/java/org/cryptomator/cryptolib/package-info.java @@ -23,8 +23,8 @@ * * // Encrypt and decrypt file name: * String cleartextFileName = "foo.txt"; - * String encryptedName = cryptor.{@link org.cryptomator.cryptolib.api.Cryptor#fileNameCryptor() fileNameCryptor()}.{@link org.cryptomator.cryptolib.api.FileNameCryptor#encryptFilename(String, byte[][]) encryptFilename(cleartextFileName, uniqueIdOfDirectory.getBytes())}; - * String decryptedName = cryptor.fileNameCryptor().{@link org.cryptomator.cryptolib.api.FileNameCryptor#decryptFilename(String, byte[][]) decryptFilename(encryptedName, uniqueIdOfDirectory.getBytes())}; + * String encryptedName = cryptor.{@link org.cryptomator.cryptolib.api.Cryptor#fileNameCryptor() fileNameCryptor()}.{@link org.cryptomator.cryptolib.api.FileNameCryptor#encryptFilename(com.google.common.io.BaseEncoding, String, byte[][]) encryptFilename(base32, cleartextFileName, uniqueIdOfDirectory.getBytes())}; + * String decryptedName = cryptor.fileNameCryptor().{@link org.cryptomator.cryptolib.api.FileNameCryptor#decryptFilename(com.google.common.io.BaseEncoding, String, byte[][]) decryptFilename(base32, encryptedName, uniqueIdOfDirectory.getBytes())}; * * // Encrypt file contents: * ByteBuffer plaintext = ...; diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java index 089ec6e..374efe6 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java @@ -47,11 +47,6 @@ public String hashDirectoryId(String cleartextDirectoryId) { } } - @Override - public String encryptFilename(String cleartextName, byte[]... associatedData) { - return encryptFilename(BASE32, cleartextName, associatedData); - } - @Override public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) { try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { @@ -61,11 +56,6 @@ public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[ } } - @Override - public String decryptFilename(String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { - return decryptFilename(BASE32, ciphertextName, associatedData); - } - @Override public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java index 8c326e3..fd7133a 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java @@ -47,11 +47,6 @@ public String hashDirectoryId(String cleartextDirectoryId) { } } - @Override - public String encryptFilename(String cleartextName, byte[]... associatedData) { - return encryptFilename(BASE32, cleartextName, associatedData); - } - @Override public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) { try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { @@ -61,11 +56,6 @@ public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[ } } - @Override - public String decryptFilename(String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { - return decryptFilename(BASE32, ciphertextName, associatedData); - } - @Override public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java index 20fb932..ec77ab0 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java @@ -11,7 +11,6 @@ import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Masterkey; -import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.siv.UnauthenticCiphertextException; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; @@ -28,6 +27,8 @@ public class FileNameCryptorImplTest { + private static final BaseEncoding BASE32 = BaseEncoding.base32(); + private final Masterkey masterkey = new Masterkey(new byte[64]); private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(masterkey); @@ -39,9 +40,9 @@ private static Stream filenameGenerator() { @ParameterizedTest(name = "decrypt(encrypt({0}))") @MethodSource("filenameGenerator") public void testDeterministicEncryptionOfFilenames(String origName) throws AuthenticationFailedException { - String encrypted1 = filenameCryptor.encryptFilename(origName); - String encrypted2 = filenameCryptor.encryptFilename(origName); - String decrypted = filenameCryptor.decryptFilename(encrypted1); + String encrypted1 = filenameCryptor.encryptFilename(BASE32, origName); + String encrypted2 = filenameCryptor.encryptFilename(BASE32, origName); + String decrypted = filenameCryptor.decryptFilename(BASE32, encrypted1); Assertions.assertEquals(encrypted1, encrypted2); Assertions.assertEquals(origName, decrypted); @@ -65,9 +66,9 @@ public void testDeterministicEncryptionOfFilenamesWithCustomEncodingAndAssociate public void testDeterministicEncryptionOf128bitFilename() throws AuthenticationFailedException { // block size length file names String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii - String encryptedPath3a = filenameCryptor.encryptFilename(originalPath3); - String encryptedPath3b = filenameCryptor.encryptFilename(originalPath3); - String decryptedPath3 = filenameCryptor.decryptFilename(encryptedPath3a); + String encryptedPath3a = filenameCryptor.encryptFilename(BASE32, originalPath3); + String encryptedPath3b = filenameCryptor.encryptFilename(BASE32, originalPath3); + String decryptedPath3 = filenameCryptor.decryptFilename(BASE32, encryptedPath3a); Assertions.assertEquals(encryptedPath3a, encryptedPath3b); Assertions.assertEquals(originalPath3, decryptedPath3); @@ -86,7 +87,7 @@ public void testDeterministicHashingOfDirectoryIds(String originalDirectoryId) { @DisplayName("decrypt non-ciphertext") public void testDecryptionOfMalformedFilename() { AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> { - filenameCryptor.decryptFilename("lol"); + filenameCryptor.decryptFilename(BASE32, "lol"); }); MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(IllegalArgumentException.class)); } @@ -94,11 +95,11 @@ public void testDecryptionOfMalformedFilename() { @Test @DisplayName("decrypt tampered ciphertext") public void testDecryptionOfManipulatedFilename() { - final byte[] encrypted = filenameCryptor.encryptFilename("test").getBytes(UTF_8); + final byte[] encrypted = filenameCryptor.encryptFilename(BASE32, "test").getBytes(UTF_8); encrypted[0] ^= (byte) 0x01; // change 1 bit in first byte AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> { - filenameCryptor.decryptFilename(new String(encrypted, UTF_8)); + filenameCryptor.decryptFilename(BASE32, new String(encrypted, UTF_8)); }); MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(UnauthenticCiphertextException.class)); } @@ -106,26 +107,26 @@ public void testDecryptionOfManipulatedFilename() { @Test @DisplayName("encrypt with different AD") public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() { - final String encrypted1 = filenameCryptor.encryptFilename("test", "ad1".getBytes(UTF_8)); - final String encrypted2 = filenameCryptor.encryptFilename("test", "ad2".getBytes(UTF_8)); + final String encrypted1 = filenameCryptor.encryptFilename(BASE32, "test", "ad1".getBytes(UTF_8)); + final String encrypted2 = filenameCryptor.encryptFilename(BASE32, "test", "ad2".getBytes(UTF_8)); Assertions.assertNotEquals(encrypted1, encrypted2); } @Test @DisplayName("decrypt ciphertext with correct AD") public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws AuthenticationFailedException { - final String encrypted = filenameCryptor.encryptFilename("test", "ad".getBytes(UTF_8)); - final String decrypted = filenameCryptor.decryptFilename(encrypted, "ad".getBytes(UTF_8)); + final String encrypted = filenameCryptor.encryptFilename(BASE32, "test", "ad".getBytes(UTF_8)); + final String decrypted = filenameCryptor.decryptFilename(BASE32, encrypted, "ad".getBytes(UTF_8)); Assertions.assertEquals("test", decrypted); } @Test @DisplayName("decrypt ciphertext with incorrect AD") public void testDeterministicEncryptionOfFilenamesWithWrongAssociatedData() { - final String encrypted = filenameCryptor.encryptFilename("test", "right".getBytes(UTF_8)); + final String encrypted = filenameCryptor.encryptFilename(BASE32, "test", "right".getBytes(UTF_8)); Assertions.assertThrows(AuthenticationFailedException.class, () -> { - filenameCryptor.decryptFilename(encrypted, "wrong".getBytes(UTF_8)); + filenameCryptor.decryptFilename(BASE32, encrypted, "wrong".getBytes(UTF_8)); }); } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java index ebfa3d2..daa5da1 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java @@ -28,6 +28,8 @@ public class FileNameCryptorImplTest { + private static final BaseEncoding BASE32 = BaseEncoding.base32(); + private final Masterkey masterkey = new Masterkey(new byte[64]); private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(masterkey); @@ -39,9 +41,9 @@ private static Stream filenameGenerator() { @ParameterizedTest(name = "decrypt(encrypt({0}))") @MethodSource("filenameGenerator") public void testDeterministicEncryptionOfFilenames(String origName) throws AuthenticationFailedException { - String encrypted1 = filenameCryptor.encryptFilename(origName); - String encrypted2 = filenameCryptor.encryptFilename(origName); - String decrypted = filenameCryptor.decryptFilename(encrypted1); + String encrypted1 = filenameCryptor.encryptFilename(BASE32, origName); + String encrypted2 = filenameCryptor.encryptFilename(BASE32, origName); + String decrypted = filenameCryptor.decryptFilename(BASE32, encrypted1); Assertions.assertEquals(encrypted1, encrypted2); Assertions.assertEquals(origName, decrypted); @@ -65,9 +67,9 @@ public void testDeterministicEncryptionOfFilenamesWithCustomEncodingAndAssociate public void testDeterministicEncryptionOf128bitFilename() throws AuthenticationFailedException { // block size length file names String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii - String encryptedPath3a = filenameCryptor.encryptFilename(originalPath3); - String encryptedPath3b = filenameCryptor.encryptFilename(originalPath3); - String decryptedPath3 = filenameCryptor.decryptFilename(encryptedPath3a); + String encryptedPath3a = filenameCryptor.encryptFilename(BASE32, originalPath3); + String encryptedPath3b = filenameCryptor.encryptFilename(BASE32, originalPath3); + String decryptedPath3 = filenameCryptor.decryptFilename(BASE32, encryptedPath3a); Assertions.assertEquals(encryptedPath3a, encryptedPath3b); Assertions.assertEquals(originalPath3, decryptedPath3); @@ -86,7 +88,7 @@ public void testDeterministicHashingOfDirectoryIds(String originalDirectoryId) { @DisplayName("decrypt non-ciphertext") public void testDecryptionOfMalformedFilename() { AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> { - filenameCryptor.decryptFilename("lol"); + filenameCryptor.decryptFilename(BASE32, "lol"); }); MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(IllegalArgumentException.class)); } @@ -94,11 +96,11 @@ public void testDecryptionOfMalformedFilename() { @Test @DisplayName("decrypt tampered ciphertext") public void testDecryptionOfManipulatedFilename() { - final byte[] encrypted = filenameCryptor.encryptFilename("test").getBytes(UTF_8); + final byte[] encrypted = filenameCryptor.encryptFilename(BASE32, "test").getBytes(UTF_8); encrypted[0] ^= (byte) 0x01; // change 1 bit in first byte AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> { - filenameCryptor.decryptFilename(new String(encrypted, UTF_8)); + filenameCryptor.decryptFilename(BASE32, new String(encrypted, UTF_8)); }); MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(UnauthenticCiphertextException.class)); } @@ -106,26 +108,26 @@ public void testDecryptionOfManipulatedFilename() { @Test @DisplayName("encrypt with different AD") public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() { - final String encrypted1 = filenameCryptor.encryptFilename("test", "ad1".getBytes(UTF_8)); - final String encrypted2 = filenameCryptor.encryptFilename("test", "ad2".getBytes(UTF_8)); + final String encrypted1 = filenameCryptor.encryptFilename(BASE32, "test", "ad1".getBytes(UTF_8)); + final String encrypted2 = filenameCryptor.encryptFilename(BASE32, "test", "ad2".getBytes(UTF_8)); Assertions.assertNotEquals(encrypted1, encrypted2); } @Test @DisplayName("decrypt ciphertext with correct AD") public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws AuthenticationFailedException { - final String encrypted = filenameCryptor.encryptFilename("test", "ad".getBytes(UTF_8)); - final String decrypted = filenameCryptor.decryptFilename(encrypted, "ad".getBytes(UTF_8)); + final String encrypted = filenameCryptor.encryptFilename(BASE32, "test", "ad".getBytes(UTF_8)); + final String decrypted = filenameCryptor.decryptFilename(BASE32, encrypted, "ad".getBytes(UTF_8)); Assertions.assertEquals("test", decrypted); } @Test @DisplayName("decrypt ciphertext with incorrect AD") public void testDeterministicEncryptionOfFilenamesWithWrongAssociatedData() { - final String encrypted = filenameCryptor.encryptFilename("test", "right".getBytes(UTF_8)); + final String encrypted = filenameCryptor.encryptFilename(BASE32, "test", "right".getBytes(UTF_8)); Assertions.assertThrows(AuthenticationFailedException.class, () -> { - filenameCryptor.decryptFilename(encrypted, "wrong".getBytes(UTF_8)); + filenameCryptor.decryptFilename(BASE32, encrypted, "wrong".getBytes(UTF_8)); }); } From 8139787c1bec7e6e348ae9600c2c41f76da46299 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 24 Jun 2021 13:58:09 +0200 Subject: [PATCH 52/59] Fixes #20 by protecting DestroyableSecretKey from unwanted mutations `CipherSupplier.forMode()` will now work on a local copy of the key --- .../cryptomator/cryptolib/api/Masterkey.java | 13 ++++++++++++ .../cryptolib/common/AesKeyWrap.java | 13 ++++++------ .../cryptolib/common/CipherSupplier.java | 20 +++++++++---------- .../common/DestroyableSecretKey.java | 3 ++- .../cryptolib/common/MasterkeyFileAccess.java | 16 ++++----------- .../cryptolib/common/AesKeyWrapTest.java | 6 +++--- .../cryptolib/common/CipherSupplierTest.java | 4 ++-- .../cryptolib/common/MasterkeyTest.java | 18 +++++++++++++++++ 8 files changed, 57 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java index 7d5ec69..4e3d640 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Masterkey.java @@ -32,6 +32,19 @@ public static Masterkey generate(SecureRandom csprng) { } } + public static Masterkey from(DestroyableSecretKey encKey, DestroyableSecretKey macKey) { + Preconditions.checkArgument(encKey.getEncoded().length == SUBKEY_LEN_BYTES, "Invalid key length of encKey"); + Preconditions.checkArgument(macKey.getEncoded().length == SUBKEY_LEN_BYTES, "Invalid key length of macKey"); + byte[] key = new byte[SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES]; + try { + System.arraycopy(encKey.getEncoded(), 0, key, 0, SUBKEY_LEN_BYTES); + System.arraycopy(macKey.getEncoded(), 0, key, SUBKEY_LEN_BYTES, SUBKEY_LEN_BYTES); + return new Masterkey(key); + } finally { + Arrays.fill(key, (byte) 0x00); + } + } + @Override public Masterkey clone() { return new Masterkey(getEncoded()); diff --git a/src/main/java/org/cryptomator/cryptolib/common/AesKeyWrap.java b/src/main/java/org/cryptomator/cryptolib/common/AesKeyWrap.java index eb574ce..48f6c3b 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/AesKeyWrap.java +++ b/src/main/java/org/cryptomator/cryptolib/common/AesKeyWrap.java @@ -8,12 +8,11 @@ *******************************************************************************/ package org.cryptomator.cryptolib.common; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.SecretKey; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; public class AesKeyWrap { @@ -22,7 +21,7 @@ public class AesKeyWrap { * @param key Key to be wrapped * @return Wrapped key */ - public static byte[] wrap(SecretKey kek, SecretKey key) { + public static byte[] wrap(DestroyableSecretKey kek, SecretKey key) { try { final Cipher cipher = CipherSupplier.RFC3394_KEYWRAP.forWrapping(kek); return cipher.wrap(key); @@ -38,15 +37,15 @@ public static byte[] wrap(SecretKey kek, SecretKey key) { * @return Unwrapped key * @throws InvalidKeyException If unwrapping failed (i.e. wrong kek) */ - public static SecretKey unwrap(SecretKey kek, byte[] wrappedKey, String wrappedKeyAlgorithm) throws InvalidKeyException { + public static DestroyableSecretKey unwrap(DestroyableSecretKey kek, byte[] wrappedKey, String wrappedKeyAlgorithm) throws InvalidKeyException { return unwrap(kek, wrappedKey, wrappedKeyAlgorithm, Cipher.SECRET_KEY); } // visible for testing - static SecretKey unwrap(SecretKey kek, byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType) throws InvalidKeyException { + static DestroyableSecretKey unwrap(DestroyableSecretKey kek, byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType) throws InvalidKeyException { final Cipher cipher = CipherSupplier.RFC3394_KEYWRAP.forUnwrapping(kek); try { - return (SecretKey) cipher.unwrap(wrappedKey, wrappedKeyAlgorithm, wrappedKeyType); + return DestroyableSecretKey.from(cipher.unwrap(wrappedKey, wrappedKeyAlgorithm, wrappedKeyType)); } catch (NoSuchAlgorithmException e) { throw new IllegalArgumentException("Invalid algorithm: " + wrappedKeyAlgorithm, e); } diff --git a/src/main/java/org/cryptomator/cryptolib/common/CipherSupplier.java b/src/main/java/org/cryptomator/cryptolib/common/CipherSupplier.java index 65d17ef..68eba32 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/CipherSupplier.java +++ b/src/main/java/org/cryptomator/cryptolib/common/CipherSupplier.java @@ -8,15 +8,13 @@ *******************************************************************************/ package org.cryptomator.cryptolib.common; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.AlgorithmParameterSpec; -import javax.crypto.Cipher; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; - public final class CipherSupplier { public static final CipherSupplier AES_CTR = new CipherSupplier("AES/CTR/NoPadding"); @@ -43,27 +41,27 @@ protected Cipher initialValue() { } } - public Cipher forEncryption(SecretKey key, AlgorithmParameterSpec params) { + public Cipher forEncryption(DestroyableSecretKey key, AlgorithmParameterSpec params) { return forMode(Cipher.ENCRYPT_MODE, key, params); } - public Cipher forDecryption(SecretKey key, AlgorithmParameterSpec params) { + public Cipher forDecryption(DestroyableSecretKey key, AlgorithmParameterSpec params) { return forMode(Cipher.DECRYPT_MODE, key, params); } - public Cipher forWrapping(SecretKey kek) { + public Cipher forWrapping(DestroyableSecretKey kek) { return forMode(Cipher.WRAP_MODE, kek, null); } - public Cipher forUnwrapping(SecretKey kek) { + public Cipher forUnwrapping(DestroyableSecretKey kek) { return forMode(Cipher.UNWRAP_MODE, kek, null); } // visible for testing - Cipher forMode(int ciphermode, SecretKey key, AlgorithmParameterSpec params) { + Cipher forMode(int ciphermode, DestroyableSecretKey key, AlgorithmParameterSpec params) { final Cipher cipher = threadLocal.get(); - try { - cipher.init(ciphermode, key, params); + try (DestroyableSecretKey clone = key.clone()) { + cipher.init(ciphermode, clone, params); // use cloned key, as this may destroy key.getEncoded() return cipher; } catch (InvalidKeyException e) { throw new IllegalArgumentException("Invalid key.", e); diff --git a/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java b/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java index 30982c9..1527c3b 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java +++ b/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java @@ -4,6 +4,7 @@ import javax.crypto.SecretKey; import javax.security.auth.Destroyable; +import java.security.Key; import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Arrays; @@ -66,7 +67,7 @@ public DestroyableSecretKey(byte[] key, int offset, int len, String algorithm) { * @param secretKey The secret key * @return Either the provided or a new key, depending on whether the provided key is already a DestroyableSecretKey */ - public static DestroyableSecretKey from(SecretKey secretKey) { + public static DestroyableSecretKey from(Key secretKey) { if (secretKey instanceof DestroyableSecretKey) { return (DestroyableSecretKey) secretKey; } else { diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index d8a3939..45e3e92 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -139,20 +139,12 @@ Masterkey unlock(MasterkeyFile parsedFile, CharSequence passphrase) throws Inval Preconditions.checkArgument(parsedFile.isValid(), "Invalid masterkey file"); Preconditions.checkNotNull(passphrase); - byte[] encKey = new byte[0], macKey = new byte[0], combined = new byte[0]; - try (DestroyableSecretKey kek = scrypt(passphrase, parsedFile.scryptSalt, pepper, parsedFile.scryptCostParam, parsedFile.scryptBlockSize)) { - encKey = AesKeyWrap.unwrap(kek, parsedFile.encMasterKey, Masterkey.ENC_ALG).getEncoded(); - macKey = AesKeyWrap.unwrap(kek, parsedFile.macMasterKey, Masterkey.MAC_ALG).getEncoded(); - combined = new byte[encKey.length + macKey.length]; - System.arraycopy(encKey, 0, combined, 0, encKey.length); - System.arraycopy(macKey, 0, combined, encKey.length, macKey.length); - return new Masterkey(combined); + try (DestroyableSecretKey kek = scrypt(passphrase, parsedFile.scryptSalt, pepper, parsedFile.scryptCostParam, parsedFile.scryptBlockSize); + DestroyableSecretKey encKey = AesKeyWrap.unwrap(kek, parsedFile.encMasterKey, Masterkey.ENC_ALG); + DestroyableSecretKey macKey = AesKeyWrap.unwrap(kek, parsedFile.macMasterKey, Masterkey.MAC_ALG)) { + return Masterkey.from(encKey, macKey); } catch (InvalidKeyException e) { throw new InvalidPassphraseException(); - } finally { - Arrays.fill(encKey, (byte) 0x00); - Arrays.fill(macKey, (byte) 0x00); - Arrays.fill(combined, (byte) 0x00); } } diff --git a/src/test/java/org/cryptomator/cryptolib/common/AesKeyWrapTest.java b/src/test/java/org/cryptomator/cryptolib/common/AesKeyWrapTest.java index 3381e12..3a09c23 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/AesKeyWrapTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/AesKeyWrapTest.java @@ -20,7 +20,7 @@ public class AesKeyWrapTest { @Test public void wrapAndUnwrap() throws InvalidKeyException { - SecretKey kek = new SecretKeySpec(new byte[32], "AES"); + DestroyableSecretKey kek = new DestroyableSecretKey(new byte[32], "AES"); SecretKey key = new SecretKeySpec(new byte[32], "AES"); byte[] wrapped = AesKeyWrap.wrap(kek, key); SecretKey unwrapped = AesKeyWrap.unwrap(kek, wrapped, "AES"); @@ -29,7 +29,7 @@ public void wrapAndUnwrap() throws InvalidKeyException { @Test public void wrapWithInvalidKey() { - SecretKey kek = new SecretKeySpec(new byte[32], "AES"); + DestroyableSecretKey kek = new DestroyableSecretKey(new byte[32], "AES"); SecretKey key = new SecretKeySpec(new byte[17], "AES"); Assertions.assertThrows(IllegalArgumentException.class, () -> { AesKeyWrap.wrap(kek, key); @@ -38,7 +38,7 @@ public void wrapWithInvalidKey() { @Test public void unwrapWithInvalidKey() { - SecretKey kek = new SecretKeySpec(new byte[32], "AES"); + DestroyableSecretKey kek = new DestroyableSecretKey(new byte[32], "AES"); SecretKey key = new SecretKeySpec(new byte[32], "AES"); byte[] wrapped = AesKeyWrap.wrap(kek, key); Assertions.assertThrows(IllegalArgumentException.class, () -> { diff --git a/src/test/java/org/cryptomator/cryptolib/common/CipherSupplierTest.java b/src/test/java/org/cryptomator/cryptolib/common/CipherSupplierTest.java index 1953891..03a97eb 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/CipherSupplierTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/CipherSupplierTest.java @@ -31,7 +31,7 @@ public void testGetUnknownCipher() { public void testGetCipherWithInvalidKey() { CipherSupplier supplier = new CipherSupplier("AES/CBC/PKCS5Padding"); IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> { - supplier.forMode(Cipher.ENCRYPT_MODE, new SecretKeySpec(new byte[13], "AES"), new IvParameterSpec(new byte[16])); + supplier.forMode(Cipher.ENCRYPT_MODE, new DestroyableSecretKey(new byte[13], "AES"), new IvParameterSpec(new byte[16])); }); MatcherAssert.assertThat(exception.getMessage(), CoreMatchers.containsString("Invalid key")); } @@ -40,7 +40,7 @@ public void testGetCipherWithInvalidKey() { public void testGetCipherWithInvalidAlgorithmParam() { CipherSupplier supplier = new CipherSupplier("AES/CBC/PKCS5Padding"); IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> { - supplier.forMode(Cipher.ENCRYPT_MODE, new SecretKeySpec(new byte[16], "AES"), new RC5ParameterSpec(1, 1, 8)); + supplier.forMode(Cipher.ENCRYPT_MODE, new DestroyableSecretKey(new byte[16], "AES"), new RC5ParameterSpec(1, 1, 8)); }); MatcherAssert.assertThat(exception.getMessage(), CoreMatchers.containsString("Algorithm parameter not appropriate for")); } diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java index 356dfab..e865fca 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java @@ -34,6 +34,24 @@ public void testGenerate() { Assertions.assertNotNull(masterkey); } + @Test + public void testFrom() { + byte[] encKeyBytes = new byte[32]; + byte[] macKeyBytes = new byte[32]; + Arrays.fill(encKeyBytes, (byte) 0x55); + Arrays.fill(macKeyBytes, (byte) 0x77); + DestroyableSecretKey encKey = Mockito.mock(DestroyableSecretKey.class); + DestroyableSecretKey macKey = Mockito.mock(DestroyableSecretKey.class); + Mockito.when(encKey.getEncoded()).thenReturn(encKeyBytes); + Mockito.when(macKey.getEncoded()).thenReturn(macKeyBytes); + + Masterkey masterkey = Masterkey.from(encKey, macKey); + + Assertions.assertNotNull(masterkey); + Assertions.assertArrayEquals(encKeyBytes, masterkey.getEncKey().getEncoded()); + Assertions.assertArrayEquals(macKeyBytes, masterkey.getMacKey().getEncoded()); + } + @Test public void testGetEncKey() { SecretKey encKey = masterkey.getEncKey(); From ff46d451ddada2c0085f2cb555025fecbfa92ebe Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Jun 2021 14:25:57 +0200 Subject: [PATCH 53/59] Replaced Cryptors.versionN(...) with CryptorProvider.forScheme(...) Use ServiceLoader to find different Cryptor implementations --- .../org/cryptomator/cryptolib/Cryptors.java | 22 ---------- .../cryptolib/api/CryptorProvider.java | 40 ++++++++++++++++++- .../cryptomator/cryptolib/package-info.java | 2 +- .../cryptomator/cryptolib/v1/CryptorImpl.java | 2 +- .../cryptolib/v1/CryptorProviderImpl.java | 12 +++--- .../cryptomator/cryptolib/v2/CryptorImpl.java | 2 +- .../cryptolib/v2/CryptorProviderImpl.java | 12 +++--- src/main/java9/module-info.java | 5 +++ ....cryptomator.cryptolib.api.CryptorProvider | 2 + .../cryptolib/CryptoLibIntegrationTest.java | 5 ++- .../cryptolib/api/CryptorProviderTest.java | 18 +++++++++ .../cryptolib/v1/CryptorProviderImplTest.java | 12 +----- .../cryptolib/v2/CryptorProviderImplTest.java | 11 +---- 13 files changed, 86 insertions(+), 59 deletions(-) create mode 100644 src/main/resources/META-INF/services/org.cryptomator.cryptolib.api.CryptorProvider create mode 100644 src/test/java/org/cryptomator/cryptolib/api/CryptorProviderTest.java diff --git a/src/main/java/org/cryptomator/cryptolib/Cryptors.java b/src/main/java/org/cryptomator/cryptolib/Cryptors.java index f432e71..33bfdb9 100644 --- a/src/main/java/org/cryptomator/cryptolib/Cryptors.java +++ b/src/main/java/org/cryptomator/cryptolib/Cryptors.java @@ -9,35 +9,13 @@ package org.cryptomator.cryptolib; import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.api.FileHeaderCryptor; -import org.cryptomator.cryptolib.common.ReseedingSecureRandom; - -import java.security.SecureRandom; import static com.google.common.base.Preconditions.checkArgument; public final class Cryptors { - /** - * @param seeder A native (if possible) SecureRandom used to seed internal CSPRNGs. - * @return A version 1 CryptorProvider - */ - public static CryptorProvider version1(SecureRandom seeder) { - SecureRandom csprng = ReseedingSecureRandom.create(seeder); - return new org.cryptomator.cryptolib.v1.CryptorProviderImpl(csprng); - } - - /** - * @param seeder A native (if possible) SecureRandom used to seed internal CSPRNGs. - * @return A version 2 CryptorProvider - */ - public static CryptorProvider version2(SecureRandom seeder) { - SecureRandom csprng = ReseedingSecureRandom.create(seeder); - return new org.cryptomator.cryptolib.v2.CryptorProviderImpl(csprng); - } - /** * Calculates the size of the cleartext resulting from the given ciphertext decrypted with the given cryptor. * diff --git a/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java b/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java index 63b36f3..e79a78e 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java +++ b/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java @@ -8,8 +8,46 @@ *******************************************************************************/ package org.cryptomator.cryptolib.api; +import java.security.SecureRandom; +import java.util.ServiceLoader; + public interface CryptorProvider { - Cryptor withKey(Masterkey masterkey); + enum Scheme { + /** + * AES-SIV for file name encryption + * AES-CTR + HMAC for content encryption + */ + SIV_CTRMAC, + + /** + * AES-SIV for file name encryption + * AES-GCM for content encryption + */ + SIV_GCM + } + + static CryptorProvider forScheme(Scheme scheme) { + for (CryptorProvider provider : ServiceLoader.load(CryptorProvider.class)) { + if (provider.scheme().equals(scheme)) { + return provider; + } + } + throw new UnsupportedOperationException("Scheme not supported: " + scheme.name()); + } + + /** + * @return The combination of ciphers used by this CryptorProvider implementation. + */ + Scheme scheme(); + + /** + * Creates a new Cryptor instance for the given key + * + * @param masterkey The key used by the returned cryptor during encryption and decryption + * @param random A native (if possible) SecureRandom used to seed internal CSPRNGs + * @return A new cryptor + */ + Cryptor provide(Masterkey masterkey, SecureRandom random); } diff --git a/src/main/java/org/cryptomator/cryptolib/package-info.java b/src/main/java/org/cryptomator/cryptolib/package-info.java index c7b856a..fe01ec6 100644 --- a/src/main/java/org/cryptomator/cryptolib/package-info.java +++ b/src/main/java/org/cryptomator/cryptolib/package-info.java @@ -16,7 +16,7 @@ * Masterkey masterkey = {@link org.cryptomator.cryptolib.common.MasterkeyFileAccess#load(java.nio.file.Path, java.lang.CharSequence) masterkeyFileAccess.load(path, passphrase}; * * // Create new cryptor: - * {@link org.cryptomator.cryptolib.api.Cryptor Cryptor} cryptor = {@link org.cryptomator.cryptolib.Cryptors#version1(java.security.SecureRandom) Cryptors.version1(SecureRandom.getInstanceStrong())}.{@link org.cryptomator.cryptolib.api.CryptorProvider#withKey(org.cryptomator.cryptolib.api.Masterkey) withKey(masterkey)}; + * {@link org.cryptomator.cryptolib.api.Cryptor Cryptor} cryptor = {@link org.cryptomator.cryptolib.api.CryptorProvider#forScheme(org.cryptomator.cryptolib.api.CryptorProvider.Scheme) CryptorProvider.forScheme(scheme)}.{@link org.cryptomator.cryptolib.api.CryptorProvider#provide(org.cryptomator.cryptolib.api.Masterkey, java.security.SecureRandom) provide(masterkey, SecureRandom.getInstanceStrong())}; * * // Each directory needs a (relatively) unique ID, which affects the encryption/decryption of child names: * String uniqueIdOfDirectory = UUID.randomUUID().toString(); diff --git a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java index d4c8dc0..e34137f 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java @@ -22,7 +22,7 @@ class CryptorImpl implements Cryptor { /** * Package-private constructor. - * Use {@link CryptorProviderImpl#withKey(Masterkey)} to obtain a Cryptor instance. + * Use {@link CryptorProviderImpl#provide(Masterkey, SecureRandom)} to obtain a Cryptor instance. */ CryptorImpl(Masterkey masterkey, SecureRandom random) { this.masterkey = masterkey; diff --git a/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java index 680462d..fad2b5a 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java @@ -10,20 +10,20 @@ import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.ReseedingSecureRandom; import java.security.SecureRandom; public class CryptorProviderImpl implements CryptorProvider { - private final SecureRandom random; - - public CryptorProviderImpl(SecureRandom random) { - this.random = random; + @Override + public Scheme scheme() { + return Scheme.SIV_CTRMAC; } @Override - public CryptorImpl withKey(Masterkey masterkey) { - return new CryptorImpl(masterkey, random); + public CryptorImpl provide(Masterkey masterkey, SecureRandom random) { + return new CryptorImpl(masterkey, ReseedingSecureRandom.create(random)); } } diff --git a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java index fa8bd68..7389ccd 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java @@ -23,7 +23,7 @@ class CryptorImpl implements Cryptor { /** * Package-private constructor. - * Use {@link CryptorProviderImpl#withKey(Masterkey)} to obtain a Cryptor instance. + * Use {@link CryptorProviderImpl#provide(Masterkey, SecureRandom)} to obtain a Cryptor instance. */ CryptorImpl(Masterkey masterkey, SecureRandom random) { this.masterkey = masterkey; diff --git a/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java index 1d1658b..1a6a018 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java @@ -10,20 +10,20 @@ import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.ReseedingSecureRandom; import java.security.SecureRandom; public class CryptorProviderImpl implements CryptorProvider { - private final SecureRandom random; - - public CryptorProviderImpl(SecureRandom random) { - this.random = random; + @Override + public Scheme scheme() { + return Scheme.SIV_GCM; } @Override - public CryptorImpl withKey(Masterkey masterkey) { - return new CryptorImpl(masterkey, random); + public CryptorImpl provide(Masterkey masterkey, SecureRandom random) { + return new CryptorImpl(masterkey, ReseedingSecureRandom.create(random)); } } diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java index 7111d6f..fe04470 100644 --- a/src/main/java9/module-info.java +++ b/src/main/java9/module-info.java @@ -9,4 +9,9 @@ exports org.cryptomator.cryptolib.common; opens org.cryptomator.cryptolib.common to com.google.gson; + + uses org.cryptomator.cryptolib.api.CryptorProvider; + + provides org.cryptomator.cryptolib.api.CryptorProvider + with org.cryptomator.cryptolib.v1.CryptorProviderImpl, org.cryptomator.cryptolib.v2.CryptorProviderImpl; } \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.cryptomator.cryptolib.api.CryptorProvider b/src/main/resources/META-INF/services/org.cryptomator.cryptolib.api.CryptorProvider new file mode 100644 index 0000000..4e7fe58 --- /dev/null +++ b/src/main/resources/META-INF/services/org.cryptomator.cryptolib.api.CryptorProvider @@ -0,0 +1,2 @@ +org.cryptomator.cryptolib.v1.CryptorProviderImpl +org.cryptomator.cryptolib.v2.CryptorProviderImpl \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java b/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java index 5cef884..f999669 100644 --- a/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java @@ -10,6 +10,7 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.cryptomator.cryptolib.common.SeekableByteChannelMock; @@ -35,8 +36,8 @@ public class CryptoLibIntegrationTest { private static Stream getCryptors() { return Stream.of( - Cryptors.version1(RANDOM_MOCK).withKey(MASTERKEY), - Cryptors.version2(RANDOM_MOCK).withKey(MASTERKEY) + CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_CTRMAC).provide(MASTERKEY, RANDOM_MOCK), + CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_GCM).provide(MASTERKEY, RANDOM_MOCK) ); } diff --git a/src/test/java/org/cryptomator/cryptolib/api/CryptorProviderTest.java b/src/test/java/org/cryptomator/cryptolib/api/CryptorProviderTest.java new file mode 100644 index 0000000..1a2b89f --- /dev/null +++ b/src/test/java/org/cryptomator/cryptolib/api/CryptorProviderTest.java @@ -0,0 +1,18 @@ +package org.cryptomator.cryptolib.api; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class CryptorProviderTest { + + @DisplayName("CryptorProvider.forScheme(...)") + @ParameterizedTest + @EnumSource(CryptorProvider.Scheme.class) + public void testForScheme(CryptorProvider.Scheme scheme) { + CryptorProvider provider = CryptorProvider.forScheme(scheme); + Assertions.assertNotNull(provider); + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java index cc877e9..77096c8 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java @@ -11,7 +11,6 @@ import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -21,17 +20,10 @@ public class CryptorProviderImplTest { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; - private CryptorProviderImpl cryptorProvider; - - @BeforeEach - public void setup() { - cryptorProvider = new CryptorProviderImpl(RANDOM_MOCK); - } - @Test - public void testWithKey() { + public void testProvide() { Masterkey masterkey = Mockito.mock(Masterkey.class); - CryptorImpl cryptor = cryptorProvider.withKey(masterkey); + CryptorImpl cryptor = new CryptorProviderImpl().provide(masterkey, RANDOM_MOCK); Assertions.assertNotNull(cryptor); } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java index 15c263f..95a2618 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java @@ -21,17 +21,10 @@ public class CryptorProviderImplTest { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; - private CryptorProviderImpl cryptorProvider; - - @BeforeEach - public void setup() { - cryptorProvider = new CryptorProviderImpl(RANDOM_MOCK); - } - @Test - public void testWithKey() { + public void testProvide() { Masterkey masterkey = Mockito.mock(Masterkey.class); - CryptorImpl cryptor = cryptorProvider.withKey(masterkey); + CryptorImpl cryptor = new CryptorProviderImpl().provide(masterkey, RANDOM_MOCK); Assertions.assertNotNull(cryptor); } From 6e382b7a211436d4e4ed05851a1a2640067996b8 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Jun 2021 14:36:04 +0200 Subject: [PATCH 54/59] moved Cryptors.cleartextSize() and Cryptors.ciphertextSize() to Cryptor --- .../org/cryptomator/cryptolib/Cryptors.java | 61 ------------- .../cryptomator/cryptolib/api/Cryptor.java | 42 +++++++++ .../cryptomator/cryptolib/api/FileHeader.java | 2 +- .../cryptomator/cryptolib/CryptorsTest.java | 89 ------------------- .../cryptolib/api/CryptorTest.java | 72 +++++++++++++++ 5 files changed, 115 insertions(+), 151 deletions(-) delete mode 100644 src/main/java/org/cryptomator/cryptolib/Cryptors.java delete mode 100644 src/test/java/org/cryptomator/cryptolib/CryptorsTest.java create mode 100644 src/test/java/org/cryptomator/cryptolib/api/CryptorTest.java diff --git a/src/main/java/org/cryptomator/cryptolib/Cryptors.java b/src/main/java/org/cryptomator/cryptolib/Cryptors.java deleted file mode 100644 index 33bfdb9..0000000 --- a/src/main/java/org/cryptomator/cryptolib/Cryptors.java +++ /dev/null @@ -1,61 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.cryptolib; - -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.FileHeader; -import org.cryptomator.cryptolib.api.FileHeaderCryptor; - -import static com.google.common.base.Preconditions.checkArgument; - -public final class Cryptors { - - /** - * Calculates the size of the cleartext resulting from the given ciphertext decrypted with the given cryptor. - * - * @param ciphertextSize Length of encrypted payload. Not including the {@link FileHeaderCryptor#headerSize() length of the header}. - * @param cryptor The cryptor which defines the cleartext/ciphertext ratio - * @return Cleartext length of a ciphertextSize-sized ciphertext decrypted with cryptor. - */ - public static long cleartextSize(long ciphertextSize, Cryptor cryptor) { - checkArgument(ciphertextSize >= 0, "expected ciphertextSize to be positive, but was %s", ciphertextSize); - long cleartextChunkSize = cryptor.fileContentCryptor().cleartextChunkSize(); - long ciphertextChunkSize = cryptor.fileContentCryptor().ciphertextChunkSize(); - long overheadPerChunk = ciphertextChunkSize - cleartextChunkSize; - long numFullChunks = ciphertextSize / ciphertextChunkSize; // floor by int-truncation - long additionalCiphertextBytes = ciphertextSize % ciphertextChunkSize; - if (additionalCiphertextBytes > 0 && additionalCiphertextBytes <= overheadPerChunk) { - throw new IllegalArgumentException("Method not defined for input value " + ciphertextSize); - } - long additionalCleartextBytes = (additionalCiphertextBytes == 0) ? 0 : additionalCiphertextBytes - overheadPerChunk; - assert additionalCleartextBytes >= 0; - return cleartextChunkSize * numFullChunks + additionalCleartextBytes; - } - - /** - * Calculates the size of the ciphertext resulting from the given cleartext encrypted with the given cryptor. - * - * @param cleartextSize Length of a unencrypted payload. - * @param cryptor The cryptor which defines the cleartext/ciphertext ratio - * @return Ciphertext length of a cleartextSize-sized cleartext encrypted with cryptor. - * Not including the {@link FileHeader#getFilesize() length of the header}. - */ - public static long ciphertextSize(long cleartextSize, Cryptor cryptor) { - checkArgument(cleartextSize >= 0, "expected cleartextSize to be positive, but was %s", cleartextSize); - long cleartextChunkSize = cryptor.fileContentCryptor().cleartextChunkSize(); - long ciphertextChunkSize = cryptor.fileContentCryptor().ciphertextChunkSize(); - long overheadPerChunk = ciphertextChunkSize - cleartextChunkSize; - long numFullChunks = cleartextSize / cleartextChunkSize; // floor by int-truncation - long additionalCleartextBytes = cleartextSize % cleartextChunkSize; - long additionalCiphertextBytes = (additionalCleartextBytes == 0) ? 0 : additionalCleartextBytes + overheadPerChunk; - assert additionalCiphertextBytes >= 0; - return ciphertextChunkSize * numFullChunks + additionalCiphertextBytes; - } - -} diff --git a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java index 5e79c2c..5a0b8c1 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java @@ -10,6 +10,8 @@ import javax.security.auth.Destroyable; +import static com.google.common.base.Preconditions.checkArgument; + public interface Cryptor extends Destroyable, AutoCloseable { FileContentCryptor fileContentCryptor(); @@ -29,4 +31,44 @@ default void close() { destroy(); } + /** + * Calculates the size of the cleartext resulting from the given ciphertext decrypted with the given cryptor. + * + * @param ciphertextSize Length of encrypted payload. Not including the {@link FileHeaderCryptor#headerSize() length of the header}. + * @return Cleartext length of a ciphertextSize-sized ciphertext decrypted with cryptor. + */ + default long cleartextSize(long ciphertextSize) { + checkArgument(ciphertextSize >= 0, "expected ciphertextSize to be positive, but was %s", ciphertextSize); + long cleartextChunkSize = fileContentCryptor().cleartextChunkSize(); + long ciphertextChunkSize = fileContentCryptor().ciphertextChunkSize(); + long overheadPerChunk = ciphertextChunkSize - cleartextChunkSize; + long numFullChunks = ciphertextSize / ciphertextChunkSize; // floor by int-truncation + long additionalCiphertextBytes = ciphertextSize % ciphertextChunkSize; + if (additionalCiphertextBytes > 0 && additionalCiphertextBytes <= overheadPerChunk) { + throw new IllegalArgumentException("Method not defined for input value " + ciphertextSize); + } + long additionalCleartextBytes = (additionalCiphertextBytes == 0) ? 0 : additionalCiphertextBytes - overheadPerChunk; + assert additionalCleartextBytes >= 0; + return cleartextChunkSize * numFullChunks + additionalCleartextBytes; + } + + /** + * Calculates the size of the ciphertext resulting from the given cleartext encrypted with the given cryptor. + * + * @param cleartextSize Length of a unencrypted payload. + * @return Ciphertext length of a cleartextSize-sized cleartext encrypted with cryptor. + * Not including the {@link FileHeader#getFilesize() length of the header}. + */ + default long ciphertextSize(long cleartextSize) { + checkArgument(cleartextSize >= 0, "expected cleartextSize to be positive, but was %s", cleartextSize); + long cleartextChunkSize = fileContentCryptor().cleartextChunkSize(); + long ciphertextChunkSize = fileContentCryptor().ciphertextChunkSize(); + long overheadPerChunk = ciphertextChunkSize - cleartextChunkSize; + long numFullChunks = cleartextSize / cleartextChunkSize; // floor by int-truncation + long additionalCleartextBytes = cleartextSize % cleartextChunkSize; + long additionalCiphertextBytes = (additionalCleartextBytes == 0) ? 0 : additionalCleartextBytes + overheadPerChunk; + assert additionalCiphertextBytes >= 0; + return ciphertextChunkSize * numFullChunks + additionalCiphertextBytes; + } + } diff --git a/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java b/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java index c15e96b..171eaa7 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java +++ b/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java @@ -11,7 +11,7 @@ public interface FileHeader { /** - * @deprecated No longer supported since vault version 5. Use {@link org.cryptomator.cryptolib.Cryptors#cleartextSize(long, Cryptor)} to calculate the cleartext size from the ciphertext size + * @deprecated No longer supported since vault version 5. Use {@link org.cryptomator.cryptolib.api.Cryptor#cleartextSize(long)} to calculate the cleartext size from the ciphertext size * @return file size stored in file header */ @Deprecated diff --git a/src/test/java/org/cryptomator/cryptolib/CryptorsTest.java b/src/test/java/org/cryptomator/cryptolib/CryptorsTest.java deleted file mode 100644 index 23ca512..0000000 --- a/src/test/java/org/cryptomator/cryptolib/CryptorsTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.cryptolib; - -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.FileContentCryptor; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.Mockito; - -public class CryptorsTest { - - @ParameterizedTest(name = "cleartextSize({1}) == {0}") - @CsvSource(value = { - "0,0", - "1,9", - "31,39", - "32,40", - "33,49", - "34,50", - "63,79", - "64,80", - "65,89" - }) - public void testCleartextSize(int cleartextSize, int ciphertextSize) { - Cryptor c = Mockito.mock(Cryptor.class); - FileContentCryptor cc = Mockito.mock(FileContentCryptor.class); - Mockito.when(c.fileContentCryptor()).thenReturn(cc); - Mockito.when(cc.cleartextChunkSize()).thenReturn(32); - Mockito.when(cc.ciphertextChunkSize()).thenReturn(40); - - Assertions.assertEquals(cleartextSize, Cryptors.cleartextSize(ciphertextSize, c)); - } - - @ParameterizedTest(name = "cleartextSize({0}) == undefined") - @ValueSource(ints = {-1, 1, 8, 41, 48, 81, 88}) - public void testCleartextSizeWithInvalidCiphertextSize(int invalidCiphertextSize) { - Cryptor c = Mockito.mock(Cryptor.class); - FileContentCryptor cc = Mockito.mock(FileContentCryptor.class); - Mockito.when(c.fileContentCryptor()).thenReturn(cc); - Mockito.when(cc.cleartextChunkSize()).thenReturn(32); - Mockito.when(cc.ciphertextChunkSize()).thenReturn(40); - - Assertions.assertThrows(IllegalArgumentException.class, () -> { - Cryptors.cleartextSize(invalidCiphertextSize, c); - }); - } - - @ParameterizedTest(name = "ciphertextSize({0}) == {1}") - @CsvSource(value = { - "0,0", - "1,9", - "31,39", - "32,40", - "33,49", - "34,50", - "63,79", - "64,80", - "65,89" - }) - public void testCiphertextSize(int cleartextSize, int ciphertextSize) { - Cryptor c = Mockito.mock(Cryptor.class); - FileContentCryptor cc = Mockito.mock(FileContentCryptor.class); - Mockito.when(c.fileContentCryptor()).thenReturn(cc); - Mockito.when(cc.cleartextChunkSize()).thenReturn(32); - Mockito.when(cc.ciphertextChunkSize()).thenReturn(40); - - Assertions.assertEquals(ciphertextSize, Cryptors.ciphertextSize(cleartextSize, c)); - } - - @ParameterizedTest(name = "ciphertextSize({0}) == undefined") - @ValueSource(ints = {-1}) - public void testCiphertextSizewithInvalidCleartextSize(int invalidCleartextSize) { - Cryptor c = Mockito.mock(Cryptor.class); - - Assertions.assertThrows(IllegalArgumentException.class, () -> { - Cryptors.ciphertextSize(invalidCleartextSize, c); - }); - } - -} diff --git a/src/test/java/org/cryptomator/cryptolib/api/CryptorTest.java b/src/test/java/org/cryptomator/cryptolib/api/CryptorTest.java new file mode 100644 index 0000000..e5f27a3 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptolib/api/CryptorTest.java @@ -0,0 +1,72 @@ +package org.cryptomator.cryptolib.api; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +public class CryptorTest { + + private final Cryptor cryptor = Mockito.mock(Cryptor.class); + + @BeforeEach + public void setup() { + FileContentCryptor contentCryptor = Mockito.mock(FileContentCryptor.class); + Mockito.when(cryptor.fileContentCryptor()).thenReturn(contentCryptor); + Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(32); + Mockito.when(contentCryptor.ciphertextChunkSize()).thenReturn(40); + Mockito.doCallRealMethod().when(cryptor).cleartextSize(Mockito.anyLong()); + Mockito.doCallRealMethod().when(cryptor).ciphertextSize(Mockito.anyLong()); + } + + @ParameterizedTest(name = "cleartextSize({1}) == {0}") + @CsvSource(value = { + "0,0", + "1,9", + "31,39", + "32,40", + "33,49", + "34,50", + "63,79", + "64,80", + "65,89" + }) + public void testCleartextSize(int cleartextSize, int ciphertextSize) { + Assertions.assertEquals(cleartextSize, cryptor.cleartextSize(ciphertextSize)); + } + + @ParameterizedTest(name = "cleartextSize({0}) == undefined") + @ValueSource(ints = {-1, 1, 8, 41, 48, 81, 88}) + public void testCleartextSizeWithInvalidCiphertextSize(int invalidCiphertextSize) { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + cryptor.cleartextSize(invalidCiphertextSize); + }); + } + + @ParameterizedTest(name = "ciphertextSize({0}) == {1}") + @CsvSource(value = { + "0,0", + "1,9", + "31,39", + "32,40", + "33,49", + "34,50", + "63,79", + "64,80", + "65,89" + }) + public void testCiphertextSize(int cleartextSize, int ciphertextSize) { + Assertions.assertEquals(ciphertextSize, cryptor.ciphertextSize(cleartextSize)); + } + + @ParameterizedTest(name = "ciphertextSize({0}) == undefined") + @ValueSource(ints = {-1}) + public void testCiphertextSizewithInvalidCleartextSize(int invalidCleartextSize) { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + cryptor.ciphertextSize(invalidCleartextSize); + }); + } + +} \ No newline at end of file From 22f1f34f49d73f4faea8537df3a428d8d5cfd650 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Jun 2021 14:39:08 +0200 Subject: [PATCH 55/59] Moved Encrypting/Decrypting..ByteChannels to common package --- .../{ => common}/DecryptingReadableByteChannel.java | 2 +- .../{ => common}/EncryptingReadableByteChannel.java | 2 +- .../{ => common}/EncryptingWritableByteChannel.java | 2 +- src/main/java/org/cryptomator/cryptolib/package-info.java | 4 ++-- .../org/cryptomator/cryptolib/CryptoLibIntegrationTest.java | 2 ++ .../{ => common}/DecryptingReadableByteChannelTest.java | 2 +- .../{ => common}/EncryptingReadableByteChannelTest.java | 3 +-- .../{ => common}/EncryptingWritableByteChannelTest.java | 5 +---- .../cryptomator/cryptolib/v1/FileContentCryptorImplTest.java | 4 ++-- .../cryptolib/v1/FileContentEncryptorBenchmark.java | 5 +---- .../cryptomator/cryptolib/v2/FileContentCryptorImplTest.java | 4 ++-- .../cryptolib/v2/FileContentEncryptorBenchmark.java | 5 +---- 12 files changed, 16 insertions(+), 24 deletions(-) rename src/main/java/org/cryptomator/cryptolib/{ => common}/DecryptingReadableByteChannel.java (99%) rename src/main/java/org/cryptomator/cryptolib/{ => common}/EncryptingReadableByteChannel.java (98%) rename src/main/java/org/cryptomator/cryptolib/{ => common}/EncryptingWritableByteChannel.java (98%) rename src/test/java/org/cryptomator/cryptolib/{ => common}/DecryptingReadableByteChannelTest.java (98%) rename src/test/java/org/cryptomator/cryptolib/{ => common}/EncryptingReadableByteChannelTest.java (97%) rename src/test/java/org/cryptomator/cryptolib/{ => common}/EncryptingWritableByteChannelTest.java (94%) diff --git a/src/main/java/org/cryptomator/cryptolib/DecryptingReadableByteChannel.java b/src/main/java/org/cryptomator/cryptolib/common/DecryptingReadableByteChannel.java similarity index 99% rename from src/main/java/org/cryptomator/cryptolib/DecryptingReadableByteChannel.java rename to src/main/java/org/cryptomator/cryptolib/common/DecryptingReadableByteChannel.java index f87f27e..dc5a599 100644 --- a/src/main/java/org/cryptomator/cryptolib/DecryptingReadableByteChannel.java +++ b/src/main/java/org/cryptomator/cryptolib/common/DecryptingReadableByteChannel.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.cryptolib; +package org.cryptomator.cryptolib.common; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; diff --git a/src/main/java/org/cryptomator/cryptolib/EncryptingReadableByteChannel.java b/src/main/java/org/cryptomator/cryptolib/common/EncryptingReadableByteChannel.java similarity index 98% rename from src/main/java/org/cryptomator/cryptolib/EncryptingReadableByteChannel.java rename to src/main/java/org/cryptomator/cryptolib/common/EncryptingReadableByteChannel.java index 0194d89..a849a55 100644 --- a/src/main/java/org/cryptomator/cryptolib/EncryptingReadableByteChannel.java +++ b/src/main/java/org/cryptomator/cryptolib/common/EncryptingReadableByteChannel.java @@ -1,4 +1,4 @@ -package org.cryptomator.cryptolib; +package org.cryptomator.cryptolib.common; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileHeader; diff --git a/src/main/java/org/cryptomator/cryptolib/EncryptingWritableByteChannel.java b/src/main/java/org/cryptomator/cryptolib/common/EncryptingWritableByteChannel.java similarity index 98% rename from src/main/java/org/cryptomator/cryptolib/EncryptingWritableByteChannel.java rename to src/main/java/org/cryptomator/cryptolib/common/EncryptingWritableByteChannel.java index b1a1a25..88c3b67 100644 --- a/src/main/java/org/cryptomator/cryptolib/EncryptingWritableByteChannel.java +++ b/src/main/java/org/cryptomator/cryptolib/common/EncryptingWritableByteChannel.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.cryptolib; +package org.cryptomator.cryptolib.common; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/src/main/java/org/cryptomator/cryptolib/package-info.java b/src/main/java/org/cryptomator/cryptolib/package-info.java index fe01ec6..7621890 100644 --- a/src/main/java/org/cryptomator/cryptolib/package-info.java +++ b/src/main/java/org/cryptomator/cryptolib/package-info.java @@ -29,13 +29,13 @@ * // Encrypt file contents: * ByteBuffer plaintext = ...; * SeekableByteChannel ciphertextOut = ...; - * try (WritableByteChannel ch = new {@link org.cryptomator.cryptolib.EncryptingWritableByteChannel EncryptingWritableByteChannel}(ciphertextOut, cryptor)) { + * try (WritableByteChannel ch = new {@link org.cryptomator.cryptolib.common.EncryptingWritableByteChannel EncryptingWritableByteChannel}(ciphertextOut, cryptor)) { * ch.write(plaintext); * } * * // Decrypt file contents: * ReadableByteChannel ciphertextIn = ...; - * try (ReadableByteChannel ch = new {@link org.cryptomator.cryptolib.DecryptingReadableByteChannel DecryptingReadableByteChannel}(ciphertextOut, cryptor, true)) { + * try (ReadableByteChannel ch = new {@link org.cryptomator.cryptolib.common.DecryptingReadableByteChannel DecryptingReadableByteChannel}(ciphertextOut, cryptor, true)) { * ch.read(plaintext); * } *

diff --git a/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java b/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java index f999669..612f401 100644 --- a/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java @@ -12,6 +12,8 @@ import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel; +import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.cryptomator.cryptolib.common.SeekableByteChannelMock; import org.hamcrest.CoreMatchers; diff --git a/src/test/java/org/cryptomator/cryptolib/DecryptingReadableByteChannelTest.java b/src/test/java/org/cryptomator/cryptolib/common/DecryptingReadableByteChannelTest.java similarity index 98% rename from src/test/java/org/cryptomator/cryptolib/DecryptingReadableByteChannelTest.java rename to src/test/java/org/cryptomator/cryptolib/common/DecryptingReadableByteChannelTest.java index ae9a88d..468dfdb 100644 --- a/src/test/java/org/cryptomator/cryptolib/DecryptingReadableByteChannelTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/DecryptingReadableByteChannelTest.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.cryptolib; +package org.cryptomator.cryptolib.common; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; diff --git a/src/test/java/org/cryptomator/cryptolib/EncryptingReadableByteChannelTest.java b/src/test/java/org/cryptomator/cryptolib/common/EncryptingReadableByteChannelTest.java similarity index 97% rename from src/test/java/org/cryptomator/cryptolib/EncryptingReadableByteChannelTest.java rename to src/test/java/org/cryptomator/cryptolib/common/EncryptingReadableByteChannelTest.java index 0f1a1c1..62fe445 100644 --- a/src/test/java/org/cryptomator/cryptolib/EncryptingReadableByteChannelTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/EncryptingReadableByteChannelTest.java @@ -1,10 +1,9 @@ -package org.cryptomator.cryptolib; +package org.cryptomator.cryptolib.common; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileContentCryptor; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.api.FileHeaderCryptor; -import org.cryptomator.cryptolib.common.SeekableByteChannelMock; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/cryptomator/cryptolib/EncryptingWritableByteChannelTest.java b/src/test/java/org/cryptomator/cryptolib/common/EncryptingWritableByteChannelTest.java similarity index 94% rename from src/test/java/org/cryptomator/cryptolib/EncryptingWritableByteChannelTest.java rename to src/test/java/org/cryptomator/cryptolib/common/EncryptingWritableByteChannelTest.java index f959ae4..a900757 100644 --- a/src/test/java/org/cryptomator/cryptolib/EncryptingWritableByteChannelTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/EncryptingWritableByteChannelTest.java @@ -6,13 +6,12 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.cryptolib; +package org.cryptomator.cryptolib.common; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileContentCryptor; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.api.FileHeaderCryptor; -import org.cryptomator.cryptolib.common.SeekableByteChannelMock; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -21,8 +20,6 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.WritableByteChannel; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java index 48084eb..11faaf6 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java @@ -9,8 +9,8 @@ package org.cryptomator.cryptolib.v1; import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptolib.DecryptingReadableByteChannel; -import org.cryptomator.cryptolib.EncryptingWritableByteChannel; +import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel; +import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.Masterkey; diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java index 66b754d..229cbea 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java @@ -15,10 +15,7 @@ import java.security.SecureRandom; import java.util.concurrent.TimeUnit; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import org.cryptomator.cryptolib.EncryptingWritableByteChannel; +import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java index 160fd91..9e1a92e 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java @@ -9,8 +9,8 @@ package org.cryptomator.cryptolib.v2; import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptolib.DecryptingReadableByteChannel; -import org.cryptomator.cryptolib.EncryptingWritableByteChannel; +import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel; +import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileHeader; diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java index 14ca1ef..c64ee04 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java @@ -15,10 +15,7 @@ import java.security.SecureRandom; import java.util.concurrent.TimeUnit; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import org.cryptomator.cryptolib.EncryptingWritableByteChannel; +import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; From deceb0f8df9b7bd7027d9bd8543ca50c40423cb1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Jun 2021 14:58:06 +0200 Subject: [PATCH 56/59] remove package org.cryptomator.cryptolib --- .../org/cryptomator/cryptolib/api/CryptorProvider.java | 10 ++++++++++ .../cryptomator/cryptolib/{ => api}/package-info.java | 8 ++++---- src/main/java9/module-info.java | 1 - .../cryptolib/{ => api}/CryptoLibIntegrationTest.java | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) rename src/main/java/org/cryptomator/cryptolib/{ => api}/package-info.java (85%) rename src/test/java/org/cryptomator/cryptolib/{ => api}/CryptoLibIntegrationTest.java (99%) diff --git a/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java b/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java index e79a78e..cfa442e 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java +++ b/src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java @@ -13,6 +13,9 @@ public interface CryptorProvider { + /** + * A combination of ciphers to use for filename and file content encryption + */ enum Scheme { /** * AES-SIV for file name encryption @@ -27,6 +30,13 @@ enum Scheme { SIV_GCM } + /** + * Finds a CryptorProvider implementation for the given combination of ciphers. + * + * @param scheme A cipher combination + * @return A CryptorProvider implementation supporting the requestes scheme + * @throws UnsupportedOperationException If the scheme is not implemented + */ static CryptorProvider forScheme(Scheme scheme) { for (CryptorProvider provider : ServiceLoader.load(CryptorProvider.class)) { if (provider.scheme().equals(scheme)) { diff --git a/src/main/java/org/cryptomator/cryptolib/package-info.java b/src/main/java/org/cryptomator/cryptolib/api/package-info.java similarity index 85% rename from src/main/java/org/cryptomator/cryptolib/package-info.java rename to src/main/java/org/cryptomator/cryptolib/api/package-info.java index 7621890..ba63c22 100644 --- a/src/main/java/org/cryptomator/cryptolib/package-info.java +++ b/src/main/java/org/cryptomator/cryptolib/api/package-info.java @@ -10,13 +10,13 @@ * // Create new masterkey and safe it to a file: * SecureRandom csprng = SecureRandom.getInstanceStrong(); * Masterkey masterkey = {@link org.cryptomator.cryptolib.api.Masterkey#generate(java.security.SecureRandom) Masterkey.generate(csprng)}; - * {@link org.cryptomator.cryptolib.common.MasterkeyFileAccess#persist(org.cryptomator.cryptolib.api.Masterkey, java.nio.file.Path, java.lang.CharSequence, int) masterkeyFileAccess.persist(masterkey, path, passphrase, vaultVersion)}; + * {@link org.cryptomator.cryptolib.common.MasterkeyFileAccess#persist(org.cryptomator.cryptolib.api.Masterkey, java.nio.file.Path, java.lang.CharSequence) masterkeyFileAccess.persist(masterkey, path, passphrase)}; * * // Load a masterkey from a file: - * Masterkey masterkey = {@link org.cryptomator.cryptolib.common.MasterkeyFileAccess#load(java.nio.file.Path, java.lang.CharSequence) masterkeyFileAccess.load(path, passphrase}; + * Masterkey masterkey = {@link org.cryptomator.cryptolib.common.MasterkeyFileAccess#load(java.nio.file.Path, java.lang.CharSequence) masterkeyFileAccess.load(path, passphrase)}; * * // Create new cryptor: - * {@link org.cryptomator.cryptolib.api.Cryptor Cryptor} cryptor = {@link org.cryptomator.cryptolib.api.CryptorProvider#forScheme(org.cryptomator.cryptolib.api.CryptorProvider.Scheme) CryptorProvider.forScheme(scheme)}.{@link org.cryptomator.cryptolib.api.CryptorProvider#provide(org.cryptomator.cryptolib.api.Masterkey, java.security.SecureRandom) provide(masterkey, SecureRandom.getInstanceStrong())}; + * {@link org.cryptomator.cryptolib.api.Cryptor Cryptor} cryptor = {@link org.cryptomator.cryptolib.api.CryptorProvider#forScheme(org.cryptomator.cryptolib.api.CryptorProvider.Scheme) CryptorProvider.forScheme(SIV_GCM)}.{@link org.cryptomator.cryptolib.api.CryptorProvider#provide(org.cryptomator.cryptolib.api.Masterkey, java.security.SecureRandom) provide(masterkey, csprng)}; * * // Each directory needs a (relatively) unique ID, which affects the encryption/decryption of child names: * String uniqueIdOfDirectory = UUID.randomUUID().toString(); @@ -40,4 +40,4 @@ * } * */ -package org.cryptomator.cryptolib; +package org.cryptomator.cryptolib.api; diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java index fe04470..655b7ed 100644 --- a/src/main/java9/module-info.java +++ b/src/main/java9/module-info.java @@ -4,7 +4,6 @@ requires com.google.common; requires org.slf4j; - exports org.cryptomator.cryptolib; exports org.cryptomator.cryptolib.api; exports org.cryptomator.cryptolib.common; diff --git a/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java b/src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java similarity index 99% rename from src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java rename to src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java index 612f401..0b8f78f 100644 --- a/src/test/java/org/cryptomator/cryptolib/CryptoLibIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.cryptolib; +package org.cryptomator.cryptolib.api; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; From cefd70baf82907ad309278a54ac0a443c0e04029 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Jun 2021 15:03:25 +0200 Subject: [PATCH 57/59] move size calculation methods to FileContentCryptor --- .../cryptomator/cryptolib/api/Cryptor.java | 42 ------------------- .../cryptolib/api/FileContentCryptor.java | 42 +++++++++++++++++++ .../cryptomator/cryptolib/api/FileHeader.java | 2 +- ...rTest.java => FileContentCryptorTest.java} | 18 ++++---- 4 files changed, 51 insertions(+), 53 deletions(-) rename src/test/java/org/cryptomator/cryptolib/api/{CryptorTest.java => FileContentCryptorTest.java} (70%) diff --git a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java index 5a0b8c1..5e79c2c 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java @@ -10,8 +10,6 @@ import javax.security.auth.Destroyable; -import static com.google.common.base.Preconditions.checkArgument; - public interface Cryptor extends Destroyable, AutoCloseable { FileContentCryptor fileContentCryptor(); @@ -31,44 +29,4 @@ default void close() { destroy(); } - /** - * Calculates the size of the cleartext resulting from the given ciphertext decrypted with the given cryptor. - * - * @param ciphertextSize Length of encrypted payload. Not including the {@link FileHeaderCryptor#headerSize() length of the header}. - * @return Cleartext length of a ciphertextSize-sized ciphertext decrypted with cryptor. - */ - default long cleartextSize(long ciphertextSize) { - checkArgument(ciphertextSize >= 0, "expected ciphertextSize to be positive, but was %s", ciphertextSize); - long cleartextChunkSize = fileContentCryptor().cleartextChunkSize(); - long ciphertextChunkSize = fileContentCryptor().ciphertextChunkSize(); - long overheadPerChunk = ciphertextChunkSize - cleartextChunkSize; - long numFullChunks = ciphertextSize / ciphertextChunkSize; // floor by int-truncation - long additionalCiphertextBytes = ciphertextSize % ciphertextChunkSize; - if (additionalCiphertextBytes > 0 && additionalCiphertextBytes <= overheadPerChunk) { - throw new IllegalArgumentException("Method not defined for input value " + ciphertextSize); - } - long additionalCleartextBytes = (additionalCiphertextBytes == 0) ? 0 : additionalCiphertextBytes - overheadPerChunk; - assert additionalCleartextBytes >= 0; - return cleartextChunkSize * numFullChunks + additionalCleartextBytes; - } - - /** - * Calculates the size of the ciphertext resulting from the given cleartext encrypted with the given cryptor. - * - * @param cleartextSize Length of a unencrypted payload. - * @return Ciphertext length of a cleartextSize-sized cleartext encrypted with cryptor. - * Not including the {@link FileHeader#getFilesize() length of the header}. - */ - default long ciphertextSize(long cleartextSize) { - checkArgument(cleartextSize >= 0, "expected cleartextSize to be positive, but was %s", cleartextSize); - long cleartextChunkSize = fileContentCryptor().cleartextChunkSize(); - long ciphertextChunkSize = fileContentCryptor().ciphertextChunkSize(); - long overheadPerChunk = ciphertextChunkSize - cleartextChunkSize; - long numFullChunks = cleartextSize / cleartextChunkSize; // floor by int-truncation - long additionalCleartextBytes = cleartextSize % cleartextChunkSize; - long additionalCiphertextBytes = (additionalCleartextBytes == 0) ? 0 : additionalCleartextBytes + overheadPerChunk; - assert additionalCiphertextBytes >= 0; - return ciphertextChunkSize * numFullChunks + additionalCiphertextBytes; - } - } diff --git a/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java b/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java index d04983b..6cecb45 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java +++ b/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java @@ -10,6 +10,8 @@ import java.nio.ByteBuffer; +import static com.google.common.base.Preconditions.checkArgument; + public interface FileContentCryptor { /** @@ -73,4 +75,44 @@ public interface FileContentCryptor { */ void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long chunkNumber, FileHeader header, boolean authenticate) throws AuthenticationFailedException; + /** + * Calculates the size of the cleartext resulting from the given ciphertext decrypted with the given cryptor. + * + * @param ciphertextSize Length of encrypted payload. Not including the {@link FileHeaderCryptor#headerSize() length of the header}. + * @return Cleartext length of a ciphertextSize-sized ciphertext decrypted with cryptor. + */ + default long cleartextSize(long ciphertextSize) { + checkArgument(ciphertextSize >= 0, "expected ciphertextSize to be positive, but was %s", ciphertextSize); + long cleartextChunkSize = cleartextChunkSize(); + long ciphertextChunkSize = ciphertextChunkSize(); + long overheadPerChunk = ciphertextChunkSize - cleartextChunkSize; + long numFullChunks = ciphertextSize / ciphertextChunkSize; // floor by int-truncation + long additionalCiphertextBytes = ciphertextSize % ciphertextChunkSize; + if (additionalCiphertextBytes > 0 && additionalCiphertextBytes <= overheadPerChunk) { + throw new IllegalArgumentException("Method not defined for input value " + ciphertextSize); + } + long additionalCleartextBytes = (additionalCiphertextBytes == 0) ? 0 : additionalCiphertextBytes - overheadPerChunk; + assert additionalCleartextBytes >= 0; + return cleartextChunkSize * numFullChunks + additionalCleartextBytes; + } + + /** + * Calculates the size of the ciphertext resulting from the given cleartext encrypted with the given cryptor. + * + * @param cleartextSize Length of a unencrypted payload. + * @return Ciphertext length of a cleartextSize-sized cleartext encrypted with cryptor. + * Not including the {@link FileHeader#getFilesize() length of the header}. + */ + default long ciphertextSize(long cleartextSize) { + checkArgument(cleartextSize >= 0, "expected cleartextSize to be positive, but was %s", cleartextSize); + long cleartextChunkSize = cleartextChunkSize(); + long ciphertextChunkSize = ciphertextChunkSize(); + long overheadPerChunk = ciphertextChunkSize - cleartextChunkSize; + long numFullChunks = cleartextSize / cleartextChunkSize; // floor by int-truncation + long additionalCleartextBytes = cleartextSize % cleartextChunkSize; + long additionalCiphertextBytes = (additionalCleartextBytes == 0) ? 0 : additionalCleartextBytes + overheadPerChunk; + assert additionalCiphertextBytes >= 0; + return ciphertextChunkSize * numFullChunks + additionalCiphertextBytes; + } + } diff --git a/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java b/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java index 171eaa7..21ac961 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java +++ b/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java @@ -11,7 +11,7 @@ public interface FileHeader { /** - * @deprecated No longer supported since vault version 5. Use {@link org.cryptomator.cryptolib.api.Cryptor#cleartextSize(long)} to calculate the cleartext size from the ciphertext size + * @deprecated No longer supported since vault version 5. Use {@link org.cryptomator.cryptolib.api.FileContentCryptor#cleartextSize(long)} to calculate the cleartext size from the ciphertext size * @return file size stored in file header */ @Deprecated diff --git a/src/test/java/org/cryptomator/cryptolib/api/CryptorTest.java b/src/test/java/org/cryptomator/cryptolib/api/FileContentCryptorTest.java similarity index 70% rename from src/test/java/org/cryptomator/cryptolib/api/CryptorTest.java rename to src/test/java/org/cryptomator/cryptolib/api/FileContentCryptorTest.java index e5f27a3..ab24ab9 100644 --- a/src/test/java/org/cryptomator/cryptolib/api/CryptorTest.java +++ b/src/test/java/org/cryptomator/cryptolib/api/FileContentCryptorTest.java @@ -7,18 +7,16 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; -public class CryptorTest { +public class FileContentCryptorTest { - private final Cryptor cryptor = Mockito.mock(Cryptor.class); + private final FileContentCryptor contentCryptor = Mockito.mock(FileContentCryptor.class); @BeforeEach public void setup() { - FileContentCryptor contentCryptor = Mockito.mock(FileContentCryptor.class); - Mockito.when(cryptor.fileContentCryptor()).thenReturn(contentCryptor); Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(32); Mockito.when(contentCryptor.ciphertextChunkSize()).thenReturn(40); - Mockito.doCallRealMethod().when(cryptor).cleartextSize(Mockito.anyLong()); - Mockito.doCallRealMethod().when(cryptor).ciphertextSize(Mockito.anyLong()); + Mockito.doCallRealMethod().when(contentCryptor).cleartextSize(Mockito.anyLong()); + Mockito.doCallRealMethod().when(contentCryptor).ciphertextSize(Mockito.anyLong()); } @ParameterizedTest(name = "cleartextSize({1}) == {0}") @@ -34,14 +32,14 @@ public void setup() { "65,89" }) public void testCleartextSize(int cleartextSize, int ciphertextSize) { - Assertions.assertEquals(cleartextSize, cryptor.cleartextSize(ciphertextSize)); + Assertions.assertEquals(cleartextSize, contentCryptor.cleartextSize(ciphertextSize)); } @ParameterizedTest(name = "cleartextSize({0}) == undefined") @ValueSource(ints = {-1, 1, 8, 41, 48, 81, 88}) public void testCleartextSizeWithInvalidCiphertextSize(int invalidCiphertextSize) { Assertions.assertThrows(IllegalArgumentException.class, () -> { - cryptor.cleartextSize(invalidCiphertextSize); + contentCryptor.cleartextSize(invalidCiphertextSize); }); } @@ -58,14 +56,14 @@ public void testCleartextSizeWithInvalidCiphertextSize(int invalidCiphertextSize "65,89" }) public void testCiphertextSize(int cleartextSize, int ciphertextSize) { - Assertions.assertEquals(ciphertextSize, cryptor.ciphertextSize(cleartextSize)); + Assertions.assertEquals(ciphertextSize, contentCryptor.ciphertextSize(cleartextSize)); } @ParameterizedTest(name = "ciphertextSize({0}) == undefined") @ValueSource(ints = {-1}) public void testCiphertextSizewithInvalidCleartextSize(int invalidCleartextSize) { Assertions.assertThrows(IllegalArgumentException.class, () -> { - cryptor.ciphertextSize(invalidCleartextSize); + contentCryptor.ciphertextSize(invalidCleartextSize); }); } From 238b293852e32a6ddb0984fd8ad04dc8de8ef09a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Jun 2021 17:20:00 +0200 Subject: [PATCH 58/59] * renamed deprecated FileHeader.get/setFilesize in preparation for future use * internalized FileHeader.Payload memory layout, simplifying FileHeaderCryptor impl code --- .../cryptolib/api/FileContentCryptor.java | 2 +- .../cryptomator/cryptolib/api/FileHeader.java | 20 +++++-- .../cryptolib/v1/FileHeaderCryptorImpl.java | 21 +++---- .../cryptolib/v1/FileHeaderImpl.java | 60 +++++++++++-------- .../cryptolib/v2/FileHeaderCryptorImpl.java | 21 +++---- .../cryptolib/v2/FileHeaderImpl.java | 60 +++++++++++-------- .../v1/FileContentCryptorImplTest.java | 31 +++++----- .../v1/FileHeaderCryptorImplTest.java | 6 +- .../cryptolib/v1/FileHeaderImplTest.java | 11 ++-- .../v2/FileContentCryptorImplTest.java | 2 +- .../v2/FileHeaderCryptorImplTest.java | 6 +- .../cryptolib/v2/FileHeaderImplTest.java | 46 ++++++++++++++ 12 files changed, 174 insertions(+), 112 deletions(-) create mode 100644 src/test/java/org/cryptomator/cryptolib/v2/FileHeaderImplTest.java diff --git a/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java b/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java index 6cecb45..9d04fe2 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java +++ b/src/main/java/org/cryptomator/cryptolib/api/FileContentCryptor.java @@ -101,7 +101,7 @@ default long cleartextSize(long ciphertextSize) { * * @param cleartextSize Length of a unencrypted payload. * @return Ciphertext length of a cleartextSize-sized cleartext encrypted with cryptor. - * Not including the {@link FileHeader#getFilesize() length of the header}. + * Not including the length of the header. */ default long ciphertextSize(long cleartextSize) { checkArgument(cleartextSize >= 0, "expected cleartextSize to be positive, but was %s", cleartextSize); diff --git a/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java b/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java index 21ac961..22f0bbb 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java +++ b/src/main/java/org/cryptomator/cryptolib/api/FileHeader.java @@ -11,17 +11,25 @@ public interface FileHeader { /** - * @deprecated No longer supported since vault version 5. Use {@link org.cryptomator.cryptolib.api.FileContentCryptor#cleartextSize(long)} to calculate the cleartext size from the ciphertext size - * @return file size stored in file header + * Returns the value of a currently unused 64 bit field in the file header. + *

+ * Formerly used for storing the plaintext file size. + * + * @return 64 bit integer for future use. + * @deprecated Don't rely on this method. It may be redefined any time. */ @Deprecated - long getFilesize(); + long getReserved(); /** - * @deprecated No longer used since vault version 5. Data stored in the header might get a different purpose in future versions. - * @param filesize number of bytes + * Sets the 64 bit field in the file header. + *

+ * Formerly used for storing the plaintext file size. + * + * @param reserved 64 bit integer for future use + * @deprecated Don't rely on this method. It may be redefined any time. */ @Deprecated - void setFilesize(long filesize); + void setReserved(long reserved); } diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java index ac9d457..aeaa7ee 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java @@ -43,7 +43,8 @@ public FileHeader create() { random.nextBytes(nonce); byte[] contentKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]; random.nextBytes(contentKey); - return new FileHeaderImpl(nonce, contentKey); + FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, contentKey); + return new FileHeaderImpl(nonce, payload); } @Override @@ -54,10 +55,7 @@ public int headerSize() { @Override public ByteBuffer encryptHeader(FileHeader header) { FileHeaderImpl headerImpl = FileHeaderImpl.cast(header); - ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE); - payloadCleartextBuf.putLong(-1l); - payloadCleartextBuf.put(headerImpl.getPayload().getContentKeyBytes()); - payloadCleartextBuf.flip(); + ByteBuffer payloadCleartextBuf = headerImpl.getPayload().encode(); try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { ByteBuffer result = ByteBuffer.allocate(FileHeaderImpl.SIZE); result.put(headerImpl.getNonce()); @@ -117,18 +115,13 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic try (DestroyableSecretKey ek = masterkey.getEncKey()) { // decrypt payload: Cipher cipher = CipherSupplier.AES_CTR.forDecryption(ek, new IvParameterSpec(nonce)); + assert cipher.getOutputSize(ciphertextPayload.length) == payloadCleartextBuf.remaining(); int decrypted = cipher.doFinal(ByteBuffer.wrap(ciphertextPayload), payloadCleartextBuf); assert decrypted == FileHeaderImpl.Payload.SIZE; + payloadCleartextBuf.flip(); + FileHeaderImpl.Payload payload = FileHeaderImpl.Payload.decode(payloadCleartextBuf); - payloadCleartextBuf.position(FileHeaderImpl.Payload.FILESIZE_POS); - long fileSize = payloadCleartextBuf.getLong(); - payloadCleartextBuf.position(FileHeaderImpl.Payload.CONTENT_KEY_POS); - byte[] contentKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]; - payloadCleartextBuf.get(contentKey); - - final FileHeaderImpl header = new FileHeaderImpl(nonce, contentKey); - header.setFilesize(fileSize); - return header; + return new FileHeaderImpl(nonce, payload); } catch (ShortBufferException e) { throw new IllegalStateException("Result buffer too small for decrypted header payload.", e); } catch (IllegalBlockSizeException | BadPaddingException e) { diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java index 6004ccf..b428cfa 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java @@ -8,10 +8,12 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v1; +import com.google.common.base.Preconditions; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import javax.security.auth.Destroyable; +import java.nio.ByteBuffer; import java.util.Arrays; class FileHeaderImpl implements FileHeader, Destroyable { @@ -27,12 +29,12 @@ class FileHeaderImpl implements FileHeader, Destroyable { private final byte[] nonce; private final Payload payload; - FileHeaderImpl(byte[] nonce, byte[] contentKey) { + FileHeaderImpl(byte[] nonce, Payload payload) { if (nonce.length != NONCE_LEN) { throw new IllegalArgumentException("Invalid nonce length. (was: " + nonce.length + ", required: " + NONCE_LEN + ")"); } this.nonce = nonce; - this.payload = new Payload(contentKey); + this.payload = payload; } static FileHeaderImpl cast(FileHeader header) { @@ -52,13 +54,13 @@ public Payload getPayload() { } @Override - public long getFilesize() { - return payload.getFilesize(); + public long getReserved() { + return payload.getReserved(); } @Override - public void setFilesize(long filesize) { - payload.setFilesize(filesize); + public void setReserved(long reserved) { + payload.setReserved(reserved); } @Override @@ -73,38 +75,45 @@ public void destroy() { public static class Payload implements Destroyable { - static final int FILESIZE_POS = 0; - static final int FILESIZE_LEN = 8; - static final int CONTENT_KEY_POS = 8; + static final int REVERSED_LEN = Long.BYTES; static final int CONTENT_KEY_LEN = 32; - static final int SIZE = FILESIZE_LEN + CONTENT_KEY_LEN; + static final int SIZE = REVERSED_LEN + CONTENT_KEY_LEN; - private long filesize = -1L; - private final byte[] contentKeyBytes; + private long reserved; private final DestroyableSecretKey contentKey; - private Payload(byte[] contentKeyBytes) { - if (contentKeyBytes.length != CONTENT_KEY_LEN) { - throw new IllegalArgumentException("Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")"); - } - this.contentKeyBytes = contentKeyBytes; + Payload(long reversed, byte[] contentKeyBytes) { + Preconditions.checkArgument(contentKeyBytes.length == CONTENT_KEY_LEN, "Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")"); + this.reserved = reversed; this.contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG); } - private long getFilesize() { - return filesize; + static Payload decode(ByteBuffer cleartextPayloadBuf) { + Preconditions.checkArgument(cleartextPayloadBuf.remaining() == SIZE, "invalid payload buffer length"); + long reserved = cleartextPayloadBuf.getLong(); + byte[] contentKeyBytes = new byte[CONTENT_KEY_LEN]; + cleartextPayloadBuf.get(contentKeyBytes); + return new Payload(reserved, contentKeyBytes); } - private void setFilesize(long filesize) { - this.filesize = filesize; + ByteBuffer encode() { + ByteBuffer buf = ByteBuffer.allocate(SIZE); + buf.putLong(reserved); + buf.put(contentKey.getEncoded()); + buf.flip(); + return buf; } - DestroyableSecretKey getContentKey() { - return contentKey; + private long getReserved() { + return reserved; + } + + private void setReserved(long reserved) { + this.reserved = reserved; } - byte[] getContentKeyBytes() { - return contentKeyBytes; + DestroyableSecretKey getContentKey() { + return contentKey; } @Override @@ -115,7 +124,6 @@ public boolean isDestroyed() { @Override public void destroy() { contentKey.destroy(); - Arrays.fill(contentKeyBytes, (byte) 0x00); } } diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java index 3c05f48..0e872ce 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java @@ -43,7 +43,8 @@ public FileHeader create() { random.nextBytes(nonce); byte[] contentKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]; random.nextBytes(contentKey); - return new FileHeaderImpl(nonce, contentKey); + FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, contentKey); + return new FileHeaderImpl(nonce, payload); } @Override @@ -54,10 +55,7 @@ public int headerSize() { @Override public ByteBuffer encryptHeader(FileHeader header) { FileHeaderImpl headerImpl = FileHeaderImpl.cast(header); - ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE); - payloadCleartextBuf.putLong(-1l); - payloadCleartextBuf.put(headerImpl.getPayload().getContentKeyBytes()); - payloadCleartextBuf.flip(); + ByteBuffer payloadCleartextBuf = headerImpl.getPayload().encode(); try (DestroyableSecretKey ek = masterkey.getEncKey()) { ByteBuffer result = ByteBuffer.allocate(FileHeaderImpl.SIZE); result.put(headerImpl.getNonce()); @@ -95,18 +93,13 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic try (DestroyableSecretKey ek = masterkey.getEncKey()) { // decrypt payload: Cipher cipher = CipherSupplier.AES_GCM.forDecryption(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); + assert cipher.getOutputSize(ciphertextAndTag.length) == payloadCleartextBuf.remaining(); int decrypted = cipher.doFinal(ByteBuffer.wrap(ciphertextAndTag), payloadCleartextBuf); assert decrypted == FileHeaderImpl.Payload.SIZE; + payloadCleartextBuf.flip(); + FileHeaderImpl.Payload payload = FileHeaderImpl.Payload.decode(payloadCleartextBuf); - payloadCleartextBuf.position(FileHeaderImpl.Payload.FILESIZE_POS); - long fileSize = payloadCleartextBuf.getLong(); - payloadCleartextBuf.position(FileHeaderImpl.Payload.CONTENT_KEY_POS); - byte[] contentKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]; - payloadCleartextBuf.get(contentKey); - - final FileHeaderImpl header = new FileHeaderImpl(nonce, contentKey); - header.setFilesize(fileSize); - return header; + return new FileHeaderImpl(nonce, payload); } catch (AEADBadTagException e) { throw new AuthenticationFailedException("Header tag mismatch.", e); } catch (ShortBufferException e) { diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java index bc0ad35..06c0d14 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java @@ -8,10 +8,12 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; +import com.google.common.base.Preconditions; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import javax.security.auth.Destroyable; +import java.nio.ByteBuffer; import java.util.Arrays; class FileHeaderImpl implements FileHeader, Destroyable { @@ -27,12 +29,12 @@ class FileHeaderImpl implements FileHeader, Destroyable { private final byte[] nonce; private final Payload payload; - FileHeaderImpl(byte[] nonce, byte[] contentKey) { + FileHeaderImpl(byte[] nonce, Payload payload) { if (nonce.length != NONCE_LEN) { throw new IllegalArgumentException("Invalid nonce length. (was: " + nonce.length + ", required: " + NONCE_LEN + ")"); } this.nonce = nonce; - this.payload = new Payload(contentKey); + this.payload = payload; } static FileHeaderImpl cast(FileHeader header) { @@ -52,13 +54,13 @@ public Payload getPayload() { } @Override - public long getFilesize() { - return payload.getFilesize(); + public long getReserved() { + return payload.getReserved(); } @Override - public void setFilesize(long filesize) { - payload.setFilesize(filesize); + public void setReserved(long reserved) { + payload.setReserved(reserved); } @Override @@ -73,38 +75,45 @@ public void destroy() { public static class Payload implements Destroyable { - static final int FILESIZE_POS = 0; - static final int FILESIZE_LEN = 8; - static final int CONTENT_KEY_POS = 8; + static final int REVERSED_LEN = Long.BYTES; static final int CONTENT_KEY_LEN = 32; - static final int SIZE = FILESIZE_LEN + CONTENT_KEY_LEN; + static final int SIZE = REVERSED_LEN + CONTENT_KEY_LEN; - private long filesize = -1L; - private final byte[] contentKeyBytes; + private long reserved; private final DestroyableSecretKey contentKey; - private Payload(byte[] contentKeyBytes) { - if (contentKeyBytes.length != CONTENT_KEY_LEN) { - throw new IllegalArgumentException("Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")"); - } - this.contentKeyBytes = contentKeyBytes; + Payload(long reversed, byte[] contentKeyBytes) { + Preconditions.checkArgument(contentKeyBytes.length == CONTENT_KEY_LEN, "Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")"); + this.reserved = reversed; this.contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG); } - private long getFilesize() { - return filesize; + static Payload decode(ByteBuffer cleartextPayloadBuf) { + Preconditions.checkArgument(cleartextPayloadBuf.remaining() == SIZE, "invalid payload buffer length"); + long reserved = cleartextPayloadBuf.getLong(); + byte[] contentKeyBytes = new byte[CONTENT_KEY_LEN]; + cleartextPayloadBuf.get(contentKeyBytes); + return new Payload(reserved, contentKeyBytes); } - private void setFilesize(long filesize) { - this.filesize = filesize; + ByteBuffer encode() { + ByteBuffer buf = ByteBuffer.allocate(SIZE); + buf.putLong(reserved); + buf.put(contentKey.getEncoded()); + buf.flip(); + return buf; } - DestroyableSecretKey getContentKey() { - return contentKey; + private long getReserved() { + return reserved; + } + + private void setReserved(long reserved) { + this.reserved = reserved; } - byte[] getContentKeyBytes() { - return contentKeyBytes; + DestroyableSecretKey getContentKey() { + return contentKey; } @Override @@ -115,7 +124,6 @@ public boolean isDestroyed() { @Override public void destroy() { contentKey.destroy(); - Arrays.fill(contentKeyBytes, (byte) 0x00); } } diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java index 11faaf6..fdb9320 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java @@ -9,12 +9,12 @@ package org.cryptomator.cryptolib.v1; import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel; -import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel; import org.cryptomator.cryptolib.common.DestroyableSecretKey; +import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.cryptomator.cryptolib.common.SeekableByteChannelMock; import org.hamcrest.CoreMatchers; @@ -47,6 +47,8 @@ public class FileContentCryptorImplTest { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; + + private FileHeaderImpl header; private FileHeaderCryptorImpl headerCryptor; private FileContentCryptorImpl fileContentCryptor; private Cryptor cryptor; @@ -54,6 +56,7 @@ public class FileContentCryptorImplTest { @BeforeEach public void setup() { Masterkey masterkey = new Masterkey(new byte[64]); + header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN])); headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK); fileContentCryptor = new FileContentCryptorImpl(masterkey, RANDOM_MOCK); cryptor = Mockito.mock(Cryptor.class); @@ -62,7 +65,7 @@ public void setup() { } @Test - public void testMacIsValidAfterEncryption() throws NoSuchAlgorithmException { + public void testMacIsValidAfterEncryption() { DestroyableSecretKey fileKey = new DestroyableSecretKey(new byte[16], "AES"); ByteBuffer ciphertext = ByteBuffer.allocate(fileContentCryptor.ciphertextChunkSize()); fileContentCryptor.encryptChunk(UTF_8.encode("asd"), ciphertext, 42l, new byte[16], fileKey); @@ -72,7 +75,7 @@ public void testMacIsValidAfterEncryption() throws NoSuchAlgorithmException { } @Test - public void testDecryptedEncryptedEqualsPlaintext() throws NoSuchAlgorithmException { + public void testDecryptedEncryptedEqualsPlaintext() { DestroyableSecretKey fileKey = new DestroyableSecretKey(new byte[16], "AES"); ByteBuffer ciphertext = ByteBuffer.allocate(fileContentCryptor.ciphertextChunkSize()); ByteBuffer cleartext = ByteBuffer.allocate(fileContentCryptor.cleartextChunkSize()); @@ -93,7 +96,7 @@ public void testEncryptChunkOfInvalidSize(int size) { ByteBuffer cleartext = ByteBuffer.allocate(size); Assertions.assertThrows(IllegalArgumentException.class, () -> { - fileContentCryptor.encryptChunk(cleartext, 0, headerCryptor.create()); + fileContentCryptor.encryptChunk(cleartext, 0, header); }); } @@ -101,7 +104,7 @@ public void testEncryptChunkOfInvalidSize(int size) { @DisplayName("encrypt chunk") public void testChunkEncryption() { ByteBuffer cleartext = US_ASCII.encode(CharBuffer.wrap("hello world")); - ByteBuffer ciphertext = fileContentCryptor.encryptChunk(cleartext, 0, headerCryptor.create()); + ByteBuffer ciphertext = fileContentCryptor.encryptChunk(cleartext, 0, header); ByteBuffer expected = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=")); Assertions.assertEquals(expected, ciphertext); } @@ -112,7 +115,7 @@ public void testChunkEncryptionWithBufferUnderflow() { ByteBuffer cleartext = US_ASCII.encode(CharBuffer.wrap("hello world")); ByteBuffer ciphertext = ByteBuffer.allocate(Constants.CHUNK_SIZE - 1); Assertions.assertThrows(IllegalArgumentException.class, () -> { - fileContentCryptor.encryptChunk(cleartext, ciphertext, 0, headerCryptor.create()); + fileContentCryptor.encryptChunk(cleartext, ciphertext, 0, header); }); } @@ -144,7 +147,7 @@ public void testDecryptChunkOfInvalidSize(int size) { ByteBuffer ciphertext = ByteBuffer.allocate(size); Assertions.assertThrows(IllegalArgumentException.class, () -> { - fileContentCryptor.decryptChunk(ciphertext, 0, headerCryptor.create(), true); + fileContentCryptor.decryptChunk(ciphertext, 0, header, true); }); } @@ -152,7 +155,7 @@ public void testDecryptChunkOfInvalidSize(int size) { @DisplayName("decrypt chunk") public void testChunkDecryption() throws AuthenticationFailedException { ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=")); - ByteBuffer cleartext = fileContentCryptor.decryptChunk(ciphertext, 0, headerCryptor.create(), true); + ByteBuffer cleartext = fileContentCryptor.decryptChunk(ciphertext, 0, header, true); ByteBuffer expected = US_ASCII.encode("hello world"); Assertions.assertEquals(expected, cleartext); } @@ -163,7 +166,7 @@ public void testChunkDecryptionWithBufferUnderflow() { ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAApsIsUSJAHAF1IqG66PAqEvceoFIiAj5736Xq")); ByteBuffer cleartext = ByteBuffer.allocate(Constants.PAYLOAD_SIZE - 1); Assertions.assertThrows(IllegalArgumentException.class, () -> { - fileContentCryptor.decryptChunk(ciphertext, cleartext, 0, headerCryptor.create(), true); + fileContentCryptor.decryptChunk(ciphertext, cleartext, 0, header, true); }); } @@ -202,7 +205,7 @@ public void testChunkDecryptionWithUnauthenticNonce() { ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("aAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=")); Assertions.assertThrows(AuthenticationFailedException.class, () -> { - fileContentCryptor.decryptChunk(ciphertext, 0, headerCryptor.create(), true); + fileContentCryptor.decryptChunk(ciphertext, 0, header, true); }); } @@ -227,7 +230,7 @@ public void testChunkDecryptionWithUnauthenticContent() { ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3YTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=")); Assertions.assertThrows(AuthenticationFailedException.class, () -> { - fileContentCryptor.decryptChunk(ciphertext, 0, headerCryptor.create(), true); + fileContentCryptor.decryptChunk(ciphertext, 0, header, true); }); } @@ -252,7 +255,7 @@ public void testChunkDecryptionWithUnauthenticMac() { ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3OG=")); Assertions.assertThrows(AuthenticationFailedException.class, () -> { - fileContentCryptor.decryptChunk(ciphertext, 0, headerCryptor.create(), true); + fileContentCryptor.decryptChunk(ciphertext, 0, header, true); }); } @@ -275,7 +278,7 @@ public void testDecryptionWithUnauthenticMac() throws InterruptedException, IOEx @DisplayName("decrypt chunk with unauthentic MAC but skipping MAC verficiation") public void testChunkDecryptionWithUnauthenticMacSkipAuth() throws AuthenticationFailedException { ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3OG=")); - ByteBuffer cleartext = fileContentCryptor.decryptChunk(ciphertext, 0, headerCryptor.create(), false); + ByteBuffer cleartext = fileContentCryptor.decryptChunk(ciphertext, 0, header, false); ByteBuffer expected = US_ASCII.encode(CharBuffer.wrap("hello world")); Assertions.assertEquals(expected, cleartext); } diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java index a3bf7cf..11fa682 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java @@ -12,7 +12,6 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.api.Masterkey; -import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -36,7 +35,8 @@ public void setup() { public void testEncryption() { // set nonce to: AAAAAAAAAAAAAAAAAAAAAA== // set payload to: //////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== - FileHeader header = headerCryptor.create(); + FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]); + FileHeader header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload); // encrypt payload: // echo -n "//////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" | base64 --decode \ // | openssl enc -aes-256-ctr -K 0000000000000000000000000000000000000000000000000000000000000000 -iv 00000000000000000000000000000000 | base64 @@ -64,7 +64,7 @@ public void testHeaderSize() { public void testDecryption() throws AuthenticationFailedException { byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="); FileHeader header = headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext)); - Assertions.assertEquals(header.getFilesize(), -1l); + Assertions.assertEquals(header.getReserved(), -1l); } @Test diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderImplTest.java index db9c3fd..09d8c3a 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderImplTest.java @@ -17,15 +17,16 @@ public class FileHeaderImplTest { @Test public void testConstructionFailsWithInvalidNonceSize() { + FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]); Assertions.assertThrows(IllegalArgumentException.class, () -> { - new FileHeaderImpl(new byte[3], new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]); + new FileHeaderImpl(new byte[3], payload); }); } @Test public void testConstructionFailsWithInvalidKeySize() { Assertions.assertThrows(IllegalArgumentException.class, () -> { - new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], new byte[3]); + new FileHeaderImpl.Payload(-1, new byte[3]); }); } @@ -33,11 +34,13 @@ public void testConstructionFailsWithInvalidKeySize() { public void testDestruction() { byte[] nonNullKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]; Arrays.fill(nonNullKey, (byte) 0x42); - FileHeaderImpl header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], nonNullKey); + FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, nonNullKey); + FileHeaderImpl header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload); Assertions.assertFalse(header.isDestroyed()); header.destroy(); Assertions.assertTrue(header.isDestroyed()); - Assertions.assertArrayEquals(new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN], nonNullKey); + Assertions.assertTrue(payload.isDestroyed()); + Assertions.assertTrue(payload.getContentKey().isDestroyed()); } } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java index 9e1a92e..846f2f1 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java @@ -59,7 +59,7 @@ public class FileContentCryptorImplTest { @BeforeEach public void setup() { Masterkey masterkey = new Masterkey(new byte[64]); - header = new FileHeaderImpl(new byte[12], new byte[32]); + header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN])); headerCryptor = new FileHeaderCryptorImpl(masterkey, CSPRNG); fileContentCryptor = new FileContentCryptorImpl(CSPRNG); cryptor = Mockito.mock(Cryptor.class); diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java index 394de95..0a674cd 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java @@ -13,7 +13,6 @@ import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.CipherSupplier; -import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -51,7 +50,8 @@ public void setup() { public void testEncryption() { // set nonce to: AAAAAAAAAAAAAAAA // set payload to: //////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== - FileHeader header = new FileHeaderImpl(new byte[12], new byte[32]); + FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]); + FileHeader header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload); // encrypt payload: // echo -n "//////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" | base64 --decode \ // | openssl enc -aes-256-gcm -K 0000000000000000000000000000000000000000000000000000000000000000 -iv 00000000000000000000000000000000 -a @@ -76,7 +76,7 @@ public void testHeaderSize() { public void testDecryption() throws AuthenticationFailedException { byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjU0="); FileHeader header = headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext)); - Assertions.assertEquals(header.getFilesize(), -1l); + Assertions.assertEquals(header.getReserved(), -1l); } @Test diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderImplTest.java new file mode 100644 index 0000000..67d2629 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderImplTest.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the accompanying LICENSE.txt. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.cryptolib.v2; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class FileHeaderImplTest { + + @Test + public void testConstructionFailsWithInvalidNonceSize() { + FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new FileHeaderImpl(new byte[3], payload); + }); + } + + @Test + public void testConstructionFailsWithInvalidKeySize() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new FileHeaderImpl.Payload(-1, new byte[3]); + }); + } + + @Test + public void testDestruction() { + byte[] nonNullKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]; + Arrays.fill(nonNullKey, (byte) 0x42); + FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, nonNullKey); + FileHeaderImpl header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload); + Assertions.assertFalse(header.isDestroyed()); + header.destroy(); + Assertions.assertTrue(header.isDestroyed()); + Assertions.assertTrue(payload.isDestroyed()); + Assertions.assertTrue(payload.getContentKey().isDestroyed()); + } + +} From 80e71a143f68ec679e80aab6b0c507a3706492f1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Jul 2021 11:46:45 +0200 Subject: [PATCH 59/59] dependency bump --- pom.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 967f3b4..c57acf2 100644 --- a/pom.xml +++ b/pom.xml @@ -18,20 +18,20 @@ 8 - 2.8.6 + 2.8.7 30.1.1-jre - 1.4.2 - 1.7.30 + 1.4.3 + 1.7.31 - 5.7.1 - 3.9.0 + 5.7.2 + 3.11.2 2.2 - 1.29 + 1.32 - 6.1.6 - 0.8.6 + 6.2.2 + 0.8.7 1.6.8 @@ -196,7 +196,7 @@ maven-javadoc-plugin - 3.2.0 + 3.3.0 attach-javadocs @@ -297,7 +297,7 @@ maven-gpg-plugin - 1.6 + 3.0.1 sign-artifacts