diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a62486..6cbec6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,8 @@ name: Build on: - [push] + push: + pull_request_target: + types: [labeled] jobs: build: name: Build and Test @@ -68,4 +70,4 @@ jobs: ``` See [README.md](https://github.com/cryptomator/siv-mode/#reproducible-builds) section regarding reproducing this build. - generate_release_notes: true \ No newline at end of file + generate_release_notes: true diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml index f50ba28..159255d 100644 --- a/.github/workflows/publish-central.yml +++ b/.github/workflows/publish-central.yml @@ -21,8 +21,6 @@ jobs: server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml server-username: MAVEN_USERNAME # env variable for username in deploy server-password: MAVEN_PASSWORD # env variable for token in deploy - gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import - gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase - name: Verify project version = ${{ github.event.inputs.tag }} run: | PROJECT_VERSION=$(./mvnw help:evaluate "-Dexpression=project.version" -q -DforceStdout) @@ -32,4 +30,5 @@ jobs: env: MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} - MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} \ No newline at end of file + MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml index 1e513bb..1bb7ca7 100644 --- a/.github/workflows/publish-github.yml +++ b/.github/workflows/publish-github.yml @@ -13,8 +13,6 @@ jobs: java-version: 21 distribution: 'zulu' cache: 'maven' - gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import - gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase - name: Verify project version = ${{ github.event.release.tag_name }} run: | PROJECT_VERSION=$(./mvnw help:evaluate "-Dexpression=project.version" -q -DforceStdout) @@ -24,6 +22,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} - name: Slack Notification uses: rtCamp/action-slack-notify@v2 env: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..acd660b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.6.0](https://github.com/cryptomator/siv-mode/compare/1.5.2...1.6.0) + +### Added + +- This CHANGELOG file +- `encrypt(SecretKey key, byte[] plaintext, byte[]... associatedData)` and `decrypt(SecretKey key, byte[] ciphertext, byte[]... associatedData)` using a single 256, 384, or 512 bit key + +### Changed + +- use `maven-gpg-plugin`'s bc-based signer diff --git a/pom.xml b/pom.xml index 17ddd6c..43172d6 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.cryptomator siv-mode - 1.5.2 + 1.6.0 SIV Mode RFC 5297 SIV mode: deterministic authenticated encryption @@ -38,17 +38,17 @@ 2024-04-16T12:32:12Z - 1.78 + 1.78.1 - 5.10.2 - 5.11.0 + 5.11.0 + 5.13.0 1.37 - 2.2 - 33.1.0-jre + 3.0 + 33.3.0-jre - 9.1.0 + 11.1.1 @@ -110,12 +110,12 @@ org.codehaus.mojo versions-maven-plugin - 2.16.2 + 2.18.0 org.apache.maven.plugins maven-enforcer-plugin - 3.4.1 + 3.5.0 enforce-java @@ -162,11 +162,11 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.5 + 3.5.2 maven-jar-plugin - 3.3.0 + 3.4.2 @@ -178,7 +178,7 @@ maven-source-plugin - 3.3.0 + 3.3.1 attach-sources @@ -190,7 +190,7 @@ maven-javadoc-plugin - 3.6.3 + 3.11.1 attach-javadocs @@ -210,7 +210,7 @@ maven-shade-plugin - 3.5.2 + 3.6.0 package @@ -285,7 +285,7 @@ org.jacoco jacoco-maven-plugin - 0.8.11 + 0.8.12 prepare-agent @@ -311,7 +311,7 @@ maven-gpg-plugin - 3.2.2 + 3.2.7 sign-artifacts @@ -320,10 +320,8 @@ sign - - --pinentry-mode - loopback - + bc + 58117AFA1F85B3EEC154677D615D449FE6E6A235 @@ -346,7 +344,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.13 + 1.7.0 true ossrh diff --git a/src/main/java/org/cryptomator/siv/SivMode.java b/src/main/java/org/cryptomator/siv/SivMode.java index 9978a31..03d9a49 100644 --- a/src/main/java/org/cryptomator/siv/SivMode.java +++ b/src/main/java/org/cryptomator/siv/SivMode.java @@ -100,11 +100,38 @@ interface CtrComputer { byte[] computeCtr(byte[] input, byte[] key, final byte[] iv); } + /** + * Convenience method using a single 256, 384, or 512 bits key. This is just a wrapper for {@link #encrypt(byte[], byte[], byte[], byte[]...)}. + * @param key Combined key, which is split in half. + * @param plaintext Your plaintext, which shall be encrypted. + * @param associatedData Optional associated data, which gets authenticated but not encrypted. + * @return IV + Ciphertext as a concatenated byte array. + */ + public byte[] encrypt(SecretKey key, byte[] plaintext, byte[]... associatedData) { + final byte[] keyBytes = key.getEncoded(); + if (keyBytes.length != 64 && keyBytes.length != 48 && keyBytes.length != 32) { + throw new IllegalArgumentException("Key length must be 256, 384, or 512 bits."); + } + final int subkeyLen = keyBytes.length / 2; + assert subkeyLen == 32 || subkeyLen == 24 || subkeyLen == 16; + final byte[] macKey = new byte[subkeyLen]; + final byte[] ctrKey = new byte[subkeyLen]; + try { + System.arraycopy(keyBytes, 0, macKey, 0, macKey.length); // K1 = leftmost(K, len(K)/2); + System.arraycopy(keyBytes, macKey.length, ctrKey, 0, ctrKey.length); // K2 = rightmost(K, len(K)/2); + return encrypt(ctrKey, macKey, plaintext, associatedData); + } finally { + Arrays.fill(macKey, (byte) 0); + Arrays.fill(ctrKey, (byte) 0); + Arrays.fill(keyBytes, (byte) 0); + } + } + /** * Convenience method, if you are using the javax.crypto API. This is just a wrapper for {@link #encrypt(byte[], byte[], byte[], byte[]...)}. * - * @param ctrKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2 - * @param macKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2 + * @param ctrKey SIV mode requires two separate keys. You can use one long key, which is split in half. See RFC 5297 Section 2.2 + * @param macKey SIV mode requires two separate keys. You can use one long key, which is split in half. See RFC 5297 Section 2.2 * @param plaintext Your plaintext, which shall be encrypted. * @param associatedData Optional associated data, which gets authenticated but not encrypted. * @return IV + Ciphertext as a concatenated byte array. @@ -127,8 +154,8 @@ public byte[] encrypt(SecretKey ctrKey, SecretKey macKey, byte[] plaintext, byte /** * Encrypts plaintext using SIV mode. A block cipher defined by the constructor is being used.
* - * @param ctrKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2 - * @param macKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2 + * @param ctrKey SIV mode requires two separate keys. You can use one long key, which is split in half. See RFC 5297 Section 2.2 + * @param macKey SIV mode requires two separate keys. You can use one long key, which is split in half. See RFC 5297 Section 2.2 * @param plaintext Your plaintext, which shall be encrypted. * @param associatedData Optional associated data, which gets authenticated but not encrypted. * @return IV + Ciphertext as a concatenated byte array. @@ -150,11 +177,41 @@ public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... return result; } + /** + * Convenience method using a single 256, 384, or 512 bits key. This is just a wrapper for {@link #decrypt(byte[], byte[], byte[], byte[]...)}. + * @param key Combined key, which is split in half. + * @param ciphertext Your cipehrtext, which shall be decrypted. + * @param associatedData Optional associated data, which gets authenticated but not encrypted. + * @return Plaintext byte array. + * @throws IllegalArgumentException If keys are invalid. + * @throws UnauthenticCiphertextException If the authentication failed, e.g. because ciphertext and/or associatedData are corrupted. + * @throws IllegalBlockSizeException If the provided ciphertext is of invalid length. + */ + public byte[] decrypt(SecretKey key, byte[] ciphertext, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException { + final byte[] keyBytes = key.getEncoded(); + if (keyBytes.length != 64 && keyBytes.length != 48 && keyBytes.length != 32) { + throw new IllegalArgumentException("Key length must be 256, 384, or 512 bits."); + } + final int subkeyLen = keyBytes.length / 2; + assert subkeyLen == 32 || subkeyLen == 24 || subkeyLen == 16; + final byte[] macKey = new byte[subkeyLen]; + final byte[] ctrKey = new byte[subkeyLen]; + try { + System.arraycopy(keyBytes, 0, macKey, 0, macKey.length); // K1 = leftmost(K, len(K)/2); + System.arraycopy(keyBytes, macKey.length, ctrKey, 0, ctrKey.length); // K2 = rightmost(K, len(K)/2); + return decrypt(ctrKey, macKey, ciphertext, associatedData); + } finally { + Arrays.fill(macKey, (byte) 0); + Arrays.fill(ctrKey, (byte) 0); + Arrays.fill(keyBytes, (byte) 0); + } + } + /** * Convenience method, if you are using the javax.crypto API. This is just a wrapper for {@link #decrypt(byte[], byte[], byte[], byte[]...)}. * - * @param ctrKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2 - * @param macKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2 + * @param ctrKey SIV mode requires two separate keys. You can use one long key, which is split in half. See RFC 5297 Section 2.2 + * @param macKey SIV mode requires two separate keys. You can use one long key, which is split in half. See RFC 5297 Section 2.2 * @param ciphertext Your cipehrtext, which shall be decrypted. * @param associatedData Optional associated data, which needs to be authenticated during decryption. * @return Plaintext byte array. @@ -179,8 +236,8 @@ public byte[] decrypt(SecretKey ctrKey, SecretKey macKey, byte[] ciphertext, byt /** * Decrypts ciphertext using SIV mode. A block cipher defined by the constructor is being used.
* - * @param ctrKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2 - * @param macKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2 + * @param ctrKey SIV mode requires two separate keys. You can use one long key, which is split in half. See RFC 5297 Section 2.2 + * @param macKey SIV mode requires two separate keys. You can use one long key, which is split in half. See RFC 5297 Section 2.2 * @param ciphertext Your ciphertext, which shall be encrypted. * @param associatedData Optional associated data, which needs to be authenticated during decryption. * @return Plaintext byte array. diff --git a/src/test/java/org/cryptomator/siv/SivModeTest.java b/src/test/java/org/cryptomator/siv/SivModeTest.java index 824259b..7002df3 100644 --- a/src/test/java/org/cryptomator/siv/SivModeTest.java +++ b/src/test/java/org/cryptomator/siv/SivModeTest.java @@ -18,6 +18,8 @@ import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import javax.crypto.IllegalBlockSizeException; @@ -66,6 +68,17 @@ public void testEncryptWithInvalidKey2() { }); } + @Test + public void testEncryptWithInvalidKey3() { + SecretKey key = Mockito.mock(SecretKey.class); + Mockito.when(key.getEncoded()).thenReturn(new byte[13]); + + SivMode sivMode = new SivMode(); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + sivMode.encrypt(key, new byte[10]); + }); + } + @Test public void testInvalidCipher1() { BlockCipherFactory factory = () -> null; @@ -111,6 +124,17 @@ public void testDecryptWithInvalidKey2() { }); } + @Test + public void testDecryptWithInvalidKey3() { + SecretKey key = Mockito.mock(SecretKey.class); + Mockito.when(key.getEncoded()).thenReturn(new byte[13]); + + SivMode sivMode = new SivMode(); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + sivMode.decrypt(key, new byte[10]); + }); + } + @Test public void testDecryptWithInvalidBlockSize() { final byte[] dummyKey = new byte[16]; @@ -437,9 +461,10 @@ public void testNonceBasedAuthenticatedEncryption() { Assertions.assertArrayEquals(expected, result); } - @Test - public void testEncryptionAndDecryptionUsingJavaxCryptoApi() throws UnauthenticCiphertextException, IllegalBlockSizeException { - final byte[] dummyKey = new byte[16]; + @ParameterizedTest + @ValueSource(ints = {16, 24, 32}) + public void testEncryptionAndDecryptionUsingJavaxCryptoApi(int keylen) throws UnauthenticCiphertextException, IllegalBlockSizeException { + final byte[] dummyKey = new byte[keylen]; final SecretKey ctrKey = new SecretKeySpec(dummyKey, "AES"); final SecretKey macKey = new SecretKeySpec(dummyKey, "AES"); final SivMode sivMode = new SivMode(); @@ -449,6 +474,18 @@ public void testEncryptionAndDecryptionUsingJavaxCryptoApi() throws UnauthenticC Assertions.assertArrayEquals(cleartext, decrypted); } + @ParameterizedTest + @ValueSource(ints = {32, 48, 64}) + public void testEncryptionAndDecryptionUsingSingleJavaxCryptoApi(int keylen) throws UnauthenticCiphertextException, IllegalBlockSizeException { + final byte[] dummyKey = new byte[keylen]; + final SecretKey key = new SecretKeySpec(dummyKey, "AES"); + final SivMode sivMode = new SivMode(); + final byte[] cleartext = "hello world".getBytes(); + final byte[] ciphertext = sivMode.encrypt(key, cleartext); + final byte[] decrypted = sivMode.decrypt(key, ciphertext); + Assertions.assertArrayEquals(cleartext, decrypted); + } + @Test public void testShiftLeft() { final byte[] output = new byte[4];