├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── dependency-check.yml │ ├── publish-central.yml │ └── publish-github.yml ├── .gitignore ├── .idea ├── .name ├── misc.xml └── vcs.xml ├── LICENSE.txt ├── README.md ├── pom.xml ├── src ├── main │ ├── java │ │ └── org │ │ │ └── cryptomator │ │ │ └── cryptolib │ │ │ ├── api │ │ │ ├── AuthenticationFailedException.java │ │ │ ├── CryptoException.java │ │ │ ├── Cryptor.java │ │ │ ├── CryptorProvider.java │ │ │ ├── FileContentCryptor.java │ │ │ ├── FileHeader.java │ │ │ ├── FileHeaderCryptor.java │ │ │ ├── FileNameCryptor.java │ │ │ ├── InvalidPassphraseException.java │ │ │ ├── Masterkey.java │ │ │ ├── MasterkeyLoader.java │ │ │ ├── MasterkeyLoadingFailedException.java │ │ │ ├── UnsupportedVaultFormatException.java │ │ │ └── package-info.java │ │ │ ├── common │ │ │ ├── AesKeyWrap.java │ │ │ ├── ByteBuffers.java │ │ │ ├── CipherSupplier.java │ │ │ ├── DecryptingReadableByteChannel.java │ │ │ ├── DestroyableSecretKey.java │ │ │ ├── Destroyables.java │ │ │ ├── ECKeyPair.java │ │ │ ├── EncryptingReadableByteChannel.java │ │ │ ├── EncryptingWritableByteChannel.java │ │ │ ├── MacSupplier.java │ │ │ ├── MasterkeyFile.java │ │ │ ├── MasterkeyFileAccess.java │ │ │ ├── MessageDigestSupplier.java │ │ │ ├── ObjectPool.java │ │ │ ├── P384KeyPair.java │ │ │ ├── Pkcs12Exception.java │ │ │ ├── Pkcs12Helper.java │ │ │ ├── Pkcs12PasswordException.java │ │ │ ├── ReseedingSecureRandom.java │ │ │ ├── Scrypt.java │ │ │ └── X509CertBuilder.java │ │ │ ├── v1 │ │ │ ├── Constants.java │ │ │ ├── CryptorImpl.java │ │ │ ├── CryptorProviderImpl.java │ │ │ ├── FileContentCryptorImpl.java │ │ │ ├── FileHeaderCryptorImpl.java │ │ │ ├── FileHeaderImpl.java │ │ │ └── FileNameCryptorImpl.java │ │ │ └── v2 │ │ │ ├── Constants.java │ │ │ ├── CryptorImpl.java │ │ │ ├── CryptorProviderImpl.java │ │ │ ├── FileContentCryptorImpl.java │ │ │ ├── FileHeaderCryptorImpl.java │ │ │ ├── FileHeaderImpl.java │ │ │ └── FileNameCryptorImpl.java │ ├── java22 │ │ └── module-info.java │ ├── java9 │ │ └── module-info.java │ └── resources │ │ ├── LICENSE.txt │ │ └── META-INF │ │ ├── native-image │ │ └── org.cryptomator │ │ │ └── cryptolib │ │ │ └── reflect-config.json │ │ └── services │ │ └── org.cryptomator.cryptolib.api.CryptorProvider └── test │ ├── java │ └── org │ │ └── cryptomator │ │ └── cryptolib │ │ ├── api │ │ ├── CryptoLibIntegrationTest.java │ │ ├── CryptorProviderTest.java │ │ └── FileContentCryptorTest.java │ │ ├── common │ │ ├── AesKeyWrapTest.java │ │ ├── ByteBuffersTest.java │ │ ├── CipherSupplierTest.java │ │ ├── DecryptingReadableByteChannelTest.java │ │ ├── DestroyableSecretKeyTest.java │ │ ├── DestroyablesTest.java │ │ ├── ECKeyPairTest.java │ │ ├── EncryptingReadableByteChannelTest.java │ │ ├── EncryptingWritableByteChannelTest.java │ │ ├── GcmTestHelper.java │ │ ├── MacSupplierTest.java │ │ ├── MasterkeyFileAccessTest.java │ │ ├── MasterkeyFileTest.java │ │ ├── MasterkeyTest.java │ │ ├── MessageDigestSupplierTest.java │ │ ├── ObjectPoolTest.java │ │ ├── P384KeyPairTest.java │ │ ├── Pkcs12HelperTest.java │ │ ├── PooledSuppliersBenchmarkTest.java │ │ ├── ReseedingSecureRandomTest.java │ │ ├── ScryptTest.java │ │ ├── SecureRandomMock.java │ │ ├── SeekableByteChannelMock.java │ │ └── X509CertBuilderTest.java │ │ ├── v1 │ │ ├── BenchmarkTest.java │ │ ├── CryptorImplTest.java │ │ ├── CryptorProviderImplTest.java │ │ ├── FileContentCryptorImplBenchmark.java │ │ ├── FileContentCryptorImplTest.java │ │ ├── FileContentEncryptorBenchmark.java │ │ ├── FileHeaderCryptorBenchmark.java │ │ ├── FileHeaderCryptorImplTest.java │ │ ├── FileHeaderImplTest.java │ │ └── FileNameCryptorImplTest.java │ │ └── v2 │ │ ├── BenchmarkTest.java │ │ ├── CryptorImplTest.java │ │ ├── CryptorProviderImplTest.java │ │ ├── FileContentCryptorImplBenchmark.java │ │ ├── FileContentCryptorImplTest.java │ │ ├── FileContentEncryptorBenchmark.java │ │ ├── FileHeaderCryptorBenchmark.java │ │ ├── FileHeaderCryptorImplTest.java │ │ ├── FileHeaderImplTest.java │ │ └── FileNameCryptorImplTest.java │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker └── suppression.xml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | groups: 8 | java-test-dependencies: 9 | patterns: 10 | - "org.junit.jupiter:*" 11 | - "org.mockito:*" 12 | - "org.hamcrest:*" 13 | - "org.openjdk.jmh:*" 14 | maven-build-plugins: 15 | patterns: 16 | - "org.apache.maven.plugins:*" 17 | - "org.codehaus.mojo:exec-maven-plugin" 18 | - "org.jacoco:jacoco-maven-plugin" 19 | - "org.owasp:dependency-check-maven" 20 | - "org.sonatype.plugins:nexus-staging-maven-plugin" 21 | java-production-dependencies: 22 | patterns: 23 | - "*" 24 | exclude-patterns: 25 | - "org.junit.jupiter:*" 26 | - "org.mockito:*" 27 | - "org.hamcrest:*" 28 | - "org.openjdk.jmh:*" 29 | - "org.apache.maven.plugins:*" 30 | - "org.codehaus.mojo:exec-maven-plugin" 31 | - "org.jacoco:jacoco-maven-plugin" 32 | - "org.owasp:dependency-check-maven" 33 | - "org.sonatype.plugins:nexus-staging-maven-plugin" 34 | 35 | 36 | - package-ecosystem: "github-actions" 37 | directory: "/" # even for `.github/workflows` 38 | schedule: 39 | interval: "monthly" 40 | groups: 41 | github-actions: 42 | patterns: 43 | - "*" 44 | labels: 45 | - "ci" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | pull_request_target: 5 | types: [labeled] 6 | jobs: 7 | build: 8 | name: Build and Test 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - uses: actions/setup-java@v4 15 | with: 16 | java-version: 23 17 | distribution: 'temurin' 18 | cache: 'maven' 19 | - name: Cache SonarCloud packages 20 | uses: actions/cache@v4 21 | with: 22 | path: ~/.sonar/cache 23 | key: ${{ runner.os }}-sonar 24 | restore-keys: ${{ runner.os }}-sonar 25 | - name: Ensure to use tagged version 26 | if: startsWith(github.ref, 'refs/tags/') 27 | run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} 28 | - name: Build and Test 29 | id: buildAndTest 30 | run: > 31 | mvn -B verify 32 | jacoco:report 33 | org.sonarsource.scanner.maven:sonar-maven-plugin:sonar 34 | -Pcoverage 35 | -Dsonar.projectKey=cryptomator_cryptolib 36 | -Dsonar.organization=cryptomator 37 | -Dsonar.host.url=https://sonarcloud.io 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 40 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 41 | - uses: actions/upload-artifact@v4 42 | with: 43 | name: artifacts 44 | path: target/*.jar 45 | - name: Create Release 46 | uses: softprops/action-gh-release@v2 47 | if: startsWith(github.ref, 'refs/tags/') 48 | with: 49 | prerelease: true 50 | token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} 51 | generate_release_notes: true 52 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | 2 | name: "CodeQL" 3 | 4 | on: 5 | push: 6 | branches: [develop, main] 7 | pull_request: 8 | branches: [develop] 9 | schedule: 10 | - cron: '0 6 * * 0' 11 | 12 | jobs: 13 | analyse: 14 | name: Analyse 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 2 20 | - uses: actions/setup-java@v4 21 | with: 22 | java-version: 23 23 | distribution: 'temurin' 24 | cache: 'maven' 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v3 27 | with: 28 | languages: java 29 | - name: Build and Test 30 | run: mvn -B install -DskipTests 31 | - name: Perform CodeQL Analysis 32 | uses: github/codeql-action/analyze@v3 -------------------------------------------------------------------------------- /.github/workflows/dependency-check.yml: -------------------------------------------------------------------------------- 1 | name: OWASP Maven Dependency Check 2 | on: 3 | schedule: 4 | - cron: '0 12 * * 0' 5 | push: 6 | branches: 7 | - 'release/**' 8 | workflow_dispatch: 9 | 10 | 11 | jobs: 12 | check-dependencies: 13 | uses: skymatic/workflows/.github/workflows/run-dependency-check.yml@v1 14 | with: 15 | runner-os: 'ubuntu-latest' 16 | java-distribution: 'temurin' 17 | java-version: 23 18 | secrets: 19 | nvd-api-key: ${{ secrets.NVD_API_KEY }} 20 | slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} 21 | -------------------------------------------------------------------------------- /.github/workflows/publish-central.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Maven Central 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-java@v4 11 | with: 12 | java-version: 23 13 | distribution: 'temurin' 14 | cache: 'maven' 15 | server-id: central 16 | server-username: MAVEN_CENTRAL_USERNAME 17 | server-password: MAVEN_CENTRAL_PASSWORD 18 | - name: Enforce project version ${{ github.event.release.tag_name }} 19 | run: mvn versions:set -B -DnewVersion="$GIT_TAG" 20 | env: 21 | GIT_TAG: ${{ github.event.release.tag_name }} 22 | - name: Deploy 23 | run: mvn deploy -B -DskipTests -Psign,deploy-central --no-transfer-progress 24 | env: 25 | MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 26 | MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 27 | MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} 28 | MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} 29 | MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} 30 | -------------------------------------------------------------------------------- /.github/workflows/publish-github.yml: -------------------------------------------------------------------------------- 1 | name: Publish to GitHub Packages 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | if: startsWith(github.ref, 'refs/tags/') # only allow publishing tagged versions 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-java@v4 12 | with: 13 | java-version: 23 14 | distribution: 'temurin' 15 | cache: 'maven' 16 | - name: Enforce project version ${{ github.event.release.tag_name }} 17 | run: mvn versions:set -B -DnewVersion=${{ github.event.release.tag_name }} 18 | - name: Deploy 19 | run: mvn deploy -B -DskipTests -Psign,deploy-github --no-transfer-progress 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} 23 | MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} 24 | MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.jar 3 | 4 | # Maven # 5 | target/ 6 | pom.xml.versionsBackup 7 | 8 | # IntelliJ Settings Files (https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems) # 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/dictionaries 12 | .idea/**/libraries/ 13 | .idea/jarRepositories.xml 14 | .idea/encodings.xml 15 | .idea/modules.xml 16 | .idea/compiler.xml 17 | .idea/inspectionProfiles/ 18 | *.iml 19 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | cryptolib -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/cryptomator/cryptolib/workflows/Build/badge.svg)](https://github.com/cryptomator/cryptolib/actions?query=workflow%3ABuild) 2 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=cryptomator_cryptolib&metric=alert_status)](https://sonarcloud.io/dashboard?id=cryptomator_cryptolib) 3 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=cryptomator_cryptolib&metric=coverage)](https://sonarcloud.io/dashboard?id=cryptomator_cryptolib) 4 | [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=cryptomator_cryptolib&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=cryptomator_cryptolib) 5 | [![Maven Central](https://img.shields.io/maven-central/v/org.cryptomator/cryptolib.svg?maxAge=86400)](https://repo1.maven.org/maven2/org/cryptomator/cryptolib/) 6 | [![Javadocs](http://www.javadoc.io/badge/org.cryptomator/cryptolib.svg)](http://www.javadoc.io/doc/org.cryptomator/cryptolib) 7 | 8 | # Cryptomator Crypto Library 9 | 10 | This library contains all cryptographic functions that are used by Cryptomator. The purpose of this project is to provide a separate light-weight library with its own release cycle that can be used in other projects, too. 11 | 12 | ## Audits 13 | 14 | - [Version 1.1.5 audit by Cure53](https://cryptomator.org/audits/2017-11-27%20crypto%20cure53.pdf) 15 | 16 | | Finding | Comment | 17 | |---|---| 18 | | 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. | 19 | | 1u1-22-002 | This issue is related to [siv-mode](https://github.com/cryptomator/siv-mode/). | 20 | 21 | ## License 22 | 23 | This project is dual-licensed under the AGPLv3 for FOSS projects as well as a commercial license derived from the LGPL for independent software vendors and resellers. If you want to use this library in applications that are *not* licensed under the AGPL, feel free to contact our [sales team](https://cryptomator.org/enterprise/). 24 | 25 | --- 26 | 27 | 1 The Cure53 pentesting was performed during the development of the apps for 1&1 Mail & Media GmbH. 28 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/AuthenticationFailedException.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * This file is licensed under the terms of the MIT license. 4 | * See the LICENSE.txt file for more info. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.api; 10 | 11 | public class AuthenticationFailedException extends CryptoException { 12 | 13 | public AuthenticationFailedException(String message) { 14 | super(message); 15 | } 16 | 17 | public AuthenticationFailedException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/CryptoException.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * This file is licensed under the terms of the MIT license. 4 | * See the LICENSE.txt file for more info. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.api; 10 | 11 | public abstract class CryptoException extends RuntimeException { 12 | 13 | protected CryptoException() { 14 | super(); 15 | } 16 | 17 | protected CryptoException(String message) { 18 | super(message); 19 | } 20 | 21 | protected CryptoException(String message, Throwable cause) { 22 | super(message, cause); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/Cryptor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.api; 10 | 11 | import javax.security.auth.Destroyable; 12 | 13 | public interface Cryptor extends Destroyable, AutoCloseable { 14 | 15 | FileContentCryptor fileContentCryptor(); 16 | 17 | FileHeaderCryptor fileHeaderCryptor(); 18 | 19 | FileNameCryptor fileNameCryptor(); 20 | 21 | @Override 22 | void destroy(); 23 | 24 | /** 25 | * Calls {@link #destroy()}. 26 | */ 27 | @Override 28 | default void close() { 29 | destroy(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/CryptorProvider.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.api; 10 | 11 | import java.security.SecureRandom; 12 | import java.util.ServiceLoader; 13 | 14 | public interface CryptorProvider { 15 | 16 | /** 17 | * A combination of ciphers to use for filename and file content encryption 18 | */ 19 | enum Scheme { 20 | /** 21 | * AES-SIV for file name encryption 22 | * AES-CTR + HMAC for content encryption 23 | */ 24 | SIV_CTRMAC, 25 | 26 | /** 27 | * AES-SIV for file name encryption 28 | * AES-GCM for content encryption 29 | */ 30 | SIV_GCM 31 | } 32 | 33 | /** 34 | * Finds a CryptorProvider implementation for the given combination of ciphers. 35 | * 36 | * @param scheme A cipher combination 37 | * @return A CryptorProvider implementation supporting the requestes scheme 38 | * @throws UnsupportedOperationException If the scheme is not implemented 39 | */ 40 | static CryptorProvider forScheme(Scheme scheme) { 41 | for (CryptorProvider provider : ServiceLoader.load(CryptorProvider.class)) { 42 | if (provider.scheme().equals(scheme)) { 43 | return provider; 44 | } 45 | } 46 | throw new UnsupportedOperationException("Scheme not supported: " + scheme.name()); 47 | } 48 | 49 | /** 50 | * @return The combination of ciphers used by this CryptorProvider implementation. 51 | */ 52 | Scheme scheme(); 53 | 54 | /** 55 | * Creates a new Cryptor instance for the given key 56 | * 57 | * @param masterkey The key used by the returned cryptor during encryption and decryption 58 | * @param random A native (if possible) SecureRandom used to seed internal CSPRNGs 59 | * @return A new cryptor 60 | */ 61 | Cryptor provide(Masterkey masterkey, SecureRandom random); 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/FileHeader.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.api; 10 | 11 | public interface FileHeader { 12 | 13 | /** 14 | * Returns the value of a currently unused 64 bit field in the file header. 15 | *

16 | * Formerly used for storing the plaintext file size. 17 | * 18 | * @return 64 bit integer for future use. 19 | * @deprecated Don't rely on this method. It may be redefined any time. 20 | */ 21 | @Deprecated 22 | long getReserved(); 23 | 24 | /** 25 | * Sets the 64 bit field in the file header. 26 | *

27 | * Formerly used for storing the plaintext file size. 28 | * 29 | * @param reserved 64 bit integer for future use 30 | * @deprecated Don't rely on this method. It may be redefined any time. 31 | */ 32 | @Deprecated 33 | void setReserved(long reserved); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/FileHeaderCryptor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.api; 10 | 11 | import java.nio.ByteBuffer; 12 | 13 | public interface FileHeaderCryptor { 14 | 15 | FileHeader create(); 16 | 17 | int headerSize(); 18 | 19 | /** 20 | * @param header The header to encrypt. 21 | * @return A buffer containing the encrypted header. The position of this buffer is 0 and its limit is at the end of the header. 22 | */ 23 | ByteBuffer encryptHeader(FileHeader header); 24 | 25 | FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws AuthenticationFailedException; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/FileNameCryptor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2015, 2016 Sebastian Stenzel and others. 3 | * This file is licensed under the terms of the MIT license. 4 | * See the LICENSE.txt file for more info. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.api; 10 | 11 | import com.google.common.io.BaseEncoding; 12 | 13 | /** 14 | * Provides deterministic encryption capabilities as filenames must not change on subsequent encryption attempts, 15 | * otherwise each change results in major directory structure changes which would be a terrible idea for cloud storage encryption. 16 | * 17 | * @see Wikipedia on deterministic encryption 18 | */ 19 | public interface FileNameCryptor { 20 | 21 | /** 22 | * @param cleartextDirectoryId an arbitrary directory id to be passed to one-way hash function 23 | * @return constant length string, that is unlikely to collide with any other name. 24 | */ 25 | String hashDirectoryId(String cleartextDirectoryId); 26 | 27 | /** 28 | * @param encoding Encoding to use to encode the returned ciphertext 29 | * @param cleartextName original filename including cleartext file extension 30 | * @param associatedData optional associated data, that will not get encrypted but needs to be provided during decryption 31 | * @return encrypted filename without any file extension 32 | */ 33 | String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData); 34 | 35 | /** 36 | * @param encoding Encoding to use to decode ciphertextName 37 | * @param ciphertextName Ciphertext only, with any additional strings like file extensions stripped first. 38 | * @param associatedData the same associated data used during encryption, otherwise and {@link AuthenticationFailedException} will be thrown 39 | * @return cleartext filename, probably including its cleartext file extension. 40 | * @throws AuthenticationFailedException if the ciphertext is malformed 41 | */ 42 | String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/InvalidPassphraseException.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * This file is licensed under the terms of the MIT license. 4 | * See the LICENSE.txt file for more info. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.api; 10 | 11 | public class InvalidPassphraseException extends MasterkeyLoadingFailedException { 12 | 13 | public InvalidPassphraseException() { 14 | super(null); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/Masterkey.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.api; 2 | 3 | import com.google.common.base.Preconditions; 4 | import org.cryptomator.cryptolib.common.DestroyableSecretKey; 5 | 6 | import java.security.SecureRandom; 7 | import java.util.Arrays; 8 | 9 | public class Masterkey extends DestroyableSecretKey { 10 | 11 | private static final String KEY_ALGORITHM = "MASTERKEY"; 12 | public static final String ENC_ALG = "AES"; 13 | public static final String MAC_ALG = "HmacSHA256"; 14 | public static final int SUBKEY_LEN_BYTES = 32; 15 | 16 | public Masterkey(byte[] key) { 17 | super(checkKeyLength(key), KEY_ALGORITHM); 18 | } 19 | 20 | private static byte[] checkKeyLength(byte[] key) { 21 | Preconditions.checkArgument(key.length == SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES, "Invalid raw key length %s", key.length); 22 | return key; 23 | } 24 | 25 | public static Masterkey generate(SecureRandom csprng) { 26 | byte[] key = new byte[SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES]; 27 | try { 28 | csprng.nextBytes(key); 29 | return new Masterkey(key); 30 | } finally { 31 | Arrays.fill(key, (byte) 0x00); 32 | } 33 | } 34 | 35 | public static Masterkey from(DestroyableSecretKey encKey, DestroyableSecretKey macKey) { 36 | Preconditions.checkArgument(encKey.getEncoded().length == SUBKEY_LEN_BYTES, "Invalid key length of encKey"); 37 | Preconditions.checkArgument(macKey.getEncoded().length == SUBKEY_LEN_BYTES, "Invalid key length of macKey"); 38 | byte[] key = new byte[SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES]; 39 | try { 40 | System.arraycopy(encKey.getEncoded(), 0, key, 0, SUBKEY_LEN_BYTES); 41 | System.arraycopy(macKey.getEncoded(), 0, key, SUBKEY_LEN_BYTES, SUBKEY_LEN_BYTES); 42 | return new Masterkey(key); 43 | } finally { 44 | Arrays.fill(key, (byte) 0x00); 45 | } 46 | } 47 | 48 | @Override 49 | public Masterkey copy() { 50 | return new Masterkey(getEncoded()); 51 | } 52 | 53 | /** 54 | * Get the encryption subkey. 55 | * 56 | * @return A new copy of the subkey used for encryption 57 | */ 58 | public DestroyableSecretKey getEncKey() { 59 | return new DestroyableSecretKey(getEncoded(), 0, SUBKEY_LEN_BYTES, ENC_ALG); 60 | } 61 | 62 | /** 63 | * Get the MAC subkey. 64 | * 65 | * @return A new copy of the subkey used for message authentication 66 | */ 67 | public DestroyableSecretKey getMacKey() { 68 | return new DestroyableSecretKey(getEncoded(), SUBKEY_LEN_BYTES, SUBKEY_LEN_BYTES, MAC_ALG); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoader.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.api; 2 | 3 | import org.cryptomator.cryptolib.common.MasterkeyFileAccess; 4 | 5 | import java.net.URI; 6 | 7 | /** 8 | * Masterkey loaders load keys to unlock Cryptomator vaults. 9 | * 10 | * @see MasterkeyFileAccess 11 | */ 12 | @FunctionalInterface 13 | public interface MasterkeyLoader { 14 | 15 | /** 16 | * Loads a master key. This might be a long-running operation, as it may require user input or expensive computations. 17 | *

18 | * 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: 19 | *

20 | 	 * {@code
21 | 	 * Masterkeyloader keyLoader;
22 | 	 * URI keyId;
23 | 	 * try (Masterkey key = keyLoader.loadKey(keyId) ){
24 | 	 *     // Do stuff with the key
25 | 	 * }
26 | 	 * }
27 | 	 * 
28 | * 29 | * @param keyId An URI uniquely identifying the source and identity of the key 30 | * @return a {@link Masterkey} object wrapping the raw key bytes. Must not be null 31 | * @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request 32 | */ 33 | Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/MasterkeyLoadingFailedException.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.api; 2 | 3 | public class MasterkeyLoadingFailedException extends CryptoException { 4 | 5 | public MasterkeyLoadingFailedException(String message, Throwable cause) { 6 | super(message, cause); 7 | } 8 | 9 | public MasterkeyLoadingFailedException(String message) { 10 | super(message); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/UnsupportedVaultFormatException.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * This file is licensed under the terms of the MIT license. 4 | * See the LICENSE.txt file for more info. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.api; 10 | 11 | public class UnsupportedVaultFormatException extends CryptoException { 12 | 13 | private final Integer detectedVersion; 14 | private final Integer supportedVersion; 15 | 16 | public UnsupportedVaultFormatException(Integer detectedVersion, Integer supportedVersion) { 17 | super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion); 18 | this.detectedVersion = detectedVersion; 19 | this.supportedVersion = supportedVersion; 20 | } 21 | 22 | public Integer getDetectedVersion() { 23 | return detectedVersion; 24 | } 25 | 26 | public Integer getSupportedVersion() { 27 | return supportedVersion; 28 | } 29 | 30 | public boolean isVaultOlderThanSoftware() { 31 | return detectedVersion == null || detectedVersion < supportedVersion; 32 | } 33 | 34 | public boolean isSoftwareOlderThanVault() { 35 | return detectedVersion > supportedVersion; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/api/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * High-level encryption library used in Cryptomator. 3 | *

4 | * Example Usage: 5 | * 6 | *

 7 |  * // Define a pepper used during JSON serialization:
 8 |  * MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(pepper, csprng);
 9 |  *
10 |  * // Create new masterkey and safe it to a file:
11 |  * SecureRandom csprng = SecureRandom.getInstanceStrong();
12 |  * Masterkey masterkey = {@link org.cryptomator.cryptolib.api.Masterkey#generate(java.security.SecureRandom) Masterkey.generate(csprng)};
13 |  * {@link org.cryptomator.cryptolib.common.MasterkeyFileAccess#persist(org.cryptomator.cryptolib.api.Masterkey, java.nio.file.Path, java.lang.CharSequence) masterkeyFileAccess.persist(masterkey, path, passphrase)};
14 |  *
15 |  * // Load a masterkey from a file:
16 |  * Masterkey masterkey = {@link org.cryptomator.cryptolib.common.MasterkeyFileAccess#load(java.nio.file.Path, java.lang.CharSequence) masterkeyFileAccess.load(path, passphrase)};
17 |  *
18 |  * // Create new cryptor:
19 |  * {@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)};
20 |  *
21 |  * // Each directory needs a (relatively) unique ID, which affects the encryption/decryption of child names:
22 |  * String uniqueIdOfDirectory = UUID.randomUUID().toString();
23 |  *
24 |  * // Encrypt and decrypt file name:
25 |  * String cleartextFileName = "foo.txt";
26 |  * 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())};
27 |  * String decryptedName = cryptor.fileNameCryptor().{@link org.cryptomator.cryptolib.api.FileNameCryptor#decryptFilename(com.google.common.io.BaseEncoding, String, byte[][])  decryptFilename(base32, encryptedName, uniqueIdOfDirectory.getBytes())};
28 |  *
29 |  * // Encrypt file contents:
30 |  * ByteBuffer plaintext = ...;
31 |  * SeekableByteChannel ciphertextOut = ...;
32 |  * try (WritableByteChannel ch = new {@link org.cryptomator.cryptolib.common.EncryptingWritableByteChannel EncryptingWritableByteChannel}(ciphertextOut, cryptor)) {
33 |  * 	ch.write(plaintext);
34 |  * }
35 |  *
36 |  * // Decrypt file contents:
37 |  * ReadableByteChannel ciphertextIn = ...;
38 |  * try (ReadableByteChannel ch = new {@link org.cryptomator.cryptolib.common.DecryptingReadableByteChannel DecryptingReadableByteChannel}(ciphertextOut, cryptor, true)) {
39 |  * 	ch.read(plaintext);
40 |  * }
41 |  * 
42 | */ 43 | package org.cryptomator.cryptolib.api; 44 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/AesKeyWrap.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import javax.crypto.Cipher; 12 | import javax.crypto.IllegalBlockSizeException; 13 | import javax.crypto.SecretKey; 14 | import java.security.InvalidKeyException; 15 | import java.security.NoSuchAlgorithmException; 16 | 17 | public class AesKeyWrap { 18 | 19 | private AesKeyWrap() { 20 | } 21 | 22 | /** 23 | * @param kek Key encrypting key 24 | * @param key Key to be wrapped 25 | * @return Wrapped key 26 | */ 27 | public static byte[] wrap(DestroyableSecretKey kek, SecretKey key) { 28 | try (DestroyableSecretKey kekCopy = kek.copy(); 29 | ObjectPool.Lease cipher = CipherSupplier.RFC3394_KEYWRAP.keyWrapCipher(kekCopy)) { 30 | return cipher.get().wrap(key); 31 | } catch (InvalidKeyException | IllegalBlockSizeException e) { 32 | throw new IllegalArgumentException("Unable to wrap key.", e); 33 | } 34 | } 35 | 36 | /** 37 | * @param kek Key encrypting key 38 | * @param wrappedKey Key to be unwrapped 39 | * @param wrappedKeyAlgorithm Key designation, i.e. algorithm to be associated with the unwrapped key. 40 | * @return Unwrapped key 41 | * @throws InvalidKeyException If unwrapping failed (i.e. wrong kek) 42 | */ 43 | public static DestroyableSecretKey unwrap(DestroyableSecretKey kek, byte[] wrappedKey, String wrappedKeyAlgorithm) throws InvalidKeyException { 44 | return unwrap(kek, wrappedKey, wrappedKeyAlgorithm, Cipher.SECRET_KEY); 45 | } 46 | 47 | // visible for testing 48 | static DestroyableSecretKey unwrap(DestroyableSecretKey kek, byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType) throws InvalidKeyException { 49 | try (DestroyableSecretKey kekCopy = kek.copy(); 50 | ObjectPool.Lease cipher = CipherSupplier.RFC3394_KEYWRAP.keyUnwrapCipher(kekCopy)) { 51 | return DestroyableSecretKey.from(cipher.get().unwrap(wrappedKey, wrappedKeyAlgorithm, wrappedKeyType)); 52 | } catch (NoSuchAlgorithmException e) { 53 | throw new IllegalArgumentException("Invalid algorithm: " + wrappedKeyAlgorithm, e); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/ByteBuffers.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2015, 2016 Sebastian Stenzel and others. 3 | * This file is licensed under the terms of the MIT license. 4 | * See the LICENSE.txt file for more info. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import java.io.IOException; 12 | import java.nio.ByteBuffer; 13 | import java.nio.channels.ReadableByteChannel; 14 | 15 | public class ByteBuffers { 16 | 17 | private ByteBuffers() { 18 | } 19 | 20 | /** 21 | * Copies as many bytes as possible from the given source to the destination buffer. 22 | * The position of both buffers will be incremented by as many bytes as have been copied. 23 | * 24 | * @param source ByteBuffer from which bytes are read 25 | * @param destination ByteBuffer into which bytes are written 26 | * @return number of bytes copied, i.e. {@link ByteBuffer#remaining() source.remaining()} or {@link ByteBuffer#remaining() destination.remaining()}, whatever is less. 27 | */ 28 | public static int copy(ByteBuffer source, ByteBuffer destination) { 29 | final int numBytes = Math.min(source.remaining(), destination.remaining()); 30 | final ByteBuffer tmp = source.asReadOnlyBuffer(); 31 | tmp.limit(tmp.position() + numBytes); 32 | destination.put(tmp); 33 | source.position(tmp.position()); // until now only tmp pos has been incremented, so we need to adjust the position 34 | return numBytes; 35 | } 36 | 37 | /** 38 | * Fills the given buffer by reading from the given source until either reaching EOF 39 | * or buffer has no more {@link ByteBuffer#hasRemaining() remaining space}. 40 | * 41 | * @param source The channel to read from 42 | * @param buffer The buffer to fill 43 | * @return Number of bytes read. Will only be less than remaining space in buffer if reaching EOF. 44 | * @throws IOException In case of I/O errors 45 | */ 46 | public static int fill(ReadableByteChannel source, ByteBuffer buffer) throws IOException { 47 | final int requested = buffer.remaining(); 48 | while (buffer.hasRemaining()) { 49 | int read = source.read(buffer); 50 | if (read == -1) { // EOF 51 | break; 52 | } 53 | } 54 | return requested - buffer.remaining(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/DecryptingReadableByteChannel.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException; 12 | import org.cryptomator.cryptolib.api.Cryptor; 13 | import org.cryptomator.cryptolib.api.FileHeader; 14 | 15 | import java.io.EOFException; 16 | import java.io.IOException; 17 | import java.nio.ByteBuffer; 18 | import java.nio.channels.ReadableByteChannel; 19 | 20 | public class DecryptingReadableByteChannel implements ReadableByteChannel { 21 | 22 | private final ReadableByteChannel delegate; 23 | private final Cryptor cryptor; 24 | private final boolean authenticate; 25 | private ByteBuffer cleartextChunk; 26 | private FileHeader header; 27 | private boolean reachedEof; 28 | private long chunk; 29 | 30 | /** 31 | * Creates a DecryptingReadableByteChannel that decrypts a whole ciphertext file beginning at its first byte. 32 | * 33 | * @param src A ciphertext channel positioned at the begin of the file header 34 | * @param cryptor The cryptor to use 35 | * @param authenticate Set to false to skip ciphertext authentication (may not be supported) 36 | */ 37 | public DecryptingReadableByteChannel(ReadableByteChannel src, Cryptor cryptor, boolean authenticate) { 38 | this(src, cryptor, authenticate, null, 0); 39 | } 40 | 41 | /** 42 | * Creates a DecryptingReadableByteChannel with a previously read header, allowing to start decryption at any chunk. 43 | * 44 | * @param src A ciphertext channel positioned at the beginning of the given firstChunk 45 | * @param cryptor The cryptor to use 46 | * @param authenticate Set to false to skip ciphertext authentication (may not be supported) 47 | * @param header The file's header 48 | * @param firstChunk The index of the chunk at which the src channel is positioned 49 | */ 50 | public DecryptingReadableByteChannel(ReadableByteChannel src, Cryptor cryptor, boolean authenticate, FileHeader header, long firstChunk) { 51 | this.delegate = src; 52 | this.cryptor = cryptor; 53 | this.authenticate = authenticate; 54 | this.cleartextChunk = ByteBuffer.allocate(0); // empty buffer will trigger loadNextCleartextChunk() on first access. 55 | this.header = header; 56 | this.reachedEof = false; 57 | this.chunk = firstChunk; 58 | } 59 | 60 | @Override 61 | public boolean isOpen() { 62 | return delegate.isOpen(); 63 | } 64 | 65 | @Override 66 | public void close() throws IOException { 67 | delegate.close(); 68 | } 69 | 70 | @Override 71 | public synchronized int read(ByteBuffer dst) throws IOException { 72 | try { 73 | loadHeaderIfNecessary(); 74 | if (reachedEof) { 75 | return -1; 76 | } else { 77 | return readInternal(dst); 78 | } 79 | } catch (AuthenticationFailedException e) { 80 | throw new IOException("Unauthentic ciphertext", e); 81 | } 82 | } 83 | 84 | private int readInternal(ByteBuffer dst) throws IOException, AuthenticationFailedException { 85 | assert header != null : "header must be initialized"; 86 | 87 | int result = 0; 88 | while (dst.hasRemaining() && !reachedEof) { 89 | if (cleartextChunk.hasRemaining() || loadNextCleartextChunk()) { 90 | result += ByteBuffers.copy(cleartextChunk, dst); 91 | } else { 92 | assert reachedEof : "no further cleartext available"; 93 | } 94 | } 95 | return result; 96 | } 97 | 98 | private void loadHeaderIfNecessary() throws IOException, AuthenticationFailedException { 99 | if (header == null) { 100 | ByteBuffer headerBuf = ByteBuffer.allocate(cryptor.fileHeaderCryptor().headerSize()); 101 | int read = ByteBuffers.fill(delegate, headerBuf); 102 | if (read != headerBuf.capacity()) { 103 | throw new EOFException("Unable to read header from channel."); 104 | } 105 | headerBuf.flip(); 106 | header = cryptor.fileHeaderCryptor().decryptHeader(headerBuf); 107 | } 108 | } 109 | 110 | private boolean loadNextCleartextChunk() throws IOException, AuthenticationFailedException { 111 | ByteBuffer ciphertextChunk = ByteBuffer.allocate(cryptor.fileContentCryptor().ciphertextChunkSize()); 112 | int read = ByteBuffers.fill(delegate, ciphertextChunk); 113 | if (read == 0) { 114 | reachedEof = true; 115 | return false; 116 | } else { 117 | ciphertextChunk.flip(); 118 | cleartextChunk = cryptor.fileContentCryptor().decryptChunk(ciphertextChunk, chunk++, header, authenticate); 119 | return true; 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import com.google.common.base.Preconditions; 4 | 5 | import javax.crypto.SecretKey; 6 | import javax.security.auth.Destroyable; 7 | import java.security.Key; 8 | import java.security.MessageDigest; 9 | import java.security.SecureRandom; 10 | import java.util.Arrays; 11 | import java.util.Objects; 12 | 13 | /** 14 | * A {@link SecretKey} that (other than JDK's SecretKeySpec) 15 | * actually implements {@link Destroyable}. 16 | *

17 | * Furthermore, this implementation will not create copies when accessing {@link #getEncoded()}. 18 | * Instead it implements {@link #copy} and {@link AutoCloseable} in an exception-free manner. To prevent mutation of the exposed key, 19 | * you would want to make sure to always work on scoped copies, such as in this example: 20 | * 21 | *

 22 |  *     // copy "key" to protect it from unwanted modifications:
 23 |  *     try (DestroyableSecretKey k = key.copy()) {
 24 |  *         // use "k":
 25 |  *         Cipher cipher = Cipher.init(k, ...)
 26 |  *         cipher.doFinal(...)
 27 |  *     } // "k" will get destroyed here
 28 |  * 
29 | */ 30 | public class DestroyableSecretKey implements SecretKey, AutoCloseable { 31 | 32 | private static final String KEY_DESTROYED_ERROR = "Key has been destroyed"; 33 | 34 | private final transient byte[] key; 35 | private final String algorithm; 36 | private boolean destroyed; 37 | 38 | /** 39 | * Convenience constructor for {@link #DestroyableSecretKey(byte[], int, int, String)} 40 | * 41 | * @param key The raw key data (will get copied) 42 | * @param algorithm The {@link #getAlgorithm() algorithm name} 43 | */ 44 | public DestroyableSecretKey(byte[] key, String algorithm) { 45 | this(key, 0, key.length, algorithm); 46 | } 47 | 48 | /** 49 | * Creates a new destroyable secret key, copying of the provided raw key bytes. 50 | * 51 | * @param key A byte[] holding the key material (relevant part will get copied) 52 | * @param offset The offset within key where the key starts 53 | * @param len The number of bytes beginning at offset to read from key 54 | * @param algorithm The {@link #getAlgorithm() algorithm name} 55 | */ 56 | public DestroyableSecretKey(byte[] key, int offset, int len, String algorithm) { 57 | Preconditions.checkArgument(offset >= 0, "Invalid offset"); 58 | Preconditions.checkArgument(len >= 0, "Invalid length"); 59 | Preconditions.checkArgument(key.length >= offset + len, "Invalid offset/len"); 60 | this.key = new byte[len]; 61 | this.algorithm = Preconditions.checkNotNull(algorithm, "Algorithm must not be null"); 62 | this.destroyed = false; 63 | System.arraycopy(key, offset, this.key, 0, len); 64 | } 65 | 66 | /** 67 | * Casts or converts a given {@link SecretKey} to a DestroyableSecretKey 68 | * 69 | * @param secretKey The secret key 70 | * @return Either the provided or a new key, depending on whether the provided key is already a DestroyableSecretKey 71 | */ 72 | public static DestroyableSecretKey from(Key secretKey) { 73 | if (secretKey instanceof DestroyableSecretKey) { 74 | return (DestroyableSecretKey) secretKey; 75 | } else { 76 | return new DestroyableSecretKey(secretKey.getEncoded(), secretKey.getAlgorithm()); 77 | } 78 | } 79 | 80 | /** 81 | * Creates a new key of given length and for use with given algorithm using entropy from the given csprng. 82 | * 83 | * @param csprng A cryptographically secure random number source 84 | * @param algorithm The {@link #getAlgorithm() key algorithm} 85 | * @param keyLenBytes The length of the key (in bytes) 86 | * @return A new secret key 87 | */ 88 | public static DestroyableSecretKey generate(SecureRandom csprng, String algorithm, int keyLenBytes) { 89 | byte[] key = new byte[keyLenBytes]; 90 | try { 91 | csprng.nextBytes(key); 92 | return new DestroyableSecretKey(key, algorithm); 93 | } finally { 94 | Arrays.fill(key, (byte) 0x00); 95 | } 96 | } 97 | 98 | @Override 99 | public String getAlgorithm() { 100 | Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR); 101 | return algorithm; 102 | } 103 | 104 | @Override 105 | public String getFormat() { 106 | Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR); 107 | return "RAW"; 108 | } 109 | 110 | /** 111 | * Returns the raw key bytes this instance wraps. 112 | *

113 | * Important: Any change to the returned array will reflect in this key. Make sure to 114 | * {@link #copy() make a local copy} if you can't rule out mutations. 115 | * 116 | * @return A byte array holding the secret key 117 | */ 118 | @Override 119 | public byte[] getEncoded() { 120 | Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR); 121 | return key; 122 | } 123 | 124 | /** 125 | * Returns an independent copy of this key 126 | * @return New copy of this 127 | */ 128 | public DestroyableSecretKey copy() { 129 | Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR); 130 | return new DestroyableSecretKey(key, algorithm); // key will get copied by the constructor as per contract 131 | } 132 | 133 | @Override 134 | public void destroy() { 135 | Arrays.fill(key, (byte) 0x00); 136 | destroyed = true; 137 | } 138 | 139 | @Override 140 | public boolean isDestroyed() { 141 | return destroyed; 142 | } 143 | 144 | /** 145 | * Same as {@link #destroy()} 146 | */ 147 | @Override 148 | public void close() { 149 | destroy(); 150 | } 151 | 152 | @Override 153 | public boolean equals(Object o) { 154 | if (this == o) return true; 155 | if (o == null || getClass() != o.getClass()) return false; 156 | DestroyableSecretKey that = (DestroyableSecretKey) o; 157 | return algorithm.equals(that.algorithm) && MessageDigest.isEqual(this.key, that.key); 158 | } 159 | 160 | @Override 161 | public int hashCode() { 162 | int result = Objects.hash(algorithm); 163 | result = 31 * result + Arrays.hashCode(key); 164 | return result; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/Destroyables.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import javax.security.auth.DestroyFailedException; 4 | import javax.security.auth.Destroyable; 5 | 6 | public class Destroyables { 7 | 8 | private Destroyables() { 9 | } 10 | 11 | public static void destroySilently(Destroyable destroyable) { 12 | if (destroyable == null) { 13 | return; 14 | } 15 | try { 16 | destroyable.destroy(); 17 | } catch (DestroyFailedException e) { 18 | // no-op 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/ECKeyPair.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import com.google.common.base.Preconditions; 4 | 5 | import javax.security.auth.Destroyable; 6 | import java.math.BigInteger; 7 | import java.security.KeyPair; 8 | import java.security.MessageDigest; 9 | import java.security.PublicKey; 10 | import java.security.interfaces.ECPrivateKey; 11 | import java.security.interfaces.ECPublicKey; 12 | import java.security.spec.ECFieldFp; 13 | import java.security.spec.ECParameterSpec; 14 | import java.security.spec.ECPoint; 15 | import java.security.spec.EllipticCurve; 16 | import java.util.Arrays; 17 | import java.util.Objects; 18 | 19 | public class ECKeyPair implements Destroyable { 20 | 21 | private static final String INVALID_KEY_ERROR = "Invalid EC Key"; 22 | 23 | private final KeyPair keyPair; 24 | private boolean destroyed; 25 | 26 | ECKeyPair(KeyPair keyPair, ECParameterSpec curveParams) { 27 | Preconditions.checkArgument(keyPair.getPrivate() instanceof ECPrivateKey); 28 | Preconditions.checkArgument(keyPair.getPublic() instanceof ECPublicKey); 29 | this.keyPair = verify(keyPair, curveParams); 30 | } 31 | 32 | public KeyPair keyPair() { 33 | return keyPair; 34 | } 35 | 36 | public ECPrivateKey getPrivate() { 37 | Preconditions.checkState(!destroyed); 38 | assert keyPair.getPrivate() instanceof ECPrivateKey; 39 | return (ECPrivateKey) keyPair.getPrivate(); 40 | } 41 | 42 | public ECPublicKey getPublic() { 43 | Preconditions.checkState(!destroyed); 44 | assert keyPair.getPublic() instanceof ECPublicKey; 45 | return (ECPublicKey) keyPair.getPublic(); 46 | } 47 | 48 | // validations taken from https://neilmadden.blog/2017/05/17/so-how-do-you-validate-nist-ecdh-public-keys/ 49 | private static KeyPair verify(KeyPair keyPair, ECParameterSpec curveParams) { 50 | PublicKey pk = keyPair.getPublic(); 51 | Preconditions.checkArgument(pk instanceof ECPublicKey, INVALID_KEY_ERROR); 52 | Preconditions.checkArgument(curveParams.getCofactor() == 1, "Verifying points on curves with cofactor not supported"); // see "Step 4" in linked post 53 | ECPublicKey publicKey = (ECPublicKey) pk; 54 | EllipticCurve curve = curveParams.getCurve(); 55 | 56 | // Step 1: Verify public key is not point at infinity. 57 | Preconditions.checkArgument(!ECPoint.POINT_INFINITY.equals(publicKey.getW()), INVALID_KEY_ERROR); 58 | 59 | final BigInteger x = publicKey.getW().getAffineX(); 60 | final BigInteger y = publicKey.getW().getAffineY(); 61 | final BigInteger p = ((ECFieldFp) curve.getField()).getP(); 62 | 63 | // Step 2: Verify x and y are in range [0,p-1] 64 | Preconditions.checkArgument(x.compareTo(BigInteger.ZERO) >= 0 && x.compareTo(p) < 0, INVALID_KEY_ERROR); 65 | Preconditions.checkArgument(y.compareTo(BigInteger.ZERO) >= 0 && y.compareTo(p) < 0, INVALID_KEY_ERROR); 66 | 67 | // Step 3: Verify that y^2 == x^3 + ax + b (mod p) 68 | final BigInteger a = curve.getA(); 69 | final BigInteger b = curve.getB(); 70 | final BigInteger ySquared = y.modPow(BigInteger.valueOf(2), p); 71 | final BigInteger xCubedPlusAXPlusB = x.modPow(BigInteger.valueOf(3), p).add(a.multiply(x)).add(b).mod(p); 72 | Preconditions.checkArgument(ySquared.equals(xCubedPlusAXPlusB), INVALID_KEY_ERROR); 73 | 74 | return keyPair; 75 | } 76 | 77 | @Override 78 | public boolean isDestroyed() { 79 | return destroyed; 80 | } 81 | 82 | @Override 83 | public void destroy() { 84 | Destroyables.destroySilently(keyPair.getPrivate()); 85 | destroyed = true; 86 | } 87 | 88 | @Override 89 | public boolean equals(Object o) { 90 | if (this == o) return true; 91 | if (o == null || getClass() != o.getClass()) return false; 92 | ECKeyPair that = (ECKeyPair) o; 93 | return MessageDigest.isEqual(this.getPublic().getEncoded(), that.getPublic().getEncoded()); 94 | } 95 | 96 | @Override 97 | public int hashCode() { 98 | int result = Objects.hash(keyPair.getPublic().getAlgorithm()); 99 | result = 31 * result + Arrays.hashCode(keyPair.getPublic().getEncoded()); 100 | return result; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/EncryptingReadableByteChannel.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.cryptomator.cryptolib.api.Cryptor; 4 | import org.cryptomator.cryptolib.api.FileHeader; 5 | 6 | import java.io.IOException; 7 | import java.nio.ByteBuffer; 8 | import java.nio.channels.ReadableByteChannel; 9 | 10 | public class EncryptingReadableByteChannel implements ReadableByteChannel { 11 | 12 | private final ReadableByteChannel delegate; 13 | private final Cryptor cryptor; 14 | private final FileHeader header; 15 | 16 | private ByteBuffer ciphertextBuffer; 17 | private long chunk = 0; 18 | private boolean reachedEof; 19 | 20 | /** 21 | * Creates an EncryptingReadableByteChannel that encrypts a whole cleartext file beginning at its first byte. 22 | * 23 | * @param src A cleartext channel positioned at its begin 24 | * @param cryptor The cryptor to use 25 | */ 26 | public EncryptingReadableByteChannel(ReadableByteChannel src, Cryptor cryptor) { 27 | this.delegate = src; 28 | this.cryptor = cryptor; 29 | this.header = cryptor.fileHeaderCryptor().create(); 30 | this.ciphertextBuffer = cryptor.fileHeaderCryptor().encryptHeader(header); 31 | } 32 | 33 | @Override 34 | public boolean isOpen() { 35 | return delegate.isOpen(); 36 | } 37 | 38 | @Override 39 | public void close() throws IOException { 40 | delegate.close(); 41 | } 42 | 43 | @Override 44 | public synchronized int read(ByteBuffer dst) throws IOException { 45 | if (reachedEof) { 46 | return -1; 47 | } else { 48 | return readInternal(dst); 49 | } 50 | } 51 | 52 | private int readInternal(ByteBuffer dst) throws IOException { 53 | int result = 0; 54 | while (dst.hasRemaining() && !reachedEof) { 55 | if (ciphertextBuffer.hasRemaining() || loadNextCiphertextChunk()) { 56 | result += ByteBuffers.copy(ciphertextBuffer, dst); 57 | } else { 58 | assert reachedEof : "no further ciphertext available"; 59 | } 60 | } 61 | return result; 62 | } 63 | 64 | private boolean loadNextCiphertextChunk() throws IOException { 65 | ByteBuffer cleartextChunk = ByteBuffer.allocate(cryptor.fileContentCryptor().cleartextChunkSize()); 66 | int read = ByteBuffers.fill(delegate, cleartextChunk); 67 | if (read == 0) { 68 | reachedEof = true; 69 | return false; 70 | } else { 71 | cleartextChunk.flip(); 72 | ciphertextBuffer = cryptor.fileContentCryptor().encryptChunk(cleartextChunk, chunk++, header); 73 | return true; 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/EncryptingWritableByteChannel.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.cryptomator.cryptolib.api.Cryptor; 4 | import org.cryptomator.cryptolib.api.FileHeader; 5 | 6 | import java.io.IOException; 7 | import java.nio.ByteBuffer; 8 | import java.nio.channels.WritableByteChannel; 9 | 10 | public class EncryptingWritableByteChannel implements WritableByteChannel { 11 | 12 | private final WritableByteChannel delegate; 13 | private final Cryptor cryptor; 14 | private final FileHeader header; 15 | private final ByteBuffer cleartextBuffer; 16 | 17 | private boolean firstWrite = true; 18 | private long chunkNumber = 0; 19 | 20 | public EncryptingWritableByteChannel(WritableByteChannel destination, Cryptor cryptor) { 21 | this.delegate = destination; 22 | this.cryptor = cryptor; 23 | this.header = cryptor.fileHeaderCryptor().create(); 24 | this.cleartextBuffer = ByteBuffer.allocate(cryptor.fileContentCryptor().cleartextChunkSize()); 25 | } 26 | 27 | @Override 28 | public boolean isOpen() { 29 | return delegate.isOpen(); 30 | } 31 | 32 | @Override 33 | public synchronized void close() throws IOException { 34 | writeHeaderOnFirstWrite(); 35 | encryptAndFlushBuffer(); 36 | delegate.close(); 37 | } 38 | 39 | @Override 40 | public synchronized int write(ByteBuffer src) throws IOException { 41 | writeHeaderOnFirstWrite(); 42 | int result = 0; 43 | while (src.hasRemaining()) { 44 | result += ByteBuffers.copy(src, cleartextBuffer); 45 | if (!cleartextBuffer.hasRemaining()) { 46 | encryptAndFlushBuffer(); 47 | } 48 | } 49 | return result; 50 | } 51 | 52 | private void writeHeaderOnFirstWrite() throws IOException { 53 | if (firstWrite) { 54 | delegate.write(cryptor.fileHeaderCryptor().encryptHeader(header)); 55 | } 56 | firstWrite = false; 57 | } 58 | 59 | private void encryptAndFlushBuffer() throws IOException { 60 | cleartextBuffer.flip(); 61 | ByteBuffer ciphertextBuffer = cryptor.fileContentCryptor().encryptChunk(cleartextBuffer, chunkNumber++, header); 62 | delegate.write(ciphertextBuffer); 63 | cleartextBuffer.clear(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/MacSupplier.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import javax.crypto.Mac; 12 | import javax.crypto.SecretKey; 13 | import java.security.InvalidKeyException; 14 | import java.security.NoSuchAlgorithmException; 15 | 16 | public final class MacSupplier { 17 | 18 | public static final MacSupplier HMAC_SHA256 = new MacSupplier("HmacSHA256"); 19 | 20 | private final String macAlgorithm; 21 | private final ObjectPool macPool; 22 | 23 | public MacSupplier(String macAlgorithm) { 24 | this.macAlgorithm = macAlgorithm; 25 | this.macPool = new ObjectPool<>(this::createMac); 26 | try (ObjectPool.Lease lease = macPool.get()) { 27 | lease.get(); // eagerly initialize to provoke exceptions 28 | } 29 | } 30 | 31 | private Mac createMac() { 32 | try { 33 | return Mac.getInstance(macAlgorithm); 34 | } catch (NoSuchAlgorithmException e) { 35 | throw new IllegalArgumentException("Invalid MAC algorithm.", e); 36 | } 37 | } 38 | 39 | /** 40 | * Leases a reusable MAC object initialized with the given key. 41 | * 42 | * @param key Key to use in keyed MAC 43 | * @return A lease supplying a refurbished MAC 44 | */ 45 | public ObjectPool.Lease keyed(SecretKey key) { 46 | ObjectPool.Lease lease = macPool.get(); 47 | init(lease.get(), key); 48 | return lease; 49 | } 50 | 51 | /** 52 | * Creates a new MAC 53 | * 54 | * @param key Key to use in keyed MAC 55 | * @return New Mac instance 56 | * @deprecated Use {@link #keyed(SecretKey)} instead 57 | */ 58 | @Deprecated 59 | public Mac withKey(SecretKey key) { 60 | final Mac mac = createMac(); 61 | init(mac, key); 62 | return mac; 63 | } 64 | 65 | private void init(Mac mac, SecretKey key) { 66 | try { 67 | mac.init(key); 68 | } catch (InvalidKeyException e) { 69 | throw new IllegalArgumentException("Invalid key.", e); 70 | } 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import com.google.common.io.BaseEncoding; 4 | import com.google.gson.Gson; 5 | import com.google.gson.GsonBuilder; 6 | import com.google.gson.JsonIOException; 7 | import com.google.gson.JsonParseException; 8 | import com.google.gson.TypeAdapter; 9 | import com.google.gson.annotations.SerializedName; 10 | import com.google.gson.stream.JsonReader; 11 | import com.google.gson.stream.JsonToken; 12 | import com.google.gson.stream.JsonWriter; 13 | 14 | import java.io.IOException; 15 | import java.io.Reader; 16 | import java.io.Writer; 17 | 18 | /** 19 | * Representation of encrypted masterkey json file. Used by {@link MasterkeyFileAccess} to load and persist keys. 20 | */ 21 | public class MasterkeyFile { 22 | 23 | private static final Gson GSON = new GsonBuilder() // 24 | .setPrettyPrinting() // 25 | .disableHtmlEscaping() // 26 | .registerTypeHierarchyAdapter(byte[].class, new MasterkeyFile.ByteArrayJsonAdapter()) // 27 | .create(); 28 | 29 | @SerializedName("version") 30 | public int version; 31 | 32 | @SerializedName("scryptSalt") 33 | public byte[] scryptSalt; 34 | 35 | @SerializedName("scryptCostParam") 36 | public int scryptCostParam; 37 | 38 | @SerializedName("scryptBlockSize") 39 | public int scryptBlockSize; 40 | 41 | @SerializedName("primaryMasterKey") 42 | public byte[] encMasterKey; 43 | 44 | @SerializedName("hmacMasterKey") 45 | public byte[] macMasterKey; 46 | 47 | @SerializedName("versionMac") 48 | public byte[] versionMac; 49 | 50 | public static MasterkeyFile read(Reader reader) throws IOException { 51 | try { 52 | MasterkeyFile result = GSON.fromJson(reader, MasterkeyFile.class); 53 | if (result == null) { 54 | throw new IOException("JSON EOF"); 55 | } else { 56 | return result; 57 | } 58 | } catch (JsonParseException e) { 59 | throw new IOException("Unreadable JSON", e); 60 | } catch (IllegalArgumentException e) { 61 | throw new IOException("Invalid JSON content", e); 62 | } 63 | } 64 | 65 | public void write(Writer writer) throws IOException { 66 | try { 67 | GSON.toJson(this, writer); 68 | } catch (JsonIOException e) { 69 | throw new IOException(e); 70 | } 71 | } 72 | 73 | boolean isValid() { 74 | return version != 0 75 | && scryptSalt != null 76 | && scryptCostParam > 1 77 | && scryptBlockSize > 0 78 | && encMasterKey != null 79 | && macMasterKey != null 80 | && versionMac != null; 81 | } 82 | 83 | private static class ByteArrayJsonAdapter extends TypeAdapter { 84 | 85 | private static final BaseEncoding BASE64 = BaseEncoding.base64(); 86 | 87 | @Override 88 | public void write(JsonWriter writer, byte[] value) throws IOException { 89 | if (value == null) { 90 | writer.nullValue(); 91 | } else { 92 | writer.value(BASE64.encode(value)); 93 | } 94 | } 95 | 96 | @Override 97 | public byte[] read(JsonReader reader) throws IOException { 98 | if (reader.peek() == JsonToken.NULL) { 99 | reader.nextNull(); 100 | return null; 101 | } else { 102 | return BASE64.decode(reader.nextString()); 103 | } 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/MessageDigestSupplier.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import java.security.MessageDigest; 12 | import java.security.NoSuchAlgorithmException; 13 | 14 | public final class MessageDigestSupplier { 15 | 16 | public static final MessageDigestSupplier SHA1 = new MessageDigestSupplier("SHA-1"); 17 | public static final MessageDigestSupplier SHA256 = new MessageDigestSupplier("SHA-256"); 18 | 19 | private final String digestAlgorithm; 20 | private final ObjectPool mdPool; 21 | 22 | public MessageDigestSupplier(String digestAlgorithm) { 23 | this.digestAlgorithm = digestAlgorithm; 24 | this.mdPool = new ObjectPool<>(this::createMessageDigest); 25 | try (ObjectPool.Lease lease = mdPool.get()) { 26 | lease.get(); // eagerly initialize to provoke exceptions 27 | } 28 | } 29 | 30 | private MessageDigest createMessageDigest() { 31 | try { 32 | return MessageDigest.getInstance(digestAlgorithm); 33 | } catch (NoSuchAlgorithmException e) { 34 | throw new IllegalArgumentException("Invalid digest algorithm.", e); 35 | } 36 | } 37 | 38 | /** 39 | * Leases a reusable MessageDigest. 40 | * 41 | * @return A ReusableMessageDigest instance holding a refurbished MessageDigest 42 | */ 43 | public ObjectPool.Lease instance() { 44 | ObjectPool.Lease lease = mdPool.get(); 45 | lease.get().reset(); 46 | return lease; 47 | } 48 | 49 | /** 50 | * Creates a new MessageDigest. 51 | * 52 | * @return New MessageDigest instance 53 | * @deprecated Use {@link #instance()} 54 | */ 55 | @Deprecated 56 | public MessageDigest get() { 57 | final MessageDigest result = createMessageDigest(); 58 | result.reset(); 59 | return result; 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/ObjectPool.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import java.lang.ref.WeakReference; 4 | import java.util.Queue; 5 | import java.util.concurrent.ConcurrentLinkedQueue; 6 | import java.util.function.Supplier; 7 | 8 | /** 9 | * A simple object pool for resources that are expensive to create but are needed frequently. 10 | *

11 | * Example Usage: 12 | *

{@code
13 |  *     Supplier fooFactory = () -> new Foo();
14 |  *     ObjectPool fooPool = new ObjectPool(fooFactory);
15 |  *     try (ObjectPool.Lease lease = fooPool.get()) { // attempts to get a pooled Foo or invokes factory
16 |  *         lease.get().foo(); // exclusively use Foo instance
17 |  *     } // releases instance back to the pool when done
18 |  * }
19 | * 20 | * @param Type of the pooled objects 21 | */ 22 | public class ObjectPool { 23 | 24 | private final Queue> returnedInstances; 25 | private final Supplier factory; 26 | 27 | public ObjectPool(Supplier factory) { 28 | this.returnedInstances = new ConcurrentLinkedQueue<>(); 29 | this.factory = factory; 30 | } 31 | 32 | public Lease get() { 33 | WeakReference ref; 34 | while ((ref = returnedInstances.poll()) != null) { 35 | T cached = ref.get(); 36 | if (cached != null) { 37 | return new Lease<>(this, cached); 38 | } 39 | } 40 | return new Lease<>(this, factory.get()); 41 | } 42 | 43 | /** 44 | * A holder for resource leased from an {@link ObjectPool}. 45 | * This is basically an {@link AutoCloseable autocloseable} {@link Supplier} that is intended to be used 46 | * via try-with-resource blocks. 47 | * 48 | * @param Type of the leased instance 49 | */ 50 | public static class Lease implements AutoCloseable, Supplier { 51 | 52 | private final ObjectPool pool; 53 | private T obj; 54 | 55 | private Lease(ObjectPool pool, T obj) { 56 | this.pool = pool; 57 | this.obj = obj; 58 | } 59 | 60 | public T get() { 61 | return obj; 62 | } 63 | 64 | @Override 65 | public void close() { 66 | pool.returnedInstances.add(new WeakReference<>(obj)); 67 | obj = null; 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/P384KeyPair.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.StandardCopyOption; 9 | import java.nio.file.StandardOpenOption; 10 | import java.security.AlgorithmParameters; 11 | import java.security.InvalidAlgorithmParameterException; 12 | import java.security.KeyFactory; 13 | import java.security.KeyPair; 14 | import java.security.KeyPairGenerator; 15 | import java.security.NoSuchAlgorithmException; 16 | import java.security.PrivateKey; 17 | import java.security.PublicKey; 18 | import java.security.spec.ECGenParameterSpec; 19 | import java.security.spec.ECParameterSpec; 20 | import java.security.spec.InvalidKeySpecException; 21 | import java.security.spec.InvalidParameterSpecException; 22 | import java.security.spec.PKCS8EncodedKeySpec; 23 | import java.security.spec.X509EncodedKeySpec; 24 | 25 | public class P384KeyPair extends ECKeyPair { 26 | 27 | private static final String EC_ALG = "EC"; 28 | private static final String EC_CURVE_NAME = "secp384r1"; 29 | private static final String SIGNATURE_ALG = "SHA384withECDSA"; 30 | 31 | private P384KeyPair(KeyPair keyPair) { 32 | super(keyPair, getCurveParams()); 33 | } 34 | 35 | public static P384KeyPair generate() { 36 | KeyPair keyPair = getKeyPairGenerator().generateKeyPair(); 37 | return new P384KeyPair(keyPair); 38 | } 39 | 40 | /** 41 | * Creates a key pair from the given key specs. 42 | * 43 | * @param publicKeySpec DER formatted public key 44 | * @param privateKeySpec DER formatted private key 45 | * @return created key pair 46 | * @throws InvalidKeySpecException If the supplied key specs are unsuitable for {@value #EC_ALG} keys 47 | */ 48 | public static P384KeyPair create(X509EncodedKeySpec publicKeySpec, PKCS8EncodedKeySpec privateKeySpec) throws InvalidKeySpecException { 49 | try { 50 | KeyFactory factory = KeyFactory.getInstance(EC_ALG); 51 | PublicKey publicKey = factory.generatePublic(publicKeySpec); 52 | PrivateKey privateKey = factory.generatePrivate(privateKeySpec); 53 | return new P384KeyPair(new KeyPair(publicKey, privateKey)); 54 | } catch (NoSuchAlgorithmException e) { 55 | throw new IllegalStateException(EC_ALG + " not supported"); 56 | } 57 | } 58 | 59 | /** 60 | * Loads a key pair from the given file 61 | * 62 | * @param p12File A .p12 file 63 | * @param passphrase The password to protect the key material 64 | * @return loaded key pair 65 | * @throws IOException In case of I/O errors 66 | * @throws Pkcs12PasswordException If the supplied password is incorrect 67 | * @throws Pkcs12Exception If any cryptographic operation fails 68 | */ 69 | public static P384KeyPair load(Path p12File, char[] passphrase) throws IOException, Pkcs12PasswordException, Pkcs12Exception { 70 | try (InputStream in = Files.newInputStream(p12File, StandardOpenOption.READ)) { 71 | return load(in, passphrase); 72 | } 73 | } 74 | 75 | /** 76 | * Loads a key pair from the given input stream 77 | * 78 | * @param in An input stream providing PKCS#12 formatted data 79 | * @param passphrase The password to protect the key material 80 | * @return loaded key pair 81 | * @throws IOException In case of I/O errors 82 | * @throws Pkcs12PasswordException If the supplied password is incorrect 83 | * @throws Pkcs12Exception If any cryptographic operation fails 84 | */ 85 | public static P384KeyPair load(InputStream in, char[] passphrase) throws IOException, Pkcs12PasswordException, Pkcs12Exception { 86 | KeyPair keyPair = Pkcs12Helper.importFrom(in, passphrase); 87 | return new P384KeyPair(keyPair); 88 | } 89 | 90 | /** 91 | * Stores this key pair in PKCS#12 format at the given path 92 | * 93 | * @param p12File The path of the .p12 file 94 | * @param passphrase The password to protect the key material 95 | * @throws IOException In case of I/O errors 96 | * @throws Pkcs12Exception If any cryptographic operation fails 97 | */ 98 | public void store(Path p12File, char[] passphrase) throws IOException, Pkcs12Exception { 99 | Path tmpFile = p12File.resolveSibling(p12File.getFileName().toString() + ".tmp"); 100 | try (OutputStream out = Files.newOutputStream(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { 101 | store(out, passphrase); 102 | } 103 | Files.move(tmpFile, p12File, StandardCopyOption.REPLACE_EXISTING); 104 | } 105 | 106 | /** 107 | * Stores this key in PKCS#12 format to the given output stream 108 | * 109 | * @param out The output stream to which the data will be written 110 | * @param passphrase The password to protect the key material 111 | * @throws IOException In case of I/O errors 112 | * @throws Pkcs12Exception If any cryptographic operation fails 113 | */ 114 | public void store(OutputStream out, char[] passphrase) throws IOException, Pkcs12Exception { 115 | Pkcs12Helper.exportTo(keyPair(), out, passphrase, SIGNATURE_ALG); 116 | } 117 | 118 | private static KeyPairGenerator getKeyPairGenerator() { 119 | try { 120 | KeyPairGenerator keyGen = KeyPairGenerator.getInstance(EC_ALG); 121 | keyGen.initialize(new ECGenParameterSpec(EC_CURVE_NAME)); 122 | return keyGen; 123 | } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { 124 | throw new IllegalStateException(EC_CURVE_NAME + " curve not supported"); 125 | } 126 | } 127 | 128 | private static ECParameterSpec getCurveParams() { 129 | try { 130 | AlgorithmParameters parameters = AlgorithmParameters.getInstance(EC_ALG); 131 | parameters.init(new ECGenParameterSpec(EC_CURVE_NAME)); 132 | return parameters.getParameterSpec(ECParameterSpec.class); 133 | } catch (NoSuchAlgorithmException | InvalidParameterSpecException e) { 134 | throw new IllegalStateException(EC_CURVE_NAME + " curve not supported"); 135 | } 136 | } 137 | 138 | 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/Pkcs12Exception.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.cryptomator.cryptolib.api.CryptoException; 4 | 5 | /** 6 | * Loading from or exporting to PKCS12 format failed. 7 | */ 8 | public class Pkcs12Exception extends CryptoException { 9 | 10 | protected Pkcs12Exception(String message, Throwable cause) { 11 | super(message, cause); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/Pkcs12Helper.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.security.GeneralSecurityException; 7 | import java.security.KeyPair; 8 | import java.security.KeyStore; 9 | import java.security.KeyStoreException; 10 | import java.security.PrivateKey; 11 | import java.security.PublicKey; 12 | import java.security.UnrecoverableKeyException; 13 | import java.security.cert.X509Certificate; 14 | import java.time.Instant; 15 | import java.time.Period; 16 | 17 | class Pkcs12Helper { 18 | 19 | private static final String X509_ISSUER = "CN=Cryptomator"; 20 | private static final String X509_SUBJECT = "CN=Self Signed Cert"; 21 | private static final int X509_VALID_DAYS = 3560; 22 | private static final String KEYSTORE_ALIAS_KEY = "key"; 23 | private static final String KEYSTORE_ALIAS_CERT = "crt"; 24 | 25 | private Pkcs12Helper() { 26 | } 27 | 28 | /** 29 | * Stores the given key pair in PKCS#12 format. 30 | * 31 | * @param keyPair The key pair to export 32 | * @param out The output stream to which the result will be written 33 | * @param pw The password to protect the key material 34 | * @param signatureAlg A suited signature algorithm to sign a x509v3 cert holding the public key 35 | * @throws IOException In case of I/O errors 36 | * @throws Pkcs12Exception If any cryptographic operation fails 37 | */ 38 | public static void exportTo(KeyPair keyPair, OutputStream out, char[] pw, String signatureAlg) throws IOException, Pkcs12Exception { 39 | try { 40 | KeyStore keyStore = getKeyStore(); 41 | keyStore.load(null, pw); 42 | X509Certificate cert = X509CertBuilder.init(keyPair, signatureAlg) // 43 | .withIssuer(X509_ISSUER) // 44 | .withSubject(X509_SUBJECT) // 45 | .withNotBefore(Instant.now()) // 46 | .withNotAfter(Instant.now().plus(Period.ofDays(X509_VALID_DAYS))) 47 | .build(); 48 | X509Certificate[] chain = new X509Certificate[]{cert}; 49 | keyStore.setKeyEntry(KEYSTORE_ALIAS_KEY, keyPair.getPrivate(), pw, chain); 50 | keyStore.setCertificateEntry(KEYSTORE_ALIAS_CERT, cert); 51 | keyStore.store(out, pw); 52 | } catch (IllegalArgumentException | GeneralSecurityException e) { 53 | throw new Pkcs12Exception("Failed to store PKCS12 file.", e); 54 | } 55 | } 56 | 57 | /** 58 | * Loads a key pair from PKCS#12 format. 59 | * 60 | * @param in Where to load the key pair from 61 | * @param pw The password to protect the key material 62 | * @throws IOException In case of I/O errors 63 | * @throws Pkcs12PasswordException If the supplied password is incorrect 64 | * @throws Pkcs12Exception If any cryptographic operation fails 65 | */ 66 | public static KeyPair importFrom(InputStream in, char[] pw) throws IOException, Pkcs12PasswordException, Pkcs12Exception { 67 | try { 68 | KeyStore keyStore = getKeyStore(); 69 | keyStore.load(in, pw); 70 | PrivateKey sk = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS_KEY, pw); 71 | PublicKey pk = keyStore.getCertificate(KEYSTORE_ALIAS_CERT).getPublicKey(); 72 | return new KeyPair(pk, sk); 73 | } catch (UnrecoverableKeyException e) { 74 | throw new Pkcs12PasswordException(e); 75 | } catch (IOException e) { 76 | if (e.getCause() instanceof UnrecoverableKeyException) { 77 | throw new Pkcs12PasswordException(e); 78 | } else { 79 | throw e; 80 | } 81 | } catch (GeneralSecurityException e) { 82 | throw new Pkcs12Exception("Failed to load PKCS12 file.", e); 83 | } 84 | } 85 | 86 | private static KeyStore getKeyStore() { 87 | try { 88 | return KeyStore.getInstance("PKCS12"); 89 | } catch (KeyStoreException e) { 90 | throw new IllegalStateException("Every implementation of the Java platform is required to support PKCS12."); 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/Pkcs12PasswordException.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | /** 4 | * Loading from PKCS12 format failed due to wrong password. 5 | */ 6 | public class Pkcs12PasswordException extends Pkcs12Exception { 7 | 8 | protected Pkcs12PasswordException(Throwable cause) { 9 | super("Wrong password", cause); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/common/ReseedingSecureRandom.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import java.security.NoSuchAlgorithmException; 12 | import java.security.Provider; 13 | import java.security.SecureRandom; 14 | import java.security.SecureRandomSpi; 15 | 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | /** 20 | * Wraps a fast CSPRNG, which gets reseeded automatically after a certain amount of bytes has been generated.
21 | * 22 | *

23 | * Java 8 Example: 24 | * 25 | *

 26 |  *  SecureRandom csprng = ReseedingSecureRandom.create(SecureRandom.getInstanceStrong());
 27 |  * 
28 | */ 29 | public class ReseedingSecureRandom extends SecureRandom { 30 | 31 | private static final Logger LOG = LoggerFactory.getLogger(ReseedingSecureRandom.class); 32 | private static final Provider PROVIDER = new ReseedingSecureRandomProvider(); 33 | 34 | /** 35 | * @param seeder RNG for high-quality random numbers. E.g. SecureRandom.getInstanceStrong() in Java 8+ environments. 36 | * @param csprng A fast csprng implementation, such as SHA1PRNG, that will be wrapped by this instance. 37 | * @param reseedAfter How many bytes can be read from the csprng, before a new seed will be generated. 38 | * @param seedLength Number of bytes generated by seeder in order to seed csprng. 39 | */ 40 | public ReseedingSecureRandom(SecureRandom seeder, SecureRandom csprng, long reseedAfter, int seedLength) { 41 | super(new ReseedingSecureRandomSpi(seeder, csprng, reseedAfter, seedLength), PROVIDER); 42 | } 43 | 44 | /** 45 | * Creates a pre-configured automatically reseeding SHA1PRNG instance, reseeding itself with 440 bits from the given seeder after generating 2^30 bytes, 46 | * thus satisfying recommendations by NIST SP 800-90A Rev 1. 47 | * 48 | * @param seeder RNG for high-quality random numbers. E.g. SecureRandom.getInstanceStrong() in Java 8+ environments. 49 | * @return An automatically reseeding SHA1PRNG suitable as CSPRNG for most applications. 50 | */ 51 | public static ReseedingSecureRandom create(SecureRandom seeder) { 52 | try { 53 | return new ReseedingSecureRandom(seeder, SecureRandom.getInstance("SHA1PRNG"), 1 << 30, 55); 54 | } catch (NoSuchAlgorithmException e) { 55 | throw new IllegalStateException("SHA1PRNG must exist in every Java platform implementation.", e); 56 | } 57 | } 58 | 59 | private static class ReseedingSecureRandomProvider extends Provider { 60 | 61 | protected ReseedingSecureRandomProvider() { 62 | super("ReseedingSecureRandomProvider", 1.0, "Provides ReseedingSecureRandom"); 63 | } 64 | 65 | } 66 | 67 | private static class ReseedingSecureRandomSpi extends SecureRandomSpi { 68 | 69 | private final SecureRandom seeder; 70 | private final SecureRandom csprng; 71 | private final long reseedAfter; 72 | private final int seedLength; 73 | private long counter; 74 | 75 | public ReseedingSecureRandomSpi(SecureRandom seeder, SecureRandom csprng, long reseedAfter, int seedLength) { 76 | this.seeder = seeder; 77 | this.csprng = csprng; 78 | this.reseedAfter = reseedAfter; 79 | this.seedLength = seedLength; 80 | this.counter = reseedAfter; // trigger reseed during first "engineNextBytes(...)" 81 | } 82 | 83 | @Override 84 | protected void engineSetSeed(byte[] seed) { 85 | csprng.setSeed(seed); 86 | } 87 | 88 | @Override 89 | protected void engineNextBytes(byte[] bytes) { 90 | if (counter + bytes.length > reseedAfter) { 91 | reseed(); 92 | } 93 | counter += bytes.length; 94 | csprng.nextBytes(bytes); 95 | } 96 | 97 | @Override 98 | protected byte[] engineGenerateSeed(int numBytes) { 99 | try { 100 | LOG.debug("Seeding CSPRNG with {} bytes...", numBytes); 101 | return seeder.generateSeed(numBytes); 102 | } finally { 103 | LOG.debug("Seeded CSPRNG."); 104 | } 105 | } 106 | 107 | private void reseed() { 108 | engineSetSeed(engineGenerateSeed(seedLength)); 109 | counter = 0; 110 | } 111 | 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/v1/Constants.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | final class Constants { 12 | 13 | private Constants() { 14 | } 15 | 16 | static final String CONTENT_ENC_ALG = "AES"; 17 | 18 | static final int NONCE_SIZE = 16; 19 | static final int PAYLOAD_SIZE = 32 * 1024; 20 | static final int MAC_SIZE = 32; 21 | static final int CHUNK_SIZE = NONCE_SIZE + PAYLOAD_SIZE + MAC_SIZE; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import org.cryptomator.cryptolib.api.Cryptor; 12 | import org.cryptomator.cryptolib.api.Masterkey; 13 | 14 | import java.security.SecureRandom; 15 | 16 | class CryptorImpl implements Cryptor { 17 | 18 | private final Masterkey masterkey; 19 | private final FileContentCryptorImpl fileContentCryptor; 20 | private final FileHeaderCryptorImpl fileHeaderCryptor; 21 | private final FileNameCryptorImpl fileNameCryptor; 22 | 23 | /** 24 | * Package-private constructor. 25 | * Use {@link CryptorProviderImpl#provide(Masterkey, SecureRandom)} to obtain a Cryptor instance. 26 | */ 27 | CryptorImpl(Masterkey masterkey, SecureRandom random) { 28 | this.masterkey = masterkey; 29 | this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random); 30 | this.fileContentCryptor = new FileContentCryptorImpl(masterkey, random); 31 | this.fileNameCryptor = new FileNameCryptorImpl(masterkey); 32 | } 33 | 34 | @Override 35 | public FileContentCryptorImpl fileContentCryptor() { 36 | assertNotDestroyed(); 37 | return fileContentCryptor; 38 | } 39 | 40 | @Override 41 | public FileHeaderCryptorImpl fileHeaderCryptor() { 42 | assertNotDestroyed(); 43 | return fileHeaderCryptor; 44 | } 45 | 46 | @Override 47 | public FileNameCryptorImpl fileNameCryptor() { 48 | assertNotDestroyed(); 49 | return fileNameCryptor; 50 | } 51 | 52 | @Override 53 | public boolean isDestroyed() { 54 | return masterkey.isDestroyed(); 55 | } 56 | 57 | @Override 58 | public void close() { 59 | destroy(); 60 | } 61 | 62 | @Override 63 | public void destroy() { 64 | masterkey.destroy(); 65 | } 66 | 67 | private void assertNotDestroyed() { 68 | if (isDestroyed()) { 69 | throw new IllegalStateException("Cryptor destroyed."); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import org.cryptomator.cryptolib.api.CryptorProvider; 12 | import org.cryptomator.cryptolib.api.Masterkey; 13 | import org.cryptomator.cryptolib.common.ReseedingSecureRandom; 14 | 15 | import java.security.SecureRandom; 16 | 17 | public class CryptorProviderImpl implements CryptorProvider { 18 | 19 | @Override 20 | public Scheme scheme() { 21 | return Scheme.SIV_CTRMAC; 22 | } 23 | 24 | @Override 25 | public CryptorImpl provide(Masterkey masterkey, SecureRandom random) { 26 | return new CryptorImpl(masterkey, ReseedingSecureRandom.create(random)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import com.google.common.base.Preconditions; 12 | import org.cryptomator.cryptolib.api.FileHeader; 13 | import org.cryptomator.cryptolib.common.DestroyableSecretKey; 14 | 15 | import javax.security.auth.Destroyable; 16 | import java.nio.ByteBuffer; 17 | 18 | class FileHeaderImpl implements FileHeader, Destroyable { 19 | 20 | static final int NONCE_POS = 0; 21 | static final int NONCE_LEN = 16; 22 | static final int PAYLOAD_POS = 16; 23 | static final int PAYLOAD_LEN = Payload.SIZE; 24 | static final int MAC_POS = 56; 25 | static final int MAC_LEN = 32; 26 | static final int SIZE = NONCE_LEN + PAYLOAD_LEN + MAC_LEN; 27 | 28 | private final byte[] nonce; 29 | private final Payload payload; 30 | 31 | FileHeaderImpl(byte[] nonce, Payload payload) { 32 | if (nonce.length != NONCE_LEN) { 33 | throw new IllegalArgumentException("Invalid nonce length. (was: " + nonce.length + ", required: " + NONCE_LEN + ")"); 34 | } 35 | this.nonce = nonce; 36 | this.payload = payload; 37 | } 38 | 39 | static FileHeaderImpl cast(FileHeader header) { 40 | if (header instanceof FileHeaderImpl) { 41 | return (FileHeaderImpl) header; 42 | } else { 43 | throw new IllegalArgumentException("Unsupported header type " + header.getClass()); 44 | } 45 | } 46 | 47 | public byte[] getNonce() { 48 | return nonce; 49 | } 50 | 51 | public Payload getPayload() { 52 | return payload; 53 | } 54 | 55 | @Override 56 | public long getReserved() { 57 | return payload.getReserved(); 58 | } 59 | 60 | @Override 61 | public void setReserved(long reserved) { 62 | payload.setReserved(reserved); 63 | } 64 | 65 | @Override 66 | public boolean isDestroyed() { 67 | return payload.isDestroyed(); 68 | } 69 | 70 | @Override 71 | public void destroy() { 72 | payload.destroy(); 73 | } 74 | 75 | public static class Payload implements Destroyable { 76 | 77 | static final int REVERSED_LEN = Long.BYTES; 78 | static final int CONTENT_KEY_LEN = 32; 79 | static final int SIZE = REVERSED_LEN + CONTENT_KEY_LEN; 80 | 81 | private long reserved; 82 | private final DestroyableSecretKey contentKey; 83 | 84 | Payload(long reversed, byte[] contentKeyBytes) { 85 | Preconditions.checkArgument(contentKeyBytes.length == CONTENT_KEY_LEN, "Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")"); 86 | this.reserved = reversed; 87 | this.contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG); 88 | } 89 | 90 | static Payload decode(ByteBuffer cleartextPayloadBuf) { 91 | Preconditions.checkArgument(cleartextPayloadBuf.remaining() == SIZE, "invalid payload buffer length"); 92 | long reserved = cleartextPayloadBuf.getLong(); 93 | byte[] contentKeyBytes = new byte[CONTENT_KEY_LEN]; 94 | cleartextPayloadBuf.get(contentKeyBytes); 95 | return new Payload(reserved, contentKeyBytes); 96 | } 97 | 98 | ByteBuffer encode() { 99 | ByteBuffer buf = ByteBuffer.allocate(SIZE); 100 | buf.putLong(reserved); 101 | buf.put(contentKey.getEncoded()); 102 | buf.flip(); 103 | return buf; 104 | } 105 | 106 | private long getReserved() { 107 | return reserved; 108 | } 109 | 110 | private void setReserved(long reserved) { 111 | this.reserved = reserved; 112 | } 113 | 114 | DestroyableSecretKey getContentKey() { 115 | return contentKey; 116 | } 117 | 118 | @Override 119 | public boolean isDestroyed() { 120 | return contentKey.isDestroyed(); 121 | } 122 | 123 | @Override 124 | public void destroy() { 125 | contentKey.destroy(); 126 | } 127 | 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2015, 2016 Sebastian Stenzel and others. 3 | * This file is licensed under the terms of the MIT license. 4 | * See the LICENSE.txt file for more info. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import com.google.common.io.BaseEncoding; 12 | import org.cryptomator.cryptolib.api.AuthenticationFailedException; 13 | import org.cryptomator.cryptolib.api.FileNameCryptor; 14 | import org.cryptomator.cryptolib.api.Masterkey; 15 | import org.cryptomator.cryptolib.common.DestroyableSecretKey; 16 | import org.cryptomator.cryptolib.common.MessageDigestSupplier; 17 | import org.cryptomator.cryptolib.common.ObjectPool; 18 | import org.cryptomator.siv.SivMode; 19 | import org.cryptomator.siv.UnauthenticCiphertextException; 20 | 21 | import javax.crypto.IllegalBlockSizeException; 22 | import java.security.MessageDigest; 23 | 24 | import static java.nio.charset.StandardCharsets.UTF_8; 25 | 26 | class FileNameCryptorImpl implements FileNameCryptor { 27 | 28 | private static final BaseEncoding BASE32 = BaseEncoding.base32(); 29 | private static final ObjectPool AES_SIV = new ObjectPool<>(SivMode::new); 30 | 31 | private final Masterkey masterkey; 32 | 33 | FileNameCryptorImpl(Masterkey masterkey) { 34 | this.masterkey = masterkey; 35 | } 36 | 37 | @Override 38 | public String hashDirectoryId(String cleartextDirectoryId) { 39 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey(); 40 | ObjectPool.Lease sha1 = MessageDigestSupplier.SHA1.instance(); 41 | ObjectPool.Lease siv = AES_SIV.get()) { 42 | byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); 43 | byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes); 44 | byte[] hashedBytes = sha1.get().digest(encryptedBytes); 45 | return BASE32.encode(hashedBytes); 46 | } 47 | } 48 | 49 | @Override 50 | public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) { 51 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey(); 52 | ObjectPool.Lease siv = AES_SIV.get()) { 53 | byte[] cleartextBytes = cleartextName.getBytes(UTF_8); 54 | byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes, associatedData); 55 | return encoding.encode(encryptedBytes); 56 | } 57 | } 58 | 59 | @Override 60 | public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { 61 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey(); 62 | ObjectPool.Lease siv = AES_SIV.get()) { 63 | byte[] encryptedBytes = encoding.decode(ciphertextName); 64 | byte[] cleartextBytes = siv.get().decrypt(ek, mk, encryptedBytes, associatedData); 65 | return new String(cleartextBytes, UTF_8); 66 | } catch (UnauthenticCiphertextException | IllegalArgumentException | IllegalBlockSizeException e) { 67 | throw new AuthenticationFailedException("Invalid Ciphertext.", e); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/v2/Constants.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | final class Constants { 12 | 13 | private Constants() { 14 | } 15 | 16 | static final String CONTENT_ENC_ALG = "AES"; 17 | 18 | static final int GCM_NONCE_SIZE = 12; // 96 bit IVs strongly recommended for GCM 19 | static final int PAYLOAD_SIZE = 32 * 1024; 20 | static final int GCM_TAG_SIZE = 16; 21 | static final int CHUNK_SIZE = GCM_NONCE_SIZE + PAYLOAD_SIZE + GCM_TAG_SIZE; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import org.cryptomator.cryptolib.api.Cryptor; 12 | import org.cryptomator.cryptolib.api.Masterkey; 13 | import org.cryptomator.cryptolib.v1.CryptorProviderImpl; 14 | 15 | import java.security.SecureRandom; 16 | 17 | class CryptorImpl implements Cryptor { 18 | 19 | private final Masterkey masterkey; 20 | private final FileContentCryptorImpl fileContentCryptor; 21 | private final FileHeaderCryptorImpl fileHeaderCryptor; 22 | private final FileNameCryptorImpl fileNameCryptor; 23 | 24 | /** 25 | * Package-private constructor. 26 | * Use {@link CryptorProviderImpl#provide(Masterkey, SecureRandom)} to obtain a Cryptor instance. 27 | */ 28 | CryptorImpl(Masterkey masterkey, SecureRandom random) { 29 | this.masterkey = masterkey; 30 | this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random); 31 | this.fileContentCryptor = new FileContentCryptorImpl(random); 32 | this.fileNameCryptor = new FileNameCryptorImpl(masterkey); 33 | } 34 | 35 | @Override 36 | public FileContentCryptorImpl fileContentCryptor() { 37 | assertNotDestroyed(); 38 | return fileContentCryptor; 39 | } 40 | 41 | @Override 42 | public FileHeaderCryptorImpl fileHeaderCryptor() { 43 | assertNotDestroyed(); 44 | return fileHeaderCryptor; 45 | } 46 | 47 | @Override 48 | public FileNameCryptorImpl fileNameCryptor() { 49 | assertNotDestroyed(); 50 | return fileNameCryptor; 51 | } 52 | 53 | @Override 54 | public boolean isDestroyed() { 55 | return masterkey.isDestroyed(); 56 | } 57 | 58 | @Override 59 | public void close() { 60 | destroy(); 61 | } 62 | 63 | @Override 64 | public void destroy() { 65 | masterkey.destroy(); 66 | } 67 | 68 | private void assertNotDestroyed() { 69 | if (isDestroyed()) { 70 | throw new IllegalStateException("Cryptor destroyed."); 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import org.cryptomator.cryptolib.api.CryptorProvider; 12 | import org.cryptomator.cryptolib.api.Masterkey; 13 | import org.cryptomator.cryptolib.common.ReseedingSecureRandom; 14 | 15 | import java.security.SecureRandom; 16 | 17 | public class CryptorProviderImpl implements CryptorProvider { 18 | 19 | @Override 20 | public Scheme scheme() { 21 | return Scheme.SIV_GCM; 22 | } 23 | 24 | @Override 25 | public CryptorImpl provide(Masterkey masterkey, SecureRandom random) { 26 | return new CryptorImpl(masterkey, ReseedingSecureRandom.create(random)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException; 12 | import org.cryptomator.cryptolib.api.FileHeader; 13 | import org.cryptomator.cryptolib.api.FileHeaderCryptor; 14 | import org.cryptomator.cryptolib.api.Masterkey; 15 | import org.cryptomator.cryptolib.common.CipherSupplier; 16 | import org.cryptomator.cryptolib.common.DestroyableSecretKey; 17 | import org.cryptomator.cryptolib.common.ObjectPool; 18 | 19 | import javax.crypto.AEADBadTagException; 20 | import javax.crypto.BadPaddingException; 21 | import javax.crypto.Cipher; 22 | import javax.crypto.IllegalBlockSizeException; 23 | import javax.crypto.ShortBufferException; 24 | import javax.crypto.spec.GCMParameterSpec; 25 | import java.nio.ByteBuffer; 26 | import java.security.SecureRandom; 27 | import java.util.Arrays; 28 | 29 | import static org.cryptomator.cryptolib.v2.Constants.GCM_TAG_SIZE; 30 | 31 | class FileHeaderCryptorImpl implements FileHeaderCryptor { 32 | 33 | private final Masterkey masterkey; 34 | private final SecureRandom random; 35 | 36 | FileHeaderCryptorImpl(Masterkey masterkey, SecureRandom random) { 37 | this.masterkey = masterkey; 38 | this.random = random; 39 | } 40 | 41 | @Override 42 | public FileHeader create() { 43 | byte[] nonce = new byte[FileHeaderImpl.NONCE_LEN]; 44 | random.nextBytes(nonce); 45 | byte[] contentKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]; 46 | random.nextBytes(contentKey); 47 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, contentKey); 48 | return new FileHeaderImpl(nonce, payload); 49 | } 50 | 51 | @Override 52 | public int headerSize() { 53 | return FileHeaderImpl.SIZE; 54 | } 55 | 56 | @Override 57 | public ByteBuffer encryptHeader(FileHeader header) { 58 | FileHeaderImpl headerImpl = FileHeaderImpl.cast(header); 59 | ByteBuffer payloadCleartextBuf = headerImpl.getPayload().encode(); 60 | try (DestroyableSecretKey ek = masterkey.getEncKey()) { 61 | ByteBuffer result = ByteBuffer.allocate(FileHeaderImpl.SIZE); 62 | result.put(headerImpl.getNonce()); 63 | 64 | // encrypt payload: 65 | try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce()))) { 66 | int encrypted = cipher.get().doFinal(payloadCleartextBuf, result); 67 | assert encrypted == FileHeaderImpl.PAYLOAD_LEN + FileHeaderImpl.TAG_LEN; 68 | } 69 | result.flip(); 70 | return result; 71 | } catch (ShortBufferException e) { 72 | throw new IllegalStateException("Result buffer too small for encrypted header payload.", e); 73 | } catch (IllegalBlockSizeException | BadPaddingException e) { 74 | throw new IllegalStateException("Unexpected exception during GCM encryption.", e); 75 | } finally { 76 | Arrays.fill(payloadCleartextBuf.array(), (byte) 0x00); 77 | } 78 | } 79 | 80 | @Override 81 | public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws AuthenticationFailedException { 82 | if (ciphertextHeaderBuf.remaining() < FileHeaderImpl.SIZE) { 83 | throw new IllegalArgumentException("Malformed ciphertext header"); 84 | } 85 | ByteBuffer buf = ciphertextHeaderBuf.duplicate(); 86 | byte[] nonce = new byte[FileHeaderImpl.NONCE_LEN]; 87 | buf.position(FileHeaderImpl.NONCE_POS); 88 | buf.get(nonce); 89 | byte[] ciphertextAndTag = new byte[FileHeaderImpl.PAYLOAD_LEN + FileHeaderImpl.TAG_LEN]; 90 | buf.position(FileHeaderImpl.PAYLOAD_POS); 91 | buf.get(ciphertextAndTag); 92 | 93 | // FileHeaderImpl.Payload.SIZE + GCM_TAG_SIZE is required to fix a bug in Android API level pre 29, see https://issuetracker.google.com/issues/197534888 and #24 94 | ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE + GCM_TAG_SIZE); 95 | try (DestroyableSecretKey ek = masterkey.getEncKey()) { 96 | // decrypt payload: 97 | try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.decryptionCipher(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) { 98 | int decrypted = cipher.get().doFinal(ByteBuffer.wrap(ciphertextAndTag), payloadCleartextBuf); 99 | assert decrypted == FileHeaderImpl.Payload.SIZE; 100 | } 101 | payloadCleartextBuf.flip(); 102 | FileHeaderImpl.Payload payload = FileHeaderImpl.Payload.decode(payloadCleartextBuf); 103 | 104 | return new FileHeaderImpl(nonce, payload); 105 | } catch (AEADBadTagException e) { 106 | throw new AuthenticationFailedException("Header tag mismatch.", e); 107 | } catch (ShortBufferException e) { 108 | throw new IllegalStateException("Result buffer too small for decrypted header payload.", e); 109 | } catch (IllegalBlockSizeException | BadPaddingException e) { 110 | throw new IllegalStateException("Unexpected exception during GCM decryption.", e); 111 | } finally { 112 | Arrays.fill(payloadCleartextBuf.array(), (byte) 0x00); 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import com.google.common.base.Preconditions; 12 | import org.cryptomator.cryptolib.api.FileHeader; 13 | import org.cryptomator.cryptolib.common.DestroyableSecretKey; 14 | 15 | import javax.security.auth.Destroyable; 16 | import java.nio.ByteBuffer; 17 | 18 | class FileHeaderImpl implements FileHeader, Destroyable { 19 | 20 | static final int NONCE_POS = 0; 21 | static final int NONCE_LEN = Constants.GCM_NONCE_SIZE; 22 | static final int PAYLOAD_POS = NONCE_POS + NONCE_LEN; // 12 23 | static final int PAYLOAD_LEN = Payload.SIZE; 24 | static final int TAG_POS = PAYLOAD_POS + PAYLOAD_LEN; // 52 25 | static final int TAG_LEN = Constants.GCM_TAG_SIZE; 26 | static final int SIZE = NONCE_LEN + PAYLOAD_LEN + TAG_LEN; 27 | 28 | private final byte[] nonce; 29 | private final Payload payload; 30 | 31 | FileHeaderImpl(byte[] nonce, Payload payload) { 32 | if (nonce.length != NONCE_LEN) { 33 | throw new IllegalArgumentException("Invalid nonce length. (was: " + nonce.length + ", required: " + NONCE_LEN + ")"); 34 | } 35 | this.nonce = nonce; 36 | this.payload = payload; 37 | } 38 | 39 | static FileHeaderImpl cast(FileHeader header) { 40 | if (header instanceof FileHeaderImpl) { 41 | return (FileHeaderImpl) header; 42 | } else { 43 | throw new IllegalArgumentException("Unsupported header type " + header.getClass()); 44 | } 45 | } 46 | 47 | public byte[] getNonce() { 48 | return nonce; 49 | } 50 | 51 | public Payload getPayload() { 52 | return payload; 53 | } 54 | 55 | @Override 56 | public long getReserved() { 57 | return payload.getReserved(); 58 | } 59 | 60 | @Override 61 | public void setReserved(long reserved) { 62 | payload.setReserved(reserved); 63 | } 64 | 65 | @Override 66 | public boolean isDestroyed() { 67 | return payload.isDestroyed(); 68 | } 69 | 70 | @Override 71 | public void destroy() { 72 | payload.destroy(); 73 | } 74 | 75 | public static class Payload implements Destroyable { 76 | 77 | static final int REVERSED_LEN = Long.BYTES; 78 | static final int CONTENT_KEY_LEN = 32; 79 | static final int SIZE = REVERSED_LEN + CONTENT_KEY_LEN; 80 | 81 | private long reserved; 82 | private final DestroyableSecretKey contentKey; 83 | 84 | Payload(long reversed, byte[] contentKeyBytes) { 85 | Preconditions.checkArgument(contentKeyBytes.length == CONTENT_KEY_LEN, "Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")"); 86 | this.reserved = reversed; 87 | this.contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG); 88 | } 89 | 90 | static Payload decode(ByteBuffer cleartextPayloadBuf) { 91 | Preconditions.checkArgument(cleartextPayloadBuf.remaining() == SIZE, "invalid payload buffer length"); 92 | long reserved = cleartextPayloadBuf.getLong(); 93 | byte[] contentKeyBytes = new byte[CONTENT_KEY_LEN]; 94 | cleartextPayloadBuf.get(contentKeyBytes); 95 | return new Payload(reserved, contentKeyBytes); 96 | } 97 | 98 | ByteBuffer encode() { 99 | ByteBuffer buf = ByteBuffer.allocate(SIZE); 100 | buf.putLong(reserved); 101 | buf.put(contentKey.getEncoded()); 102 | buf.flip(); 103 | return buf; 104 | } 105 | 106 | private long getReserved() { 107 | return reserved; 108 | } 109 | 110 | private void setReserved(long reserved) { 111 | this.reserved = reserved; 112 | } 113 | 114 | DestroyableSecretKey getContentKey() { 115 | return contentKey; 116 | } 117 | 118 | @Override 119 | public boolean isDestroyed() { 120 | return contentKey.isDestroyed(); 121 | } 122 | 123 | @Override 124 | public void destroy() { 125 | contentKey.destroy(); 126 | } 127 | 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2015, 2016 Sebastian Stenzel and others. 3 | * This file is licensed under the terms of the MIT license. 4 | * See the LICENSE.txt file for more info. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import com.google.common.io.BaseEncoding; 12 | import org.cryptomator.cryptolib.api.AuthenticationFailedException; 13 | import org.cryptomator.cryptolib.api.FileNameCryptor; 14 | import org.cryptomator.cryptolib.api.Masterkey; 15 | import org.cryptomator.cryptolib.common.DestroyableSecretKey; 16 | import org.cryptomator.cryptolib.common.MessageDigestSupplier; 17 | import org.cryptomator.cryptolib.common.ObjectPool; 18 | import org.cryptomator.siv.SivMode; 19 | import org.cryptomator.siv.UnauthenticCiphertextException; 20 | 21 | import javax.crypto.IllegalBlockSizeException; 22 | import java.security.MessageDigest; 23 | 24 | import static java.nio.charset.StandardCharsets.UTF_8; 25 | 26 | class FileNameCryptorImpl implements FileNameCryptor { 27 | 28 | private static final BaseEncoding BASE32 = BaseEncoding.base32(); 29 | private static final ObjectPool AES_SIV = new ObjectPool<>(SivMode::new); 30 | 31 | private final Masterkey masterkey; 32 | 33 | FileNameCryptorImpl(Masterkey masterkey) { 34 | this.masterkey = masterkey; 35 | } 36 | 37 | @Override 38 | public String hashDirectoryId(String cleartextDirectoryId) { 39 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey(); 40 | ObjectPool.Lease sha1 = MessageDigestSupplier.SHA1.instance(); 41 | ObjectPool.Lease siv = AES_SIV.get()) { 42 | byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); 43 | byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes); 44 | byte[] hashedBytes = sha1.get().digest(encryptedBytes); 45 | return BASE32.encode(hashedBytes); 46 | } 47 | } 48 | 49 | @Override 50 | public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) { 51 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey(); 52 | ObjectPool.Lease siv = AES_SIV.get()) { 53 | byte[] cleartextBytes = cleartextName.getBytes(UTF_8); 54 | byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes, associatedData); 55 | return encoding.encode(encryptedBytes); 56 | } 57 | } 58 | 59 | @Override 60 | public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { 61 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey(); 62 | ObjectPool.Lease siv = AES_SIV.get()) { 63 | byte[] encryptedBytes = encoding.decode(ciphertextName); 64 | byte[] cleartextBytes = siv.get().decrypt(ek, mk, encryptedBytes, associatedData); 65 | return new String(cleartextBytes, UTF_8); 66 | } catch (IllegalArgumentException | UnauthenticCiphertextException | IllegalBlockSizeException e) { 67 | throw new AuthenticationFailedException("Invalid Ciphertext.", e); 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java22/module-info.java: -------------------------------------------------------------------------------- 1 | import org.cryptomator.cryptolib.api.CryptorProvider; 2 | 3 | /** 4 | * This module provides the highlevel cryptographic API used by Cryptomator. 5 | * 6 | * @uses CryptorProvider See {@link CryptorProvider#forScheme(CryptorProvider.Scheme)} 7 | * @provides CryptorProvider Providers for {@link org.cryptomator.cryptolib.api.CryptorProvider.Scheme#SIV_CTRMAC SIV/CTR-then-MAC} 8 | * and {@link org.cryptomator.cryptolib.api.CryptorProvider.Scheme#SIV_GCM SIV/GCM} 9 | */ 10 | module org.cryptomator.cryptolib { 11 | requires static org.bouncycastle.provider; // will be shaded 12 | requires static org.bouncycastle.pkix; // will be shaded 13 | requires org.cryptomator.siv; 14 | requires com.google.gson; 15 | requires transitive com.google.common; 16 | requires org.slf4j; 17 | 18 | exports org.cryptomator.cryptolib.api; 19 | exports org.cryptomator.cryptolib.common; 20 | 21 | opens org.cryptomator.cryptolib.common to com.google.gson; 22 | 23 | uses CryptorProvider; 24 | 25 | provides CryptorProvider 26 | with org.cryptomator.cryptolib.v1.CryptorProviderImpl, org.cryptomator.cryptolib.v2.CryptorProviderImpl; 27 | } -------------------------------------------------------------------------------- /src/main/java9/module-info.java: -------------------------------------------------------------------------------- 1 | import org.cryptomator.cryptolib.api.CryptorProvider; 2 | 3 | /** 4 | * This module provides the highlevel cryptographic API used by Cryptomator. 5 | * 6 | * @uses CryptorProvider See {@link CryptorProvider#forScheme(CryptorProvider.Scheme)} 7 | * @provides CryptorProvider Providers for {@link org.cryptomator.cryptolib.api.CryptorProvider.Scheme#SIV_CTRMAC SIV/CTR-then-MAC} 8 | * and {@link org.cryptomator.cryptolib.api.CryptorProvider.Scheme#SIV_GCM SIV/GCM} 9 | */ 10 | module org.cryptomator.cryptolib { 11 | requires static org.bouncycastle.provider; // will be shaded 12 | requires static org.bouncycastle.pkix; // will be shaded 13 | requires jdk.crypto.ec; // required at runtime for ECC 14 | requires org.cryptomator.siv; 15 | requires com.google.gson; 16 | requires transitive com.google.common; 17 | requires org.slf4j; 18 | 19 | exports org.cryptomator.cryptolib.api; 20 | exports org.cryptomator.cryptolib.common; 21 | 22 | opens org.cryptomator.cryptolib.common to com.google.gson; 23 | 24 | uses CryptorProvider; 25 | 26 | provides CryptorProvider 27 | with org.cryptomator.cryptolib.v1.CryptorProviderImpl, org.cryptomator.cryptolib.v2.CryptorProviderImpl; 28 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/org.cryptomator/cryptolib/reflect-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"byte[]" 4 | }, 5 | { 6 | "name":"com.sun.crypto.provider.AESWrapCipher$General", 7 | "methods":[{"name":"","parameterTypes":[] }] 8 | }, 9 | { 10 | "name":"com.sun.crypto.provider.HmacCore$HmacSHA256", 11 | "methods":[{"name":"","parameterTypes":[] }] 12 | }, 13 | { 14 | "name":"java.lang.reflect.AccessibleObject", 15 | "fields":[{"name":"override"}] 16 | }, 17 | { 18 | "name":"java.security.MessageDigestSpi" 19 | }, 20 | { 21 | "name":"java.security.SecureRandomParameters" 22 | }, 23 | { 24 | "name":"org.cryptomator.cryptolib.common.MasterkeyFile", 25 | "allDeclaredFields":true, 26 | "methods":[{"name":"","parameterTypes":[] }] 27 | }, 28 | { 29 | "name":"sun.misc.Unsafe", 30 | "fields":[{"name":"theUnsafe"}], 31 | "methods":[ 32 | {"name":"objectFieldOffset","parameterTypes":["java.lang.reflect.Field"] }, 33 | {"name":"putBoolean","parameterTypes":["java.lang.Object","long","boolean"] } 34 | ] 35 | }, 36 | { 37 | "name":"sun.security.provider.NativePRNG", 38 | "methods":[{"name":"","parameterTypes":[] }] 39 | }, 40 | { 41 | "name":"sun.security.provider.SHA2$SHA256", 42 | "methods":[ 43 | {"name":"","parameterTypes":[] } 44 | ] 45 | } 46 | ] -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/org.cryptomator.cryptolib.api.CryptorProvider: -------------------------------------------------------------------------------- 1 | org.cryptomator.cryptolib.v1.CryptorProviderImpl 2 | org.cryptomator.cryptolib.v2.CryptorProviderImpl -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.api; 10 | 11 | import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel; 12 | import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel; 13 | import org.cryptomator.cryptolib.common.SecureRandomMock; 14 | import org.cryptomator.cryptolib.common.SeekableByteChannelMock; 15 | import org.hamcrest.CoreMatchers; 16 | import org.hamcrest.MatcherAssert; 17 | import org.junit.jupiter.api.Assertions; 18 | import org.junit.jupiter.api.Assumptions; 19 | import org.junit.jupiter.params.ParameterizedTest; 20 | import org.junit.jupiter.params.provider.MethodSource; 21 | 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | import java.nio.channels.ReadableByteChannel; 25 | import java.nio.channels.WritableByteChannel; 26 | import java.security.SecureRandom; 27 | import java.util.Arrays; 28 | import java.util.stream.Stream; 29 | 30 | public class CryptoLibIntegrationTest { 31 | 32 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; 33 | 34 | private static Stream getCryptors() { 35 | return Stream.of( 36 | CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_CTRMAC).provide(Masterkey.generate(RANDOM_MOCK), RANDOM_MOCK), 37 | CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_GCM).provide(Masterkey.generate(RANDOM_MOCK), RANDOM_MOCK) 38 | ); 39 | } 40 | 41 | @ParameterizedTest 42 | @MethodSource("getCryptors") 43 | public void testDecryptEncrypted(Cryptor cryptor) throws IOException { 44 | int size = 1 * 1024 * 1024; 45 | ByteBuffer ciphertextBuffer = ByteBuffer.allocate(2 * size); 46 | 47 | ByteBuffer cleartext = ByteBuffer.allocate(size); 48 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor)) { 49 | int written = ch.write(cleartext); 50 | Assertions.assertEquals(size, written); 51 | } 52 | 53 | ciphertextBuffer.flip(); 54 | 55 | ByteBuffer result = ByteBuffer.allocate(size + 1); 56 | try (ReadableByteChannel ch = new DecryptingReadableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor, true)) { 57 | int read = ch.read(result); 58 | Assertions.assertEquals(size, read); 59 | } 60 | 61 | Assertions.assertArrayEquals(cleartext.array(), Arrays.copyOfRange(result.array(), 0, size)); 62 | } 63 | 64 | @ParameterizedTest 65 | @MethodSource("getCryptors") 66 | public void testDecryptManipulatedEncrypted(Cryptor cryptor) throws IOException { 67 | int size = 1 * 1024 * 1024; 68 | ByteBuffer ciphertextBuffer = ByteBuffer.allocate(2 * size); 69 | 70 | ByteBuffer cleartext = ByteBuffer.allocate(size); 71 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor)) { 72 | int written = ch.write(cleartext); 73 | Assertions.assertEquals(size, written); 74 | } 75 | 76 | ciphertextBuffer.position(0); 77 | int firstByteOfFirstChunk = cryptor.fileHeaderCryptor().headerSize() + 1; // not inside chunk MAC 78 | ciphertextBuffer.put(firstByteOfFirstChunk, (byte) ~ciphertextBuffer.get(firstByteOfFirstChunk)); 79 | 80 | ByteBuffer result = ByteBuffer.allocate(size + 1); 81 | try (ReadableByteChannel ch = new DecryptingReadableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor, true)) { 82 | IOException thrown = Assertions.assertThrows(IOException.class, () -> { 83 | ch.read(result); 84 | }); 85 | MatcherAssert.assertThat(thrown.getCause(), CoreMatchers.instanceOf(AuthenticationFailedException.class)); 86 | } 87 | } 88 | 89 | @ParameterizedTest 90 | @MethodSource("getCryptors") 91 | public void testDecryptManipulatedEncryptedSkipAuth(Cryptor cryptor) throws IOException { 92 | Assumptions.assumeTrue(cryptor.fileContentCryptor().canSkipAuthentication(), "cryptor doesn't support decryption of unauthentic ciphertext"); 93 | int size = 1 * 1024 * 1024; 94 | ByteBuffer ciphertextBuffer = ByteBuffer.allocate(2 * size); 95 | 96 | ByteBuffer cleartext = ByteBuffer.allocate(size); 97 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor)) { 98 | int written = ch.write(cleartext); 99 | Assertions.assertEquals(size, written); 100 | } 101 | 102 | ciphertextBuffer.flip(); 103 | int lastByteOfFirstChunk = cryptor.fileHeaderCryptor().headerSize() + cryptor.fileContentCryptor().ciphertextChunkSize() - 1; // inside chunk MAC 104 | ciphertextBuffer.put(lastByteOfFirstChunk, (byte) ~ciphertextBuffer.get(lastByteOfFirstChunk)); 105 | 106 | ByteBuffer result = ByteBuffer.allocate(size + 1); 107 | try (ReadableByteChannel ch = new DecryptingReadableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor, false)) { 108 | int read = ch.read(result); 109 | Assertions.assertEquals(size, read); 110 | } 111 | 112 | Assertions.assertArrayEquals(cleartext.array(), Arrays.copyOfRange(result.array(), 0, size)); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/api/CryptorProviderTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.api; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.EnumSource; 7 | 8 | public class CryptorProviderTest { 9 | 10 | @DisplayName("CryptorProvider.forScheme(...)") 11 | @ParameterizedTest 12 | @EnumSource(CryptorProvider.Scheme.class) 13 | public void testForScheme(CryptorProvider.Scheme scheme) { 14 | CryptorProvider provider = CryptorProvider.forScheme(scheme); 15 | Assertions.assertNotNull(provider); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/api/FileContentCryptorTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.api; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.CsvSource; 7 | import org.junit.jupiter.params.provider.ValueSource; 8 | import org.mockito.Mockito; 9 | 10 | public class FileContentCryptorTest { 11 | 12 | private final FileContentCryptor contentCryptor = Mockito.mock(FileContentCryptor.class); 13 | 14 | @BeforeEach 15 | public void setup() { 16 | Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(32); 17 | Mockito.when(contentCryptor.ciphertextChunkSize()).thenReturn(40); 18 | Mockito.doCallRealMethod().when(contentCryptor).cleartextSize(Mockito.anyLong()); 19 | Mockito.doCallRealMethod().when(contentCryptor).ciphertextSize(Mockito.anyLong()); 20 | } 21 | 22 | @ParameterizedTest(name = "cleartextSize({1}) == {0}") 23 | @CsvSource(value = { 24 | "0,0", 25 | "1,9", 26 | "31,39", 27 | "32,40", 28 | "33,49", 29 | "34,50", 30 | "63,79", 31 | "64,80", 32 | "65,89" 33 | }) 34 | public void testCleartextSize(int cleartextSize, int ciphertextSize) { 35 | Assertions.assertEquals(cleartextSize, contentCryptor.cleartextSize(ciphertextSize)); 36 | } 37 | 38 | @ParameterizedTest(name = "cleartextSize({0}) == undefined") 39 | @ValueSource(ints = {-1, 1, 8, 41, 48, 81, 88}) 40 | public void testCleartextSizeWithInvalidCiphertextSize(int invalidCiphertextSize) { 41 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 42 | contentCryptor.cleartextSize(invalidCiphertextSize); 43 | }); 44 | } 45 | 46 | @ParameterizedTest(name = "ciphertextSize({0}) == {1}") 47 | @CsvSource(value = { 48 | "0,0", 49 | "1,9", 50 | "31,39", 51 | "32,40", 52 | "33,49", 53 | "34,50", 54 | "63,79", 55 | "64,80", 56 | "65,89" 57 | }) 58 | public void testCiphertextSize(int cleartextSize, int ciphertextSize) { 59 | Assertions.assertEquals(ciphertextSize, contentCryptor.ciphertextSize(cleartextSize)); 60 | } 61 | 62 | @ParameterizedTest(name = "ciphertextSize({0}) == undefined") 63 | @ValueSource(ints = {-1}) 64 | public void testCiphertextSizewithInvalidCleartextSize(int invalidCleartextSize) { 65 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 66 | contentCryptor.ciphertextSize(invalidCleartextSize); 67 | }); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/AesKeyWrapTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import javax.crypto.Cipher; 15 | import javax.crypto.SecretKey; 16 | import javax.crypto.spec.SecretKeySpec; 17 | import java.security.InvalidKeyException; 18 | 19 | public class AesKeyWrapTest { 20 | 21 | @Test 22 | public void wrapAndUnwrap() throws InvalidKeyException { 23 | DestroyableSecretKey kek = new DestroyableSecretKey(new byte[32], "AES"); 24 | SecretKey key = new SecretKeySpec(new byte[32], "AES"); 25 | byte[] wrapped = AesKeyWrap.wrap(kek, key); 26 | SecretKey unwrapped = AesKeyWrap.unwrap(kek, wrapped, "AES"); 27 | Assertions.assertEquals(key, unwrapped); 28 | } 29 | 30 | @Test 31 | public void wrapWithInvalidKey() { 32 | DestroyableSecretKey kek = new DestroyableSecretKey(new byte[32], "AES"); 33 | SecretKey key = new SecretKeySpec(new byte[17], "AES"); 34 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 35 | AesKeyWrap.wrap(kek, key); 36 | }); 37 | } 38 | 39 | @Test 40 | public void unwrapWithInvalidKey() { 41 | DestroyableSecretKey kek = new DestroyableSecretKey(new byte[32], "AES"); 42 | SecretKey key = new SecretKeySpec(new byte[32], "AES"); 43 | byte[] wrapped = AesKeyWrap.wrap(kek, key); 44 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 45 | AesKeyWrap.unwrap(kek, wrapped, "FOO", Cipher.PRIVATE_KEY); 46 | }); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/CipherSupplierTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import org.hamcrest.CoreMatchers; 12 | import org.hamcrest.MatcherAssert; 13 | import org.junit.jupiter.api.Assertions; 14 | import org.junit.jupiter.api.Test; 15 | 16 | import javax.crypto.SecretKey; 17 | import javax.crypto.spec.IvParameterSpec; 18 | import javax.crypto.spec.RC5ParameterSpec; 19 | import java.security.spec.AlgorithmParameterSpec; 20 | 21 | public class CipherSupplierTest { 22 | 23 | @Test 24 | public void testGetUnknownCipher() { 25 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 26 | new CipherSupplier("doesNotExist"); 27 | }); 28 | } 29 | 30 | @Test 31 | public void testGetCipherWithInvalidKey() { 32 | CipherSupplier supplier = new CipherSupplier("AES/CBC/PKCS5Padding"); 33 | SecretKey key = new DestroyableSecretKey(new byte[13], "AES"); 34 | AlgorithmParameterSpec params = new IvParameterSpec(new byte[16]); 35 | IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> { 36 | supplier.encryptionCipher(key, params); 37 | }); 38 | MatcherAssert.assertThat(exception.getMessage(), CoreMatchers.containsString("Invalid key")); 39 | } 40 | 41 | @Test 42 | public void testGetCipherWithInvalidAlgorithmParam() { 43 | CipherSupplier supplier = new CipherSupplier("AES/CBC/PKCS5Padding"); 44 | SecretKey key = new DestroyableSecretKey(new byte[16], "AES"); 45 | AlgorithmParameterSpec params = new RC5ParameterSpec(1, 1, 8); 46 | IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> { 47 | supplier.encryptionCipher(key, params); 48 | }); 49 | MatcherAssert.assertThat(exception.getMessage(), CoreMatchers.containsString("Algorithm parameter not appropriate for")); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/DecryptingReadableByteChannelTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException; 12 | import org.cryptomator.cryptolib.api.Cryptor; 13 | import org.cryptomator.cryptolib.api.FileContentCryptor; 14 | import org.cryptomator.cryptolib.api.FileHeader; 15 | import org.cryptomator.cryptolib.api.FileHeaderCryptor; 16 | import org.junit.jupiter.api.Assertions; 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | import org.mockito.Mockito; 20 | 21 | import java.io.ByteArrayInputStream; 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | import java.nio.channels.Channels; 25 | import java.nio.channels.ReadableByteChannel; 26 | import java.util.Arrays; 27 | 28 | import static java.nio.charset.StandardCharsets.UTF_8; 29 | 30 | public class DecryptingReadableByteChannelTest { 31 | 32 | private Cryptor cryptor; 33 | private FileContentCryptor contentCryptor; 34 | private FileHeaderCryptor headerCryptor; 35 | private FileHeader header; 36 | 37 | @BeforeEach 38 | public void setup() throws AuthenticationFailedException { 39 | cryptor = Mockito.mock(Cryptor.class); 40 | contentCryptor = Mockito.mock(FileContentCryptor.class); 41 | headerCryptor = Mockito.mock(FileHeaderCryptor.class); 42 | header = Mockito.mock(FileHeader.class); 43 | Mockito.when(cryptor.fileContentCryptor()).thenReturn(contentCryptor); 44 | Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(headerCryptor); 45 | Mockito.when(contentCryptor.ciphertextChunkSize()).thenReturn(10); 46 | Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(10); 47 | Mockito.when(headerCryptor.headerSize()).thenReturn(5); 48 | Mockito.when(headerCryptor.decryptHeader(Mockito.any(ByteBuffer.class))).thenReturn(header); 49 | Mockito.when(contentCryptor.decryptChunk(Mockito.any(ByteBuffer.class), Mockito.anyLong(), Mockito.any(FileHeader.class), Mockito.anyBoolean())).thenAnswer(invocation -> { 50 | ByteBuffer input = invocation.getArgument(0); 51 | String inStr = UTF_8.decode(input).toString(); 52 | return ByteBuffer.wrap(inStr.toLowerCase().getBytes(UTF_8)); 53 | }); 54 | } 55 | 56 | @Test 57 | public void testDecryption() throws IOException, AuthenticationFailedException { 58 | ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream("hhhhhTOPSECRET!TOPSECRET!".getBytes())); 59 | ByteBuffer result = ByteBuffer.allocate(30); 60 | try (DecryptingReadableByteChannel ch = new DecryptingReadableByteChannel(src, cryptor, true)) { 61 | int read1 = ch.read(result); 62 | Assertions.assertEquals(20, read1); 63 | int read2 = ch.read(result); 64 | Assertions.assertEquals(-1, read2); 65 | Assertions.assertArrayEquals("topsecret!topsecret!".getBytes(), Arrays.copyOfRange(result.array(), 0, read1)); 66 | } 67 | Mockito.verify(contentCryptor).decryptChunk(Mockito.any(), Mockito.eq(0l), Mockito.eq(header), Mockito.eq(true)); 68 | Mockito.verify(contentCryptor).decryptChunk(Mockito.any(), Mockito.eq(1l), Mockito.eq(header), Mockito.eq(true)); 69 | } 70 | 71 | @Test 72 | public void testRandomAccessDecryption() throws IOException, AuthenticationFailedException { 73 | ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream("TOPSECRET!".getBytes())); 74 | ByteBuffer result = ByteBuffer.allocate(30); 75 | try (DecryptingReadableByteChannel ch = new DecryptingReadableByteChannel(src, cryptor, true, header, 1)) { 76 | int read1 = ch.read(result); 77 | Assertions.assertEquals(10, read1); 78 | int read2 = ch.read(result); 79 | Assertions.assertEquals(-1, read2); 80 | Assertions.assertArrayEquals("topsecret!".getBytes(), Arrays.copyOfRange(result.array(), 0, read1)); 81 | } 82 | Mockito.verify(contentCryptor).decryptChunk(Mockito.any(), Mockito.eq(1l), Mockito.eq(header), Mockito.eq(true)); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/DestroyablesTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.mockito.Mockito; 6 | 7 | import javax.security.auth.DestroyFailedException; 8 | import javax.security.auth.Destroyable; 9 | 10 | public class DestroyablesTest { 11 | 12 | @Test 13 | public void testDestroySilently() throws DestroyFailedException { 14 | Destroyable destroyable = Mockito.mock(Destroyable.class); 15 | 16 | Assertions.assertDoesNotThrow(() -> { 17 | Destroyables.destroySilently(destroyable); 18 | }); 19 | 20 | Mockito.verify(destroyable).destroy(); 21 | } 22 | 23 | @Test 24 | public void testDestroySilentlyIgnoresNull() { 25 | Assertions.assertDoesNotThrow(() -> { 26 | Destroyables.destroySilently(null); 27 | }); 28 | } 29 | 30 | @Test 31 | public void testDestroySilentlySuppressesException() throws DestroyFailedException { 32 | Destroyable destroyable = Mockito.mock(Destroyable.class); 33 | Mockito.doThrow(new DestroyFailedException()).when(destroyable).destroy(); 34 | 35 | Assertions.assertDoesNotThrow(() -> { 36 | Destroyables.destroySilently(destroyable); 37 | }); 38 | 39 | Mockito.verify(destroyable).destroy(); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/EncryptingReadableByteChannelTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.cryptomator.cryptolib.api.Cryptor; 4 | import org.cryptomator.cryptolib.api.FileContentCryptor; 5 | import org.cryptomator.cryptolib.api.FileHeader; 6 | import org.cryptomator.cryptolib.api.FileHeaderCryptor; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.mockito.Mockito; 11 | 12 | import java.io.ByteArrayInputStream; 13 | import java.io.IOException; 14 | import java.nio.ByteBuffer; 15 | import java.nio.channels.Channels; 16 | import java.nio.channels.ReadableByteChannel; 17 | import java.util.Arrays; 18 | 19 | import static java.nio.charset.StandardCharsets.UTF_8; 20 | 21 | class EncryptingReadableByteChannelTest { 22 | 23 | private ByteBuffer dstFile; 24 | private ReadableByteChannel srcFileChannel; 25 | private Cryptor cryptor; 26 | private FileContentCryptor contentCryptor; 27 | private FileHeaderCryptor headerCryptor; 28 | private FileHeader header; 29 | 30 | @BeforeEach 31 | public void setup() { 32 | dstFile = ByteBuffer.allocate(100); 33 | srcFileChannel = new SeekableByteChannelMock(dstFile); 34 | cryptor = Mockito.mock(Cryptor.class); 35 | contentCryptor = Mockito.mock(FileContentCryptor.class); 36 | headerCryptor = Mockito.mock(FileHeaderCryptor.class); 37 | header = Mockito.mock(FileHeader.class); 38 | Mockito.when(cryptor.fileContentCryptor()).thenReturn(contentCryptor); 39 | Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(headerCryptor); 40 | Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(10); 41 | Mockito.when(headerCryptor.create()).thenReturn(header); 42 | Mockito.when(headerCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".getBytes())); 43 | Mockito.when(contentCryptor.encryptChunk(Mockito.any(ByteBuffer.class), Mockito.anyLong(), Mockito.any(FileHeader.class))).thenAnswer(invocation -> { 44 | ByteBuffer input = invocation.getArgument(0); 45 | String inStr = UTF_8.decode(input).toString(); 46 | return ByteBuffer.wrap(inStr.toUpperCase().getBytes(UTF_8)); 47 | }); 48 | } 49 | 50 | @Test 51 | public void testEncryptionOfEmptyCleartext() throws IOException { 52 | ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream(new byte[0])); 53 | ByteBuffer result = ByteBuffer.allocate(10); 54 | try (EncryptingReadableByteChannel ch = new EncryptingReadableByteChannel(src, cryptor)) { 55 | int read1 = ch.read(result); 56 | Assertions.assertEquals(5, read1); 57 | int read2 = ch.read(result); 58 | Assertions.assertEquals(-1, read2); 59 | Assertions.assertArrayEquals("hhhhh".getBytes(), Arrays.copyOfRange(result.array(), 0, read1)); 60 | } 61 | Mockito.verify(contentCryptor, Mockito.never()).encryptChunk(Mockito.any(), Mockito.anyLong(), Mockito.any()); 62 | } 63 | 64 | @Test 65 | public void testEncryptionOfCleartext() throws IOException { 66 | ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream("hello world 1 hello world 2".getBytes())); 67 | ByteBuffer result = ByteBuffer.allocate(50); 68 | try (EncryptingReadableByteChannel ch = new EncryptingReadableByteChannel(src, cryptor)) { 69 | int read1 = ch.read(result); 70 | Assertions.assertEquals(32, read1); 71 | int read2 = ch.read(result); 72 | Assertions.assertEquals(-1, read2); 73 | Assertions.assertArrayEquals("hhhhhHELLO WORLD 1 HELLO WORLD 2".getBytes(), Arrays.copyOfRange(result.array(), 0, read1)); 74 | } 75 | Mockito.verify(contentCryptor).encryptChunk(Mockito.any(), Mockito.eq(0l), Mockito.eq(header)); 76 | Mockito.verify(contentCryptor).encryptChunk(Mockito.any(), Mockito.eq(1l), Mockito.eq(header)); 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/EncryptingWritableByteChannelTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.cryptomator.cryptolib.api.Cryptor; 4 | import org.cryptomator.cryptolib.api.FileContentCryptor; 5 | import org.cryptomator.cryptolib.api.FileHeader; 6 | import org.cryptomator.cryptolib.api.FileHeaderCryptor; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.mockito.Mockito; 11 | 12 | import java.io.IOException; 13 | import java.nio.ByteBuffer; 14 | import java.nio.channels.WritableByteChannel; 15 | import java.util.Arrays; 16 | 17 | import static java.nio.charset.StandardCharsets.UTF_8; 18 | 19 | public class EncryptingWritableByteChannelTest { 20 | 21 | private ByteBuffer dstFile; 22 | private WritableByteChannel dstFileChannel; 23 | private Cryptor cryptor; 24 | private FileContentCryptor contentCryptor; 25 | private FileHeaderCryptor headerCryptor; 26 | private FileHeader header; 27 | 28 | @BeforeEach 29 | public void setup() { 30 | dstFile = ByteBuffer.allocate(100); 31 | dstFileChannel = new SeekableByteChannelMock(dstFile); 32 | cryptor = Mockito.mock(Cryptor.class); 33 | contentCryptor = Mockito.mock(FileContentCryptor.class); 34 | headerCryptor = Mockito.mock(FileHeaderCryptor.class); 35 | header = Mockito.mock(FileHeader.class); 36 | Mockito.when(cryptor.fileContentCryptor()).thenReturn(contentCryptor); 37 | Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(headerCryptor); 38 | Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(10); 39 | Mockito.when(headerCryptor.create()).thenReturn(header); 40 | Mockito.when(headerCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".getBytes())); 41 | Mockito.when(contentCryptor.encryptChunk(Mockito.any(ByteBuffer.class), Mockito.anyLong(), Mockito.any(FileHeader.class))).thenAnswer(invocation -> { 42 | ByteBuffer input = invocation.getArgument(0); 43 | String inStr = UTF_8.decode(input).toString(); 44 | String outStr = "<" + inStr.toUpperCase() + ">"; 45 | return UTF_8.encode(outStr); 46 | }); 47 | } 48 | 49 | @Test 50 | public void testEncryption() throws IOException { 51 | try (EncryptingWritableByteChannel ch = new EncryptingWritableByteChannel(dstFileChannel, cryptor)) { 52 | ch.write(UTF_8.encode("hello world 1")); 53 | ch.write(UTF_8.encode("hello world 2")); 54 | } 55 | dstFile.flip(); 56 | Assertions.assertEquals("hhhhh", UTF_8.decode(dstFile).toString()); 57 | } 58 | 59 | @Test 60 | public void testEncryptionOfEmptyFile() throws IOException { 61 | try (EncryptingWritableByteChannel ch = new EncryptingWritableByteChannel(dstFileChannel, cryptor)) { 62 | // empty, so write nothing 63 | } 64 | dstFile.flip(); 65 | Assertions.assertEquals("hhhhh<>", UTF_8.decode(dstFile).toString()); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/GcmTestHelper.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import javax.crypto.Cipher; 4 | import javax.crypto.SecretKey; 5 | import javax.crypto.spec.GCMParameterSpec; 6 | import javax.crypto.spec.SecretKeySpec; 7 | import java.security.InvalidAlgorithmParameterException; 8 | import java.security.InvalidKeyException; 9 | import java.security.Key; 10 | import java.security.spec.AlgorithmParameterSpec; 11 | import java.util.Random; 12 | 13 | public class GcmTestHelper { 14 | 15 | private static final Random RNG = new Random(42L); 16 | 17 | /** 18 | * Java's default GCM implementation has built-in IV-reuse protection. In order to run certain unit tests, 19 | * this needs to be bypassed. The easiest way would be to re-initialize the cipher before running a test. 20 | *

21 | * This method can be used to init a cipher using randomized key-iv-pairs during test setup and avoid 22 | * InvalidAlgorithmParameterExceptions. 23 | * 24 | * @param cipherInitializer The {@link Cipher#init(int, Key, AlgorithmParameterSpec) cipher.init()} or equivalent method 25 | */ 26 | public static void reset(CipherInitializer cipherInitializer) { 27 | byte[] keyBytes = new byte[16]; 28 | byte[] nonceBytes = new byte[12]; 29 | RNG.nextBytes(keyBytes); 30 | RNG.nextBytes(nonceBytes); 31 | SecretKey key = new SecretKeySpec(keyBytes, "AES"); 32 | AlgorithmParameterSpec params = new GCMParameterSpec(96, nonceBytes); 33 | try { 34 | cipherInitializer.init(Cipher.ENCRYPT_MODE, key, params); 35 | } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { 36 | throw new IllegalArgumentException("Failed to reset cipher"); 37 | } 38 | } 39 | 40 | @FunctionalInterface 41 | public interface CipherInitializer { 42 | 43 | void init(int opmode, SecretKey key, AlgorithmParameterSpec params) 44 | throws InvalidKeyException, InvalidAlgorithmParameterException; 45 | 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/MacSupplierTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import javax.crypto.Mac; 15 | import javax.crypto.SecretKey; 16 | import javax.crypto.spec.SecretKeySpec; 17 | import java.lang.reflect.Method; 18 | import java.security.Key; 19 | import java.security.KeyPairGenerator; 20 | import java.security.NoSuchAlgorithmException; 21 | 22 | public class MacSupplierTest { 23 | 24 | @Test 25 | public void testConstructorWithInvalidDigest() { 26 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 27 | new MacSupplier("FOO3000"); 28 | }); 29 | } 30 | 31 | @Test 32 | public void testGetMac() { 33 | SecretKey key = new SecretKeySpec(new byte[16], "HmacSHA256"); 34 | try (ObjectPool.Lease mac1 = MacSupplier.HMAC_SHA256.keyed(key)) { 35 | Assertions.assertNotNull(mac1); 36 | } 37 | 38 | try (ObjectPool.Lease mac2 = MacSupplier.HMAC_SHA256.keyed(key)) { 39 | Assertions.assertNotNull(mac2); 40 | } 41 | } 42 | 43 | @Test 44 | public void testGetMacWithInvalidKey() throws NoSuchAlgorithmException, ReflectiveOperationException { 45 | Key key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate(); 46 | // invoked via reflection, as we can not cast Key to SecretKey. 47 | Method m = MacSupplier.class.getMethod("keyed", SecretKey.class); 48 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 49 | m.invoke(MacSupplier.HMAC_SHA256, key); 50 | }); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.hamcrest.CoreMatchers; 4 | import org.hamcrest.MatcherAssert; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.io.IOException; 9 | import java.io.StringReader; 10 | import java.io.StringWriter; 11 | 12 | public class MasterkeyFileTest { 13 | 14 | @Test 15 | public void testRead() throws IOException { 16 | MasterkeyFile masterkeyFile = MasterkeyFile.read(new StringReader("{\"scryptSalt\": \"Zm9v\"}")); 17 | 18 | Assertions.assertArrayEquals("foo".getBytes(), masterkeyFile.scryptSalt); 19 | } 20 | 21 | @Test 22 | public void testWrite() throws IOException { 23 | MasterkeyFile masterkeyFile = new MasterkeyFile(); 24 | masterkeyFile.scryptSalt = "foo".getBytes(); 25 | 26 | StringWriter jsonWriter = new StringWriter(); 27 | masterkeyFile.write(jsonWriter); 28 | String json = jsonWriter.toString(); 29 | 30 | MatcherAssert.assertThat(json, CoreMatchers.containsString("\"scryptSalt\": \"Zm9v\"")); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.cryptomator.cryptolib.api.Masterkey; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.mockito.Mockito; 8 | 9 | import javax.crypto.SecretKey; 10 | import java.security.SecureRandom; 11 | import java.util.Arrays; 12 | 13 | public class MasterkeyTest { 14 | 15 | private byte[] raw; 16 | private Masterkey masterkey; 17 | 18 | @BeforeEach 19 | public void setup() { 20 | raw = new byte[64]; 21 | for (byte b=0; b { 21 | new MessageDigestSupplier("FOO3000"); 22 | }); 23 | } 24 | 25 | @Test 26 | public void testGetSha1() { 27 | try (ObjectPool.Lease digest = MessageDigestSupplier.SHA1.instance()) { 28 | Assertions.assertNotNull(digest); 29 | } 30 | 31 | try (ObjectPool.Lease digest = MessageDigestSupplier.SHA1.instance()) { 32 | Assertions.assertNotNull(digest); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/ObjectPoolTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | import org.mockito.Mockito; 8 | 9 | import java.util.function.Supplier; 10 | 11 | public class ObjectPoolTest { 12 | 13 | private Supplier factory = Mockito.mock(Supplier.class); 14 | private ObjectPool pool = new ObjectPool<>(factory); 15 | 16 | @BeforeEach 17 | public void setup() { 18 | Mockito.doAnswer(invocation -> new Foo()).when(factory).get(); 19 | } 20 | 21 | @Test 22 | @DisplayName("new instance is created if pool is empty") 23 | public void testCreateNewObjWhenPoolIsEmpty() { 24 | try (ObjectPool.Lease lease1 = pool.get()) { 25 | try (ObjectPool.Lease lease2 = pool.get()) { 26 | Assertions.assertNotSame(lease1.get(), lease2.get()); 27 | } 28 | } 29 | Mockito.verify(factory, Mockito.times(2)).get(); 30 | } 31 | 32 | @Test 33 | @DisplayName("recycle existing instance") 34 | public void testRecycleExistingObj() { 35 | Foo foo1; 36 | try (ObjectPool.Lease lease = pool.get()) { 37 | foo1 = lease.get(); 38 | } 39 | try (ObjectPool.Lease lease = pool.get()) { 40 | Assertions.assertSame(foo1, lease.get()); 41 | } 42 | Mockito.verify(factory, Mockito.times(1)).get(); 43 | } 44 | 45 | @Test 46 | @DisplayName("create new instance when pool is GC'ed") 47 | public void testGc() { 48 | try (ObjectPool.Lease lease = pool.get()) { 49 | Assertions.assertNotNull(lease.get()); 50 | } 51 | System.gc(); // seems to be reliable on Temurin 17 with @RepeatedTest(1000) 52 | try (ObjectPool.Lease lease = pool.get()) { 53 | Assertions.assertNotNull(lease.get()); 54 | } 55 | Mockito.verify(factory, Mockito.times(2)).get(); 56 | } 57 | 58 | private static class Foo { 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/P384KeyPairTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Nested; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.io.TempDir; 9 | 10 | import java.io.IOException; 11 | import java.nio.file.Path; 12 | import java.security.spec.InvalidKeySpecException; 13 | import java.security.spec.PKCS8EncodedKeySpec; 14 | import java.security.spec.X509EncodedKeySpec; 15 | import java.util.Base64; 16 | 17 | public class P384KeyPairTest { 18 | 19 | @Test 20 | @DisplayName("generate()") 21 | public void testGenerate() { 22 | P384KeyPair keyPair1 = P384KeyPair.generate(); 23 | P384KeyPair keyPair2 = P384KeyPair.generate(); 24 | Assertions.assertNotNull(keyPair1); 25 | Assertions.assertNotNull(keyPair2); 26 | Assertions.assertNotEquals(keyPair1, keyPair2); 27 | } 28 | 29 | @Test 30 | @DisplayName("create()") 31 | public void testCreate() throws InvalidKeySpecException { 32 | X509EncodedKeySpec publicKey = new X509EncodedKeySpec(Base64.getDecoder().decode("MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERxQR+NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0aq6uqmUy4jUhuxnKxsv59A6JeK7Unn+mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu")); 33 | PKCS8EncodedKeySpec privateKey = new PKCS8EncodedKeySpec(Base64.getDecoder().decode("ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ")); 34 | 35 | P384KeyPair keyPair = P384KeyPair.create(publicKey, privateKey); 36 | Assertions.assertNotNull(keyPair); 37 | } 38 | 39 | @Test 40 | @DisplayName("store(...)") 41 | public void testStore(@TempDir Path tmpDir) { 42 | Path p12File = tmpDir.resolve("test.p12"); 43 | P384KeyPair keyPair = P384KeyPair.generate(); 44 | 45 | Assertions.assertDoesNotThrow(() -> { 46 | keyPair.store(p12File, "topsecret".toCharArray()); 47 | }); 48 | } 49 | 50 | @Nested 51 | @DisplayName("With stored PKCS12 file...") 52 | public class WithStored { 53 | 54 | private P384KeyPair origKeyPair; 55 | private Path p12File; 56 | 57 | @BeforeEach 58 | public void setup(@TempDir Path tmpDir) throws IOException { 59 | this.origKeyPair = P384KeyPair.generate(); 60 | this.p12File = tmpDir.resolve("test.p12"); 61 | origKeyPair.store(p12File, "topsecret".toCharArray()); 62 | } 63 | 64 | @Test 65 | @DisplayName("load(...) with invalid passphrase") 66 | public void testLoadWithInvalidPassphrase() { 67 | char[] wrongPassphrase = "bottompublic".toCharArray(); 68 | Assertions.assertThrows(Pkcs12PasswordException.class, () -> { 69 | P384KeyPair.load(p12File, wrongPassphrase); 70 | }); 71 | } 72 | 73 | @Test 74 | @DisplayName("load(...) with valid passphrase") 75 | public void testLoadWithValidPassphrase() throws IOException { 76 | P384KeyPair keyPair = P384KeyPair.load(p12File, "topsecret".toCharArray()); 77 | Assertions.assertEquals(origKeyPair, keyPair); 78 | } 79 | 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/Pkcs12HelperTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Nested; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.io.TempDir; 9 | 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.StandardOpenOption; 16 | import java.security.InvalidAlgorithmParameterException; 17 | import java.security.KeyPair; 18 | import java.security.KeyPairGenerator; 19 | import java.security.NoSuchAlgorithmException; 20 | import java.security.spec.ECGenParameterSpec; 21 | 22 | public class Pkcs12HelperTest { 23 | 24 | private Path p12File; 25 | 26 | @BeforeEach 27 | public void setup(@TempDir Path tmpDir) throws NoSuchAlgorithmException { 28 | this.p12File = tmpDir.resolve("test.p12"); 29 | } 30 | 31 | @Test 32 | @DisplayName("attempt export RSA key pair with EC signature alg") 33 | public void testExportWithInappropriateSignatureAlg() throws NoSuchAlgorithmException, IOException { 34 | KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); 35 | try (OutputStream out = Files.newOutputStream(p12File, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { 36 | char[] passphrase = "topsecret".toCharArray(); 37 | Assertions.assertThrows(Pkcs12Exception.class, () -> { 38 | Pkcs12Helper.exportTo(keyPair, out, passphrase, "SHA256withECDSA"); 39 | }); 40 | } 41 | } 42 | 43 | @Test 44 | @DisplayName("attempt export EC key pair with EC signature alg") 45 | public void testExport() throws NoSuchAlgorithmException, IOException { 46 | KeyPair keyPair = KeyPairGenerator.getInstance("EC").generateKeyPair(); 47 | try (OutputStream out = Files.newOutputStream(p12File, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { 48 | char[] passphrase = "topsecret".toCharArray(); 49 | Assertions.assertDoesNotThrow(() -> { 50 | Pkcs12Helper.exportTo(keyPair, out, passphrase, "SHA256withECDSA"); 51 | }); 52 | } 53 | } 54 | 55 | @Nested 56 | @DisplayName("With exported PKCS12 file...") 57 | public class WithExported { 58 | 59 | private KeyPair keyPair; 60 | private char[] passphrase = "topsecret".toCharArray(); 61 | 62 | @BeforeEach 63 | public void setup() throws NoSuchAlgorithmException, IOException, InvalidAlgorithmParameterException { 64 | KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("EC"); 65 | keyPairGen.initialize(new ECGenParameterSpec("secp384r1")); 66 | this.keyPair = keyPairGen.generateKeyPair(); 67 | try (OutputStream out = Files.newOutputStream(p12File, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { 68 | Pkcs12Helper.exportTo(keyPair, out, passphrase, "SHA384withECDSA"); 69 | } 70 | } 71 | 72 | @Test 73 | @DisplayName("attempt import with invalid passphrase") 74 | public void testImportWithInvalidPassphrase() throws IOException { 75 | try (InputStream in = Files.newInputStream(p12File, StandardOpenOption.READ)) { 76 | char[] wrongPassphrase = "bottompublic".toCharArray(); 77 | Assertions.assertThrows(Pkcs12PasswordException.class, () -> { 78 | Pkcs12Helper.importFrom(in, wrongPassphrase); 79 | }); 80 | } 81 | } 82 | 83 | @Test 84 | @DisplayName("attempt import with valid passphrase") 85 | public void testImportWithValidPassphrase() throws IOException { 86 | try (InputStream in = Files.newInputStream(p12File, StandardOpenOption.READ)) { 87 | KeyPair imported = Pkcs12Helper.importFrom(in, passphrase); 88 | Assertions.assertEquals(keyPair.getPublic().getAlgorithm(), imported.getPublic().getAlgorithm()); 89 | Assertions.assertArrayEquals(keyPair.getPublic().getEncoded(), imported.getPublic().getEncoded()); 90 | } 91 | } 92 | 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/PooledSuppliersBenchmarkTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.openjdk.jmh.annotations.Benchmark; 6 | import org.openjdk.jmh.annotations.BenchmarkMode; 7 | import org.openjdk.jmh.annotations.Level; 8 | import org.openjdk.jmh.annotations.Measurement; 9 | import org.openjdk.jmh.annotations.Mode; 10 | import org.openjdk.jmh.annotations.OutputTimeUnit; 11 | import org.openjdk.jmh.annotations.Scope; 12 | import org.openjdk.jmh.annotations.Setup; 13 | import org.openjdk.jmh.annotations.State; 14 | import org.openjdk.jmh.annotations.Warmup; 15 | import org.openjdk.jmh.infra.Blackhole; 16 | import org.openjdk.jmh.runner.Runner; 17 | import org.openjdk.jmh.runner.RunnerException; 18 | import org.openjdk.jmh.runner.options.Options; 19 | import org.openjdk.jmh.runner.options.OptionsBuilder; 20 | 21 | import javax.crypto.Cipher; 22 | import javax.crypto.Mac; 23 | import javax.crypto.SecretKey; 24 | import javax.crypto.spec.GCMParameterSpec; 25 | import javax.crypto.spec.SecretKeySpec; 26 | import java.security.MessageDigest; 27 | import java.util.Random; 28 | import java.util.concurrent.TimeUnit; 29 | 30 | @State(Scope.Thread) 31 | @Warmup(iterations = 2) 32 | @Measurement(iterations = 2) 33 | @BenchmarkMode(value = {Mode.AverageTime}) 34 | @OutputTimeUnit(TimeUnit.MICROSECONDS) 35 | public class PooledSuppliersBenchmarkTest { 36 | 37 | private static final Random RNG = new Random(42); 38 | private static final CipherSupplier CIPHER_SUPPLIER = CipherSupplier.AES_GCM; 39 | private static final MessageDigestSupplier MD_SUPPLIER = MessageDigestSupplier.SHA256; 40 | private static final MacSupplier MAC_SUPPLIER = MacSupplier.HMAC_SHA256; 41 | private SecretKey key; 42 | private GCMParameterSpec gcmParams; 43 | 44 | @Disabled("only on demand") 45 | @Test 46 | public void runBenchmarks() throws RunnerException { 47 | Options opt = new OptionsBuilder() // 48 | .include(getClass().getName()) // 49 | .threads(2).forks(1) // 50 | .shouldFailOnError(true).shouldDoGC(true) // 51 | .build(); 52 | new Runner(opt).run(); 53 | } 54 | 55 | @Setup(Level.Invocation) 56 | public void shuffleData() { 57 | byte[] bytes = new byte[28]; 58 | RNG.nextBytes(bytes); 59 | this.key = new SecretKeySpec(bytes, 0, 16, "AES"); 60 | this.gcmParams = new GCMParameterSpec(128, bytes, 16, 12); 61 | } 62 | 63 | @Benchmark 64 | public void createCipher(Blackhole blackHole) { 65 | blackHole.consume(CIPHER_SUPPLIER.forEncryption(key, gcmParams)); 66 | } 67 | 68 | @Benchmark 69 | public void recycleCipher(Blackhole blackHole) { 70 | try (ObjectPool.Lease lease = CIPHER_SUPPLIER.encryptionCipher(key, gcmParams)) { 71 | blackHole.consume(lease.get()); 72 | } 73 | } 74 | 75 | @Benchmark 76 | public void createMac(Blackhole blackHole) { 77 | blackHole.consume(MAC_SUPPLIER.withKey(key)); 78 | } 79 | 80 | @Benchmark 81 | public void recycleMac(Blackhole blackHole) { 82 | try (ObjectPool.Lease lease = MAC_SUPPLIER.keyed(key)) { 83 | blackHole.consume(lease.get()); 84 | } 85 | } 86 | 87 | @Benchmark 88 | public void createMd(Blackhole blackHole) { 89 | blackHole.consume(MD_SUPPLIER.get()); 90 | } 91 | 92 | @Benchmark 93 | public void recycleMd(Blackhole blackHole) { 94 | try (ObjectPool.Lease lease = MD_SUPPLIER.instance()) { 95 | blackHole.consume(lease.get()); 96 | } 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/ReseedingSecureRandomTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.mockito.Mockito; 14 | 15 | import java.security.SecureRandom; 16 | 17 | public class ReseedingSecureRandomTest { 18 | 19 | private SecureRandom seeder, csprng; 20 | 21 | @BeforeEach 22 | public void setup() { 23 | seeder = Mockito.mock(SecureRandom.class); 24 | csprng = Mockito.mock(SecureRandom.class); 25 | Mockito.when(seeder.generateSeed(Mockito.anyInt())).then(invocation -> { 26 | int num = invocation.getArgument(0); 27 | return new byte[num]; 28 | }); 29 | 30 | } 31 | 32 | @Test 33 | public void testReseedAfterLimitReached() { 34 | SecureRandom rand = new ReseedingSecureRandom(seeder, csprng, 10, 3); 35 | Mockito.verify(seeder, Mockito.never()).generateSeed(3); 36 | rand.nextBytes(new byte[4]); 37 | Mockito.verify(seeder, Mockito.times(1)).generateSeed(3); 38 | rand.nextBytes(new byte[4]); 39 | Mockito.verify(seeder, Mockito.times(1)).generateSeed(3); 40 | rand.nextBytes(new byte[4]); 41 | Mockito.verify(seeder, Mockito.times(2)).generateSeed(3); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/ScryptTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static java.nio.charset.StandardCharsets.US_ASCII; 7 | 8 | /** 9 | * Tests from https://tools.ietf.org/html/rfc7914#section-12 10 | */ 11 | public class ScryptTest { 12 | 13 | @Test 14 | public void testEmptyString() { 15 | byte[] key = Scrypt.scrypt("", "".getBytes(US_ASCII), 16, 1, 64); 16 | byte[] expected = new byte[] { // 17 | (byte) 0x77, (byte) 0xd6, (byte) 0x57, (byte) 0x62, (byte) 0x38, (byte) 0x65, (byte) 0x7b, (byte) 0x20, // 18 | (byte) 0x3b, (byte) 0x19, (byte) 0xca, (byte) 0x42, (byte) 0xc1, (byte) 0x8a, (byte) 0x04, (byte) 0x97, // 19 | (byte) 0xf1, (byte) 0x6b, (byte) 0x48, (byte) 0x44, (byte) 0xe3, (byte) 0x07, (byte) 0x4a, (byte) 0xe8, // 20 | (byte) 0xdf, (byte) 0xdf, (byte) 0xfa, (byte) 0x3f, (byte) 0xed, (byte) 0xe2, (byte) 0x14, (byte) 0x42, // 21 | (byte) 0xfc, (byte) 0xd0, (byte) 0x06, (byte) 0x9d, (byte) 0xed, (byte) 0x09, (byte) 0x48, (byte) 0xf8, // 22 | (byte) 0x32, (byte) 0x6a, (byte) 0x75, (byte) 0x3a, (byte) 0x0f, (byte) 0xc8, (byte) 0x1f, (byte) 0x17, // 23 | (byte) 0xe8, (byte) 0xd3, (byte) 0xe0, (byte) 0xfb, (byte) 0x2e, (byte) 0x0d, (byte) 0x36, (byte) 0x28, // 24 | (byte) 0xcf, (byte) 0x35, (byte) 0xe2, (byte) 0x0c, (byte) 0x38, (byte) 0xd1, (byte) 0x89, (byte) 0x06 // 25 | }; 26 | Assertions.assertArrayEquals(expected, key); 27 | } 28 | 29 | @Test 30 | public void testPleaseLetMeInString() { 31 | byte[] key = Scrypt.scrypt("pleaseletmein", "SodiumChloride".getBytes(US_ASCII), 16384, 8, 64); 32 | byte[] expected = new byte[] { // 33 | (byte) 0x70, (byte) 0x23, (byte) 0xbd, (byte) 0xcb, (byte) 0x3a, (byte) 0xfd, (byte) 0x73, (byte) 0x48, // 34 | (byte) 0x46, (byte) 0x1c, (byte) 0x06, (byte) 0xcd, (byte) 0x81, (byte) 0xfd, (byte) 0x38, (byte) 0xeb, // 35 | (byte) 0xfd, (byte) 0xa8, (byte) 0xfb, (byte) 0xba, (byte) 0x90, (byte) 0x4f, (byte) 0x8e, (byte) 0x3e, // 36 | (byte) 0xa9, (byte) 0xb5, (byte) 0x43, (byte) 0xf6, (byte) 0x54, (byte) 0x5d, (byte) 0xa1, (byte) 0xf2, // 37 | (byte) 0xd5, (byte) 0x43, (byte) 0x29, (byte) 0x55, (byte) 0x61, (byte) 0x3f, (byte) 0x0f, (byte) 0xcf, // 38 | (byte) 0x62, (byte) 0xd4, (byte) 0x97, (byte) 0x05, (byte) 0x24, (byte) 0x2a, (byte) 0x9a, (byte) 0xf9, // 39 | (byte) 0xe6, (byte) 0x1e, (byte) 0x85, (byte) 0xdc, (byte) 0x0d, (byte) 0x65, (byte) 0x1e, (byte) 0x40, // 40 | (byte) 0xdf, (byte) 0xcf, (byte) 0x01, (byte) 0x7b, (byte) 0x45, (byte) 0x57, (byte) 0x58, (byte) 0x87 // 41 | }; 42 | Assertions.assertArrayEquals(expected, key); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/SecureRandomMock.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import com.google.common.collect.Iterators; 12 | import com.google.common.primitives.Bytes; 13 | 14 | import java.security.SecureRandom; 15 | import java.util.Arrays; 16 | import java.util.Iterator; 17 | import java.util.Random; 18 | 19 | public class SecureRandomMock extends SecureRandom { 20 | 21 | private static final ByteFiller NULL_FILLER = bytes -> Arrays.fill(bytes, (byte) 0x00); 22 | public static final SecureRandomMock NULL_RANDOM = new SecureRandomMock(NULL_FILLER); 23 | private static final ByteFiller PRNG_FILLER = new ByteFiller() { 24 | 25 | private final Random random = new Random(); 26 | 27 | @Override 28 | public void fill(byte[] bytes) { 29 | random.nextBytes(bytes); 30 | } 31 | 32 | }; 33 | public static final SecureRandomMock PRNG_RANDOM = new SecureRandomMock(PRNG_FILLER); 34 | 35 | private final ByteFiller byteFiller; 36 | 37 | public SecureRandomMock(ByteFiller byteFiller) { 38 | this.byteFiller = byteFiller; 39 | } 40 | 41 | @Override 42 | public void nextBytes(byte[] bytes) { 43 | byteFiller.fill(bytes); 44 | } 45 | 46 | public static SecureRandomMock cycle(byte... bytes) { 47 | return new SecureRandomMock(new CyclicByteFiller(bytes)); 48 | } 49 | 50 | public interface ByteFiller { 51 | void fill(byte[] bytes); 52 | } 53 | 54 | private static class CyclicByteFiller implements ByteFiller { 55 | 56 | private final Iterator source; 57 | 58 | CyclicByteFiller(byte... bytes) { 59 | source = Iterators.cycle(Bytes.asList(bytes)); 60 | } 61 | 62 | @Override 63 | public void fill(byte[] bytes) { 64 | Arrays.fill(bytes, source.next()); 65 | } 66 | 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/SeekableByteChannelMock.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.common; 10 | 11 | import java.io.IOException; 12 | import java.nio.ByteBuffer; 13 | import java.nio.channels.SeekableByteChannel; 14 | 15 | public class SeekableByteChannelMock implements SeekableByteChannel { 16 | 17 | boolean open = true; 18 | private final ByteBuffer buf; 19 | 20 | public SeekableByteChannelMock(ByteBuffer buf) { 21 | this.buf = buf; 22 | } 23 | 24 | @Override 25 | public boolean isOpen() { 26 | return open; 27 | } 28 | 29 | @Override 30 | public void close() { 31 | open = false; 32 | } 33 | 34 | @Override 35 | public int read(ByteBuffer dst) throws IOException { 36 | if (!buf.hasRemaining()) { 37 | return -1; 38 | } else { 39 | int num = Math.min(buf.remaining(), dst.remaining()); 40 | ByteBuffer limitedSrc = buf.asReadOnlyBuffer(); 41 | limitedSrc.limit(limitedSrc.position() + num); 42 | dst.put(limitedSrc); 43 | buf.position(limitedSrc.position()); 44 | return num; 45 | } 46 | } 47 | 48 | @Override 49 | public int write(ByteBuffer src) { 50 | int num = Math.min(buf.remaining(), src.remaining()); 51 | ByteBuffer limitedSrc = src.asReadOnlyBuffer(); 52 | limitedSrc.limit(limitedSrc.position() + num); 53 | buf.put(limitedSrc); 54 | return num; 55 | } 56 | 57 | @Override 58 | public long position() { 59 | return buf.position(); 60 | } 61 | 62 | @Override 63 | public SeekableByteChannel position(long newPosition) { 64 | assert newPosition < Integer.MAX_VALUE; 65 | buf.position((int) newPosition); 66 | return this; 67 | } 68 | 69 | @Override 70 | public long size() throws IOException { 71 | return buf.limit(); 72 | } 73 | 74 | @Override 75 | public SeekableByteChannel truncate(long size) { 76 | assert size < Integer.MAX_VALUE; 77 | if (size < buf.position()) { 78 | buf.position((int) size); 79 | } 80 | buf.limit((int) size); 81 | return this; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/common/X509CertBuilderTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.cryptolib.common; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Nested; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.security.KeyPair; 10 | import java.security.KeyPairGenerator; 11 | import java.security.NoSuchAlgorithmException; 12 | import java.security.cert.CertificateException; 13 | import java.security.cert.X509Certificate; 14 | import java.time.Instant; 15 | 16 | public class X509CertBuilderTest { 17 | 18 | @Test 19 | @DisplayName("init() with RSA key and EC signature") 20 | public void testInitWithInvalidKeyPair() throws NoSuchAlgorithmException { 21 | KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); 22 | String signingAlg = "SHA256withECDSA"; 23 | 24 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 25 | X509CertBuilder.init(keyPair, signingAlg); 26 | }); 27 | } 28 | 29 | @Test 30 | @DisplayName("init() with RSA key and RSA signature") 31 | public void testInitWithRSAKeyPair() throws NoSuchAlgorithmException { 32 | KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); 33 | String signingAlg = "SHA256withRSA"; 34 | 35 | Assertions.assertDoesNotThrow(() -> { 36 | X509CertBuilder.init(keyPair, signingAlg); 37 | }); 38 | } 39 | 40 | @Test 41 | @DisplayName("init() with EC key and EC signature") 42 | public void testInitWithECKeyPair() throws NoSuchAlgorithmException { 43 | KeyPair keyPair = KeyPairGenerator.getInstance("EC").generateKeyPair(); 44 | String signingAlg = "SHA256withECDSA"; 45 | 46 | Assertions.assertDoesNotThrow(() -> { 47 | X509CertBuilder.init(keyPair, signingAlg); 48 | }); 49 | } 50 | 51 | @Nested 52 | @DisplayName("With initialized builder...") 53 | public class WithInitialized { 54 | 55 | private KeyPair keyPair; 56 | private X509CertBuilder builder; 57 | 58 | @BeforeEach 59 | public void setup() throws NoSuchAlgorithmException { 60 | this.keyPair = KeyPairGenerator.getInstance("EC").generateKeyPair(); 61 | this.builder = X509CertBuilder.init(keyPair, "SHA256withECDSA"); 62 | } 63 | 64 | @Test 65 | @DisplayName("set invalid issuer") 66 | public void testWithInvalidIssuer() { 67 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 68 | builder.withIssuer("Test"); 69 | }); 70 | } 71 | 72 | @Test 73 | @DisplayName("set invalid subject") 74 | public void testWithInvalidSubject() { 75 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 76 | builder.withSubject("Test"); 77 | }); 78 | } 79 | 80 | @Test 81 | @DisplayName("build() with missing issuer") 82 | public void testBuildWithMissingIssuer() { 83 | builder.withSubject("CN=Test") // 84 | .withNotBefore(Instant.now()) // 85 | .withNotAfter(Instant.now().minusSeconds(3600)); 86 | Assertions.assertThrows(IllegalStateException.class, () -> { 87 | builder.build(); 88 | }); 89 | } 90 | 91 | @Test 92 | @DisplayName("build() with missing subject") 93 | public void testBuildWithMissingSubject() { 94 | builder.withIssuer("CN=Test") // 95 | .withNotBefore(Instant.now()) // 96 | .withNotAfter(Instant.now().minusSeconds(3600)); 97 | Assertions.assertThrows(IllegalStateException.class, () -> { 98 | builder.build(); 99 | }); 100 | } 101 | 102 | @Test 103 | @DisplayName("build() with missing notBefore") 104 | public void testBuildWithMissingNotBefore() { 105 | builder.withIssuer("CN=Test") // 106 | .withSubject("CN=Test") // 107 | .withNotAfter(Instant.now().minusSeconds(3600)); 108 | Assertions.assertThrows(IllegalStateException.class, () -> { 109 | builder.build(); 110 | }); 111 | } 112 | 113 | @Test 114 | @DisplayName("build() with missing notAfter") 115 | public void testBuildWithMissingNotAfter() { 116 | builder.withIssuer("CN=Test") // 117 | .withSubject("CN=Test") // 118 | .withNotBefore(Instant.now()); 119 | Assertions.assertThrows(IllegalStateException.class, () -> { 120 | builder.build(); 121 | }); 122 | } 123 | 124 | @Test 125 | @DisplayName("build() with invalid notAfter") 126 | public void testBuildWithInvalidNotAfter() { 127 | builder.withIssuer("CN=Test") // 128 | .withSubject("CN=Test") // 129 | .withNotBefore(Instant.now()) // 130 | .withNotAfter(Instant.now().minusSeconds(1)); 131 | Assertions.assertThrows(IllegalStateException.class, () -> { 132 | builder.build(); 133 | }); 134 | } 135 | 136 | @Test 137 | @DisplayName("build() with all params set") 138 | public void testBuild() throws CertificateException { 139 | X509Certificate cert = builder // 140 | .withIssuer("CN=Test") // 141 | .withSubject("CN=Test") // 142 | .withNotBefore(Instant.now()) // 143 | .withNotAfter(Instant.now().plusSeconds(3600)) // 144 | .build(); 145 | 146 | Assertions.assertNotNull(cert); 147 | Assertions.assertDoesNotThrow(() -> { 148 | cert.verify(keyPair.getPublic()); 149 | }); 150 | Assertions.assertDoesNotThrow(() -> { 151 | cert.checkValidity(); 152 | }); 153 | } 154 | 155 | } 156 | 157 | } -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v1/BenchmarkTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import org.junit.jupiter.api.Disabled; 12 | import org.junit.jupiter.api.Test; 13 | import org.openjdk.jmh.runner.Runner; 14 | import org.openjdk.jmh.runner.RunnerException; 15 | import org.openjdk.jmh.runner.options.Options; 16 | import org.openjdk.jmh.runner.options.OptionsBuilder; 17 | 18 | public class BenchmarkTest { 19 | 20 | @Disabled("only on demand") 21 | @Test 22 | public void runBenchmarks() throws RunnerException { 23 | // Taken from http://stackoverflow.com/a/30486197/4014509: 24 | Options opt = new OptionsBuilder() 25 | // Specify which benchmarks to run 26 | .include(getClass().getPackage().getName() + ".*Benchmark.*") 27 | // Set the following options as needed 28 | .threads(2).forks(1) // 29 | .shouldFailOnError(true).shouldDoGC(true) 30 | // .jvmArgs("-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintInlining") 31 | // .addProfiler(WinPerfAsmProfiler.class) 32 | .build(); 33 | 34 | new Runner(opt).run(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import org.cryptomator.cryptolib.api.Masterkey; 12 | import org.cryptomator.cryptolib.common.SecureRandomMock; 13 | import org.hamcrest.CoreMatchers; 14 | import org.hamcrest.MatcherAssert; 15 | import org.junit.jupiter.api.Assertions; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.mockito.Mockito; 19 | 20 | import java.security.SecureRandom; 21 | 22 | public class CryptorImplTest { 23 | 24 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; 25 | 26 | private Masterkey masterkey; 27 | 28 | @BeforeEach 29 | public void setup() { 30 | this.masterkey = new Masterkey(new byte[64]); 31 | } 32 | 33 | @Test 34 | public void testGetFileContentCryptor() { 35 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { 36 | MatcherAssert.assertThat(cryptor.fileContentCryptor(), CoreMatchers.instanceOf(FileContentCryptorImpl.class)); 37 | } 38 | } 39 | 40 | @Test 41 | public void testGetFileHeaderCryptor() { 42 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { 43 | MatcherAssert.assertThat(cryptor.fileHeaderCryptor(), CoreMatchers.instanceOf(FileHeaderCryptorImpl.class)); 44 | } 45 | } 46 | 47 | @Test 48 | public void testGetFileNameCryptor() { 49 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { 50 | MatcherAssert.assertThat(cryptor.fileNameCryptor(), CoreMatchers.instanceOf(FileNameCryptorImpl.class)); 51 | } 52 | } 53 | 54 | @Test 55 | public void testExplicitDestruction() { 56 | Masterkey masterkey = Mockito.mock(Masterkey.class); 57 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { 58 | cryptor.destroy(); 59 | Mockito.verify(masterkey).destroy(); 60 | Mockito.when(masterkey.isDestroyed()).thenReturn(true); 61 | Assertions.assertTrue(cryptor.isDestroyed()); 62 | } 63 | } 64 | 65 | @Test 66 | public void testImplicitDestruction() { 67 | Masterkey masterkey = Mockito.mock(Masterkey.class); 68 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { 69 | Assertions.assertFalse(cryptor.isDestroyed()); 70 | } 71 | Mockito.verify(masterkey).destroy(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import org.cryptomator.cryptolib.api.Masterkey; 12 | import org.cryptomator.cryptolib.common.SecureRandomMock; 13 | import org.junit.jupiter.api.Assertions; 14 | import org.junit.jupiter.api.Test; 15 | import org.mockito.Mockito; 16 | 17 | import java.security.SecureRandom; 18 | 19 | public class CryptorProviderImplTest { 20 | 21 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; 22 | 23 | @Test 24 | public void testProvide() { 25 | Masterkey masterkey = Mockito.mock(Masterkey.class); 26 | CryptorImpl cryptor = new CryptorProviderImpl().provide(masterkey, RANDOM_MOCK); 27 | Assertions.assertNotNull(cryptor); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import java.nio.ByteBuffer; 12 | import java.security.SecureRandom; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | import javax.crypto.SecretKey; 16 | import javax.crypto.spec.SecretKeySpec; 17 | 18 | import org.cryptomator.cryptolib.api.Masterkey; 19 | import org.cryptomator.cryptolib.common.DestroyableSecretKey; 20 | import org.cryptomator.cryptolib.common.SecureRandomMock; 21 | import org.openjdk.jmh.annotations.Benchmark; 22 | import org.openjdk.jmh.annotations.BenchmarkMode; 23 | import org.openjdk.jmh.annotations.Level; 24 | import org.openjdk.jmh.annotations.Measurement; 25 | import org.openjdk.jmh.annotations.Mode; 26 | import org.openjdk.jmh.annotations.OutputTimeUnit; 27 | import org.openjdk.jmh.annotations.Scope; 28 | import org.openjdk.jmh.annotations.Setup; 29 | import org.openjdk.jmh.annotations.State; 30 | import org.openjdk.jmh.annotations.Warmup; 31 | 32 | /** 33 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff... 34 | */ 35 | @State(Scope.Thread) 36 | @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) 37 | @Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) 38 | @BenchmarkMode(value = {Mode.AverageTime}) 39 | @OutputTimeUnit(TimeUnit.MICROSECONDS) 40 | public class FileContentCryptorImplBenchmark { 41 | 42 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; 43 | private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);; 44 | private final byte[] headerNonce = new byte[Constants.NONCE_SIZE]; 45 | private final ByteBuffer cleartextChunk = ByteBuffer.allocate(Constants.PAYLOAD_SIZE); 46 | private final ByteBuffer ciphertextChunk = ByteBuffer.allocate(Constants.CHUNK_SIZE); 47 | private final FileContentCryptorImpl fileContentCryptor = new FileContentCryptorImpl(MASTERKEY, RANDOM_MOCK); 48 | private long chunkNumber; 49 | 50 | @Setup(Level.Invocation) 51 | public void shuffleData() { 52 | chunkNumber = RANDOM_MOCK.nextLong(); 53 | cleartextChunk.rewind(); 54 | ciphertextChunk.rewind(); 55 | RANDOM_MOCK.nextBytes(headerNonce); 56 | RANDOM_MOCK.nextBytes(cleartextChunk.array()); 57 | RANDOM_MOCK.nextBytes(ciphertextChunk.array()); 58 | } 59 | 60 | @Benchmark 61 | public void benchmarkEncryption() { 62 | fileContentCryptor.encryptChunk(cleartextChunk, ciphertextChunk, chunkNumber, headerNonce, MASTERKEY.getEncKey()); 63 | } 64 | 65 | @Benchmark 66 | public void benchmarkAuthentication() { 67 | fileContentCryptor.checkChunkMac(headerNonce, chunkNumber, ciphertextChunk); 68 | } 69 | 70 | @Benchmark 71 | public void benchmarkDecryption() { 72 | fileContentCryptor.decryptChunk(ciphertextChunk, ciphertextChunk, MASTERKEY.getEncKey()); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import java.io.IOException; 12 | import java.nio.ByteBuffer; 13 | import java.nio.channels.SeekableByteChannel; 14 | import java.nio.channels.WritableByteChannel; 15 | import java.security.SecureRandom; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel; 19 | import org.cryptomator.cryptolib.api.Masterkey; 20 | import org.cryptomator.cryptolib.common.SecureRandomMock; 21 | import org.openjdk.jmh.annotations.Benchmark; 22 | import org.openjdk.jmh.annotations.BenchmarkMode; 23 | import org.openjdk.jmh.annotations.Level; 24 | import org.openjdk.jmh.annotations.Measurement; 25 | import org.openjdk.jmh.annotations.Mode; 26 | import org.openjdk.jmh.annotations.OutputTimeUnit; 27 | import org.openjdk.jmh.annotations.Scope; 28 | import org.openjdk.jmh.annotations.Setup; 29 | import org.openjdk.jmh.annotations.State; 30 | import org.openjdk.jmh.annotations.Warmup; 31 | 32 | /** 33 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff... 34 | */ 35 | @State(Scope.Thread) 36 | @Warmup(iterations = 2) 37 | @Measurement(iterations = 2) 38 | @BenchmarkMode(value = {Mode.SingleShotTime}) 39 | @OutputTimeUnit(TimeUnit.MILLISECONDS) 40 | public class FileContentEncryptorBenchmark { 41 | 42 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; 43 | private static final Masterkey MASTERKEY = new Masterkey(new byte[64]); 44 | 45 | private CryptorImpl cryptor; 46 | 47 | @Setup(Level.Iteration) 48 | public void shuffleData() { 49 | cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK); 50 | } 51 | 52 | @Benchmark 53 | public void benchmark100MegabytesEncryption() throws IOException { 54 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024); 55 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) { 56 | for (int i = 0; i < 100; i++) { 57 | ch.write(megabyte); 58 | megabyte.clear(); 59 | } 60 | } 61 | } 62 | 63 | @Benchmark 64 | public void benchmark10MegabytesEncryption() throws IOException { 65 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024); 66 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) { 67 | for (int i = 0; i < 10; i++) { 68 | ch.write(megabyte); 69 | megabyte.clear(); 70 | } 71 | } 72 | } 73 | 74 | @Benchmark 75 | public void benchmark1MegabytesEncryption() throws IOException { 76 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024); 77 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) { 78 | ch.write(megabyte); 79 | megabyte.clear(); 80 | } 81 | } 82 | 83 | private static class NullSeekableByteChannel implements SeekableByteChannel { 84 | 85 | boolean open; 86 | 87 | @Override 88 | public boolean isOpen() { 89 | return open; 90 | } 91 | 92 | @Override 93 | public void close() throws IOException { 94 | open = false; 95 | } 96 | 97 | @Override 98 | public int read(ByteBuffer dst) throws IOException { 99 | throw new UnsupportedOperationException(); 100 | } 101 | 102 | @Override 103 | public int write(ByteBuffer src) throws IOException { 104 | int delta = src.remaining(); 105 | src.position(src.position() + delta); 106 | return delta; 107 | } 108 | 109 | @Override 110 | public long position() throws IOException { 111 | return 0; 112 | } 113 | 114 | @Override 115 | public SeekableByteChannel position(long newPosition) throws IOException { 116 | return this; 117 | } 118 | 119 | @Override 120 | public long size() throws IOException { 121 | return 0; 122 | } 123 | 124 | @Override 125 | public SeekableByteChannel truncate(long size) throws IOException { 126 | return this; 127 | } 128 | 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException; 12 | import org.cryptomator.cryptolib.api.FileHeader; 13 | import org.cryptomator.cryptolib.api.Masterkey; 14 | import org.cryptomator.cryptolib.common.DestroyableSecretKey; 15 | import org.cryptomator.cryptolib.common.SecureRandomMock; 16 | import org.openjdk.jmh.annotations.Benchmark; 17 | import org.openjdk.jmh.annotations.BenchmarkMode; 18 | import org.openjdk.jmh.annotations.Level; 19 | import org.openjdk.jmh.annotations.Measurement; 20 | import org.openjdk.jmh.annotations.Mode; 21 | import org.openjdk.jmh.annotations.OutputTimeUnit; 22 | import org.openjdk.jmh.annotations.Scope; 23 | import org.openjdk.jmh.annotations.Setup; 24 | import org.openjdk.jmh.annotations.State; 25 | import org.openjdk.jmh.annotations.Warmup; 26 | 27 | import java.nio.ByteBuffer; 28 | import java.security.SecureRandom; 29 | import java.util.concurrent.TimeUnit; 30 | 31 | /** 32 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff... 33 | */ 34 | @State(Scope.Thread) 35 | @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) 36 | @Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) 37 | @BenchmarkMode(value = {Mode.AverageTime}) 38 | @OutputTimeUnit(TimeUnit.MICROSECONDS) 39 | public class FileHeaderCryptorBenchmark { 40 | 41 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; 42 | private static final Masterkey MASTERKEY = new Masterkey(new byte[64]); 43 | private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK); 44 | private FileHeader header; 45 | private ByteBuffer validHeaderCiphertextBuf; 46 | 47 | @Setup(Level.Iteration) 48 | public void shuffleData() { 49 | header = HEADER_CRYPTOR.create(); 50 | validHeaderCiphertextBuf = HEADER_CRYPTOR.encryptHeader(header); 51 | } 52 | 53 | @Benchmark 54 | public void benchmarkEncryption() { 55 | HEADER_CRYPTOR.encryptHeader(header); 56 | } 57 | 58 | @Benchmark 59 | public void benchmarkDecryption() throws AuthenticationFailedException { 60 | HEADER_CRYPTOR.decryptHeader(validHeaderCiphertextBuf); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import com.google.common.io.BaseEncoding; 12 | import org.cryptomator.cryptolib.api.AuthenticationFailedException; 13 | import org.cryptomator.cryptolib.api.FileHeader; 14 | import org.cryptomator.cryptolib.api.Masterkey; 15 | import org.cryptomator.cryptolib.common.SecureRandomMock; 16 | import org.junit.jupiter.api.Assertions; 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | 20 | import java.nio.ByteBuffer; 21 | import java.security.SecureRandom; 22 | 23 | public class FileHeaderCryptorImplTest { 24 | 25 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; 26 | private FileHeaderCryptorImpl headerCryptor; 27 | 28 | @BeforeEach 29 | public void setup() { 30 | Masterkey masterkey = new Masterkey(new byte[64]); 31 | headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK); 32 | } 33 | 34 | @Test 35 | public void testEncryption() { 36 | // set nonce to: AAAAAAAAAAAAAAAAAAAAAA== 37 | // set payload to: //////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== 38 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]); 39 | FileHeader header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload); 40 | // encrypt payload: 41 | // echo -n "//////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" | base64 --decode \ 42 | // | openssl enc -aes-256-ctr -K 0000000000000000000000000000000000000000000000000000000000000000 -iv 00000000000000000000000000000000 | base64 43 | // -> I2o/h12/dnatSKIUkoQgh1MPivvHRTa5qWO08cTLc4vOp0A9TWBrbg== 44 | 45 | // mac nonce + encrypted payload: 46 | // (openssl dgst -sha256 -mac HMAC -macopt hexkey:0000000000000000000000000000000000000000000000000000000000000000 -binary) 47 | 48 | // concat nonce + encrypted payload + mac: 49 | final String expected = "AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="; 50 | 51 | ByteBuffer result = headerCryptor.encryptHeader(header); 52 | 53 | Assertions.assertArrayEquals(BaseEncoding.base64().decode(expected), result.array()); 54 | } 55 | 56 | @Test 57 | public void testHeaderSize() { 58 | Assertions.assertEquals(FileHeaderImpl.SIZE, headerCryptor.headerSize()); 59 | Assertions.assertEquals(FileHeaderImpl.SIZE, headerCryptor.encryptHeader(headerCryptor.create()).limit()); 60 | } 61 | 62 | @Test 63 | @SuppressWarnings("deprecation") 64 | public void testDecryption() throws AuthenticationFailedException { 65 | byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="); 66 | FileHeader header = headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext)); 67 | Assertions.assertEquals(header.getReserved(), -1l); 68 | } 69 | 70 | @Test 71 | public void testDecryptionWithTooShortHeader() { 72 | ByteBuffer ciphertext = ByteBuffer.allocate(7); 73 | 74 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 75 | headerCryptor.decryptHeader(ciphertext); 76 | }); 77 | } 78 | 79 | @Test 80 | public void testDecryptionWithInvalidMac1() { 81 | ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJa==")); 82 | 83 | Assertions.assertThrows(AuthenticationFailedException.class, () -> { 84 | headerCryptor.decryptHeader(ciphertext); 85 | }); 86 | } 87 | 88 | @Test 89 | public void testDecryptionWithInvalidMac2() { 90 | ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("aAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==")); 91 | 92 | Assertions.assertThrows(AuthenticationFailedException.class, () -> { 93 | headerCryptor.decryptHeader(ciphertext); 94 | }); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v1/FileHeaderImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v1; 10 | 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.util.Arrays; 15 | 16 | public class FileHeaderImplTest { 17 | 18 | @Test 19 | public void testConstructionFailsWithInvalidNonceSize() { 20 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]); 21 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 22 | new FileHeaderImpl(new byte[3], payload); 23 | }); 24 | } 25 | 26 | @Test 27 | public void testConstructionFailsWithInvalidKeySize() { 28 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 29 | new FileHeaderImpl.Payload(-1, new byte[3]); 30 | }); 31 | } 32 | 33 | @Test 34 | public void testDestruction() { 35 | byte[] nonNullKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]; 36 | Arrays.fill(nonNullKey, (byte) 0x42); 37 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, nonNullKey); 38 | FileHeaderImpl header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload); 39 | Assertions.assertFalse(header.isDestroyed()); 40 | header.destroy(); 41 | Assertions.assertTrue(header.isDestroyed()); 42 | Assertions.assertTrue(payload.isDestroyed()); 43 | Assertions.assertTrue(payload.getContentKey().isDestroyed()); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v2/BenchmarkTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import org.junit.jupiter.api.Disabled; 12 | import org.junit.jupiter.api.Test; 13 | import org.openjdk.jmh.runner.Runner; 14 | import org.openjdk.jmh.runner.RunnerException; 15 | import org.openjdk.jmh.runner.options.Options; 16 | import org.openjdk.jmh.runner.options.OptionsBuilder; 17 | 18 | public class BenchmarkTest { 19 | 20 | @Disabled("only on demand") 21 | @Test 22 | public void runBenchmarks() throws RunnerException { 23 | // Taken from http://stackoverflow.com/a/30486197/4014509: 24 | Options opt = new OptionsBuilder() 25 | // Specify which benchmarks to run 26 | .include(getClass().getPackage().getName() + ".*Benchmark.*") 27 | // Set the following options as needed 28 | .threads(2).forks(1) // 29 | .shouldFailOnError(true).shouldDoGC(true) 30 | // .jvmArgs("-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintInlining") 31 | // .addProfiler(WinPerfAsmProfiler.class) 32 | .build(); 33 | 34 | new Runner(opt).run(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import org.cryptomator.cryptolib.api.Masterkey; 12 | import org.cryptomator.cryptolib.common.SecureRandomMock; 13 | import org.hamcrest.CoreMatchers; 14 | import org.hamcrest.MatcherAssert; 15 | import org.junit.jupiter.api.Assertions; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.mockito.Mockito; 19 | 20 | import java.security.SecureRandom; 21 | 22 | public class CryptorImplTest { 23 | 24 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; 25 | 26 | private Masterkey masterkey; 27 | 28 | @BeforeEach 29 | public void setup() { 30 | this.masterkey = new Masterkey(new byte[64]); 31 | } 32 | 33 | @Test 34 | public void testGetFileContentCryptor() { 35 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { 36 | MatcherAssert.assertThat(cryptor.fileContentCryptor(), CoreMatchers.instanceOf(FileContentCryptorImpl.class)); 37 | } 38 | } 39 | 40 | @Test 41 | public void testGetFileHeaderCryptor() { 42 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { 43 | MatcherAssert.assertThat(cryptor.fileHeaderCryptor(), CoreMatchers.instanceOf(FileHeaderCryptorImpl.class)); 44 | } 45 | } 46 | 47 | @Test 48 | public void testGetFileNameCryptor() { 49 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { 50 | MatcherAssert.assertThat(cryptor.fileNameCryptor(), CoreMatchers.instanceOf(FileNameCryptorImpl.class)); 51 | } 52 | } 53 | 54 | @Test 55 | public void testExplicitDestruction() { 56 | Masterkey masterkey = Mockito.mock(Masterkey.class); 57 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { 58 | cryptor.destroy(); 59 | Mockito.verify(masterkey).destroy(); 60 | Mockito.when(masterkey.isDestroyed()).thenReturn(true); 61 | Assertions.assertTrue(cryptor.isDestroyed()); 62 | } 63 | } 64 | 65 | @Test 66 | public void testImplicitDestruction() { 67 | Masterkey masterkey = Mockito.mock(Masterkey.class); 68 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { 69 | Assertions.assertFalse(cryptor.isDestroyed()); 70 | } 71 | Mockito.verify(masterkey).destroy(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import org.cryptomator.cryptolib.api.Masterkey; 12 | import org.cryptomator.cryptolib.common.SecureRandomMock; 13 | import org.junit.jupiter.api.Assertions; 14 | import org.junit.jupiter.api.BeforeEach; 15 | import org.junit.jupiter.api.Test; 16 | import org.mockito.Mockito; 17 | 18 | import java.security.SecureRandom; 19 | 20 | public class CryptorProviderImplTest { 21 | 22 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; 23 | 24 | @Test 25 | public void testProvide() { 26 | Masterkey masterkey = Mockito.mock(Masterkey.class); 27 | CryptorImpl cryptor = new CryptorProviderImpl().provide(masterkey, RANDOM_MOCK); 28 | Assertions.assertNotNull(cryptor); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplBenchmark.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException; 12 | import org.cryptomator.cryptolib.common.DestroyableSecretKey; 13 | import org.cryptomator.cryptolib.common.SecureRandomMock; 14 | import org.openjdk.jmh.annotations.Benchmark; 15 | import org.openjdk.jmh.annotations.BenchmarkMode; 16 | import org.openjdk.jmh.annotations.Level; 17 | import org.openjdk.jmh.annotations.Measurement; 18 | import org.openjdk.jmh.annotations.Mode; 19 | import org.openjdk.jmh.annotations.OutputTimeUnit; 20 | import org.openjdk.jmh.annotations.Scope; 21 | import org.openjdk.jmh.annotations.Setup; 22 | import org.openjdk.jmh.annotations.State; 23 | import org.openjdk.jmh.annotations.Warmup; 24 | 25 | import java.nio.ByteBuffer; 26 | import java.security.SecureRandom; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | /** 30 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff... 31 | */ 32 | @State(Scope.Thread) 33 | @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) 34 | @Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) 35 | @BenchmarkMode(value = {Mode.AverageTime}) 36 | @OutputTimeUnit(TimeUnit.MICROSECONDS) 37 | public class FileContentCryptorImplBenchmark { 38 | 39 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; 40 | private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES"); 41 | private final byte[] headerNonce = new byte[FileHeaderImpl.NONCE_LEN]; 42 | private final ByteBuffer cleartextChunk = ByteBuffer.allocate(Constants.PAYLOAD_SIZE); 43 | private final ByteBuffer ciphertextChunk = ByteBuffer.allocate(Constants.CHUNK_SIZE); 44 | private final FileContentCryptorImpl fileContentCryptor = new FileContentCryptorImpl(RANDOM_MOCK); 45 | private long chunkNumber; 46 | 47 | @Setup(Level.Trial) 48 | public void prepareData() { 49 | cleartextChunk.rewind(); 50 | fileContentCryptor.encryptChunk(cleartextChunk, ciphertextChunk, 0l, new byte[12], ENC_KEY); 51 | ciphertextChunk.flip(); 52 | } 53 | 54 | @Setup(Level.Invocation) 55 | public void shuffleData() { 56 | chunkNumber = RANDOM_MOCK.nextLong(); 57 | cleartextChunk.rewind(); 58 | ciphertextChunk.rewind(); 59 | RANDOM_MOCK.nextBytes(headerNonce); 60 | RANDOM_MOCK.nextBytes(cleartextChunk.array()); 61 | } 62 | 63 | @Benchmark 64 | public void benchmarkEncryption() { 65 | fileContentCryptor.encryptChunk(cleartextChunk, ciphertextChunk, chunkNumber, headerNonce, ENC_KEY); 66 | } 67 | 68 | @Benchmark 69 | public void benchmarkDecryption() throws AuthenticationFailedException { 70 | fileContentCryptor.decryptChunk(ciphertextChunk, cleartextChunk, 0l, new byte[12], ENC_KEY); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import java.io.IOException; 12 | import java.nio.ByteBuffer; 13 | import java.nio.channels.SeekableByteChannel; 14 | import java.nio.channels.WritableByteChannel; 15 | import java.security.SecureRandom; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel; 19 | import org.cryptomator.cryptolib.api.Masterkey; 20 | import org.cryptomator.cryptolib.common.SecureRandomMock; 21 | import org.openjdk.jmh.annotations.Benchmark; 22 | import org.openjdk.jmh.annotations.BenchmarkMode; 23 | import org.openjdk.jmh.annotations.Level; 24 | import org.openjdk.jmh.annotations.Measurement; 25 | import org.openjdk.jmh.annotations.Mode; 26 | import org.openjdk.jmh.annotations.OutputTimeUnit; 27 | import org.openjdk.jmh.annotations.Scope; 28 | import org.openjdk.jmh.annotations.Setup; 29 | import org.openjdk.jmh.annotations.State; 30 | import org.openjdk.jmh.annotations.Warmup; 31 | 32 | /** 33 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff... 34 | */ 35 | @State(Scope.Thread) 36 | @Warmup(iterations = 2) 37 | @Measurement(iterations = 2) 38 | @BenchmarkMode(value = {Mode.SingleShotTime}) 39 | @OutputTimeUnit(TimeUnit.MILLISECONDS) 40 | public class FileContentEncryptorBenchmark { 41 | 42 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; 43 | private static final Masterkey MASTERKEY = new Masterkey(new byte[64]); 44 | 45 | private CryptorImpl cryptor; 46 | 47 | @Setup(Level.Iteration) 48 | public void shuffleData() { 49 | cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK); 50 | } 51 | 52 | @Benchmark 53 | public void benchmark100MegabytesEncryption() throws IOException { 54 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024); 55 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) { 56 | for (int i = 0; i < 100; i++) { 57 | ch.write(megabyte); 58 | megabyte.clear(); 59 | } 60 | } 61 | } 62 | 63 | @Benchmark 64 | public void benchmark10MegabytesEncryption() throws IOException { 65 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024); 66 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) { 67 | for (int i = 0; i < 10; i++) { 68 | ch.write(megabyte); 69 | megabyte.clear(); 70 | } 71 | } 72 | } 73 | 74 | @Benchmark 75 | public void benchmark1MegabytesEncryption() throws IOException { 76 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024); 77 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) { 78 | ch.write(megabyte); 79 | megabyte.clear(); 80 | } 81 | } 82 | 83 | private static class NullSeekableByteChannel implements SeekableByteChannel { 84 | 85 | boolean open; 86 | 87 | @Override 88 | public boolean isOpen() { 89 | return open; 90 | } 91 | 92 | @Override 93 | public void close() { 94 | open = false; 95 | } 96 | 97 | @Override 98 | public int read(ByteBuffer dst) { 99 | throw new UnsupportedOperationException(); 100 | } 101 | 102 | @Override 103 | public int write(ByteBuffer src) { 104 | int delta = src.remaining(); 105 | src.position(src.position() + delta); 106 | return delta; 107 | } 108 | 109 | @Override 110 | public long position() { 111 | return 0; 112 | } 113 | 114 | @Override 115 | public SeekableByteChannel position(long newPosition) { 116 | return this; 117 | } 118 | 119 | @Override 120 | public long size() { 121 | return 0; 122 | } 123 | 124 | @Override 125 | public SeekableByteChannel truncate(long size) { 126 | return this; 127 | } 128 | 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException; 12 | import org.cryptomator.cryptolib.api.FileHeader; 13 | import org.cryptomator.cryptolib.api.Masterkey; 14 | import org.cryptomator.cryptolib.common.DestroyableSecretKey; 15 | import org.cryptomator.cryptolib.common.SecureRandomMock; 16 | import org.openjdk.jmh.annotations.Benchmark; 17 | import org.openjdk.jmh.annotations.BenchmarkMode; 18 | import org.openjdk.jmh.annotations.Level; 19 | import org.openjdk.jmh.annotations.Measurement; 20 | import org.openjdk.jmh.annotations.Mode; 21 | import org.openjdk.jmh.annotations.OutputTimeUnit; 22 | import org.openjdk.jmh.annotations.Scope; 23 | import org.openjdk.jmh.annotations.Setup; 24 | import org.openjdk.jmh.annotations.State; 25 | import org.openjdk.jmh.annotations.Warmup; 26 | 27 | import java.nio.ByteBuffer; 28 | import java.security.SecureRandom; 29 | import java.util.concurrent.TimeUnit; 30 | 31 | /** 32 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff... 33 | */ 34 | @State(Scope.Thread) 35 | @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) 36 | @Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) 37 | @BenchmarkMode(value = {Mode.AverageTime}) 38 | @OutputTimeUnit(TimeUnit.MICROSECONDS) 39 | public class FileHeaderCryptorBenchmark { 40 | 41 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; 42 | private static final Masterkey MASTERKEY = new Masterkey(new byte[64]); 43 | private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK); 44 | 45 | private ByteBuffer validHeaderCiphertextBuf; 46 | private FileHeader header; 47 | 48 | @Setup(Level.Iteration) 49 | public void prepareData() { 50 | validHeaderCiphertextBuf = HEADER_CRYPTOR.encryptHeader(HEADER_CRYPTOR.create()); 51 | } 52 | 53 | @Setup(Level.Invocation) 54 | public void shuffleData() { 55 | header = HEADER_CRYPTOR.create(); 56 | } 57 | 58 | @Benchmark 59 | public void benchmarkEncryption() { 60 | HEADER_CRYPTOR.encryptHeader(header); 61 | } 62 | 63 | @Benchmark 64 | public void benchmarkDecryption() throws AuthenticationFailedException { 65 | HEADER_CRYPTOR.decryptHeader(validHeaderCiphertextBuf); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import com.google.common.io.BaseEncoding; 12 | import org.cryptomator.cryptolib.api.AuthenticationFailedException; 13 | import org.cryptomator.cryptolib.api.FileHeader; 14 | import org.cryptomator.cryptolib.api.Masterkey; 15 | import org.cryptomator.cryptolib.common.CipherSupplier; 16 | import org.cryptomator.cryptolib.common.GcmTestHelper; 17 | import org.cryptomator.cryptolib.common.ObjectPool; 18 | import org.cryptomator.cryptolib.common.SecureRandomMock; 19 | import org.junit.jupiter.api.Assertions; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Test; 22 | 23 | import javax.crypto.Cipher; 24 | import java.nio.ByteBuffer; 25 | import java.security.SecureRandom; 26 | 27 | public class FileHeaderCryptorImplTest { 28 | 29 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; 30 | 31 | private FileHeaderCryptorImpl headerCryptor; 32 | 33 | @BeforeEach 34 | public void setup() { 35 | Masterkey masterkey = new Masterkey(new byte[64]); 36 | headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK); 37 | 38 | // reset cipher state to avoid InvalidAlgorithmParameterExceptions due to IV-reuse 39 | GcmTestHelper.reset((mode, key, params) -> { 40 | try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(key, params)) { 41 | cipher.get(); 42 | } 43 | }); 44 | } 45 | 46 | @Test 47 | public void testEncryption() { 48 | // set nonce to: AAAAAAAAAAAAAAAA 49 | // set payload to: //////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== 50 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]); 51 | FileHeader header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload); 52 | // encrypt payload: 53 | // echo -n "//////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" | base64 --decode \ 54 | // | openssl enc -aes-256-gcm -K 0000000000000000000000000000000000000000000000000000000000000000 -iv 00000000000000000000000000000000 -a 55 | // -> MVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhew== 56 | 57 | // the following string contains nonce + ciphertext + tag. The tag is not produced by openssl, though. 58 | final String expected = "AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjU0="; 59 | 60 | ByteBuffer result = headerCryptor.encryptHeader(header); 61 | 62 | Assertions.assertArrayEquals(BaseEncoding.base64().decode(expected), result.array()); 63 | } 64 | 65 | @Test 66 | public void testHeaderSize() { 67 | Assertions.assertEquals(org.cryptomator.cryptolib.v2.FileHeaderImpl.SIZE, headerCryptor.headerSize()); 68 | Assertions.assertEquals(org.cryptomator.cryptolib.v2.FileHeaderImpl.SIZE, headerCryptor.encryptHeader(headerCryptor.create()).limit()); 69 | } 70 | 71 | @Test 72 | @SuppressWarnings("deprecation") 73 | public void testDecryption() throws AuthenticationFailedException { 74 | byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjU0="); 75 | FileHeader header = headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext)); 76 | Assertions.assertEquals(header.getReserved(), -1l); 77 | } 78 | 79 | @Test 80 | public void testDecryptionWithTooShortHeader() { 81 | ByteBuffer ciphertext = ByteBuffer.allocate(7); 82 | 83 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 84 | headerCryptor.decryptHeader(ciphertext); 85 | }); 86 | } 87 | 88 | @Test 89 | public void testDecryptionWithInvalidTag1() { 90 | ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjUA=")); 91 | 92 | Assertions.assertThrows(AuthenticationFailedException.class, () -> { 93 | headerCryptor.decryptHeader(ciphertext); 94 | }); 95 | } 96 | 97 | @Test 98 | public void testDecryptionWithInvalidTag2() { 99 | ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjUa=")); 100 | 101 | Assertions.assertThrows(AuthenticationFailedException.class, () -> { 102 | headerCryptor.decryptHeader(ciphertext); 103 | }); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/cryptolib/v2/FileHeaderImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.cryptolib.v2; 10 | 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.util.Arrays; 15 | 16 | public class FileHeaderImplTest { 17 | 18 | @Test 19 | public void testConstructionFailsWithInvalidNonceSize() { 20 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]); 21 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 22 | new FileHeaderImpl(new byte[3], payload); 23 | }); 24 | } 25 | 26 | @Test 27 | public void testConstructionFailsWithInvalidKeySize() { 28 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 29 | new FileHeaderImpl.Payload(-1, new byte[3]); 30 | }); 31 | } 32 | 33 | @Test 34 | public void testDestruction() { 35 | byte[] nonNullKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]; 36 | Arrays.fill(nonNullKey, (byte) 0x42); 37 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, nonNullKey); 38 | FileHeaderImpl header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload); 39 | Assertions.assertFalse(header.isDestroyed()); 40 | header.destroy(); 41 | Assertions.assertTrue(header.isDestroyed()); 42 | Assertions.assertTrue(payload.isDestroyed()); 43 | Assertions.assertTrue(payload.getContentKey().isDestroyed()); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /suppression.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | org\.cryptomator:.* 10 | cpe:/a:cryptomator:cryptomator 11 | CVE-2022-25366 12 | 13 | 14 | 15 | 18 | 19 | ^pkg:maven/com\.google\.guava/guava@.*$ 20 | CVE-2020-8908 21 | CVE-2020-8908 22 | 23 | 24 | --------------------------------------------------------------------------------