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];